Compare commits
55 commits
47dfd034c5
...
3c030b69ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c030b69ae | ||
|
|
b5ea1d9621 | ||
|
|
fc4230518d | ||
|
|
d38b312c21 | ||
|
|
c488121207 | ||
|
|
14b518770a | ||
|
|
f7c81735b1 | ||
|
|
643b7bd83b | ||
|
|
6ec5d002ff | ||
|
|
97f89eeeb7 | ||
|
|
32486dce98 | ||
|
|
c74d8f4227 | ||
|
|
840314e680 | ||
|
|
41f4d4ce79 | ||
|
|
e91373120d | ||
|
|
4beab4e6c8 | ||
|
|
0182935e67 | ||
|
|
0171f17e2e | ||
|
|
5678892dc2 | ||
|
|
5d54f3bc2f | ||
|
|
85f4a62aa1 | ||
|
|
575b0766d1 | ||
|
|
bf306ffad3 | ||
|
|
5c460dd2a1 | ||
|
|
4f1f8713e9 | ||
|
|
6a14a12a58 | ||
|
|
b8e52c6a45 | ||
|
|
eebb8609a7 | ||
|
|
1af20a9567 | ||
|
|
534b8be8d0 | ||
|
|
efcd5e7dbf | ||
|
|
a980b1352f | ||
|
|
d3beeb2d00 | ||
|
|
233f53494e | ||
|
|
520e048ed5 | ||
|
|
45114c8165 | ||
|
|
988dd57cf4 | ||
|
|
ffd69c1b54 | ||
|
|
0aee5a41ae | ||
|
|
e37cb98c7c | ||
|
|
dac940807a | ||
|
|
8457e22863 | ||
|
|
b931418c1e | ||
|
|
c09d7e412e | ||
|
|
ae33447994 | ||
|
|
23b52487c4 | ||
|
|
3d5fee4d36 | ||
|
|
f8f27fff13 | ||
|
|
89f2b83104 | ||
|
|
f0ed05b399 | ||
|
|
069bf4cc82 | ||
|
|
820ee1c03c | ||
|
|
c4279a6bcb | ||
|
|
a2540bd233 | ||
|
|
30e65abf1b |
218 changed files with 5290 additions and 2144 deletions
|
|
@ -24,6 +24,10 @@ RAY_ENABLED=false
|
|||
# Enable Laravel Telescope for debugging
|
||||
TELESCOPE_ENABLED=false
|
||||
|
||||
# Enable Laravel Nightwatch monitoring
|
||||
NIGHTWATCH_ENABLED=false
|
||||
NIGHTWATCH_TOKEN=
|
||||
|
||||
# Selenium Driver URL for Dusk
|
||||
DUSK_DRIVER_URL=http://selenium:4444
|
||||
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ ROOT_USERNAME=
|
|||
ROOT_USER_EMAIL=
|
||||
ROOT_USER_PASSWORD=
|
||||
|
||||
REGISTRY_URL=ghcr.io
|
||||
REGISTRY_URL=forgejo.mapledeploy.ca
|
||||
CDN_URL=https://updates.mapledeploy.ca
|
||||
|
|
|
|||
103
.forgejo/workflows/build.yml
Normal file
103
.forgejo/workflows/build.yml
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
name: Build MapleDeploy Coolify Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [mapledeploy]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- ".github/**"
|
||||
- "templates/**"
|
||||
|
||||
env:
|
||||
REGISTRY: forgejo.mapledeploy.ca
|
||||
CDN_STORAGE_ZONE: coolify-update
|
||||
CDN_PULL_ZONE_ID: "5338895"
|
||||
CDN_BASE_URL: https://updates.mapledeploy.ca
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
BASE_VERSION=$(sed -n "s/.*'version' => '\([^']*\)'.*/\1/p" config/constants.php)
|
||||
TIMESTAMP=$(date -u +%Y%m%d%H%M)
|
||||
VERSION="${BASE_VERSION}.${TIMESTAMP}"
|
||||
HELPER_VERSION=$(sed -n "s/.*'helper_version' => '\([^']*\)'.*/\1/p" config/constants.php)
|
||||
REALTIME_VERSION=$(sed -n "s/.*'realtime_version' => '\([^']*\)'.*/\1/p" config/constants.php)
|
||||
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "HELPER_VERSION=${HELPER_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "REALTIME_VERSION=${REALTIME_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "Building version: ${VERSION} (helper: ${HELPER_VERSION}, realtime: ${REALTIME_VERSION})"
|
||||
|
||||
- name: Login to Forgejo registry
|
||||
run: |
|
||||
echo "${{ secrets.FORGEJO_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.repository_owner }} --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
DOCKER_BUILDKIT=1 docker build -f docker/production/Dockerfile \
|
||||
--build-arg MAPLEDEPLOY_VERSION=${{ steps.version.outputs.VERSION }} \
|
||||
-t ${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.version.outputs.VERSION }} \
|
||||
-t ${{ env.REGISTRY }}/${{ github.repository }}:latest \
|
||||
.
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push ${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.version.outputs.VERSION }}
|
||||
docker push ${{ env.REGISTRY }}/${{ github.repository }}:latest
|
||||
|
||||
- name: Generate versions.json
|
||||
run: |
|
||||
cat > versions.json <<EOF
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "${{ steps.version.outputs.VERSION }}"
|
||||
},
|
||||
"helper": {
|
||||
"version": "${{ steps.version.outputs.HELPER_VERSION }}"
|
||||
},
|
||||
"realtime": {
|
||||
"version": "${{ steps.version.outputs.REALTIME_VERSION }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "Generated versions.json:"
|
||||
cat versions.json
|
||||
|
||||
- name: Install curl
|
||||
run: apk add --no-cache curl
|
||||
|
||||
- name: Upload artifacts to Bunny CDN
|
||||
run: |
|
||||
STORAGE_URL="https://storage.bunnycdn.com/${{ env.CDN_STORAGE_ZONE }}/coolify"
|
||||
|
||||
upload() {
|
||||
local file="$1"
|
||||
local dest="$2"
|
||||
echo "Uploading ${file} -> ${dest}"
|
||||
curl -fsSL -X PUT "${STORAGE_URL}/${dest}" \
|
||||
-H "AccessKey: ${{ secrets.BUNNY_CDN_STORAGE_KEY }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${file}"
|
||||
}
|
||||
|
||||
upload versions.json versions.json
|
||||
upload scripts/upgrade.sh upgrade.sh
|
||||
upload docker-compose.yml docker-compose.yml
|
||||
upload docker-compose.prod.yml docker-compose.prod.yml
|
||||
upload .env.production .env.production
|
||||
|
||||
echo "All artifacts uploaded."
|
||||
|
||||
- name: Purge CDN cache
|
||||
run: |
|
||||
curl -fsSL -X POST "https://api.bunny.net/pullzone/${{ env.CDN_PULL_ZONE_ID }}/purgeCache" \
|
||||
-H "AccessKey: ${{ secrets.BUNNY_API_KEY }}" \
|
||||
-H "Content-Type: application/json"
|
||||
echo "CDN cache purged."
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
name: Lock closed Issues, Discussions, and PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock-threads:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Lock threads after 30 days of inactivity
|
||||
uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-inactive-days: '30'
|
||||
discussion-inactive-days: '30'
|
||||
pr-inactive-days: '30'
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
name: Manage Stale Issues and PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
manage-stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Manage stale issues and PRs
|
||||
uses: actions/stale@v9
|
||||
id: stale
|
||||
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-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-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-close: 7
|
||||
stale-issue-label: '⏱︎ Stale'
|
||||
stale-pr-label: '⏱︎ Stale'
|
||||
only-labels: '💤 Waiting for feedback, 💤 Waiting for changes'
|
||||
remove-stale-when-updated: true
|
||||
operations-per-run: 100
|
||||
labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback, 💤 Waiting for changes'
|
||||
close-issue-reason: 'not_planned'
|
||||
exempt-all-milestones: false
|
||||
49
.github/workflows/chore-pr-comments.yml
vendored
49
.github/workflows/chore-pr-comments.yml
vendored
|
|
@ -1,49 +0,0 @@
|
|||
name: Add comment based on label
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
add-comment:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- label: "⚙️ Service"
|
||||
body: |
|
||||
Hi @${{ github.event.pull_request.user.login }}! 👋
|
||||
|
||||
It appears to us that you are either adding a new service or making changes to an existing one.
|
||||
We kindly ask you to also review and update the **Coolify Documentation** to include this new service or it's new configuration needs.
|
||||
This will help ensure that our documentation remains accurate and up-to-date for all users.
|
||||
|
||||
Coolify Docs Repository: https://github.com/coollabsio/coolify-docs
|
||||
How to Contribute a new Service to the Docs: https://coolify.io/docs/get-started/contribute/service#adding-a-new-service-template-to-the-coolify-documentation
|
||||
- label: "🛠️ Feature"
|
||||
body: |
|
||||
Hi @${{ github.event.pull_request.user.login }}! 👋
|
||||
|
||||
It appears to us that you are adding a new feature to Coolify.
|
||||
We kindly ask you to also update the **Coolify Documentation** to include information about this new feature.
|
||||
This will help ensure that our documentation remains accurate and up-to-date for all users.
|
||||
|
||||
Coolify Docs Repository: https://github.com/coollabsio/coolify-docs
|
||||
How to Contribute to the Docs: https://coolify.io/docs/get-started/contribute/documentation
|
||||
# - label: "✨ Enhancement"
|
||||
# body: |
|
||||
# It appears to us that you are making an enhancement to Coolify.
|
||||
# We kindly ask you to also review and update the Coolify Documentation to include information about this enhancement if applicable.
|
||||
# This will help ensure that our documentation remains accurate and up-to-date for all users.
|
||||
steps:
|
||||
- name: Add comment
|
||||
if: github.event.label.name == matrix.label
|
||||
run: gh pr comment "$NUMBER" --body "$BODY"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.pull_request.number }}
|
||||
BODY: ${{ matrix.body }}
|
||||
37
.github/workflows/claude.yml
vendored
37
.github/workflows/claude.yml
vendored
|
|
@ -1,37 +0,0 @@
|
|||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
claude_args: '--model opus'
|
||||
22
.github/workflows/cleanup-ghcr-untagged.yml
vendored
22
.github/workflows/cleanup-ghcr-untagged.yml
vendored
|
|
@ -1,22 +0,0 @@
|
|||
name: Cleanup Untagged GHCR Images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
cleanup-all-packages:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package: ['coolify', 'coolify-helper', 'coolify-realtime', 'coolify-testing-host']
|
||||
steps:
|
||||
- name: Delete untagged ${{ matrix.package }} images
|
||||
uses: actions/delete-package-versions@v5
|
||||
with:
|
||||
package-name: ${{ matrix.package }}
|
||||
package-type: 'container'
|
||||
min-versions-to-keep: 0
|
||||
delete-only-untagged-versions: 'true'
|
||||
117
.github/workflows/coolify-helper-next.yml
vendored
117
.github/workflows/coolify-helper-next.yml
vendored
|
|
@ -1,117 +0,0 @@
|
|||
name: Coolify Helper Image Development
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "next" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-helper"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
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 (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
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 }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
|
||||
116
.github/workflows/coolify-helper.yml
vendored
116
.github/workflows/coolify-helper.yml
vendored
|
|
@ -1,116 +0,0 @@
|
|||
name: Coolify Helper Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "v4.x" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-helper"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
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 (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
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 }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
|
||||
|
||||
122
.github/workflows/coolify-production-build.yml
vendored
122
.github/workflows/coolify-production-build.yml
vendored
|
|
@ -1,122 +0,0 @@
|
|||
name: Production Build (v4)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["v4.x"]
|
||||
paths-ignore:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/testing-host/Dockerfile
|
||||
- templates/**
|
||||
- CHANGELOG.md
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
|
||||
120
.github/workflows/coolify-realtime-next.yml
vendored
120
.github/workflows/coolify-realtime-next.yml
vendored
|
|
@ -1,120 +0,0 @@
|
|||
name: Coolify Realtime Development
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "next" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/coolify-realtime/terminal-server.js
|
||||
- docker/coolify-realtime/package.json
|
||||
- docker/coolify-realtime/package-lock.json
|
||||
- docker/coolify-realtime/soketi-entrypoint.sh
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-realtime"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
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 (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
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 }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
120
.github/workflows/coolify-realtime.yml
vendored
120
.github/workflows/coolify-realtime.yml
vendored
|
|
@ -1,120 +0,0 @@
|
|||
name: Coolify Realtime
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "v4.x" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/coolify-realtime/terminal-server.js
|
||||
- docker/coolify-realtime/package.json
|
||||
- docker/coolify-realtime/package-lock.json
|
||||
- docker/coolify-realtime/soketi-entrypoint.sh
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-realtime"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
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 (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
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 }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
|
||||
134
.github/workflows/coolify-staging-build.yml
vendored
134
.github/workflows/coolify-staging-build.yml
vendored
|
|
@ -1,134 +0,0 @@
|
|||
name: Staging Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- v4.x
|
||||
- v3.x
|
||||
- '**v5.x**'
|
||||
paths-ignore:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/testing-host/Dockerfile
|
||||
- templates/**
|
||||
- CHANGELOG.md
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sanitize branch name for Docker tag
|
||||
id: sanitize
|
||||
run: |
|
||||
# Replace slashes and other invalid characters with dashes
|
||||
SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g')
|
||||
echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-${{ matrix.arch }}
|
||||
cache-from: |
|
||||
type=gha,scope=build-${{ matrix.arch }}
|
||||
type=registry,ref=${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }}
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sanitize branch name for Docker tag
|
||||
id: sanitize
|
||||
run: |
|
||||
# Replace slashes and other invalid characters with dashes
|
||||
SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g')
|
||||
echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
104
.github/workflows/coolify-testing-host.yml
vendored
104
.github/workflows/coolify-testing-host.yml
vendored
|
|
@ -1,104 +0,0 @@
|
|||
name: Coolify Testing Host
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "next" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-testing-host.yml
|
||||
- docker/testing-host/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-testing-host"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/testing-host/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
42
.github/workflows/generate-changelog.yml
vendored
42
.github/workflows/generate-changelog.yml
vendored
|
|
@ -1,42 +0,0 @@
|
|||
name: Generate Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ v4.x ]
|
||||
paths-ignore:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
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 v4.x
|
||||
7
.github/workflows/pr-quality.yaml
vendored
7
.github/workflows/pr-quality.yaml
vendored
|
|
@ -40,7 +40,10 @@ jobs:
|
|||
max-emoji-count: 2
|
||||
max-code-references: 5
|
||||
require-linked-issue: false
|
||||
blocked-terms: "STRAWBERRY"
|
||||
blocked-terms: |
|
||||
STRAWBERRY
|
||||
🤖 Generated with Claude Code
|
||||
Generated with Claude Code
|
||||
blocked-issue-numbers: 8154
|
||||
|
||||
# PR Template Checks
|
||||
|
|
@ -97,7 +100,7 @@ jobs:
|
|||
exempt-pr-milestones: ""
|
||||
|
||||
# PR Success Actions
|
||||
success-add-pr-labels: "quality/verified"
|
||||
success-add-pr-labels: ""
|
||||
|
||||
# PR Failure Actions
|
||||
failure-remove-pr-labels: ""
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ ### Big Sponsors
|
|||
* [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions
|
||||
* [BC Direct](https://bc.direct?ref=coolify.io) - Your trusted technology consulting partner
|
||||
* [Blacksmith](https://blacksmith.sh?ref=coolify.io) - Infrastructure automation platform
|
||||
* [Brand.dev](https://brand.dev?ref=coolify.io) - API to personalize your product with logos, colors, and company info from any domain
|
||||
* [Context.dev](https://context.dev?ref=coolify.io) - API to personalize your product with logos, colors, and company info from any domain
|
||||
* [ByteBase](https://www.bytebase.com?ref=coolify.io) - Database CI/CD and Security at Scale
|
||||
* [CodeRabbit](https://coderabbit.ai?ref=coolify.io) - Cut Code Review Time & Bugs in Half
|
||||
* [COMIT](https://comit.international?ref=coolify.io) - New York Times award–winning contractor
|
||||
|
|
@ -90,6 +90,7 @@ ### Big Sponsors
|
|||
* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers
|
||||
* [Macarne](https://macarne.com?ref=coolify.io) - Best IP Transit & Carrier Ethernet Solutions for Simplified Network Connectivity
|
||||
* [Mobb](https://vibe.mobb.ai/?ref=coolify.io) - Secure Your AI-Generated Code to Unlock Dev Productivity
|
||||
* [PetroSky Cloud](https://petrosky.io?ref=coolify.io) - Open source cloud deployment solutions
|
||||
* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang
|
||||
* [Ramnode](https://ramnode.com/?ref=coolify.io) - High Performance Cloud VPS Hosting
|
||||
* [SaasyKit](https://saasykit.com?ref=coolify.io) - Complete SaaS starter kit for developers
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Jobs\NotifySetupCompleteJob;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
|
@ -35,6 +36,14 @@ public function create(array $input): User
|
|||
])->validate();
|
||||
|
||||
if (User::count() == 0) {
|
||||
// MapleDeploy: validate setup token for first user registration
|
||||
if ($settings->setup_token) {
|
||||
$providedToken = $input['setup_token'] ?? null;
|
||||
if (! $providedToken || ! hash_equals($settings->setup_token, $providedToken)) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the first user, make them the root user
|
||||
// Team is already created in the database/seeders/ProductionSeeder.php
|
||||
$user = User::create([
|
||||
|
|
@ -48,7 +57,18 @@ public function create(array $input): User
|
|||
// Disable registration after first user is created
|
||||
$settings = instanceSettings();
|
||||
$settings->is_registration_enabled = false;
|
||||
|
||||
// MapleDeploy: notify control plane that setup is complete
|
||||
// Capture token before clearing so the job can authenticate with it
|
||||
$callbackUrl = $settings->setup_callback_url;
|
||||
$token = $settings->setup_token;
|
||||
$settings->setup_token = null;
|
||||
$settings->setup_callback_url = null;
|
||||
$settings->save();
|
||||
|
||||
if ($callbackUrl && $token) {
|
||||
NotifySetupCompleteJob::dispatch($token, $callbackUrl);
|
||||
}
|
||||
} else {
|
||||
$user = User::create([
|
||||
'name' => $input['name'],
|
||||
|
|
|
|||
|
|
@ -102,7 +102,8 @@ public function handle(Server $server, $fromUI = false): bool
|
|||
foreach ($conflicts as $port => $conflict) {
|
||||
if ($conflict) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>");
|
||||
// MapleDeploy branding: support links
|
||||
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Support: <a target='_blank' class='dark:text-white hover:underline' href='https://mapledeploy.ca/contact'>https://mapledeploy.ca/contact</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use App\Services\ProxyDashboardCacheService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class GetProxyConfiguration
|
||||
{
|
||||
|
|
@ -24,6 +26,17 @@ public function handle(Server $server, bool $forceRegenerate = false): string
|
|||
// Primary source: database
|
||||
$proxy_configuration = $server->proxy->get('last_saved_proxy_configuration');
|
||||
|
||||
// Validate stored config matches current proxy type
|
||||
if (! empty(trim($proxy_configuration ?? ''))) {
|
||||
if (! $this->configMatchesProxyType($proxyType, $proxy_configuration)) {
|
||||
Log::warning('Stored proxy config does not match current proxy type, will regenerate', [
|
||||
'server_id' => $server->id,
|
||||
'proxy_type' => $proxyType,
|
||||
]);
|
||||
$proxy_configuration = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill: existing servers may not have DB config yet — read from disk once
|
||||
if (empty(trim($proxy_configuration ?? ''))) {
|
||||
$proxy_configuration = $this->backfillFromDisk($server);
|
||||
|
|
@ -55,6 +68,29 @@ public function handle(Server $server, bool $forceRegenerate = false): string
|
|||
return $proxy_configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the stored docker-compose YAML contains the expected service
|
||||
* for the server's current proxy type. Returns false if the config belongs
|
||||
* to a different proxy type (e.g. Traefik config on a CADDY server).
|
||||
*/
|
||||
private function configMatchesProxyType(string $proxyType, string $configuration): bool
|
||||
{
|
||||
try {
|
||||
$yaml = Yaml::parse($configuration);
|
||||
$services = data_get($yaml, 'services', []);
|
||||
|
||||
return match ($proxyType) {
|
||||
ProxyTypes::TRAEFIK->value => isset($services['traefik']),
|
||||
ProxyTypes::CADDY->value => isset($services['caddy']),
|
||||
ProxyTypes::NGINX->value => isset($services['nginx']),
|
||||
default => true,
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
// If YAML is unparseable, don't block — let the existing flow handle it
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfill: read config from disk for servers that predate DB storage.
|
||||
* Stores the result in the database so future reads skip SSH entirely.
|
||||
|
|
|
|||
|
|
@ -11,11 +11,8 @@ class InstallDocker
|
|||
{
|
||||
use AsAction;
|
||||
|
||||
private string $dockerVersion;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$this->dockerVersion = config('constants.docker.minimum_required_version');
|
||||
$supported_os_type = $server->validateOS();
|
||||
if (! $supported_os_type) {
|
||||
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
|
||||
|
|
@ -118,7 +115,7 @@ public function handle(Server $server)
|
|||
|
||||
private function getDebianDockerInstallCommand(): string
|
||||
{
|
||||
return "curl --max-time 300 --retry 3 https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl --max-time 300 --retry 3 https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
return 'curl -fsSL https://get.docker.com | sh || ('.
|
||||
'. /etc/os-release && '.
|
||||
'install -m 0755 -d /etc/apt/keyrings && '.
|
||||
'curl -fsSL https://download.docker.com/linux/${ID}/gpg -o /etc/apt/keyrings/docker.asc && '.
|
||||
|
|
@ -131,7 +128,7 @@ private function getDebianDockerInstallCommand(): string
|
|||
|
||||
private function getRhelDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
return 'curl -fsSL https://get.docker.com | sh || ('.
|
||||
'dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && '.
|
||||
'dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && '.
|
||||
'systemctl start docker && '.
|
||||
|
|
@ -141,7 +138,7 @@ private function getRhelDockerInstallCommand(): string
|
|||
|
||||
private function getSuseDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
return 'curl -fsSL https://get.docker.com | sh || ('.
|
||||
'zypper addrepo https://download.docker.com/linux/sles/docker-ce.repo && '.
|
||||
'zypper refresh && '.
|
||||
'zypper install -y --no-confirm docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && '.
|
||||
|
|
@ -152,10 +149,6 @@ private function getSuseDockerInstallCommand(): string
|
|||
|
||||
private function getArchDockerInstallCommand(): string
|
||||
{
|
||||
// Use -Syu to perform full system upgrade before installing Docker
|
||||
// Partial upgrades (-Sy without -u) are discouraged on Arch Linux
|
||||
// as they can lead to broken dependencies and system instability
|
||||
// Use --needed to skip reinstalling packages that are already up-to-date (idempotent)
|
||||
return 'pacman -Syu --noconfirm --needed docker docker-compose && '.
|
||||
'systemctl enable docker.service && '.
|
||||
'systemctl start docker.service';
|
||||
|
|
@ -163,6 +156,6 @@ private function getArchDockerInstallCommand(): string
|
|||
|
||||
private function getGenericDockerInstallCommand(): string
|
||||
{
|
||||
return "curl --max-time 300 --retry 3 https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl --max-time 300 --retry 3 https://get.docker.com | sh -s -- --version {$this->dockerVersion}";
|
||||
return 'curl -fsSL https://get.docker.com | sh';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,9 +119,11 @@ private function update()
|
|||
$latestHelperImageVersion = getHelperVersion();
|
||||
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
||||
|
||||
$registryUrl = config('constants.coolify.registry_url');
|
||||
|
||||
remote_process([
|
||||
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion $registryUrl",
|
||||
], $this->server);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -263,15 +263,11 @@ private function restoreCoolifyDbBackup()
|
|||
}
|
||||
}
|
||||
|
||||
// MapleDeploy branding: telemetry disabled — no phone-home signal
|
||||
private function sendAliveSignal()
|
||||
{
|
||||
$id = config('app.id');
|
||||
$version = config('constants.coolify.version');
|
||||
try {
|
||||
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in sending live signal: {$e->getMessage()}\n";
|
||||
}
|
||||
// Disabled for MapleDeploy: do not send telemetry to coolify.io
|
||||
return;
|
||||
}
|
||||
|
||||
private function replaceSlashInEnvironmentName()
|
||||
|
|
|
|||
22
app/Console/Commands/Nightwatch.php
Normal file
22
app/Console/Commands/Nightwatch.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class Nightwatch extends Command
|
||||
{
|
||||
protected $signature = 'start:nightwatch';
|
||||
|
||||
protected $description = 'Start Nightwatch';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (config('constants.nightwatch.is_nightwatch_enabled')) {
|
||||
$this->info('Nightwatch is enabled on this server.');
|
||||
$this->call('nightwatch:agent');
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
255
app/Console/Commands/ScheduledJobDiagnostics.php
Normal file
255
app/Console/Commands/ScheduledJobDiagnostics.php
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\DockerCleanupExecution;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ScheduledJobDiagnostics extends Command
|
||||
{
|
||||
protected $signature = 'scheduled:diagnostics
|
||||
{--type=all : Type to inspect: docker-cleanup, backups, tasks, server-jobs, all}
|
||||
{--server= : Filter by server ID}';
|
||||
|
||||
protected $description = 'Inspect dedup cache state and scheduling decisions for all scheduled jobs';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$type = $this->option('type');
|
||||
$serverFilter = $this->option('server');
|
||||
|
||||
$this->outputHeartbeat();
|
||||
|
||||
if (in_array($type, ['all', 'docker-cleanup'])) {
|
||||
$this->inspectDockerCleanups($serverFilter);
|
||||
}
|
||||
|
||||
if (in_array($type, ['all', 'backups'])) {
|
||||
$this->inspectBackups();
|
||||
}
|
||||
|
||||
if (in_array($type, ['all', 'tasks'])) {
|
||||
$this->inspectTasks();
|
||||
}
|
||||
|
||||
if (in_array($type, ['all', 'server-jobs'])) {
|
||||
$this->inspectServerJobs($serverFilter);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function outputHeartbeat(): void
|
||||
{
|
||||
$heartbeat = Cache::get('scheduled-job-manager:heartbeat');
|
||||
if ($heartbeat) {
|
||||
$age = Carbon::parse($heartbeat)->diffForHumans();
|
||||
$this->info("Scheduler heartbeat: {$heartbeat} ({$age})");
|
||||
} else {
|
||||
$this->error('Scheduler heartbeat: MISSING — ScheduledJobManager may not be running');
|
||||
}
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function inspectDockerCleanups(?string $serverFilter): void
|
||||
{
|
||||
$this->info('=== Docker Cleanup Jobs ===');
|
||||
|
||||
$servers = $this->getServers($serverFilter);
|
||||
|
||||
$rows = [];
|
||||
foreach ($servers as $server) {
|
||||
$frequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$dedupKey = "docker-cleanup:{$server->id}";
|
||||
$cacheValue = Cache::get($dedupKey);
|
||||
$timezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
|
||||
|
||||
if (validate_timezone($timezone) === false) {
|
||||
$timezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$wouldFire = shouldRunCronNow($frequency, $timezone, $dedupKey);
|
||||
|
||||
$lastExecution = DockerCleanupExecution::where('server_id', $server->id)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
$rows[] = [
|
||||
$server->id,
|
||||
$server->name,
|
||||
$timezone,
|
||||
$frequency,
|
||||
$dedupKey,
|
||||
$cacheValue ?? '<missing>',
|
||||
$wouldFire ? 'YES' : 'no',
|
||||
$lastExecution ? $lastExecution->status.' @ '.$lastExecution->created_at : 'never',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['ID', 'Server', 'TZ', 'Frequency', 'Dedup Key', 'Cache Value', 'Would Fire', 'Last Execution'],
|
||||
$rows
|
||||
);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function inspectBackups(): void
|
||||
{
|
||||
$this->info('=== Scheduled Backups ===');
|
||||
|
||||
$backups = ScheduledDatabaseBackup::with(['database'])
|
||||
->where('enabled', true)
|
||||
->get();
|
||||
|
||||
$rows = [];
|
||||
foreach ($backups as $backup) {
|
||||
$server = $backup->server();
|
||||
$frequency = $backup->frequency;
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$dedupKey = "scheduled-backup:{$backup->id}";
|
||||
$cacheValue = Cache::get($dedupKey);
|
||||
$timezone = $server ? data_get($server->settings, 'server_timezone', config('app.timezone')) : config('app.timezone');
|
||||
|
||||
if (validate_timezone($timezone) === false) {
|
||||
$timezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$wouldFire = shouldRunCronNow($frequency, $timezone, $dedupKey);
|
||||
|
||||
$rows[] = [
|
||||
$backup->id,
|
||||
$backup->database_type ?? 'unknown',
|
||||
$server?->name ?? 'N/A',
|
||||
$frequency,
|
||||
$cacheValue ?? '<missing>',
|
||||
$wouldFire ? 'YES' : 'no',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Backup ID', 'DB Type', 'Server', 'Frequency', 'Cache Value', 'Would Fire'],
|
||||
$rows
|
||||
);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function inspectTasks(): void
|
||||
{
|
||||
$this->info('=== Scheduled Tasks ===');
|
||||
|
||||
$tasks = ScheduledTask::with(['service', 'application'])
|
||||
->where('enabled', true)
|
||||
->get();
|
||||
|
||||
$rows = [];
|
||||
foreach ($tasks as $task) {
|
||||
$server = $task->server();
|
||||
$frequency = $task->frequency;
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$dedupKey = "scheduled-task:{$task->id}";
|
||||
$cacheValue = Cache::get($dedupKey);
|
||||
$timezone = $server ? data_get($server->settings, 'server_timezone', config('app.timezone')) : config('app.timezone');
|
||||
|
||||
if (validate_timezone($timezone) === false) {
|
||||
$timezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$wouldFire = shouldRunCronNow($frequency, $timezone, $dedupKey);
|
||||
|
||||
$rows[] = [
|
||||
$task->id,
|
||||
$task->name,
|
||||
$server?->name ?? 'N/A',
|
||||
$frequency,
|
||||
$cacheValue ?? '<missing>',
|
||||
$wouldFire ? 'YES' : 'no',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Task ID', 'Name', 'Server', 'Frequency', 'Cache Value', 'Would Fire'],
|
||||
$rows
|
||||
);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function inspectServerJobs(?string $serverFilter): void
|
||||
{
|
||||
$this->info('=== Server Manager Jobs ===');
|
||||
|
||||
$servers = $this->getServers($serverFilter);
|
||||
|
||||
$rows = [];
|
||||
foreach ($servers as $server) {
|
||||
$timezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
|
||||
if (validate_timezone($timezone) === false) {
|
||||
$timezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$dedupKeys = [
|
||||
"sentinel-restart:{$server->id}" => '0 0 * * *',
|
||||
"server-patch-check:{$server->id}" => '0 0 * * 0',
|
||||
"server-check:{$server->id}" => isCloud() ? '*/5 * * * *' : '* * * * *',
|
||||
"server-storage-check:{$server->id}" => data_get($server->settings, 'server_disk_usage_check_frequency', '0 23 * * *'),
|
||||
];
|
||||
|
||||
foreach ($dedupKeys as $dedupKey => $frequency) {
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$cacheValue = Cache::get($dedupKey);
|
||||
$wouldFire = shouldRunCronNow($frequency, $timezone, $dedupKey);
|
||||
|
||||
$rows[] = [
|
||||
$server->id,
|
||||
$server->name,
|
||||
$dedupKey,
|
||||
$frequency,
|
||||
$cacheValue ?? '<missing>',
|
||||
$wouldFire ? 'YES' : 'no',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Server ID', 'Server', 'Dedup Key', 'Frequency', 'Cache Value', 'Would Fire'],
|
||||
$rows
|
||||
);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
private function getServers(?string $serverFilter): \Illuminate\Support\Collection
|
||||
{
|
||||
$query = Server::with('settings')->where('ip', '!=', '1.2.3.4');
|
||||
|
||||
if ($serverFilter) {
|
||||
$query->where('id', $serverFilter);
|
||||
}
|
||||
|
||||
if (isCloud()) {
|
||||
$servers = $query->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get();
|
||||
$own = Team::find(0)?->servers()->with('settings')->get() ?? collect();
|
||||
|
||||
return $servers->merge($own);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
|
|
@ -4026,9 +4028,10 @@ public function storages(Request $request): JsonResponse
|
|||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['id', 'type'],
|
||||
required: ['type'],
|
||||
properties: [
|
||||
'id' => ['type' => 'integer', 'description' => 'The ID of the storage.'],
|
||||
'uuid' => ['type' => 'string', 'description' => 'The UUID of the storage (preferred).'],
|
||||
'id' => ['type' => 'integer', 'description' => 'The ID of the storage (deprecated, use uuid instead).'],
|
||||
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage: persistent or file.'],
|
||||
'is_preview_suffix_enabled' => ['type' => 'boolean', 'description' => 'Whether to add -pr-N suffix for preview deployments.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The volume name (persistent only, not allowed for read-only storages).'],
|
||||
|
|
@ -4078,7 +4081,7 @@ public function update_storage(Request $request): JsonResponse
|
|||
return $return;
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->route('uuid'))->first();
|
||||
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
|
|
@ -4089,7 +4092,8 @@ public function update_storage(Request $request): JsonResponse
|
|||
$this->authorize('update', $application);
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'id' => 'required|integer',
|
||||
'uuid' => 'string',
|
||||
'id' => 'integer',
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'is_preview_suffix_enabled' => 'boolean',
|
||||
'name' => 'string',
|
||||
|
|
@ -4098,7 +4102,7 @@ public function update_storage(Request $request): JsonResponse
|
|||
'content' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$allAllowedFields = ['id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'host_path', 'content'];
|
||||
$allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'host_path', 'content'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
|
|
@ -4114,10 +4118,23 @@ public function update_storage(Request $request): JsonResponse
|
|||
], 422);
|
||||
}
|
||||
|
||||
$storageUuid = $request->input('uuid');
|
||||
$storageId = $request->input('id');
|
||||
|
||||
if (! $storageUuid && ! $storageId) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['uuid' => 'Either uuid or id is required.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$lookupField = $storageUuid ? 'uuid' : 'id';
|
||||
$lookupValue = $storageUuid ?? $storageId;
|
||||
|
||||
if ($request->type === 'persistent') {
|
||||
$storage = $application->persistentStorages->where('id', $request->id)->first();
|
||||
$storage = $application->persistentStorages->where($lookupField, $lookupValue)->first();
|
||||
} else {
|
||||
$storage = $application->fileStorages->where('id', $request->id)->first();
|
||||
$storage = $application->fileStorages->where($lookupField, $lookupValue)->first();
|
||||
}
|
||||
|
||||
if (! $storage) {
|
||||
|
|
@ -4183,4 +4200,254 @@ public function update_storage(Request $request): JsonResponse
|
|||
|
||||
return response()->json($storage);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Storage',
|
||||
description: 'Create a persistent storage or file storage for an application.',
|
||||
path: '/applications/{uuid}/storages',
|
||||
operationId: 'create-storage-by-application-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')
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['type', 'mount_path'],
|
||||
properties: [
|
||||
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage.'],
|
||||
'name' => ['type' => 'string', 'description' => 'Volume name (persistent only, required for persistent).'],
|
||||
'mount_path' => ['type' => 'string', 'description' => 'The container mount path.'],
|
||||
'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, optional).'],
|
||||
'content' => ['type' => 'string', 'nullable' => true, 'description' => 'File content (file only, optional).'],
|
||||
'is_directory' => ['type' => 'boolean', 'description' => 'Whether this is a directory mount (file only, default false).'],
|
||||
'fs_path' => ['type' => 'string', 'description' => 'Host directory path (required when is_directory is true).'],
|
||||
],
|
||||
additionalProperties: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Storage created.',
|
||||
content: new OA\JsonContent(type: 'object'),
|
||||
),
|
||||
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'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
]
|
||||
)]
|
||||
public function create_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $application);
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'name' => 'string',
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => 'string|nullable',
|
||||
'content' => 'string|nullable',
|
||||
'is_directory' => 'boolean',
|
||||
'fs_path' => 'string',
|
||||
]);
|
||||
|
||||
$allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'fs_path'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($request->type === 'persistent') {
|
||||
if (! $request->name) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['name' => 'The name field is required for persistent storages.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'fs_path'], array_keys($request->all()));
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type 'persistent'."]),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$storage = LocalPersistentVolume::create([
|
||||
'name' => $application->uuid.'-'.$request->name,
|
||||
'mount_path' => $request->mount_path,
|
||||
'host_path' => $request->host_path,
|
||||
'resource_id' => $application->id,
|
||||
'resource_type' => $application->getMorphClass(),
|
||||
]);
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
// File storage
|
||||
$typeSpecificInvalidFields = array_intersect(['name', 'host_path'], array_keys($request->all()));
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type 'file'."]),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$isDirectory = $request->boolean('is_directory', false);
|
||||
|
||||
if ($isDirectory) {
|
||||
if (! $request->fs_path) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['fs_path' => 'The fs_path field is required for directory mounts.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$fsPath = str($request->fs_path)->trim()->start('/')->value();
|
||||
$mountPath = str($request->mount_path)->trim()->start('/')->value();
|
||||
|
||||
validateShellSafePath($fsPath, 'storage source path');
|
||||
validateShellSafePath($mountPath, 'storage destination path');
|
||||
|
||||
$storage = LocalFileVolume::create([
|
||||
'fs_path' => $fsPath,
|
||||
'mount_path' => $mountPath,
|
||||
'is_directory' => true,
|
||||
'resource_id' => $application->id,
|
||||
'resource_type' => get_class($application),
|
||||
]);
|
||||
} else {
|
||||
$mountPath = str($request->mount_path)->trim()->start('/')->value();
|
||||
$fsPath = application_configuration_dir().'/'.$application->uuid.$mountPath;
|
||||
|
||||
$storage = LocalFileVolume::create([
|
||||
'fs_path' => $fsPath,
|
||||
'mount_path' => $mountPath,
|
||||
'content' => $request->content,
|
||||
'is_directory' => false,
|
||||
'resource_id' => $application->id,
|
||||
'resource_type' => get_class($application),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Storage',
|
||||
description: 'Delete a persistent storage or file storage by application UUID.',
|
||||
path: '/applications/{uuid}/storages/{storage_uuid}',
|
||||
operationId: 'delete-storage-by-application-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')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'storage_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the storage.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Storage deleted.', content: new OA\JsonContent(
|
||||
properties: [new OA\Property(property: 'message', 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'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
]
|
||||
)]
|
||||
public function delete_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $application);
|
||||
|
||||
$storageUuid = $request->route('storage_uuid');
|
||||
|
||||
$storage = $application->persistentStorages->where('uuid', $storageUuid)->first();
|
||||
if (! $storage) {
|
||||
$storage = $application->fileStorages->where('uuid', $storageUuid)->first();
|
||||
}
|
||||
|
||||
if (! $storage) {
|
||||
return response()->json(['message' => 'Storage not found.'], 404);
|
||||
}
|
||||
|
||||
if ($storage->shouldBeReadOnlyInUI()) {
|
||||
return response()->json([
|
||||
'message' => 'This storage is read-only (managed by docker-compose or service definition) and cannot be deleted.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($storage instanceof LocalFileVolume) {
|
||||
$storage->deleteStorageOnServer();
|
||||
}
|
||||
|
||||
$storage->delete();
|
||||
|
||||
return response()->json(['message' => 'Storage deleted.']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,14 @@
|
|||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\Project;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
|
@ -3298,4 +3301,520 @@ public function delete_env_by_uuid(Request $request)
|
|||
|
||||
return response()->json(['message' => 'Environment variable deleted.']);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Storages',
|
||||
description: 'List all persistent storages and file storages by database UUID.',
|
||||
path: '/databases/{uuid}/storages',
|
||||
operationId: 'list-storages-by-database-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the database.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'All storages by database UUID.',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'persistent_storages', type: 'array', items: new OA\Items(type: 'object')),
|
||||
new OA\Property(property: 'file_storages', type: 'array', items: new OA\Items(type: 'object')),
|
||||
],
|
||||
),
|
||||
),
|
||||
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 storages(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('view', $database);
|
||||
|
||||
$persistentStorages = $database->persistentStorages->sortBy('id')->values();
|
||||
$fileStorages = $database->fileStorages->sortBy('id')->values();
|
||||
|
||||
return response()->json([
|
||||
'persistent_storages' => $persistentStorages,
|
||||
'file_storages' => $fileStorages,
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Storage',
|
||||
description: 'Create a persistent storage or file storage for a database.',
|
||||
path: '/databases/{uuid}/storages',
|
||||
operationId: 'create-storage-by-database-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the database.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['type', 'mount_path'],
|
||||
properties: [
|
||||
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage.'],
|
||||
'name' => ['type' => 'string', 'description' => 'Volume name (persistent only, required for persistent).'],
|
||||
'mount_path' => ['type' => 'string', 'description' => 'The container mount path.'],
|
||||
'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, optional).'],
|
||||
'content' => ['type' => 'string', 'nullable' => true, 'description' => 'File content (file only, optional).'],
|
||||
'is_directory' => ['type' => 'boolean', 'description' => 'Whether this is a directory mount (file only, default false).'],
|
||||
'fs_path' => ['type' => 'string', 'description' => 'Host directory path (required when is_directory is true).'],
|
||||
],
|
||||
additionalProperties: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Storage created.',
|
||||
content: new OA\JsonContent(type: 'object'),
|
||||
),
|
||||
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'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
]
|
||||
)]
|
||||
public function create_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $database);
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'name' => 'string',
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => 'string|nullable',
|
||||
'content' => 'string|nullable',
|
||||
'is_directory' => 'boolean',
|
||||
'fs_path' => 'string',
|
||||
]);
|
||||
|
||||
$allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'fs_path'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($request->type === 'persistent') {
|
||||
if (! $request->name) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['name' => 'The name field is required for persistent storages.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'fs_path'], array_keys($request->all()));
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type 'persistent'."]),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$storage = LocalPersistentVolume::create([
|
||||
'name' => $database->uuid.'-'.$request->name,
|
||||
'mount_path' => $request->mount_path,
|
||||
'host_path' => $request->host_path,
|
||||
'resource_id' => $database->id,
|
||||
'resource_type' => $database->getMorphClass(),
|
||||
]);
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
// File storage
|
||||
$typeSpecificInvalidFields = array_intersect(['name', 'host_path'], array_keys($request->all()));
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type 'file'."]),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$isDirectory = $request->boolean('is_directory', false);
|
||||
|
||||
if ($isDirectory) {
|
||||
if (! $request->fs_path) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['fs_path' => 'The fs_path field is required for directory mounts.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$fsPath = str($request->fs_path)->trim()->start('/')->value();
|
||||
$mountPath = str($request->mount_path)->trim()->start('/')->value();
|
||||
|
||||
validateShellSafePath($fsPath, 'storage source path');
|
||||
validateShellSafePath($mountPath, 'storage destination path');
|
||||
|
||||
$storage = LocalFileVolume::create([
|
||||
'fs_path' => $fsPath,
|
||||
'mount_path' => $mountPath,
|
||||
'is_directory' => true,
|
||||
'resource_id' => $database->id,
|
||||
'resource_type' => get_class($database),
|
||||
]);
|
||||
} else {
|
||||
$mountPath = str($request->mount_path)->trim()->start('/')->value();
|
||||
$fsPath = database_configuration_dir().'/'.$database->uuid.$mountPath;
|
||||
|
||||
$storage = LocalFileVolume::create([
|
||||
'fs_path' => $fsPath,
|
||||
'mount_path' => $mountPath,
|
||||
'content' => $request->content,
|
||||
'is_directory' => false,
|
||||
'resource_id' => $database->id,
|
||||
'resource_type' => get_class($database),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Storage',
|
||||
description: 'Update a persistent storage or file storage by database UUID.',
|
||||
path: '/databases/{uuid}/storages',
|
||||
operationId: 'update-storage-by-database-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the database.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Storage updated. For read-only storages (from docker-compose or services), only is_preview_suffix_enabled can be updated.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['type'],
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'The UUID of the storage (preferred).'],
|
||||
'id' => ['type' => 'integer', 'description' => 'The ID of the storage (deprecated, use uuid instead).'],
|
||||
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage: persistent or file.'],
|
||||
'is_preview_suffix_enabled' => ['type' => 'boolean', 'description' => 'Whether to add -pr-N suffix for preview deployments.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The volume name (persistent only, not allowed for read-only storages).'],
|
||||
'mount_path' => ['type' => 'string', 'description' => 'The container mount path (not allowed for read-only storages).'],
|
||||
'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, not allowed for read-only storages).'],
|
||||
'content' => ['type' => 'string', 'nullable' => true, 'description' => 'The file content (file only, not allowed for read-only storages).'],
|
||||
],
|
||||
additionalProperties: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Storage updated.',
|
||||
content: new OA\JsonContent(type: 'object'),
|
||||
),
|
||||
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',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$database = queryDatabaseByUuidWithinTeam($request->route('uuid'), $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $database);
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'uuid' => 'string',
|
||||
'id' => 'integer',
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'is_preview_suffix_enabled' => 'boolean',
|
||||
'name' => 'string',
|
||||
'mount_path' => 'string',
|
||||
'host_path' => 'string|nullable',
|
||||
'content' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'host_path', 'content'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$storageUuid = $request->input('uuid');
|
||||
$storageId = $request->input('id');
|
||||
|
||||
if (! $storageUuid && ! $storageId) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['uuid' => 'Either uuid or id is required.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$lookupField = $storageUuid ? 'uuid' : 'id';
|
||||
$lookupValue = $storageUuid ?? $storageId;
|
||||
|
||||
if ($request->type === 'persistent') {
|
||||
$storage = $database->persistentStorages->where($lookupField, $lookupValue)->first();
|
||||
} else {
|
||||
$storage = $database->fileStorages->where($lookupField, $lookupValue)->first();
|
||||
}
|
||||
|
||||
if (! $storage) {
|
||||
return response()->json([
|
||||
'message' => 'Storage not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$isReadOnly = $storage->shouldBeReadOnlyInUI();
|
||||
$editableOnlyFields = ['name', 'mount_path', 'host_path', 'content'];
|
||||
$requestedEditableFields = array_intersect($editableOnlyFields, array_keys($request->all()));
|
||||
|
||||
if ($isReadOnly && ! empty($requestedEditableFields)) {
|
||||
return response()->json([
|
||||
'message' => 'This storage is read-only (managed by docker-compose or service definition). Only is_preview_suffix_enabled can be updated.',
|
||||
'read_only_fields' => array_values($requestedEditableFields),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Reject fields that don't apply to the given storage type
|
||||
if (! $isReadOnly) {
|
||||
$typeSpecificInvalidFields = $request->type === 'persistent'
|
||||
? array_intersect(['content'], array_keys($request->all()))
|
||||
: array_intersect(['name', 'host_path'], array_keys($request->all()));
|
||||
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type '{$request->type}'."]),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
// Always allowed
|
||||
if ($request->has('is_preview_suffix_enabled')) {
|
||||
$storage->is_preview_suffix_enabled = $request->is_preview_suffix_enabled;
|
||||
}
|
||||
|
||||
// Only for editable storages
|
||||
if (! $isReadOnly) {
|
||||
if ($request->type === 'persistent') {
|
||||
if ($request->has('name')) {
|
||||
$storage->name = $request->name;
|
||||
}
|
||||
if ($request->has('mount_path')) {
|
||||
$storage->mount_path = $request->mount_path;
|
||||
}
|
||||
if ($request->has('host_path')) {
|
||||
$storage->host_path = $request->host_path;
|
||||
}
|
||||
} else {
|
||||
if ($request->has('mount_path')) {
|
||||
$storage->mount_path = $request->mount_path;
|
||||
}
|
||||
if ($request->has('content')) {
|
||||
$storage->content = $request->content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$storage->save();
|
||||
|
||||
return response()->json($storage);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Storage',
|
||||
description: 'Delete a persistent storage or file storage by database UUID.',
|
||||
path: '/databases/{uuid}/storages/{storage_uuid}',
|
||||
operationId: 'delete-storage-by-database-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the database.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'storage_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the storage.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Storage deleted.', content: new OA\JsonContent(
|
||||
properties: [new OA\Property(property: 'message', 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'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
]
|
||||
)]
|
||||
public function delete_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $database);
|
||||
|
||||
$storageUuid = $request->route('storage_uuid');
|
||||
|
||||
$storage = $database->persistentStorages->where('uuid', $storageUuid)->first();
|
||||
if (! $storage) {
|
||||
$storage = $database->fileStorages->where('uuid', $storageUuid)->first();
|
||||
}
|
||||
|
||||
if (! $storage) {
|
||||
return response()->json(['message' => 'Storage not found.'], 404);
|
||||
}
|
||||
|
||||
if ($storage->shouldBeReadOnlyInUI()) {
|
||||
return response()->json([
|
||||
'message' => 'This storage is read-only (managed by docker-compose or service definition) and cannot be deleted.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($storage instanceof LocalFileVolume) {
|
||||
$storage->deleteStorageOnServer();
|
||||
}
|
||||
|
||||
$storage->delete();
|
||||
|
||||
return response()->json(['message' => 'Storage deleted.']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -586,7 +586,8 @@ public function createServer(Request $request)
|
|||
}
|
||||
|
||||
// Check server limit
|
||||
if (Team::serverLimitReached()) {
|
||||
$team = Team::find($teamId);
|
||||
if (Team::serverLimitReached($team)) {
|
||||
return response()->json(['message' => 'Server limit reached for your subscription.'], 400);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Info(title: 'Coolify', version: '0.1')]
|
||||
#[OA\Server(url: 'https://app.coolify.io/api/v1', description: 'Coolify Cloud API. Change the host to your own instance if you are self-hosting.')]
|
||||
// MapleDeploy branding: API documentation
|
||||
#[OA\Info(title: 'MapleDeploy', version: '0.1')]
|
||||
#[OA\Server(url: '/api/v1', description: 'MapleDeploy API. Powered by Coolify.')]
|
||||
#[OA\SecurityScheme(
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class OtherController extends Controller
|
||||
|
|
@ -145,19 +144,6 @@ public function disable_api(Request $request)
|
|||
return response()->json(['message' => 'API disabled.'], 200);
|
||||
}
|
||||
|
||||
public function feedback(Request $request)
|
||||
{
|
||||
$content = $request->input('content');
|
||||
$webhook_url = config('constants.webhooks.feedback_discord_webhook');
|
||||
if ($webhook_url) {
|
||||
Http::post($webhook_url, [
|
||||
'content' => $content,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Feedback sent.'], 200);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Healthcheck',
|
||||
description: 'Healthcheck endpoint.',
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@
|
|||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
|
@ -1849,4 +1852,606 @@ public function action_restart(Request $request)
|
|||
200
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Storages',
|
||||
description: 'List all persistent storages and file storages by service UUID.',
|
||||
path: '/services/{uuid}/storages',
|
||||
operationId: 'list-storages-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Services'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'All storages by service UUID.',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'persistent_storages', type: 'array', items: new OA\Items(type: 'object')),
|
||||
new OA\Property(property: 'file_storages', type: 'array', items: new OA\Items(type: 'object')),
|
||||
],
|
||||
),
|
||||
),
|
||||
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 storages(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
|
||||
if (! $service) {
|
||||
return response()->json([
|
||||
'message' => 'Service not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$this->authorize('view', $service);
|
||||
|
||||
$persistentStorages = collect();
|
||||
$fileStorages = collect();
|
||||
|
||||
foreach ($service->applications as $app) {
|
||||
$persistentStorages = $persistentStorages->merge(
|
||||
$app->persistentStorages->map(fn ($s) => $s->setAttribute('resource_uuid', $app->uuid)->setAttribute('resource_type', 'application'))
|
||||
);
|
||||
$fileStorages = $fileStorages->merge(
|
||||
$app->fileStorages->map(fn ($s) => $s->setAttribute('resource_uuid', $app->uuid)->setAttribute('resource_type', 'application'))
|
||||
);
|
||||
}
|
||||
foreach ($service->databases as $db) {
|
||||
$persistentStorages = $persistentStorages->merge(
|
||||
$db->persistentStorages->map(fn ($s) => $s->setAttribute('resource_uuid', $db->uuid)->setAttribute('resource_type', 'database'))
|
||||
);
|
||||
$fileStorages = $fileStorages->merge(
|
||||
$db->fileStorages->map(fn ($s) => $s->setAttribute('resource_uuid', $db->uuid)->setAttribute('resource_type', 'database'))
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'persistent_storages' => $persistentStorages->sortBy('id')->values(),
|
||||
'file_storages' => $fileStorages->sortBy('id')->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Storage',
|
||||
description: 'Create a persistent storage or file storage for a service sub-resource.',
|
||||
path: '/services/{uuid}/storages',
|
||||
operationId: 'create-storage-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Services'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['type', 'mount_path', 'resource_uuid'],
|
||||
properties: [
|
||||
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage.'],
|
||||
'resource_uuid' => ['type' => 'string', 'description' => 'UUID of the service application or database sub-resource.'],
|
||||
'name' => ['type' => 'string', 'description' => 'Volume name (persistent only, required for persistent).'],
|
||||
'mount_path' => ['type' => 'string', 'description' => 'The container mount path.'],
|
||||
'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, optional).'],
|
||||
'content' => ['type' => 'string', 'nullable' => true, 'description' => 'File content (file only, optional).'],
|
||||
'is_directory' => ['type' => 'boolean', 'description' => 'Whether this is a directory mount (file only, default false).'],
|
||||
'fs_path' => ['type' => 'string', 'description' => 'Host directory path (required when is_directory is true).'],
|
||||
],
|
||||
additionalProperties: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Storage created.',
|
||||
content: new OA\JsonContent(type: 'object'),
|
||||
),
|
||||
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'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
]
|
||||
)]
|
||||
public function create_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $service);
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'resource_uuid' => 'required|string',
|
||||
'name' => 'string',
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => 'string|nullable',
|
||||
'content' => 'string|nullable',
|
||||
'is_directory' => 'boolean',
|
||||
'fs_path' => 'string',
|
||||
]);
|
||||
|
||||
$allAllowedFields = ['type', 'resource_uuid', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'fs_path'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$subResource = $service->applications()->where('uuid', $request->resource_uuid)->first();
|
||||
if (! $subResource) {
|
||||
$subResource = $service->databases()->where('uuid', $request->resource_uuid)->first();
|
||||
}
|
||||
if (! $subResource) {
|
||||
return response()->json(['message' => 'Service resource not found.'], 404);
|
||||
}
|
||||
|
||||
if ($request->type === 'persistent') {
|
||||
if (! $request->name) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['name' => 'The name field is required for persistent storages.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'fs_path'], array_keys($request->all()));
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type 'persistent'."]),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$storage = LocalPersistentVolume::create([
|
||||
'name' => $subResource->uuid.'-'.$request->name,
|
||||
'mount_path' => $request->mount_path,
|
||||
'host_path' => $request->host_path,
|
||||
'resource_id' => $subResource->id,
|
||||
'resource_type' => $subResource->getMorphClass(),
|
||||
]);
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
// File storage
|
||||
$typeSpecificInvalidFields = array_intersect(['name', 'host_path'], array_keys($request->all()));
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type 'file'."]),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$isDirectory = $request->boolean('is_directory', false);
|
||||
|
||||
if ($isDirectory) {
|
||||
if (! $request->fs_path) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['fs_path' => 'The fs_path field is required for directory mounts.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$fsPath = str($request->fs_path)->trim()->start('/')->value();
|
||||
$mountPath = str($request->mount_path)->trim()->start('/')->value();
|
||||
|
||||
validateShellSafePath($fsPath, 'storage source path');
|
||||
validateShellSafePath($mountPath, 'storage destination path');
|
||||
|
||||
$storage = LocalFileVolume::create([
|
||||
'fs_path' => $fsPath,
|
||||
'mount_path' => $mountPath,
|
||||
'is_directory' => true,
|
||||
'resource_id' => $subResource->id,
|
||||
'resource_type' => get_class($subResource),
|
||||
]);
|
||||
} else {
|
||||
$mountPath = str($request->mount_path)->trim()->start('/')->value();
|
||||
$fsPath = service_configuration_dir().'/'.$service->uuid.$mountPath;
|
||||
|
||||
$storage = LocalFileVolume::create([
|
||||
'fs_path' => $fsPath,
|
||||
'mount_path' => $mountPath,
|
||||
'content' => $request->content,
|
||||
'is_directory' => false,
|
||||
'resource_id' => $subResource->id,
|
||||
'resource_type' => get_class($subResource),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Storage',
|
||||
description: 'Update a persistent storage or file storage by service UUID.',
|
||||
path: '/services/{uuid}/storages',
|
||||
operationId: 'update-storage-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Services'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Storage updated. For read-only storages (from docker-compose or services), only is_preview_suffix_enabled can be updated.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['type'],
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'The UUID of the storage (preferred).'],
|
||||
'id' => ['type' => 'integer', 'description' => 'The ID of the storage (deprecated, use uuid instead).'],
|
||||
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage: persistent or file.'],
|
||||
'is_preview_suffix_enabled' => ['type' => 'boolean', 'description' => 'Whether to add -pr-N suffix for preview deployments.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The volume name (persistent only, not allowed for read-only storages).'],
|
||||
'mount_path' => ['type' => 'string', 'description' => 'The container mount path (not allowed for read-only storages).'],
|
||||
'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, not allowed for read-only storages).'],
|
||||
'content' => ['type' => 'string', 'nullable' => true, 'description' => 'The file content (file only, not allowed for read-only storages).'],
|
||||
],
|
||||
additionalProperties: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Storage updated.',
|
||||
content: new OA\JsonContent(type: 'object'),
|
||||
),
|
||||
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',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->route('uuid'))->first();
|
||||
|
||||
if (! $service) {
|
||||
return response()->json([
|
||||
'message' => 'Service not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $service);
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'uuid' => 'string',
|
||||
'id' => 'integer',
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'is_preview_suffix_enabled' => 'boolean',
|
||||
'name' => 'string',
|
||||
'mount_path' => 'string',
|
||||
'host_path' => 'string|nullable',
|
||||
'content' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'host_path', 'content'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$storageUuid = $request->input('uuid');
|
||||
$storageId = $request->input('id');
|
||||
|
||||
if (! $storageUuid && ! $storageId) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['uuid' => 'Either uuid or id is required.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$lookupField = $storageUuid ? 'uuid' : 'id';
|
||||
$lookupValue = $storageUuid ?? $storageId;
|
||||
|
||||
$storage = null;
|
||||
if ($request->type === 'persistent') {
|
||||
foreach ($service->applications as $app) {
|
||||
$storage = $app->persistentStorages->where($lookupField, $lookupValue)->first();
|
||||
if ($storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! $storage) {
|
||||
foreach ($service->databases as $db) {
|
||||
$storage = $db->persistentStorages->where($lookupField, $lookupValue)->first();
|
||||
if ($storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($service->applications as $app) {
|
||||
$storage = $app->fileStorages->where($lookupField, $lookupValue)->first();
|
||||
if ($storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! $storage) {
|
||||
foreach ($service->databases as $db) {
|
||||
$storage = $db->fileStorages->where($lookupField, $lookupValue)->first();
|
||||
if ($storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $storage) {
|
||||
return response()->json([
|
||||
'message' => 'Storage not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$isReadOnly = $storage->shouldBeReadOnlyInUI();
|
||||
$editableOnlyFields = ['name', 'mount_path', 'host_path', 'content'];
|
||||
$requestedEditableFields = array_intersect($editableOnlyFields, array_keys($request->all()));
|
||||
|
||||
if ($isReadOnly && ! empty($requestedEditableFields)) {
|
||||
return response()->json([
|
||||
'message' => 'This storage is read-only (managed by docker-compose or service definition). Only is_preview_suffix_enabled can be updated.',
|
||||
'read_only_fields' => array_values($requestedEditableFields),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Reject fields that don't apply to the given storage type
|
||||
if (! $isReadOnly) {
|
||||
$typeSpecificInvalidFields = $request->type === 'persistent'
|
||||
? array_intersect(['content'], array_keys($request->all()))
|
||||
: array_intersect(['name', 'host_path'], array_keys($request->all()));
|
||||
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type '{$request->type}'."]),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
// Always allowed
|
||||
if ($request->has('is_preview_suffix_enabled')) {
|
||||
$storage->is_preview_suffix_enabled = $request->is_preview_suffix_enabled;
|
||||
}
|
||||
|
||||
// Only for editable storages
|
||||
if (! $isReadOnly) {
|
||||
if ($request->type === 'persistent') {
|
||||
if ($request->has('name')) {
|
||||
$storage->name = $request->name;
|
||||
}
|
||||
if ($request->has('mount_path')) {
|
||||
$storage->mount_path = $request->mount_path;
|
||||
}
|
||||
if ($request->has('host_path')) {
|
||||
$storage->host_path = $request->host_path;
|
||||
}
|
||||
} else {
|
||||
if ($request->has('mount_path')) {
|
||||
$storage->mount_path = $request->mount_path;
|
||||
}
|
||||
if ($request->has('content')) {
|
||||
$storage->content = $request->content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$storage->save();
|
||||
|
||||
return response()->json($storage);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Storage',
|
||||
description: 'Delete a persistent storage or file storage by service UUID.',
|
||||
path: '/services/{uuid}/storages/{storage_uuid}',
|
||||
operationId: 'delete-storage-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Services'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'storage_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the storage.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Storage deleted.', content: new OA\JsonContent(
|
||||
properties: [new OA\Property(property: 'message', 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'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
]
|
||||
)]
|
||||
public function delete_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $service);
|
||||
|
||||
$storageUuid = $request->route('storage_uuid');
|
||||
|
||||
$storage = null;
|
||||
foreach ($service->applications as $app) {
|
||||
$storage = $app->persistentStorages->where('uuid', $storageUuid)->first();
|
||||
if ($storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! $storage) {
|
||||
foreach ($service->databases as $db) {
|
||||
$storage = $db->persistentStorages->where('uuid', $storageUuid)->first();
|
||||
if ($storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! $storage) {
|
||||
foreach ($service->applications as $app) {
|
||||
$storage = $app->fileStorages->where('uuid', $storageUuid)->first();
|
||||
if ($storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! $storage) {
|
||||
foreach ($service->databases as $db) {
|
||||
$storage = $db->fileStorages->where('uuid', $storageUuid)->first();
|
||||
if ($storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $storage) {
|
||||
return response()->json(['message' => 'Storage not found.'], 404);
|
||||
}
|
||||
|
||||
if ($storage->shouldBeReadOnlyInUI()) {
|
||||
return response()->json([
|
||||
'message' => 'This storage is read-only (managed by docker-compose or service definition) and cannot be deleted.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($storage instanceof LocalFileVolume) {
|
||||
$storage->deleteStorageOnServer();
|
||||
}
|
||||
|
||||
$storage->delete();
|
||||
|
||||
return response()->json(['message' => 'Storage deleted.']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ public function manual(Request $request)
|
|||
$after_sha = data_get($payload, 'after', data_get($payload, 'pull_request.head.sha'));
|
||||
$author_association = data_get($payload, 'pull_request.author_association');
|
||||
}
|
||||
if (! in_array($x_github_event, ['push', 'pull_request'])) {
|
||||
return response("Nothing to do. Event '$x_github_event' is not supported.");
|
||||
}
|
||||
if (! $branch) {
|
||||
return response('Nothing to do. No branch found in the request.');
|
||||
}
|
||||
|
|
@ -246,6 +249,9 @@ public function normal(Request $request)
|
|||
$after_sha = data_get($payload, 'after', data_get($payload, 'pull_request.head.sha'));
|
||||
$author_association = data_get($payload, 'pull_request.author_association');
|
||||
}
|
||||
if (! in_array($x_github_event, ['push', 'pull_request'])) {
|
||||
return response("Nothing to do. Event '$x_github_event' is not supported.");
|
||||
}
|
||||
if (! $id || ! $branch) {
|
||||
return response('Nothing to do. No id or branch found.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2334,13 +2334,13 @@ private function nixpacks_build_cmd()
|
|||
$this->generate_nixpacks_env_variables();
|
||||
$nixpacks_command = "nixpacks plan -f json {$this->env_nixpacks_args}";
|
||||
if ($this->application->build_command) {
|
||||
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
|
||||
$nixpacks_command .= ' --build-cmd '.escapeShellValue($this->application->build_command);
|
||||
}
|
||||
if ($this->application->start_command) {
|
||||
$nixpacks_command .= " --start-cmd \"{$this->application->start_command}\"";
|
||||
$nixpacks_command .= ' --start-cmd '.escapeShellValue($this->application->start_command);
|
||||
}
|
||||
if ($this->application->install_command) {
|
||||
$nixpacks_command .= " --install-cmd \"{$this->application->install_command}\"";
|
||||
$nixpacks_command .= ' --install-cmd '.escapeShellValue($this->application->install_command);
|
||||
}
|
||||
$nixpacks_command .= " {$this->workdir}";
|
||||
|
||||
|
|
@ -2353,13 +2353,15 @@ private function generate_nixpacks_env_variables()
|
|||
if ($this->pull_request_id === 0) {
|
||||
foreach ($this->application->nixpacks_environment_variables as $env) {
|
||||
if (! is_null($env->real_value) && $env->real_value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
|
||||
$value = ($env->is_literal || $env->is_multiline) ? trim($env->real_value, "'") : $env->real_value;
|
||||
$this->env_nixpacks_args->push('--env '.escapeShellValue("{$env->key}={$value}"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
|
||||
if (! is_null($env->real_value) && $env->real_value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
|
||||
$value = ($env->is_literal || $env->is_multiline) ? trim($env->real_value, "'") : $env->real_value;
|
||||
$this->env_nixpacks_args->push('--env '.escapeShellValue("{$env->key}={$value}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2369,7 +2371,7 @@ private function generate_nixpacks_env_variables()
|
|||
$coolify_envs->each(function ($value, $key) {
|
||||
// Only add environment variables with non-null and non-empty values
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$key}={$value}");
|
||||
$this->env_nixpacks_args->push('--env '.escapeShellValue("{$key}={$value}"));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -46,14 +46,20 @@ public function __construct(
|
|||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
if (! $this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->execution_log = DockerCleanupExecution::create([
|
||||
'server_id' => $this->server->id,
|
||||
]);
|
||||
|
||||
if (! $this->server->isFunctional()) {
|
||||
$this->execution_log->update([
|
||||
'status' => 'failed',
|
||||
'message' => 'Server is not functional (unreachable, unusable, or disabled)',
|
||||
'finished_at' => Carbon::now()->toImmutable(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->usageBefore = $this->server->getDiskUsage();
|
||||
|
||||
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
|
||||
|
|
|
|||
55
app/Jobs/NotifySetupCompleteJob.php
Normal file
55
app/Jobs/NotifySetupCompleteJob.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
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;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Notify MapleDeploy that the first user has registered on this Coolify instance.
|
||||
*
|
||||
* Sends the setup token as a Bearer token so MapleDeploy can verify authenticity
|
||||
* and clear its stored copy. The token acts as a one-time shared secret.
|
||||
*/
|
||||
class NotifySetupCompleteJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 5;
|
||||
|
||||
public array $backoff = [10, 30, 60, 120, 300];
|
||||
|
||||
public int $maxExceptions = 5;
|
||||
|
||||
public function __construct(
|
||||
public string $setupToken,
|
||||
public string $callbackUrl
|
||||
) {
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$response = Http::withToken($this->setupToken)
|
||||
->timeout(15)
|
||||
->post($this->callbackUrl);
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::warning('Setup-complete callback failed', [
|
||||
'status' => $response->status(),
|
||||
'url' => $this->callbackUrl,
|
||||
]);
|
||||
|
||||
// Throw so the job retries
|
||||
throw new \RuntimeException(
|
||||
"Setup-complete callback returned HTTP {$response->status()}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@
|
|||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
|
@ -185,7 +184,7 @@ private function processScheduledBackups(): void
|
|||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
if ($this->shouldRunNow($frequency, $serverTimezone, "scheduled-backup:{$backup->id}")) {
|
||||
if (shouldRunCronNow($frequency, $serverTimezone, "scheduled-backup:{$backup->id}", $this->executionTime)) {
|
||||
DatabaseBackupJob::dispatch($backup);
|
||||
$this->dispatchedCount++;
|
||||
Log::channel('scheduled')->info('Backup dispatched', [
|
||||
|
|
@ -239,7 +238,7 @@ private function processScheduledTasks(): void
|
|||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
if (! $this->shouldRunNow($frequency, $serverTimezone, "scheduled-task:{$task->id}")) {
|
||||
if (! shouldRunCronNow($frequency, $serverTimezone, "scheduled-task:{$task->id}", $this->executionTime)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -336,51 +335,6 @@ private function getTaskRuntimeSkipReason(ScheduledTask $task): ?string
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a cron schedule should run now.
|
||||
*
|
||||
* When a dedupKey is provided, uses getPreviousRunDate() + last-dispatch tracking
|
||||
* instead of isDue(). This is resilient to queue delays — even if the job is delayed
|
||||
* by minutes, it still catches the missed cron window. Without dedupKey, falls back
|
||||
* to simple isDue() check.
|
||||
*/
|
||||
private function shouldRunNow(string $frequency, string $timezone, ?string $dedupKey = null): bool
|
||||
{
|
||||
$cron = new CronExpression($frequency);
|
||||
$baseTime = $this->executionTime ?? Carbon::now();
|
||||
$executionTime = $baseTime->copy()->setTimezone($timezone);
|
||||
|
||||
// No dedup key → simple isDue check
|
||||
if ($dedupKey === null) {
|
||||
return $cron->isDue($executionTime);
|
||||
}
|
||||
|
||||
// Get the most recent time this cron was due (including current minute)
|
||||
$previousDue = Carbon::instance($cron->getPreviousRunDate($executionTime, allowCurrentDate: true));
|
||||
|
||||
$lastDispatched = Cache::get($dedupKey);
|
||||
|
||||
if ($lastDispatched === null) {
|
||||
// First run after restart or cache loss: only fire if actually due right now.
|
||||
// Seed the cache so subsequent runs can use tolerance/catch-up logic.
|
||||
$isDue = $cron->isDue($executionTime);
|
||||
if ($isDue) {
|
||||
Cache::put($dedupKey, $executionTime->toIso8601String(), 86400);
|
||||
}
|
||||
|
||||
return $isDue;
|
||||
}
|
||||
|
||||
// Subsequent runs: fire if there's been a due time since last dispatch
|
||||
if ($previousDue->gt(Carbon::parse($lastDispatched))) {
|
||||
Cache::put($dedupKey, $executionTime->toIso8601String(), 86400);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processDockerCleanups(): void
|
||||
{
|
||||
// Get all servers that need cleanup checks
|
||||
|
|
@ -411,7 +365,7 @@ private function processDockerCleanups(): void
|
|||
}
|
||||
|
||||
// Use the frozen execution time for consistent evaluation
|
||||
if ($this->shouldRunNow($frequency, $serverTimezone, "docker-cleanup:{$server->id}")) {
|
||||
if (shouldRunCronNow($frequency, $serverTimezone, "docker-cleanup:{$server->id}", $this->executionTime)) {
|
||||
DockerCleanupJob::dispatch(
|
||||
$server,
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -14,7 +13,6 @@
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ServerManagerJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
|
@ -81,7 +79,7 @@ private function getServers(): Collection
|
|||
private function dispatchConnectionChecks(Collection $servers): void
|
||||
{
|
||||
|
||||
if ($this->shouldRunNow($this->checkFrequency, dedupKey: 'server-connection-checks')) {
|
||||
if (shouldRunCronNow($this->checkFrequency, $this->instanceTimezone, 'server-connection-checks', $this->executionTime)) {
|
||||
$servers->each(function (Server $server) {
|
||||
try {
|
||||
// Skip SSH connection check if Sentinel is healthy — its heartbeat already proves connectivity
|
||||
|
|
@ -130,13 +128,13 @@ private function processServerTasks(Server $server): void
|
|||
|
||||
if ($sentinelOutOfSync) {
|
||||
// Dispatch ServerCheckJob if Sentinel is out of sync
|
||||
if ($this->shouldRunNow($this->checkFrequency, $serverTimezone, "server-check:{$server->id}")) {
|
||||
if (shouldRunCronNow($this->checkFrequency, $serverTimezone, "server-check:{$server->id}", $this->executionTime)) {
|
||||
ServerCheckJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
|
||||
$isSentinelEnabled = $server->isSentinelEnabled();
|
||||
$shouldRestartSentinel = $isSentinelEnabled && $this->shouldRunNow('0 0 * * *', $serverTimezone, "sentinel-restart:{$server->id}");
|
||||
$shouldRestartSentinel = $isSentinelEnabled && shouldRunCronNow('0 0 * * *', $serverTimezone, "sentinel-restart:{$server->id}", $this->executionTime);
|
||||
// Dispatch Sentinel restart if due (daily for Sentinel-enabled servers)
|
||||
|
||||
if ($shouldRestartSentinel) {
|
||||
|
|
@ -150,7 +148,7 @@ private function processServerTasks(Server $server): void
|
|||
if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) {
|
||||
$serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency];
|
||||
}
|
||||
$shouldRunStorageCheck = $this->shouldRunNow($serverDiskUsageCheckFrequency, $serverTimezone, "server-storage-check:{$server->id}");
|
||||
$shouldRunStorageCheck = shouldRunCronNow($serverDiskUsageCheckFrequency, $serverTimezone, "server-storage-check:{$server->id}", $this->executionTime);
|
||||
|
||||
if ($shouldRunStorageCheck) {
|
||||
ServerStorageCheckJob::dispatch($server);
|
||||
|
|
@ -158,7 +156,7 @@ private function processServerTasks(Server $server): void
|
|||
}
|
||||
|
||||
// Dispatch ServerPatchCheckJob if due (weekly)
|
||||
$shouldRunPatchCheck = $this->shouldRunNow('0 0 * * 0', $serverTimezone, "server-patch-check:{$server->id}");
|
||||
$shouldRunPatchCheck = shouldRunCronNow('0 0 * * 0', $serverTimezone, "server-patch-check:{$server->id}", $this->executionTime);
|
||||
|
||||
if ($shouldRunPatchCheck) { // Weekly on Sunday at midnight
|
||||
ServerPatchCheckJob::dispatch($server);
|
||||
|
|
@ -167,45 +165,4 @@ private function processServerTasks(Server $server): void
|
|||
// Note: CheckAndStartSentinelJob is only dispatched daily (line above) for version updates.
|
||||
// Crash recovery is handled by sentinelOutOfSync → ServerCheckJob → CheckAndStartSentinelJob.
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a cron schedule should run now.
|
||||
*
|
||||
* When a dedupKey is provided, uses getPreviousRunDate() + last-dispatch tracking
|
||||
* instead of isDue(). This is resilient to queue delays — even if the job is delayed
|
||||
* by minutes, it still catches the missed cron window.
|
||||
*/
|
||||
private function shouldRunNow(string $frequency, ?string $timezone = null, ?string $dedupKey = null): bool
|
||||
{
|
||||
$cron = new CronExpression($frequency);
|
||||
|
||||
// Use the frozen execution time, not the current time
|
||||
$baseTime = $this->executionTime ?? Carbon::now();
|
||||
$executionTime = $baseTime->copy()->setTimezone($timezone ?? config('app.timezone'));
|
||||
|
||||
if ($dedupKey === null) {
|
||||
return $cron->isDue($executionTime);
|
||||
}
|
||||
|
||||
$previousDue = Carbon::instance($cron->getPreviousRunDate($executionTime, allowCurrentDate: true));
|
||||
|
||||
$lastDispatched = Cache::get($dedupKey);
|
||||
|
||||
if ($lastDispatched === null) {
|
||||
$isDue = $cron->isDue($executionTime);
|
||||
if ($isDue) {
|
||||
Cache::put($dedupKey, $executionTime->toIso8601String(), 86400);
|
||||
}
|
||||
|
||||
return $isDue;
|
||||
}
|
||||
|
||||
if ($previousDue->gt(Carbon::parse($lastDispatched))) {
|
||||
Cache::put($dedupKey, $executionTime->toIso8601String(), 86400);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,25 +73,15 @@ public function handle(): void
|
|||
// send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
|
||||
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
// send_internal_notification('Old subscription activated for team: '.$teamId);
|
||||
$subscription->update([
|
||||
Subscription::updateOrCreate(
|
||||
['team_id' => $teamId],
|
||||
[
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_past_due' => false,
|
||||
]);
|
||||
} else {
|
||||
// send_internal_notification('New subscription for team: '.$teamId);
|
||||
Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_past_due' => false,
|
||||
]);
|
||||
}
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'invoice.paid':
|
||||
$customerId = data_get($data, 'customer');
|
||||
|
|
@ -227,18 +217,14 @@ public function handle(): void
|
|||
// send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
|
||||
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
// send_internal_notification("Subscription already exists for team: {$teamId}");
|
||||
throw new \RuntimeException("Subscription already exists for team: {$teamId}");
|
||||
} else {
|
||||
Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
Subscription::updateOrCreate(
|
||||
['team_id' => $teamId],
|
||||
[
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
}
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'customer.subscription.updated':
|
||||
$teamId = data_get($data, 'metadata.team_id');
|
||||
|
|
@ -254,20 +240,19 @@ public function handle(): void
|
|||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
if ($status === 'incomplete_expired') {
|
||||
// send_internal_notification('Subscription incomplete expired');
|
||||
throw new \RuntimeException('Subscription incomplete expired');
|
||||
}
|
||||
if ($teamId) {
|
||||
$subscription = Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
if (! $teamId) {
|
||||
throw new \RuntimeException('No subscription and team id found');
|
||||
}
|
||||
$subscription = Subscription::firstOrCreate(
|
||||
['team_id' => $teamId],
|
||||
[
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
} else {
|
||||
// send_internal_notification('No subscription and team id found');
|
||||
throw new \RuntimeException('No subscription and team id found');
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
|
||||
$feedback = data_get($data, 'cancellation_details.feedback');
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Help extends Component
|
||||
{
|
||||
use WithRateLimiting;
|
||||
|
||||
#[Validate(['required', 'min:10', 'max:1000'])]
|
||||
public string $description;
|
||||
|
||||
#[Validate(['required', 'min:3'])]
|
||||
public string $subject;
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->rateLimit(3, 30);
|
||||
|
||||
$settings = instanceSettings();
|
||||
$mail = new MailMessage;
|
||||
$mail->view(
|
||||
'emails.help',
|
||||
[
|
||||
'description' => $this->description,
|
||||
]
|
||||
);
|
||||
$mail->subject("[HELP]: {$this->subject}");
|
||||
$type = set_transanctional_email_settings($settings);
|
||||
|
||||
// Sending feedback through Cloud API
|
||||
if (blank($type)) {
|
||||
$url = 'https://app.coolify.io/api/feedback';
|
||||
Http::post($url, [
|
||||
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`',
|
||||
]);
|
||||
} else {
|
||||
send_user_an_email($mail, auth()->user()?->email, 'feedback@coollabs.io');
|
||||
}
|
||||
$this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.');
|
||||
$this->reset('description', 'subject');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.help')->layout('layouts.app');
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +98,9 @@ public function getResourceProperty()
|
|||
|
||||
public function refresh()
|
||||
{
|
||||
if (! $this->env->exists || ! $this->env->fresh()) {
|
||||
return;
|
||||
}
|
||||
$this->syncData();
|
||||
$this->checkEnvs();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ class Advanced extends Component
|
|||
|
||||
public ?string $allowed_ips = null;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_sponsorship_popup_enabled;
|
||||
// MapleDeploy branding: is_sponsorship_popup_enabled removed (popup removed)
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $disable_two_step_confirmation;
|
||||
|
|
@ -46,7 +45,6 @@ public function rules()
|
|||
'custom_dns_servers' => 'nullable|string',
|
||||
'is_api_enabled' => 'boolean',
|
||||
'allowed_ips' => ['nullable', 'string', new ValidIpOrCidr],
|
||||
'is_sponsorship_popup_enabled' => 'boolean',
|
||||
'disable_two_step_confirmation' => 'boolean',
|
||||
'is_wire_navigate_enabled' => 'boolean',
|
||||
];
|
||||
|
|
@ -65,7 +63,6 @@ public function mount()
|
|||
$this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
|
||||
$this->is_api_enabled = $this->settings->is_api_enabled;
|
||||
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
|
||||
$this->is_sponsorship_popup_enabled = $this->settings->is_sponsorship_popup_enabled;
|
||||
$this->is_wire_navigate_enabled = $this->settings->is_wire_navigate_enabled ?? true;
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +144,6 @@ public function instantSave()
|
|||
$this->settings->custom_dns_servers = $this->custom_dns_servers;
|
||||
$this->settings->is_api_enabled = $this->is_api_enabled;
|
||||
$this->settings->allowed_ips = $this->allowed_ips;
|
||||
$this->settings->is_sponsorship_popup_enabled = $this->is_sponsorship_popup_enabled;
|
||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
|
||||
$this->settings->is_wire_navigate_enabled = $this->is_wire_navigate_enabled;
|
||||
$this->settings->save();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ class PricingPlans extends Component
|
|||
{
|
||||
public function subscribeStripe($type)
|
||||
{
|
||||
if (currentTeam()->subscription?->stripe_invoice_paid) {
|
||||
$this->dispatch('error', 'Team already has an active subscription.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Stripe::setApiKey(config('subscription.stripe_api_key'));
|
||||
|
||||
$priceId = match ($type) {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class LocalPersistentVolume extends Model
|
||||
class LocalPersistentVolume extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1471,6 +1471,9 @@ public function changeProxy(string $proxyType, bool $async = true)
|
|||
if ($validProxyTypes->contains(str($proxyType)->lower())) {
|
||||
$this->proxy->set('type', str($proxyType)->upper());
|
||||
$this->proxy->set('status', 'exited');
|
||||
$this->proxy->set('last_saved_proxy_configuration', null);
|
||||
$this->proxy->set('last_saved_settings', null);
|
||||
$this->proxy->set('last_applied_settings', null);
|
||||
$this->save();
|
||||
if ($this->proxySet()) {
|
||||
if ($async) {
|
||||
|
|
|
|||
|
|
@ -89,10 +89,13 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
public static function serverLimitReached()
|
||||
public static function serverLimitReached(?Team $team = null)
|
||||
{
|
||||
$serverLimit = Team::serverLimit();
|
||||
$team = currentTeam();
|
||||
$team = $team ?? currentTeam();
|
||||
if (! $team) {
|
||||
return true;
|
||||
}
|
||||
$serverLimit = Team::serverLimit($team);
|
||||
$servers = $team->servers->count();
|
||||
|
||||
return $servers >= $serverLimit;
|
||||
|
|
@ -109,19 +112,23 @@ public function subscriptionPastOverDue()
|
|||
|
||||
public function serverOverflow()
|
||||
{
|
||||
if ($this->serverLimit() < $this->servers->count()) {
|
||||
if (Team::serverLimit($this) < $this->servers->count()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function serverLimit()
|
||||
public static function serverLimit(?Team $team = null)
|
||||
{
|
||||
if (currentTeam()->id === 0 && isDev()) {
|
||||
$team = $team ?? currentTeam();
|
||||
if (! $team) {
|
||||
return 0;
|
||||
}
|
||||
if ($team->id === 0 && isDev()) {
|
||||
return 9999999;
|
||||
}
|
||||
$team = Team::find(currentTeam()->id);
|
||||
$team = Team::find($team->id);
|
||||
if (! $team) {
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ protected static function boot()
|
|||
$team = [
|
||||
'name' => $user->name."'s Team",
|
||||
'personal_team' => true,
|
||||
'show_boarding' => true,
|
||||
'show_boarding' => false,
|
||||
];
|
||||
if ($user->id === 0) {
|
||||
$team['id'] = 0;
|
||||
|
|
@ -184,7 +184,7 @@ public function recreate_personal_team()
|
|||
$team = [
|
||||
'name' => $this->name."'s Team",
|
||||
'personal_team' => true,
|
||||
'show_boarding' => true,
|
||||
'show_boarding' => false,
|
||||
];
|
||||
if ($this->id === 0) {
|
||||
$team['id'] = 0;
|
||||
|
|
|
|||
|
|
@ -44,15 +44,24 @@ public function boot(): void
|
|||
{
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::registerView(function () {
|
||||
$isFirstUser = User::count() === 0;
|
||||
|
||||
$settings = instanceSettings();
|
||||
if (! $settings->is_registration_enabled) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
$isFirstUser = User::count() === 0;
|
||||
|
||||
// MapleDeploy: token-gated registration for first user
|
||||
if ($isFirstUser && $settings->setup_token) {
|
||||
$token = request()->query('setup_token');
|
||||
if (! $token || ! hash_equals($settings->setup_token, $token)) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
return view('auth.register', [
|
||||
'isFirstUser' => $isFirstUser,
|
||||
'setupToken' => request()->query('setup_token'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -61,7 +70,15 @@ public function boot(): void
|
|||
$enabled_oauth_providers = OauthSetting::where('enabled', true)->get();
|
||||
$users = User::count();
|
||||
if ($users == 0) {
|
||||
// If there are no users, redirect to registration
|
||||
// MapleDeploy: don't redirect to register if setup token is required
|
||||
if ($settings->setup_token) {
|
||||
return view('auth.login', [
|
||||
'setup_pending' => true,
|
||||
'is_registration_enabled' => false,
|
||||
'enabled_oauth_providers' => collect(),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('register');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,12 +62,15 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
|||
// Ignore errors when facades are not available (e.g., in unit tests)
|
||||
}
|
||||
|
||||
$fail('The :attribute contains invalid characters. Only lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
|
||||
$fail('The :attribute contains invalid characters. Only letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize to lowercase for validation (RFC 1123 hostnames are case-insensitive)
|
||||
$hostname = strtolower($hostname);
|
||||
|
||||
// Additional validation: hostname should not start or end with a dot
|
||||
if (str_starts_with($hostname, '.') || str_ends_with($hostname, '.')) {
|
||||
$fail('The :attribute cannot start or end with a dot.');
|
||||
|
|
@ -100,9 +103,9 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if label contains only valid characters (lowercase letters, digits, hyphens)
|
||||
// Check if label contains only valid characters (letters, digits, hyphens)
|
||||
if (! preg_match('/^[a-z0-9-]+$/', $label)) {
|
||||
$fail('The :attribute contains invalid characters. Only lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
|
||||
$fail('The :attribute contains invalid characters. Only letters (a-z, A-Z), numbers (0-9), hyphens (-), and dots (.) are allowed.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ValidationPatterns
|
|||
/**
|
||||
* Pattern for names excluding all dangerous characters
|
||||
*/
|
||||
public const NAME_PATTERN = '/^[\p{L}\p{M}\p{N}\s\-_.@\/&]+$/u';
|
||||
public const NAME_PATTERN = '/^[\p{L}\p{M}\p{N}\s\-_.@\/&()#,:+]+$/u';
|
||||
|
||||
/**
|
||||
* Pattern for descriptions excluding all dangerous characters with some additional allowed characters
|
||||
|
|
@ -96,7 +96,7 @@ public static function descriptionRules(bool $required = false, int $maxLength =
|
|||
public static function nameMessages(): array
|
||||
{
|
||||
return [
|
||||
'name.regex' => 'The name may only contain letters (including Unicode), numbers, spaces, and these characters: - _ . / @ &',
|
||||
'name.regex' => 'The name may only contain letters (including Unicode), numbers, spaces, and these characters: - _ . / @ & ( ) # , : +',
|
||||
'name.min' => 'The name must be at least :min characters.',
|
||||
'name.max' => 'The name may not be greater than :max characters.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -137,6 +137,11 @@ function checkMinimumDockerEngineVersion($dockerVersion)
|
|||
|
||||
return $dockerVersion;
|
||||
}
|
||||
function escapeShellValue(string $value): string
|
||||
{
|
||||
return "'".str_replace("'", "'\\''", $value)."'";
|
||||
}
|
||||
|
||||
function executeInDocker(string $containerId, string $command)
|
||||
{
|
||||
$escapedCommand = str_replace("'", "'\\''", $command);
|
||||
|
|
|
|||
|
|
@ -990,16 +990,17 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
}
|
||||
if ($key->value() === $parsedValue->value()) {
|
||||
// Simple variable reference (e.g. DATABASE_URL: ${DATABASE_URL})
|
||||
// Use firstOrCreate to avoid overwriting user-saved values on redeploy
|
||||
$envVar = $resource->environment_variables()->firstOrCreate([
|
||||
// Ensure the variable exists in DB for .env generation and UI display
|
||||
$resource->environment_variables()->firstOrCreate([
|
||||
'key' => $key,
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'is_preview' => false,
|
||||
]);
|
||||
// Add the variable to the environment using the saved DB value
|
||||
$environment[$key->value()] = $envVar->value;
|
||||
// Keep the ${VAR} reference in compose — Docker Compose resolves from .env at deploy time.
|
||||
// Do NOT replace with DB value: if user updates env var without re-parsing compose,
|
||||
// a stale resolved value in environment: would override the correct .env value.
|
||||
} else {
|
||||
if ($value->startsWith('$')) {
|
||||
$isRequired = false;
|
||||
|
|
@ -2341,8 +2342,8 @@ function serviceParser(Service $resource): Collection
|
|||
}
|
||||
if ($key->value() === $parsedValue->value()) {
|
||||
// Simple variable reference (e.g. DATABASE_URL: ${DATABASE_URL})
|
||||
// Use firstOrCreate to avoid overwriting user-saved values on redeploy
|
||||
$envVar = $resource->environment_variables()->firstOrCreate([
|
||||
// Ensure the variable exists in DB for .env generation and UI display
|
||||
$resource->environment_variables()->firstOrCreate([
|
||||
'key' => $key,
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
|
|
@ -2350,8 +2351,9 @@ function serviceParser(Service $resource): Collection
|
|||
'is_preview' => false,
|
||||
'comment' => $envComments[$originalKey] ?? null,
|
||||
]);
|
||||
// Add the variable to the environment using the saved DB value
|
||||
$environment[$key->value()] = $envVar->value;
|
||||
// Keep the ${VAR} reference in compose — Docker Compose resolves from .env at deploy time.
|
||||
// Do NOT replace with DB value: if user updates env var without re-parsing compose,
|
||||
// a stale resolved value in environment: would override the correct .env value.
|
||||
} else {
|
||||
if ($value->startsWith('$')) {
|
||||
$isRequired = false;
|
||||
|
|
|
|||
|
|
@ -339,7 +339,18 @@ function generate_application_name(string $git_repository, string $git_branch, ?
|
|||
$cuid = new Cuid2;
|
||||
}
|
||||
|
||||
return Str::kebab("$git_repository:$git_branch-$cuid");
|
||||
$repo_name = str_contains($git_repository, '/') ? last(explode('/', $git_repository)) : $git_repository;
|
||||
|
||||
$name = Str::kebab("$repo_name:$git_branch-$cuid");
|
||||
|
||||
// Strip characters not allowed by NAME_PATTERN
|
||||
$name = preg_replace('/[^\p{L}\p{M}\p{N}\s\-_.@\/&()#,:+]+/u', '', $name);
|
||||
|
||||
if (empty($name) || mb_strlen($name) < 3) {
|
||||
return generate_random_name($cuid);
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -466,6 +477,36 @@ function validate_cron_expression($expression_to_validate): bool
|
|||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a cron schedule should run now, with deduplication.
|
||||
*
|
||||
* Uses getPreviousRunDate() + last-dispatch tracking to be resilient to queue delays.
|
||||
* Even if the job runs minutes late, it still catches the missed cron window.
|
||||
* Without a dedupKey, falls back to a simple isDue() check.
|
||||
*/
|
||||
function shouldRunCronNow(string $frequency, string $timezone, ?string $dedupKey = null, ?\Illuminate\Support\Carbon $executionTime = null): bool
|
||||
{
|
||||
$cron = new \Cron\CronExpression($frequency);
|
||||
$executionTime = ($executionTime ?? \Illuminate\Support\Carbon::now())->copy()->setTimezone($timezone);
|
||||
|
||||
if ($dedupKey === null) {
|
||||
return $cron->isDue($executionTime);
|
||||
}
|
||||
|
||||
$previousDue = \Illuminate\Support\Carbon::instance($cron->getPreviousRunDate($executionTime, allowCurrentDate: true));
|
||||
$lastDispatched = Cache::get($dedupKey);
|
||||
|
||||
$shouldFire = $lastDispatched === null
|
||||
? $cron->isDue($executionTime)
|
||||
: $previousDue->gt(\Illuminate\Support\Carbon::parse($lastDispatched));
|
||||
|
||||
// Always write: seeds on first miss, refreshes on dispatch.
|
||||
// 30-day static TTL covers all intervals; orphan keys self-clean.
|
||||
Cache::put($dedupKey, ($shouldFire ? $executionTime : $previousDue)->toIso8601String(), 2592000);
|
||||
|
||||
return $shouldFire;
|
||||
}
|
||||
|
||||
function validate_timezone(string $timezone): bool
|
||||
{
|
||||
return in_array($timezone, timezone_identifiers_list());
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"laravel/fortify": "^1.34.0",
|
||||
"laravel/framework": "^12.49.0",
|
||||
"laravel/horizon": "^5.43.0",
|
||||
"laravel/nightwatch": "^1.24",
|
||||
"laravel/pail": "^1.2.4",
|
||||
"laravel/prompts": "^0.3.11|^0.3.11|^0.3.11",
|
||||
"laravel/sanctum": "^4.3.0",
|
||||
|
|
|
|||
98
composer.lock
generated
98
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "19bb661d294e5cf623e68830604e4f60",
|
||||
"content-hash": "40bddea995c1744e4aec517263109a2f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
|
@ -2065,6 +2065,100 @@
|
|||
},
|
||||
"time": "2026-02-21T14:20:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/nightwatch",
|
||||
"version": "v1.24.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/nightwatch.git",
|
||||
"reference": "127e9bb9928f0fcf69b52b244053b393c90347c8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/nightwatch/zipball/127e9bb9928f0fcf69b52b244053b393c90347c8",
|
||||
"reference": "127e9bb9928f0fcf69b52b244053b393c90347c8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-zlib": "*",
|
||||
"guzzlehttp/promises": "^2.0",
|
||||
"laravel/framework": "^10.0|^11.0|^12.0|^13.0",
|
||||
"monolog/monolog": "^3.6",
|
||||
"nesbot/carbon": "^2.0|^3.0",
|
||||
"php": "^8.2",
|
||||
"psr/http-message": "^1.0|^2.0",
|
||||
"psr/log": "^1.0|^2.0|^3.0",
|
||||
"ramsey/uuid": "^4.0",
|
||||
"symfony/console": "^6.0|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^6.0|^7.0|^8.0",
|
||||
"symfony/polyfill-php84": "^1.29"
|
||||
},
|
||||
"require-dev": {
|
||||
"aws/aws-sdk-php": "^3.349",
|
||||
"ext-pcntl": "*",
|
||||
"ext-pdo": "*",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"guzzlehttp/psr7": "^2.0",
|
||||
"laravel/horizon": "^5.4",
|
||||
"laravel/pint": "1.21.0",
|
||||
"laravel/vapor-core": "^2.38.2",
|
||||
"livewire/livewire": "^2.0|^3.0",
|
||||
"mockery/mockery": "^1.0",
|
||||
"mongodb/laravel-mongodb": "^4.0|^5.0",
|
||||
"orchestra/testbench": "^8.0|^9.0|^10.0",
|
||||
"orchestra/testbench-core": "^8.0|^9.0|^10.0",
|
||||
"orchestra/workbench": "^8.0|^9.0|^10.0",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpunit/phpunit": "^10.0|^11.0|^12.0",
|
||||
"singlestoredb/singlestoredb-laravel": "^1.0|^2.0",
|
||||
"spatie/laravel-ignition": "^2.0",
|
||||
"symfony/mailer": "^6.0|^7.0|^8.0",
|
||||
"symfony/mime": "^6.0|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.0|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Nightwatch": "Laravel\\Nightwatch\\Facades\\Nightwatch"
|
||||
},
|
||||
"providers": [
|
||||
"Laravel\\Nightwatch\\NightwatchServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"agent/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Laravel\\Nightwatch\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The official Laravel Nightwatch package.",
|
||||
"homepage": "https://nightwatch.laravel.com",
|
||||
"keywords": [
|
||||
"Insights",
|
||||
"laravel",
|
||||
"monitoring"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://nightwatch.laravel.com/docs",
|
||||
"issues": "https://github.com/laravel/nightwatch/issues",
|
||||
"source": "https://github.com/laravel/nightwatch"
|
||||
},
|
||||
"time": "2026-03-18T23:25:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pail",
|
||||
"version": "v1.2.6",
|
||||
|
|
@ -17209,5 +17303,5 @@
|
|||
"php": "^8.4"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Coolify'),
|
||||
'name' => env('APP_NAME', 'MapleDeploy'), // MapleDeploy branding
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// MapleDeploy branding: registry pointed to Forgejo, auto-update disabled by default
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.469',
|
||||
'version' => '4.0.0-beta.470',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.11',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
'autoupdate' => env('AUTOUPDATE', false),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
'registry_url' => env('REGISTRY_URL', 'ghcr.io'),
|
||||
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
|
||||
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'),
|
||||
'registry_url' => env('REGISTRY_URL', 'forgejo.mapledeploy.ca'),
|
||||
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'),
|
||||
'realtime_image' => env('REALTIME_IMAGE', 'ghcr.io/coollabsio/coolify-realtime'),
|
||||
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
||||
'cdn_url' => env('CDN_URL', 'https://cdn.coollabs.io'),
|
||||
'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/versions.json'),
|
||||
'upgrade_script_url' => env('UPGRADE_SCRIPT_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/upgrade.sh'),
|
||||
'cdn_url' => env('CDN_URL', 'https://updates.mapledeploy.ca'),
|
||||
'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://updates.mapledeploy.ca').'/coolify/versions.json'),
|
||||
'upgrade_script_url' => env('UPGRADE_SCRIPT_URL', env('CDN_URL', 'https://updates.mapledeploy.ca').'/coolify/upgrade.sh'),
|
||||
'releases_url' => 'https://cdn.coolify.io/releases.json',
|
||||
],
|
||||
|
||||
'urls' => [
|
||||
'docs' => 'https://coolify.io/docs',
|
||||
'contact' => 'https://coolify.io/docs/contact',
|
||||
'docs' => 'https://mapledeploy.ca/docs',
|
||||
'contact' => 'https://mapledeploy.ca/contact',
|
||||
],
|
||||
|
||||
'services' => [
|
||||
|
|
@ -55,6 +56,10 @@
|
|||
'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true),
|
||||
],
|
||||
|
||||
'nightwatch' => [
|
||||
'is_nightwatch_enabled' => env('NIGHTWATCH_ENABLED', false),
|
||||
],
|
||||
|
||||
'docker' => [
|
||||
'minimum_required_version' => '24.0',
|
||||
],
|
||||
|
|
@ -85,12 +90,12 @@
|
|||
'verification_code_expiry_minutes' => 10,
|
||||
],
|
||||
|
||||
// MapleDeploy branding: telemetry disabled
|
||||
'sentry' => [
|
||||
'sentry_dsn' => env('SENTRY_DSN'),
|
||||
'sentry_dsn' => null,
|
||||
],
|
||||
|
||||
'webhooks' => [
|
||||
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
|
||||
'dev_webhook' => env('SERVEO_URL'),
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@
|
|||
|
||||
'navigate' => [
|
||||
'show_progress_bar' => true,
|
||||
'progress_bar_color' => '#ffff00',
|
||||
'progress_bar_color' => '#fde047',
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/scheduled.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 1,
|
||||
'days' => 7,
|
||||
],
|
||||
|
||||
'scheduled-errors' => [
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public function up(): void
|
|||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_dns_validation_enabled')->default(true);
|
||||
$table->string('custom_dns_servers')->nullable()->default('1.1.1.1');
|
||||
$table->string('custom_dns_servers')->nullable()->default('149.112.121.10,149.112.122.10');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->string('setup_token')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('setup_token');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->string('setup_callback_url')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('setup_callback_url');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('local_persistent_volumes', function (Blueprint $table) {
|
||||
$table->string('uuid')->nullable()->after('id');
|
||||
});
|
||||
|
||||
DB::table('local_persistent_volumes')
|
||||
->whereNull('uuid')
|
||||
->orderBy('id')
|
||||
->chunk(1000, function ($volumes) {
|
||||
foreach ($volumes as $volume) {
|
||||
DB::table('local_persistent_volumes')
|
||||
->where('id', $volume->id)
|
||||
->update(['uuid' => (string) new Cuid2]);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('local_persistent_volumes', function (Blueprint $table) {
|
||||
$table->string('uuid')->nullable(false)->unique()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('local_persistent_volumes', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
coolify:
|
||||
image: "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}"
|
||||
image: "${REGISTRY_URL:-forgejo.mapledeploy.ca}/rosslh/coolify:${LATEST_IMAGE:-latest}"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /data/coolify/source/.env
|
||||
|
|
@ -60,7 +60,7 @@ services:
|
|||
retries: 10
|
||||
timeout: 2s
|
||||
soketi:
|
||||
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.11'
|
||||
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.11'
|
||||
ports:
|
||||
- "${SOKETI_PORT:-6001}:6001"
|
||||
- "6002:6002"
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#!/command/execlineb -P
|
||||
|
||||
# Use with-contenv to ensure environment variables are available
|
||||
with-contenv
|
||||
cd /var/www/html
|
||||
|
||||
foreground {
|
||||
php
|
||||
artisan
|
||||
start:nightwatch
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
longrun
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -116,6 +116,16 @@ COPY --chown=www-data:www-data composer.json composer.lock ./
|
|||
COPY --chown=www-data:www-data app ./app
|
||||
COPY --chown=www-data:www-data bootstrap ./bootstrap
|
||||
COPY --chown=www-data:www-data config ./config
|
||||
|
||||
# MapleDeploy: inject build version into constants.php at build time.
|
||||
# The version in git stays at the upstream value to avoid rebase conflicts.
|
||||
# CI passes the timestamped version (e.g. 4.0.0-beta.468.202603140006) as a build arg.
|
||||
ARG MAPLEDEPLOY_VERSION=""
|
||||
RUN if [ -n "$MAPLEDEPLOY_VERSION" ]; then \
|
||||
sed -i "s/'version' => '[^']*'/'version' => '$MAPLEDEPLOY_VERSION'/" config/constants.php && \
|
||||
chown www-data:www-data config/constants.php; \
|
||||
fi
|
||||
|
||||
COPY --chown=www-data:www-data database ./database
|
||||
COPY --chown=www-data:www-data lang ./lang
|
||||
COPY --chown=www-data:www-data public ./public
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
#!/command/execlineb -P
|
||||
|
||||
# Use with-contenv to ensure environment variables are available
|
||||
with-contenv
|
||||
cd /var/www/html
|
||||
foreground {
|
||||
php
|
||||
artisan
|
||||
start:nightwatch
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
longrun
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
829
openapi.json
829
openapi.json
|
|
@ -3502,6 +3502,105 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Create Storage",
|
||||
"description": "Create a persistent storage or file storage for an application.",
|
||||
"operationId": "create-storage-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"type",
|
||||
"mount_path"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"persistent",
|
||||
"file"
|
||||
],
|
||||
"description": "The type of storage."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Volume name (persistent only, required for persistent)."
|
||||
},
|
||||
"mount_path": {
|
||||
"type": "string",
|
||||
"description": "The container mount path."
|
||||
},
|
||||
"host_path": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The host path (persistent only, optional)."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "File content (file only, optional)."
|
||||
},
|
||||
"is_directory": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this is a directory mount (file only, default false)."
|
||||
},
|
||||
"fs_path": {
|
||||
"type": "string",
|
||||
"description": "Host directory path (required when is_directory is true)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Storage created.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
|
|
@ -3527,13 +3626,16 @@
|
|||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the storage (preferred)."
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "The ID of the storage."
|
||||
"description": "The ID of the storage (deprecated, use uuid instead)."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
|
|
@ -3603,6 +3705,70 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/storages\/{storage_uuid}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Delete Storage",
|
||||
"description": "Delete a persistent storage or file storage by application UUID.",
|
||||
"operationId": "delete-storage-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storage_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the storage.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Storage deleted.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/cloud-tokens": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -6513,6 +6679,333 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"\/databases\/{uuid}\/storages": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Databases"
|
||||
],
|
||||
"summary": "List Storages",
|
||||
"description": "List all persistent storages and file storages by database UUID.",
|
||||
"operationId": "list-storages-by-database-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the database.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All storages by database UUID.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"persistent_storages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"file_storages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Databases"
|
||||
],
|
||||
"summary": "Create Storage",
|
||||
"description": "Create a persistent storage or file storage for a database.",
|
||||
"operationId": "create-storage-by-database-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the database.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"type",
|
||||
"mount_path"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"persistent",
|
||||
"file"
|
||||
],
|
||||
"description": "The type of storage."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Volume name (persistent only, required for persistent)."
|
||||
},
|
||||
"mount_path": {
|
||||
"type": "string",
|
||||
"description": "The container mount path."
|
||||
},
|
||||
"host_path": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The host path (persistent only, optional)."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "File content (file only, optional)."
|
||||
},
|
||||
"is_directory": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this is a directory mount (file only, default false)."
|
||||
},
|
||||
"fs_path": {
|
||||
"type": "string",
|
||||
"description": "Host directory path (required when is_directory is true)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Storage created.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Databases"
|
||||
],
|
||||
"summary": "Update Storage",
|
||||
"description": "Update a persistent storage or file storage by database UUID.",
|
||||
"operationId": "update-storage-by-database-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the database.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Storage updated. For read-only storages (from docker-compose or services), only is_preview_suffix_enabled can be updated.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the storage (preferred)."
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "The ID of the storage (deprecated, use uuid instead)."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"persistent",
|
||||
"file"
|
||||
],
|
||||
"description": "The type of storage: persistent or file."
|
||||
},
|
||||
"is_preview_suffix_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to add -pr-N suffix for preview deployments."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The volume name (persistent only, not allowed for read-only storages)."
|
||||
},
|
||||
"mount_path": {
|
||||
"type": "string",
|
||||
"description": "The container mount path (not allowed for read-only storages)."
|
||||
},
|
||||
"host_path": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The host path (persistent only, not allowed for read-only storages)."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The file content (file only, not allowed for read-only storages)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Storage updated.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/databases\/{uuid}\/storages\/{storage_uuid}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Databases"
|
||||
],
|
||||
"summary": "Delete Storage",
|
||||
"description": "Delete a persistent storage or file storage by database UUID.",
|
||||
"operationId": "delete-storage-by-database-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the database.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storage_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the storage.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Storage deleted.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/deployments": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -11238,6 +11731,338 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"\/services\/{uuid}\/storages": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Services"
|
||||
],
|
||||
"summary": "List Storages",
|
||||
"description": "List all persistent storages and file storages by service UUID.",
|
||||
"operationId": "list-storages-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All storages by service UUID.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"persistent_storages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"file_storages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Services"
|
||||
],
|
||||
"summary": "Create Storage",
|
||||
"description": "Create a persistent storage or file storage for a service sub-resource.",
|
||||
"operationId": "create-storage-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"type",
|
||||
"mount_path",
|
||||
"resource_uuid"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"persistent",
|
||||
"file"
|
||||
],
|
||||
"description": "The type of storage."
|
||||
},
|
||||
"resource_uuid": {
|
||||
"type": "string",
|
||||
"description": "UUID of the service application or database sub-resource."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Volume name (persistent only, required for persistent)."
|
||||
},
|
||||
"mount_path": {
|
||||
"type": "string",
|
||||
"description": "The container mount path."
|
||||
},
|
||||
"host_path": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The host path (persistent only, optional)."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "File content (file only, optional)."
|
||||
},
|
||||
"is_directory": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this is a directory mount (file only, default false)."
|
||||
},
|
||||
"fs_path": {
|
||||
"type": "string",
|
||||
"description": "Host directory path (required when is_directory is true)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Storage created.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Services"
|
||||
],
|
||||
"summary": "Update Storage",
|
||||
"description": "Update a persistent storage or file storage by service UUID.",
|
||||
"operationId": "update-storage-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Storage updated. For read-only storages (from docker-compose or services), only is_preview_suffix_enabled can be updated.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the storage (preferred)."
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "The ID of the storage (deprecated, use uuid instead)."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"persistent",
|
||||
"file"
|
||||
],
|
||||
"description": "The type of storage: persistent or file."
|
||||
},
|
||||
"is_preview_suffix_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to add -pr-N suffix for preview deployments."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The volume name (persistent only, not allowed for read-only storages)."
|
||||
},
|
||||
"mount_path": {
|
||||
"type": "string",
|
||||
"description": "The container mount path (not allowed for read-only storages)."
|
||||
},
|
||||
"host_path": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The host path (persistent only, not allowed for read-only storages)."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The file content (file only, not allowed for read-only storages)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Storage updated.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/services\/{uuid}\/storages\/{storage_uuid}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Services"
|
||||
],
|
||||
"summary": "Delete Storage",
|
||||
"description": "Delete a persistent storage or file storage by service UUID.",
|
||||
"operationId": "delete-storage-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "storage_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the storage.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Storage deleted.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/teams": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
|
|||
545
openapi.yaml
545
openapi.yaml
|
|
@ -2204,6 +2204,73 @@ paths:
|
|||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
post:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Create Storage'
|
||||
description: 'Create a persistent storage or file storage for an application.'
|
||||
operationId: create-storage-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- type
|
||||
- mount_path
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [persistent, file]
|
||||
description: 'The type of storage.'
|
||||
name:
|
||||
type: string
|
||||
description: 'Volume name (persistent only, required for persistent).'
|
||||
mount_path:
|
||||
type: string
|
||||
description: 'The container mount path.'
|
||||
host_path:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The host path (persistent only, optional).'
|
||||
content:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'File content (file only, optional).'
|
||||
is_directory:
|
||||
type: boolean
|
||||
description: 'Whether this is a directory mount (file only, default false).'
|
||||
fs_path:
|
||||
type: string
|
||||
description: 'Host directory path (required when is_directory is true).'
|
||||
type: object
|
||||
additionalProperties: false
|
||||
responses:
|
||||
'201':
|
||||
description: 'Storage created.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
patch:
|
||||
tags:
|
||||
- Applications
|
||||
|
|
@ -2225,12 +2292,14 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
properties:
|
||||
uuid:
|
||||
type: string
|
||||
description: 'The UUID of the storage (preferred).'
|
||||
id:
|
||||
type: integer
|
||||
description: 'The ID of the storage.'
|
||||
description: 'The ID of the storage (deprecated, use uuid instead).'
|
||||
type:
|
||||
type: string
|
||||
enum: [persistent, file]
|
||||
|
|
@ -2272,6 +2341,48 @@ paths:
|
|||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/storages/{storage_uuid}':
|
||||
delete:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Delete Storage'
|
||||
description: 'Delete a persistent storage or file storage by application UUID.'
|
||||
operationId: delete-storage-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: storage_uuid
|
||||
in: path
|
||||
description: 'UUID of the storage.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Storage deleted.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/cloud-tokens:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -4209,6 +4320,219 @@ paths:
|
|||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/databases/{uuid}/storages':
|
||||
get:
|
||||
tags:
|
||||
- Databases
|
||||
summary: 'List Storages'
|
||||
description: 'List all persistent storages and file storages by database UUID.'
|
||||
operationId: list-storages-by-database-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the database.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'All storages by database UUID.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
persistent_storages: { type: array, items: { type: object } }
|
||||
file_storages: { type: array, items: { type: object } }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
post:
|
||||
tags:
|
||||
- Databases
|
||||
summary: 'Create Storage'
|
||||
description: 'Create a persistent storage or file storage for a database.'
|
||||
operationId: create-storage-by-database-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the database.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- type
|
||||
- mount_path
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [persistent, file]
|
||||
description: 'The type of storage.'
|
||||
name:
|
||||
type: string
|
||||
description: 'Volume name (persistent only, required for persistent).'
|
||||
mount_path:
|
||||
type: string
|
||||
description: 'The container mount path.'
|
||||
host_path:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The host path (persistent only, optional).'
|
||||
content:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'File content (file only, optional).'
|
||||
is_directory:
|
||||
type: boolean
|
||||
description: 'Whether this is a directory mount (file only, default false).'
|
||||
fs_path:
|
||||
type: string
|
||||
description: 'Host directory path (required when is_directory is true).'
|
||||
type: object
|
||||
additionalProperties: false
|
||||
responses:
|
||||
'201':
|
||||
description: 'Storage created.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
patch:
|
||||
tags:
|
||||
- Databases
|
||||
summary: 'Update Storage'
|
||||
description: 'Update a persistent storage or file storage by database UUID.'
|
||||
operationId: update-storage-by-database-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the database.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: 'Storage updated. For read-only storages (from docker-compose or services), only is_preview_suffix_enabled can be updated.'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- type
|
||||
properties:
|
||||
uuid:
|
||||
type: string
|
||||
description: 'The UUID of the storage (preferred).'
|
||||
id:
|
||||
type: integer
|
||||
description: 'The ID of the storage (deprecated, use uuid instead).'
|
||||
type:
|
||||
type: string
|
||||
enum: [persistent, file]
|
||||
description: 'The type of storage: persistent or file.'
|
||||
is_preview_suffix_enabled:
|
||||
type: boolean
|
||||
description: 'Whether to add -pr-N suffix for preview deployments.'
|
||||
name:
|
||||
type: string
|
||||
description: 'The volume name (persistent only, not allowed for read-only storages).'
|
||||
mount_path:
|
||||
type: string
|
||||
description: 'The container mount path (not allowed for read-only storages).'
|
||||
host_path:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The host path (persistent only, not allowed for read-only storages).'
|
||||
content:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The file content (file only, not allowed for read-only storages).'
|
||||
type: object
|
||||
additionalProperties: false
|
||||
responses:
|
||||
'200':
|
||||
description: 'Storage updated.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/databases/{uuid}/storages/{storage_uuid}':
|
||||
delete:
|
||||
tags:
|
||||
- Databases
|
||||
summary: 'Delete Storage'
|
||||
description: 'Delete a persistent storage or file storage by database UUID.'
|
||||
operationId: delete-storage-by-database-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the database.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: storage_uuid
|
||||
in: path
|
||||
description: 'UUID of the storage.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Storage deleted.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/deployments:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -7070,6 +7394,223 @@ paths:
|
|||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/services/{uuid}/storages':
|
||||
get:
|
||||
tags:
|
||||
- Services
|
||||
summary: 'List Storages'
|
||||
description: 'List all persistent storages and file storages by service UUID.'
|
||||
operationId: list-storages-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'All storages by service UUID.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
persistent_storages: { type: array, items: { type: object } }
|
||||
file_storages: { type: array, items: { type: object } }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
post:
|
||||
tags:
|
||||
- Services
|
||||
summary: 'Create Storage'
|
||||
description: 'Create a persistent storage or file storage for a service sub-resource.'
|
||||
operationId: create-storage-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- type
|
||||
- mount_path
|
||||
- resource_uuid
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [persistent, file]
|
||||
description: 'The type of storage.'
|
||||
resource_uuid:
|
||||
type: string
|
||||
description: 'UUID of the service application or database sub-resource.'
|
||||
name:
|
||||
type: string
|
||||
description: 'Volume name (persistent only, required for persistent).'
|
||||
mount_path:
|
||||
type: string
|
||||
description: 'The container mount path.'
|
||||
host_path:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The host path (persistent only, optional).'
|
||||
content:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'File content (file only, optional).'
|
||||
is_directory:
|
||||
type: boolean
|
||||
description: 'Whether this is a directory mount (file only, default false).'
|
||||
fs_path:
|
||||
type: string
|
||||
description: 'Host directory path (required when is_directory is true).'
|
||||
type: object
|
||||
additionalProperties: false
|
||||
responses:
|
||||
'201':
|
||||
description: 'Storage created.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
patch:
|
||||
tags:
|
||||
- Services
|
||||
summary: 'Update Storage'
|
||||
description: 'Update a persistent storage or file storage by service UUID.'
|
||||
operationId: update-storage-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: 'Storage updated. For read-only storages (from docker-compose or services), only is_preview_suffix_enabled can be updated.'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- type
|
||||
properties:
|
||||
uuid:
|
||||
type: string
|
||||
description: 'The UUID of the storage (preferred).'
|
||||
id:
|
||||
type: integer
|
||||
description: 'The ID of the storage (deprecated, use uuid instead).'
|
||||
type:
|
||||
type: string
|
||||
enum: [persistent, file]
|
||||
description: 'The type of storage: persistent or file.'
|
||||
is_preview_suffix_enabled:
|
||||
type: boolean
|
||||
description: 'Whether to add -pr-N suffix for preview deployments.'
|
||||
name:
|
||||
type: string
|
||||
description: 'The volume name (persistent only, not allowed for read-only storages).'
|
||||
mount_path:
|
||||
type: string
|
||||
description: 'The container mount path (not allowed for read-only storages).'
|
||||
host_path:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The host path (persistent only, not allowed for read-only storages).'
|
||||
content:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The file content (file only, not allowed for read-only storages).'
|
||||
type: object
|
||||
additionalProperties: false
|
||||
responses:
|
||||
'200':
|
||||
description: 'Storage updated.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/services/{uuid}/storages/{storage_uuid}':
|
||||
delete:
|
||||
tags:
|
||||
- Services
|
||||
summary: 'Delete Storage'
|
||||
description: 'Delete a persistent storage or file storage by service UUID.'
|
||||
operationId: delete-storage-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: storage_uuid
|
||||
in: path
|
||||
description: 'UUID of the storage.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Storage deleted.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/teams:
|
||||
get:
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ DATE=$(date +"%Y%m%d-%H%M%S")
|
|||
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
DOCKER_VERSION="27.0"
|
||||
DOCKER_VERSION="latest"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
|
||||
|
|
@ -499,13 +499,10 @@ fi
|
|||
|
||||
install_docker() {
|
||||
set +e
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1 || true
|
||||
curl -fsSL https://get.docker.com | sh 2>&1 || true
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo "Automated Docker installation failed. Trying manual installation."
|
||||
install_docker_manually
|
||||
fi
|
||||
echo "Automated Docker installation failed. Trying manual installation."
|
||||
install_docker_manually
|
||||
fi
|
||||
set -e
|
||||
}
|
||||
|
|
@ -548,16 +545,6 @@ if ! [ -x "$(command -v docker)" ]; then
|
|||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
getAJoke
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine" | "postmarketos")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
|
|
@ -569,8 +556,9 @@ if ! [ -x "$(command -v docker)" ]; then
|
|||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
pacman -Syu --noconfirm --needed docker docker-compose >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
systemctl start docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
|
|
@ -581,7 +569,7 @@ if ! [ -x "$(command -v docker)" ]; then
|
|||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
|
|
@ -591,34 +579,28 @@ if ! [ -x "$(command -v docker)" ]; then
|
|||
exit 1
|
||||
fi
|
||||
;;
|
||||
"centos" | "fedora" | "rhel" | "tencentos")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
"almalinux" | "tencentos")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"ubuntu" | "debian" | "raspbian")
|
||||
"ubuntu" | "debian" | "raspbian" | "centos" | "fedora" | "rhel" | "sles")
|
||||
install_docker
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Automated Docker installation failed. Trying manual installation."
|
||||
install_docker_manually
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
install_docker
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Automated Docker installation failed. Trying manual installation."
|
||||
install_docker_manually
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
|
@ -627,6 +609,19 @@ else
|
|||
echo " - Docker is installed."
|
||||
fi
|
||||
|
||||
# Verify minimum Docker version
|
||||
MIN_DOCKER_VERSION=24
|
||||
INSTALLED_DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1)
|
||||
if [ -z "$INSTALLED_DOCKER_VERSION" ]; then
|
||||
echo " - WARNING: Could not determine Docker version. Please ensure Docker $MIN_DOCKER_VERSION+ is installed."
|
||||
elif [ "$INSTALLED_DOCKER_VERSION" -lt "$MIN_DOCKER_VERSION" ]; then
|
||||
echo " - ERROR: Docker version $INSTALLED_DOCKER_VERSION is too old. Coolify requires Docker $MIN_DOCKER_VERSION or newer."
|
||||
echo " Please upgrade Docker: https://docs.docker.com/engine/install/"
|
||||
exit 1
|
||||
else
|
||||
echo " - Docker version $(docker version --format '{{.Server.Version}}' 2>/dev/null) meets minimum requirement ($MIN_DOCKER_VERSION+)."
|
||||
fi
|
||||
|
||||
log_section "Step 4/9: Checking Docker configuration"
|
||||
echo "4/9 Checking Docker configuration..."
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.469"
|
||||
"version": "4.0.0-beta.470"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0"
|
||||
|
|
@ -13,17 +13,17 @@
|
|||
"version": "1.0.11"
|
||||
},
|
||||
"sentinel": {
|
||||
"version": "0.0.19"
|
||||
"version": "0.0.21"
|
||||
}
|
||||
},
|
||||
"traefik": {
|
||||
"v3.6": "3.6.5",
|
||||
"v3.6": "3.6.11",
|
||||
"v3.5": "3.5.6",
|
||||
"v3.4": "3.4.5",
|
||||
"v3.3": "3.3.7",
|
||||
"v3.2": "3.2.5",
|
||||
"v3.1": "3.1.7",
|
||||
"v3.0": "3.0.4",
|
||||
"v2.11": "2.11.32"
|
||||
"v2.11": "2.11.40"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
|
||||
<env name="SESSION_DRIVER" value="array" force="true"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
|
|
|
|||
BIN
public/mapledeploy-favicon.ico
Normal file
BIN
public/mapledeploy-favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
82
public/svgs/espocrm.svg
Normal file
82
public/svgs/espocrm.svg
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="379.36536"
|
||||
height="83.256203"
|
||||
enable-background="new 0 0 307.813 75"
|
||||
overflow="visible"
|
||||
version="1.1"
|
||||
viewBox="0 0 303.49228 66.604962"
|
||||
xml:space="preserve"
|
||||
id="svg20"
|
||||
sodipodi:docname="logo2.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs24" /><sodipodi:namedview
|
||||
id="namedview22"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
scale-x="0.8"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="2.172956"
|
||||
inkscape:cx="109.75832"
|
||||
inkscape:cy="79.384949"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1074"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg20" />
|
||||
|
||||
<switch
|
||||
transform="matrix(1.089,0,0,1.089,-14.949525,-4.9304545)"
|
||||
id="switch18">
|
||||
<foreignObject
|
||||
width="1"
|
||||
height="1"
|
||||
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
|
||||
</foreignObject>
|
||||
<g
|
||||
transform="matrix(0.96767,0,0,0.96767,3.9659,-1.2011)"
|
||||
id="g16">
|
||||
|
||||
<path
|
||||
d="m 169.53,21.864 c -7.453,2.972 -9.569,11.987 -9.005,19.212 1.587,2.982 3.845,5.562 5.783,8.312 l 4.262,-1.083 c -1.796,-4.447 -1.689,-9.424 -0.806,-14.066 0.585,-3.001 2.309,-6.476 5.634,-7.032 5.307,-0.847 10.733,-0.271 16.088,-0.369 0.091,-2.196 0.115,-4.392 0.107,-6.585 -7.333,0.387 -15.043,-1.038 -22.063,1.611 z m 52.714,-1.294 c -8.12,-0.952 -16.332,-0.149 -24.492,-0.387 -0.021,6.43 -0.003,12.854 0.078,19.274 2.625,-0.849 5.251,-1.739 7.909,-2.532 0.042,-3.272 0.028,-6.527 -0.071,-9.789 4.869,-0.029 9.874,-0.757 14.639,0.451 1.838,0.298 2.051,2.25 2.687,3.641 2.541,-0.891 5.111,-1.717 7.672,-2.574 -0.703,-4.246 -4.129,-7.633 -8.422,-8.084 z m 23.522,-0.593 c -3.954,0.072 -7.912,0.064 -11.864,0.047 0.051,2.544 0.063,5.074 0.072,7.617 4.263,-1.482 8.553,-2.889 12.848,-4.268 -0.35,-1.128 -0.706,-2.268 -1.056,-3.396 z"
|
||||
fill="#6a3201"
|
||||
id="path2" />
|
||||
<path
|
||||
d="m 161.96,69.125 c 7.886,-3.717 15.757,-7.463 23.72,-11.018 5.563,0.359 11.146,0.021 16.722,0.193 1.14,-0.036 2.292,-0.061 3.432,-0.088 -0.011,-3.195 -0.025,-6.38 -0.082,-9.564 3.428,-1.502 10.227,-4.623 10.227,-4.623 l 15.215,13.941 11.096,0.106 -0.715,-26.236 0.803,-0.211 9.005,26.344 8.834,-0.066 8.99,-28.394 -0.308,28.434 8.074,-0.021 -0.231,-37.932 -9.279,0.071 30.625,-14.141 c 0,0 -37.593,14.279 -56.404,21.385 -2.996,1.022 -5.878,2.315 -8.853,3.394 -2.278,0.867 -4.558,1.713 -6.834,2.58 -20.071,7.526 -39.945,15.604 -60.126,22.803 C 159.094,45.56 150.557,36.228 144.103,25.497 Z m 72.116,-17.961 c -0.108,0.154 -0.324,0.458 -0.429,0.611 -3.448,-3.018 -6.765,-6.189 -10.21,-9.205 1.745,-1.096 3.47,-2.242 5.026,-3.597 1.625,-1.386 3.479,-2.469 5.345,-3.499 0.293,5.227 0.258,10.452 0.268,15.69 z m 23.942,-9.67 c -0.857,2.578 -1.825,5.137 -2.793,7.682 -1.644,-6.217 -3.94,-12.238 -5.856,-18.383 -0.119,-0.52 -0.366,-1.574 -0.487,-2.093 3.428,-1.709 10.585,-4.854 15.229,-6.815 -1.647,5.969 -4.306,14.029 -6.093,19.609 z"
|
||||
fill="#ffb300"
|
||||
id="path4" />
|
||||
|
||||
<g
|
||||
fill="#6a3201"
|
||||
id="g14">
|
||||
<path
|
||||
d="M 45.672,58.148 H 27.146 c -2.861,0 -5.614,-0.651 -8.257,-1.953 -2.861,-1.409 -5.043,-3.651 -6.547,-6.725 -1.503,-3.074 -2.254,-6.455 -2.254,-10.145 0,-3.652 0.724,-6.961 2.173,-9.926 1.594,-3.219 3.803,-5.569 6.628,-7.052 1.557,-0.795 3.052,-1.355 4.482,-1.682 1.43,-0.325 3.07,-0.488 4.917,-0.488 h 17.168 v 6.789 H 29.57 c -1.415,0 -2.602,0.187 -3.563,0.558 -0.961,0.372 -1.912,1.037 -2.855,1.994 -0.943,0.957 -1.597,1.887 -1.959,2.791 -0.363,0.902 -0.543,2.027 -0.543,3.375 h 25.023 v 6.789 H 20.648 c 0,1.24 0.164,2.325 0.491,3.256 0.327,0.93 0.919,1.887 1.776,2.871 0.856,0.985 1.749,1.732 2.677,2.242 0.929,0.512 2.03,0.767 3.306,0.767 h 16.774 z"
|
||||
id="path6" />
|
||||
<path
|
||||
d="m 76.499,49.519 c 0,2.397 -0.771,4.449 -2.312,6.154 -1.541,1.706 -3.49,2.56 -5.846,2.56 H 49.688 V 53.12 h 15.326 c 1.087,0 2.001,-0.272 2.744,-0.817 0.743,-0.545 1.115,-1.327 1.115,-2.345 0,-2.362 -1.595,-3.543 -4.783,-3.543 h -7.825 c -1.666,0 -3.278,-0.79 -4.836,-2.369 -1.559,-1.58 -2.336,-3.287 -2.336,-5.119 0,-2.585 0.579,-4.667 1.738,-6.248 1.34,-1.794 3.313,-2.692 5.922,-2.692 h 17.928 v 5.364 H 58.743 c -0.614,0 -1.147,0.289 -1.599,0.868 -0.452,0.579 -0.677,1.235 -0.677,1.972 0,0.807 0.298,1.498 0.896,2.076 0.597,0.579 1.311,0.867 2.144,0.867 h 8.415 c 2.643,0 4.733,0.79 6.271,2.369 1.536,1.579 2.306,3.584 2.306,6.016 z"
|
||||
id="path8" />
|
||||
<path
|
||||
d="m 109.29,43.414 c 0,4.495 -1.166,8.074 -3.497,10.738 -2.331,2.664 -5.395,3.996 -9.188,3.996 H 88.419 V 68.457 H 80.792 V 29.985 h 15.09 c 4.27,0 7.6,1.269 9.989,3.806 2.279,2.428 3.419,5.637 3.419,9.623 z m -7.627,0.405 c 0,-2.356 -0.754,-4.286 -2.262,-5.793 -1.509,-1.505 -3.388,-2.258 -5.641,-2.258 h -5.341 v 16.429 h 5.886 c 2.179,0 3.951,-0.771 5.313,-2.313 1.363,-1.54 2.045,-3.562 2.045,-6.065 z"
|
||||
id="path10" />
|
||||
<path
|
||||
d="m 145.1,43.967 c 0,4.896 -1.557,8.65 -4.669,11.261 -2.86,2.394 -6.751,3.591 -11.673,3.591 -4.923,0 -8.742,-1.087 -11.456,-3.264 -3.15,-2.502 -4.724,-6.401 -4.724,-11.696 0,-4.424 1.701,-7.906 5.104,-10.446 3.04,-2.283 6.786,-3.427 11.238,-3.427 4.887,0 8.805,1.225 11.754,3.673 2.949,2.448 4.426,5.884 4.426,10.308 z m -8.382,-0.065 c 0,-2.285 -0.716,-4.197 -2.146,-5.738 -1.432,-1.54 -3.379,-2.312 -5.841,-2.312 -2.246,0 -4.103,0.79 -5.57,2.366 -1.467,1.577 -2.2,3.563 -2.2,5.955 0,2.756 0.743,4.949 2.228,6.581 1.485,1.632 3.405,2.448 5.76,2.448 2.679,0 4.673,-0.852 5.977,-2.557 1.193,-1.557 1.792,-3.805 1.792,-6.743 z"
|
||||
id="path12" />
|
||||
</g>
|
||||
</g>
|
||||
</switch>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
|
@ -13,33 +13,35 @@
|
|||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* MapleDeploy branding: Canadian red accent, stone greys */
|
||||
@theme {
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-display: 'Overlock', sans-serif;
|
||||
|
||||
--color-base: #101010;
|
||||
--color-warning: #fcd452;
|
||||
--color-base: #1c1917;
|
||||
--color-warning: #fde047;
|
||||
--color-warning-50: #fefce8;
|
||||
--color-warning-100: #fef9c3;
|
||||
--color-warning-200: #fef08a;
|
||||
--color-warning-300: #fde047;
|
||||
--color-warning-400: #fcd452;
|
||||
--color-warning-500: #facc15;
|
||||
--color-warning-400: #facc15;
|
||||
--color-warning-500: #eab308;
|
||||
--color-warning-600: #ca8a04;
|
||||
--color-warning-700: #a16207;
|
||||
--color-warning-800: #854d0e;
|
||||
--color-warning-900: #713f12;
|
||||
--color-success: #22C55E;
|
||||
--color-error: #dc2626;
|
||||
--color-coollabs-50: #f5f0ff;
|
||||
--color-coollabs: #6b16ed;
|
||||
--color-coollabs-100: #7317ff;
|
||||
--color-coollabs-200: #5a12c7;
|
||||
--color-coollabs-300: #4a0fa3;
|
||||
--color-coolgray-100: #181818;
|
||||
--color-coolgray-200: #202020;
|
||||
--color-coolgray-300: #242424;
|
||||
--color-coolgray-400: #282828;
|
||||
--color-coolgray-500: #323232;
|
||||
--color-coollabs-50: #fef3f2;
|
||||
--color-coollabs: #d52b1e;
|
||||
--color-coollabs-100: #f34d40;
|
||||
--color-coollabs-200: #bc2519;
|
||||
--color-coollabs-300: #9c2118;
|
||||
--color-coolgray-100: #1c1917;
|
||||
--color-coolgray-200: #292524;
|
||||
--color-coolgray-300: #44403c;
|
||||
--color-coolgray-400: #57534e;
|
||||
--color-coolgray-500: #78716c;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -122,19 +124,19 @@ button[isHighlighted]:not(:disabled) {
|
|||
}
|
||||
|
||||
h1 {
|
||||
@apply text-3xl font-bold dark:text-white;
|
||||
@apply text-3xl font-bold font-display dark:text-white;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-xl font-bold dark:text-white;
|
||||
@apply text-xl font-bold font-display dark:text-white;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-lg font-bold dark:text-white;
|
||||
@apply text-lg font-bold font-display dark:text-white;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-base font-bold dark:text-white;
|
||||
@apply text-base font-bold font-display dark:text-white;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
|||
|
|
@ -70,3 +70,12 @@ @font-face {
|
|||
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* MapleDeploy branding: Overlock for headings */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Overlock';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url('../fonts/overlock-v19-latin-900.woff2') format('woff2'),
|
||||
url('../fonts/overlock-v19-latin-900.ttf') format('truetype');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ @utility input-sticky {
|
|||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 1px #e5e5e5;
|
||||
box-shadow: inset 4px 0 0 #d52b1e, inset 0 0 0 1px #e5e5e5;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 1px #242424;
|
||||
box-shadow: inset 4px 0 0 #fde047, inset 0 0 0 1px #242424;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,11 +82,11 @@ @utility input {
|
|||
@apply focus-visible:outline-none;
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;
|
||||
box-shadow: inset 4px 0 0 #d52b1e, inset 0 0 0 2px #e5e5e5;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424;
|
||||
box-shadow: inset 4px 0 0 #fde047, inset 0 0 0 2px #242424;
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
|
|
@ -113,11 +113,11 @@ @utility select {
|
|||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;
|
||||
box-shadow: inset 4px 0 0 #d52b1e, inset 0 0 0 2px #e5e5e5;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424;
|
||||
box-shadow: inset 4px 0 0 #fde047, inset 0 0 0 2px #242424;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -209,7 +209,8 @@ @utility icon {
|
|||
}
|
||||
|
||||
@utility scrollbar {
|
||||
@apply scrollbar-thumb-coollabs-100 scrollbar-track-neutral-200 dark:scrollbar-track-coolgray-200 scrollbar-thin;
|
||||
/* MapleDeploy branding: yellow scrollbar thumb instead of Coolify red */
|
||||
@apply scrollbar-thumb-warning scrollbar-track-neutral-200 dark:scrollbar-track-coolgray-200 scrollbar-thin;
|
||||
}
|
||||
|
||||
@utility main {
|
||||
|
|
@ -269,7 +270,8 @@ @utility description {
|
|||
}
|
||||
|
||||
@utility bg-coollabs-gradient {
|
||||
@apply from-purple-500 via-pink-500 to-red-500 bg-linear-to-r;
|
||||
/* MapleDeploy branding */
|
||||
@apply from-red-700 via-red-500 to-red-400 bg-linear-to-r;
|
||||
}
|
||||
|
||||
@utility text-helper {
|
||||
|
|
@ -333,7 +335,7 @@ @utility log-warning {
|
|||
}
|
||||
|
||||
@utility log-debug {
|
||||
@apply bg-purple-500/10 dark:bg-purple-500/15;
|
||||
@apply bg-stone-500/10 dark:bg-stone-500/15;
|
||||
}
|
||||
|
||||
@utility log-info {
|
||||
|
|
|
|||
BIN
resources/fonts/overlock-v19-latin-900.ttf
Normal file
BIN
resources/fonts/overlock-v19-latin-900.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/overlock-v19-latin-900.woff2
Normal file
BIN
resources/fonts/overlock-v19-latin-900.woff2
Normal file
Binary file not shown.
|
|
@ -3,9 +3,10 @@
|
|||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
||||
Coolify
|
||||
</h1>
|
||||
<div class="flex justify-center">
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
|
||||
</div>
|
||||
<p class="text-lg dark:text-neutral-400">
|
||||
Confirm Your Password
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
||||
Coolify
|
||||
</h1>
|
||||
<div class="flex justify-center">
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
|
||||
</div>
|
||||
<p class="text-lg dark:text-neutral-400">
|
||||
{{ __('auth.forgot_password_heading') }}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,33 @@
|
|||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
||||
Coolify
|
||||
</h1>
|
||||
<div class="flex justify-center">
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if (!empty($setup_pending))
|
||||
{{-- MapleDeploy: setup token required but not provided --}}
|
||||
<div class="mb-6 p-4 bg-warning/10 border border-warning rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<svg class="size-5 text-warning flex-shrink-0 mt-0.5" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-bold text-warning">Setup pending</p>
|
||||
<p class="text-sm dark:text-white text-black">
|
||||
Initial setup has not been completed. Please use the setup link from your
|
||||
<a href="https://app.mapledeploy.ca" class="underline hover:text-warning">MapleDeploy dashboard</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@if (session('status'))
|
||||
<div class="mb-6 p-4 bg-success/10 border border-success rounded-lg">
|
||||
<p class="text-sm text-success">{{ session('status') }}</p>
|
||||
|
|
@ -95,6 +116,7 @@ class="block w-full text-center py-3 px-4 rounded-lg border border-neutral-300 d
|
|||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endif {{-- end setup_pending --}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ function getOldOrLocal($key, $localValue)
|
|||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
||||
Coolify
|
||||
</h1>
|
||||
<div class="flex justify-center">
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
|
||||
</div>
|
||||
<p class="text-lg dark:text-neutral-400">
|
||||
Create your account
|
||||
</p>
|
||||
|
|
@ -52,6 +53,9 @@ function getOldOrLocal($key, $localValue)
|
|||
|
||||
<form action="/register" method="POST" class="flex flex-col gap-4">
|
||||
@csrf
|
||||
@if (isset($setupToken))
|
||||
<input type="hidden" name="setup_token" value="{{ $setupToken }}" />
|
||||
@endif
|
||||
<x-forms.input id="name" required type="text" name="name" value="{{ $name }}"
|
||||
label="{{ __('input.name') }}" />
|
||||
<x-forms.input id="email" required type="email" name="email" value="{{ $email }}"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
||||
Coolify
|
||||
</h1>
|
||||
<div class="flex justify-center">
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
|
||||
</div>
|
||||
<p class="text-lg dark:text-neutral-400">
|
||||
{{ __('auth.reset_password') }}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@
|
|||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
||||
Coolify
|
||||
</h1>
|
||||
<div class="flex justify-center">
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80" alt="MapleDeploy" class="h-12 dark:hidden" />
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=80&dark=true" alt="MapleDeploy" class="hidden h-12 dark:block" />
|
||||
</div>
|
||||
<p class="text-lg dark:text-neutral-400">
|
||||
Two-Factor Authentication
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
{!! $icon !!}
|
||||
</div>
|
||||
<div class="ml-3 {{ $dismissible ? 'pr-8' : '' }}">
|
||||
<div class="text-base font-bold {{ $colorScheme['title'] }}">
|
||||
<div class="text-base font-bold font-display {{ $colorScheme['title'] }}">
|
||||
{{ $title }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm {{ $colorScheme['text'] }}">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{{ Illuminate\Mail\Markdown::parse('---') }}
|
||||
|
||||
Thank you,<br>
|
||||
{{ config('app.name') ?? 'Coolify' }}
|
||||
{{ config('app.name') ?? 'MapleDeploy' }}
|
||||
|
||||
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://coolify.io/docs/contact)') }}
|
||||
{{-- MapleDeploy branding: support link --}}
|
||||
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://mapledeploy.ca/contact)') }}
|
||||
|
|
|
|||
|
|
@ -99,9 +99,10 @@
|
|||
{{-- Unified Input Container with Tags Inside --}}
|
||||
<div @click="$refs.searchInput.focus()" x-data="{ focused: false }" @focusin="focused = true" @focusout="focused = false"
|
||||
class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-full text-sm rounded-sm border-0 bg-white dark:bg-coolgray-100 cursor-text px-1 text-black dark:text-white"
|
||||
{{-- MapleDeploy branding: red accent instead of Coolify purple --}}
|
||||
:style="(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const accent = isDark ? '#fcd452' : '#6b16ed';
|
||||
const accent = isDark ? '#fde047' : '#d52b1e';
|
||||
const border = isDark ? '#242424' : '#e5e5e5';
|
||||
return focused
|
||||
? 'box-shadow: inset 4px 0 0 ' + accent + ', inset 0 0 0 2px ' + border + ';'
|
||||
|
|
@ -110,7 +111,7 @@ class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-
|
|||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" wire:loading.class="opacity-50"
|
||||
wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]">
|
||||
wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]">
|
||||
|
||||
{{-- Selected Tags Inside Input --}}
|
||||
<template x-for="value in selected" :key="value">
|
||||
|
|
@ -231,9 +232,10 @@ class="w-4 h-4 rounded border-neutral-300 dark:border-neutral-600 bg-white dark:
|
|||
{{-- Input Container --}}
|
||||
<div @click="openDropdown()" x-data="{ focused: false }" @focusin="focused = true" @focusout="focused = false"
|
||||
class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 bg-white dark:bg-coolgray-100 cursor-text text-black dark:text-white"
|
||||
{{-- MapleDeploy branding: red accent instead of Coolify purple --}}
|
||||
:style="(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const accent = isDark ? '#fcd452' : '#6b16ed';
|
||||
const accent = isDark ? '#fde047' : '#d52b1e';
|
||||
const border = isDark ? '#242424' : '#e5e5e5';
|
||||
return focused
|
||||
? 'box-shadow: inset 4px 0 0 ' + accent + ', inset 0 0 0 2px ' + border + ';'
|
||||
|
|
@ -241,7 +243,7 @@ class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 bg-whit
|
|||
})()"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" wire:loading.class="opacity-50" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]">
|
||||
}" wire:loading.class="opacity-50" wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]">
|
||||
|
||||
{{-- Display Selected Value or Search Input --}}
|
||||
<div class="flex-1 flex items-center min-w-0 px-1">
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
|
|||
@endif
|
||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
|
|
@ -47,7 +47,7 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
|
|||
@else
|
||||
<input autocomplete="{{ $autocomplete }}" @if ($value) value="{{ $value }}" @endif
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}"
|
||||
max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class="flex gap-1 items-center mb-1 text-sm font-medium {{ $disabled ? 'text-neu
|
|||
@endif
|
||||
<select {{ $attributes->merge(['class' => $defaultClass]) }} @disabled($disabled) @required($required)
|
||||
wire:loading.attr="disabled" name={{ $modelBinding }} id="{{ $htmlId }}"
|
||||
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @else wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif>
|
||||
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @else wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif>
|
||||
{{ $slot }}
|
||||
</select>
|
||||
@error($modelBinding)
|
||||
|
|
|
|||
|
|
@ -45,16 +45,16 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer
|
|||
@endif
|
||||
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
|
||||
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]"
|
||||
@else
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" name={{ $modelBinding }}
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||
|
|
@ -64,9 +64,9 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer
|
|||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}"
|
||||
{{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}"
|
||||
{{ !$spellcheck ? 'spellcheck=false' : '' }} {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]"
|
||||
@else
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1e,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" name={{ $modelBinding }}
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="flex flex-col items-center justify-center h-32">
|
||||
<span class="text-xl font-bold dark:text-white">You have reached the limit of {{ $name }} you can create.</span>
|
||||
<span class="text-xl font-bold font-display dark:text-white">You have reached the limit of {{ $name }} you can create.</span>
|
||||
<span>Please <a class="dark:text-white underline" {{ wireNavigate() }} href="{{ route('subscription.show') }}">upgrade your
|
||||
subscription</a> to create more
|
||||
{{ $name }}.</span>
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ class="relative w-auto h-auto">
|
|||
@endif
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 left-0 z-99 flex items-center justify-center w-screen h-screen p-4" x-cloak>
|
||||
class="fixed top-0 left-0 z-99 flex items-center justify-center w-screen h-screen p-0 sm:p-4" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs">
|
||||
</div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
|
|
@ -199,7 +199,7 @@ class="fixed top-0 left-0 z-99 flex items-center justify-center w-screen h-scree
|
|||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] max-h-[calc(100vh-2rem)] bg-neutral-100 border-neutral-400 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||
class="relative w-full border rounded-none sm:rounded-sm min-w-full lg:min-w-[36rem] max-w-full sm:max-w-[48rem] h-screen sm:h-auto max-h-screen sm:max-h-[calc(100vh-2rem)] bg-neutral-100 border-neutral-400 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||
<div class="flex justify-between items-center py-6 px-7 shrink-0">
|
||||
<h3 class="pr-8 text-2xl font-bold">{{ $title }}</h3>
|
||||
<button @click="modalOpen = false; resetModal()"
|
||||
|
|
|
|||
|
|
@ -77,10 +77,14 @@
|
|||
}
|
||||
}
|
||||
}">
|
||||
<div class="flex lg:pt-6 pt-4 pb-4 pl-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<a href="/" {{ wireNavigate() }} class="text-2xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<x-version />
|
||||
<div class="flex lg:pt-6 pt-4 pb-4 pl-2 gap-1">
|
||||
{{-- MapleDeploy branding --}}
|
||||
<div class="flex flex-col min-w-0 flex-1">
|
||||
<a href="/" {{ wireNavigate() }} class="hover:opacity-80 transition-opacity">
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=40" alt="MapleDeploy" class="max-h-6 w-auto max-w-full dark:hidden" />
|
||||
<img src="https://mapledeploy.ca/api/logo/lockup?height=40&dark=true" alt="MapleDeploy" class="hidden max-h-6 w-auto max-w-full dark:block" />
|
||||
</a>
|
||||
<span class="text-xs opacity-75 dark:text-neutral-400">Powered by Coolify</span>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Search button that triggers global search modal -->
|
||||
|
|
@ -291,20 +295,7 @@ class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}
|
|||
<span class="menu-item-label">Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
@if (isCloud() && auth()->user()->isAdmin())
|
||||
<li>
|
||||
<a title="Subscription" {{ wireNavigate() }}
|
||||
class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('subscription.show') }}">
|
||||
<svg class="menu-item-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm0 2h18M7 15h.01M11 15h2" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Subscription</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
{{-- MapleDeploy branding: Cloud subscription menu removed --}}
|
||||
@if (isInstanceAdmin())
|
||||
<li>
|
||||
|
||||
|
|
@ -324,20 +315,7 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item
|
|||
</li>
|
||||
@endif
|
||||
|
||||
@if (isCloud() || isDev())
|
||||
@if (isInstanceAdmin() || session('impersonating'))
|
||||
<li>
|
||||
<a title="Admin" class="menu-item" href="/admin" {{ wireNavigate() }}>
|
||||
<svg class="text-pink-500 menu-item-icon" viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M177.62 159.6a52 52 0 0 1-34 34a12.2 12.2 0 0 1-3.6.55a12 12 0 0 1-3.6-23.45a28 28 0 0 0 18.32-18.32a12 12 0 0 1 22.9 7.2ZM220 144a92 92 0 0 1-184 0c0-28.81 11.27-58.18 33.48-87.28a12 12 0 0 1 17.9-1.33l19.69 19.11L127 19.89a12 12 0 0 1 18.94-5.12C168.2 33.25 220 82.85 220 144m-24 0c0-41.71-30.61-78.39-52.52-99.29l-20.21 55.4a12 12 0 0 1-19.63 4.5L80.71 82.36C67 103.38 60 124.06 60 144a68 68 0 0 0 136 0" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Admin</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
{{-- MapleDeploy branding: Cloud admin menu removed --}}
|
||||
<div class="flex-1"></div>
|
||||
@if (isInstanceAdmin() && !isCloud())
|
||||
@persist('upgrade')
|
||||
|
|
@ -357,39 +335,19 @@ class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-it
|
|||
Onboarding
|
||||
</a>
|
||||
</li> --}}
|
||||
{{-- MapleDeploy branding: AGPL source code link (license requirement) --}}
|
||||
<li>
|
||||
<a title="Sponsor us" class="menu-item" href="https://coolify.io/sponsorships"
|
||||
<a title="Source code (AGPL-3.0)" class="menu-item" href="https://forgejo.mapledeploy.ca/rosslh/coolify"
|
||||
target="_blank">
|
||||
<svg class="text-pink-500 menu-item-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M19.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
|
||||
<path
|
||||
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
|
||||
</g>
|
||||
<svg class="menu-item-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M16 18l6-6-6-6M8 6l-6 6 6 6" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Sponsor us</span>
|
||||
<span class="menu-item-label">Source code</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (!isSubscribed() && isCloud() && auth()->user()->teams()->get()->count() > 1)
|
||||
<livewire:navbar-delete-team />
|
||||
@endif
|
||||
<li>
|
||||
<x-modal-input title="How can we help?">
|
||||
<x-slot:content>
|
||||
<div title="Send us feedback or get help!" class="cursor-pointer menu-item"
|
||||
wire:click="help">
|
||||
<svg class="menu-item-icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Feedback</span>
|
||||
</div>
|
||||
</x-slot:content>
|
||||
<livewire:help />
|
||||
</x-modal-input>
|
||||
</li>
|
||||
<li>
|
||||
<form action="/logout" method="POST">
|
||||
@csrf
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue