Merge branch 'next' into next
This commit is contained in:
@@ -6,13 +6,7 @@ APP_KEY=
|
||||
APP_URL=http://localhost
|
||||
APP_PORT=8000
|
||||
APP_DEBUG=true
|
||||
MUX_ENABLED=false
|
||||
|
||||
# Enable Laravel Telescope for debugging
|
||||
TELESCOPE_ENABLED=false
|
||||
|
||||
# Selenium Driver URL for Dusk
|
||||
DUSK_DRIVER_URL=http://selenium:4444
|
||||
SSH_MUX_ENABLED=true
|
||||
|
||||
# PostgreSQL Database Configuration
|
||||
DB_DATABASE=coolify
|
||||
@@ -25,7 +19,13 @@ DB_PORT=5432
|
||||
# Set to true to enable Ray
|
||||
RAY_ENABLED=false
|
||||
# Set custom ray port
|
||||
RAY_PORT=
|
||||
# RAY_PORT=
|
||||
|
||||
# Enable Laravel Telescope for debugging
|
||||
TELESCOPE_ENABLED=false
|
||||
|
||||
# Selenium Driver URL for Dusk
|
||||
DUSK_DRIVER_URL=http://selenium:4444
|
||||
|
||||
# Special Keys for Andras
|
||||
# For cache purging
|
||||
|
||||
75
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
75
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
@@ -1,46 +1,65 @@
|
||||
name: Bug report
|
||||
description: "Create a new bug report."
|
||||
name: 🐞 Bug Report
|
||||
description: "File a new bug report."
|
||||
title: "[Bug]: "
|
||||
labels: ["🐛 Bug", "🔍 Triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >-
|
||||
# 💎 Bounty program (with
|
||||
[algora.io](https://console.algora.io/org/coollabsio/bounties/new))
|
||||
value: |
|
||||
> [!IMPORTANT]
|
||||
> **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.)
|
||||
|
||||
# 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
|
||||
- If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new).
|
||||
|
||||
If you would like to prioritize the issue resolution, you can add bounty
|
||||
to this issue.
|
||||
|
||||
|
||||
Click [here](https://console.algora.io/org/coollabsio/bounties/new) to
|
||||
get started.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of the problem
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Minimal Reproduction (if possible, example repository)
|
||||
description: Please provide a step by step guide to reproduce the issue.
|
||||
label: Error Message and Logs
|
||||
description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Exception or Error
|
||||
description: Please provide error logs if possible.
|
||||
label: Steps to Reproduce
|
||||
description: Please provide a step-by-step guide to reproduce the issue. Be as detailed as possible, otherwise we may not be able to assist you.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Coolify's version (see top of your screen).
|
||||
label: Example Repository URL
|
||||
description: If applicable, provide a URL to a repository demonstrating the issue.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Coolify Version
|
||||
description: Please provide the Coolify version you are using. This can be found in the top left corner of your Coolify dashboard.
|
||||
placeholder: "v4.0.0-beta.335"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Cloud?
|
||||
description: "Are you using the cloud version of Coolify?"
|
||||
label: Are you using Coolify Cloud?
|
||||
options:
|
||||
- label: 'Yes'
|
||||
required: false
|
||||
- label: 'No'
|
||||
required: false
|
||||
- "No (self-hosted)"
|
||||
- "Yes (Coolify Cloud)"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System and Version (self-hosted)
|
||||
description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version.
|
||||
placeholder: "Ubuntu 22.04"
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other relevant details about the issue.
|
||||
|
||||
31
.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: 💎 Enhancement Bounty
|
||||
description: "Propose a new feature, service, or improvement with an attached bounty."
|
||||
title: "[Enhancement]: "
|
||||
labels: ["✨ Enhancement", "🔍 Triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> [!IMPORTANT]
|
||||
> **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions).
|
||||
|
||||
# 💎 Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
|
||||
- [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new)
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Request Type
|
||||
description: Select the type of request you are making.
|
||||
options:
|
||||
- New Feature
|
||||
- New Service
|
||||
- Improvement
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Provide a detailed description of the feature, improvement, or service you are proposing.
|
||||
validations:
|
||||
required: true
|
||||
20
.github/ISSUE_TEMPLATE/config.yml
vendored
20
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,18 @@
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: 🤔 Community Support (Chat)
|
||||
- name: 🤔 Questions and Community Support
|
||||
url: https://coollabs.io/discord
|
||||
about: Reach out to us on Discord.
|
||||
- name: 🙋♂️ Feature Requests
|
||||
url: https://github.com/coollabsio/coolify/discussions/categories/new-features
|
||||
about: All feature requests will be discussed here.
|
||||
about: If you have any questions, reach out to us on Discord inside the "#support" channel.
|
||||
|
||||
- name: 💡 Feature Request
|
||||
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests
|
||||
about: Suggest a new feature for Coolify.
|
||||
|
||||
- name: ⚙️ Service Request
|
||||
url: https://github.com/coollabsio/coolify/discussions/categories/service-requests
|
||||
about: Request a new service integration for Coolify.
|
||||
|
||||
- name: 🔧 Improvements
|
||||
url: https://github.com/coollabsio/coolify/discussions/categories/improvements
|
||||
about: Suggest improvements to existing features for Coolify.
|
||||
|
||||
14
.github/pull_request_template.md
vendored
14
.github/pull_request_template.md
vendored
@@ -1 +1,13 @@
|
||||
> Always use `next` branch as destination branch for PRs, not `main`
|
||||
## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
|
||||
- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
|
||||
- [ ] I have listed all changes in the `Changes` section.
|
||||
- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
|
||||
- [ ] I have tested my changes.
|
||||
- [ ] I have considered backwards compatibility.
|
||||
- [ ] I have removed this checklist and any unused sections.
|
||||
|
||||
## Changes
|
||||
-
|
||||
|
||||
## Issues
|
||||
- fix #
|
||||
|
||||
5
.github/workflows/coolify-helper-next.yml
vendored
5
.github/workflows/coolify-helper-next.yml
vendored
@@ -38,6 +38,8 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
aarch64:
|
||||
runs-on: [ self-hosted, arm64 ]
|
||||
permissions:
|
||||
@@ -64,6 +66,8 @@ jobs:
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -94,3 +98,4 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
|
||||
|
||||
4
.github/workflows/coolify-helper.yml
vendored
4
.github/workflows/coolify-helper.yml
vendored
@@ -38,6 +38,8 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
aarch64:
|
||||
runs-on: [ self-hosted, arm64 ]
|
||||
permissions:
|
||||
@@ -64,6 +66,8 @@ jobs:
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
103
.github/workflows/coolify-realtime.yml
vendored
Normal file
103
.github/workflows/coolify-realtime.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: Coolify Realtime (v4)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "next" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/coolify-realtime/terminal-server.js
|
||||
- docker/coolify-realtime/package.json
|
||||
- docker/coolify-realtime/soketi-entrypoint.sh
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: "coollabsio/coolify-realtime"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
|
||||
- name: Build image and push to registry
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
no-cache: true
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
aarch64:
|
||||
runs-on: [ self-hosted, arm64 ]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
|
||||
- name: Build image and push to registry
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
no-cache: true
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [ amd64, aarch64 ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
|
||||
- name: Create & publish manifest
|
||||
run: |
|
||||
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
|
||||
12
.github/workflows/pr-build.yml
vendored
12
.github/workflows/pr-build.yml
vendored
@@ -16,6 +16,12 @@ env:
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
actions: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
@@ -37,6 +43,9 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
actions: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
@@ -58,6 +67,9 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
actions: write
|
||||
needs: [amd64, aarch64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
78
.github/workflows/remove-labels-and-assignees-on-close.yml
vendored
Normal file
78
.github/workflows/remove-labels-and-assignees-on-close.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Remove Labels and Assignees on Issue Close
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
pull_request:
|
||||
types: [closed]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
remove-labels-and-assignees:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remove labels and assignees
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
async function processIssue(issueNumber) {
|
||||
try {
|
||||
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber
|
||||
});
|
||||
|
||||
const labelsToKeep = currentLabels
|
||||
.filter(label => label.name === '⏱︎ Stale')
|
||||
.map(label => label.name);
|
||||
|
||||
await github.rest.issues.setLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
labels: labelsToKeep
|
||||
});
|
||||
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber
|
||||
});
|
||||
|
||||
if (issue.assignees && issue.assignees.length > 0) {
|
||||
await github.rest.issues.removeAssignees({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
assignees: issue.assignees.map(assignee => assignee.login)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
console.error(`Error processing issue ${issueNumber}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
|
||||
const issue = context.payload.issue || context.payload.pull_request;
|
||||
await processIssue(issue.number);
|
||||
}
|
||||
|
||||
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
|
||||
const pr = context.payload.pull_request;
|
||||
if (pr.body) {
|
||||
const issueReferences = pr.body.match(/#(\d+)/g);
|
||||
if (issueReferences) {
|
||||
for (const reference of issueReferences) {
|
||||
const issueNumber = parseInt(reference.substring(1));
|
||||
await processIssue(issueNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
CONTRIBUTING.md
128
CONTRIBUTING.md
@@ -1,17 +1,31 @@
|
||||
# Contributing
|
||||
# Contributing to Coolify
|
||||
|
||||
> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
|
||||
|
||||
You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
## Code Contribution
|
||||
- [Contributing to Coolify](#contributing-to-coolify)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [1. Setup Development Environment](#1-setup-development-environment)
|
||||
- [2. Verify Installation (Optional)](#2-verify-installation-optional)
|
||||
- [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
|
||||
- [4. Set up Environment Variables](#4-set-up-environment-variables)
|
||||
- [5. Start Coolify](#5-start-coolify)
|
||||
- [6. Start Development](#6-start-development)
|
||||
- [7. Development Notes](#7-development-notes)
|
||||
- [8. Create a Pull Request](#8-create-a-pull-request)
|
||||
- [Additional Contribution Guidelines](#additional-contribution-guidelines)
|
||||
- [Contributing a New Service](#contributing-a-new-service)
|
||||
- [Contributing to Documentation](#contributing-to-documentation)
|
||||
|
||||
## 1. Setup your development environment
|
||||
## 1. Setup Development Environment
|
||||
|
||||
Follow the steps below for your operating system:
|
||||
|
||||
### Windows
|
||||
<details>
|
||||
<summary><strong>Windows</strong></summary>
|
||||
|
||||
1. Install `docker-ce`, Docker Desktop (or similar):
|
||||
- Docker CE (recommended):
|
||||
@@ -25,7 +39,10 @@ Follow the steps below for your operating system:
|
||||
2. Install Spin:
|
||||
- Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2)
|
||||
|
||||
### MacOS
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>MacOS</strong></summary>
|
||||
|
||||
1. Install Orbstack, Docker Desktop (or similar):
|
||||
- Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
|
||||
@@ -36,7 +53,10 @@ Follow the steps below for your operating system:
|
||||
2. Install Spin:
|
||||
- Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin)
|
||||
|
||||
### Linux
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Linux</strong></summary>
|
||||
|
||||
1. Install Docker Engine, Docker Desktop (or similar):
|
||||
- Docker Engine (recommended, as there is no VM overhead):
|
||||
@@ -47,8 +67,9 @@ Follow the steps below for your operating system:
|
||||
2. Install Spin:
|
||||
- Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions)
|
||||
|
||||
</details>
|
||||
|
||||
## 2. Verify installation (optional)
|
||||
## 2. Verify Installation (Optional)
|
||||
|
||||
After installing Docker (or Orbstack) and Spin, verify the installation:
|
||||
|
||||
@@ -60,25 +81,20 @@ After installing Docker (or Orbstack) and Spin, verify the installation:
|
||||
```
|
||||
You should see version information for both Docker and Spin.
|
||||
|
||||
|
||||
## 3. Fork the Coolify repository and setup your local repository
|
||||
## 3. Fork and Setup Local Repository
|
||||
|
||||
1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account.
|
||||
|
||||
2. Install a code editor on your machine (below are some popular choices, choose one):
|
||||
2. Install a code editor on your machine (choose one):
|
||||
|
||||
- Visual Studio Code (recommended free):
|
||||
- Windows/macOS/Linux: Download and install from [https://code.visualstudio.com/download](https://code.visualstudio.com/download)
|
||||
|
||||
- Cursor (recommended but paid for getting the full benefits):
|
||||
- Windows/macOS/Linux: Download and install from [https://www.cursor.com/](https://www.cursor.com/)
|
||||
|
||||
- Zed (very fast code editor):
|
||||
- macOS/Linux: Download and install from [https://zed.dev/download](https://zed.dev/download)
|
||||
- Windows: Not available yet
|
||||
| Editor | Platform | Download Link |
|
||||
|--------|----------|---------------|
|
||||
| Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) |
|
||||
| Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) |
|
||||
| Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) |
|
||||
|
||||
3. Clone the Coolify Repository from your fork to your local machine
|
||||
- Use `git clone` in the command line
|
||||
- Use `git clone` in the command line, or
|
||||
- Use GitHub Desktop (recommended):
|
||||
- Download and install from [https://desktop.github.com/](https://desktop.github.com/)
|
||||
- Open GitHub Desktop and login with your GitHub account
|
||||
@@ -86,37 +102,32 @@ After installing Docker (or Orbstack) and Spin, verify the installation:
|
||||
|
||||
4. Open the cloned Coolify Repository in your chosen code editor.
|
||||
|
||||
|
||||
## 4. Set up Environment Variables
|
||||
|
||||
1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository.
|
||||
|
||||
2. Duplicate the `.env.development.example` file and rename the copy to `.env`.
|
||||
|
||||
3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup.
|
||||
|
||||
4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`.
|
||||
|
||||
5. Save the changes to your `.env` file.
|
||||
|
||||
|
||||
## 5. Start Coolify
|
||||
|
||||
1. Open a terminal in the local Coolify directory.
|
||||
|
||||
2. Run the following command in the terminal (leave that terminal open):
|
||||
```
|
||||
```bash
|
||||
spin up
|
||||
```
|
||||
Note: You may see some errors, but don't worry; this is expected.
|
||||
|
||||
> [!NOTE]
|
||||
> You may see some errors, but don't worry; this is expected.
|
||||
|
||||
3. If you encounter permission errors, especially on macOS, use:
|
||||
```
|
||||
```bash
|
||||
sudo spin up
|
||||
```
|
||||
|
||||
Note: If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
|
||||
|
||||
> [!NOTE]
|
||||
> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
|
||||
|
||||
## 6. Start Development
|
||||
|
||||
@@ -126,15 +137,17 @@ Note: If you change environment variables afterwards or anything seems broken, p
|
||||
- Password: `password`
|
||||
|
||||
2. Additional development tools:
|
||||
- Laravel Horizon (scheduler): `http://localhost:8000/horizon`
|
||||
Note: Only accessible when logged in as root user
|
||||
- Mailpit (email catcher): `http://localhost:8025`
|
||||
- Telescope (debugging tool): `http://localhost:8000/telescope`
|
||||
Note: Disabled by default (so the database is not overloaded), enable by adding the following environment variable to your `.env` file:
|
||||
```env
|
||||
TELESCOPE_ENABLED=true
|
||||
```
|
||||
| Tool | URL | Note |
|
||||
|------|-----|------|
|
||||
| Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
|
||||
| Mailpit (email catcher) | `http://localhost:8025` | |
|
||||
| Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default |
|
||||
|
||||
> [!NOTE]
|
||||
> To enable Telescope, add the following to your `.env` file:
|
||||
> ```env
|
||||
> TELESCOPE_ENABLED=true
|
||||
> ```
|
||||
|
||||
## 7. Development Notes
|
||||
|
||||
@@ -150,18 +163,12 @@ When working on Coolify, keep the following in mind:
|
||||
docker exec -it coolify php artisan migrate:fresh --seed
|
||||
```
|
||||
|
||||
3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any envrionement specific issues.
|
||||
3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues.
|
||||
|
||||
Remember, forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
|
||||
> [!IMPORTANT]
|
||||
> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
|
||||
|
||||
|
||||
## 8. Contributing a New Service
|
||||
|
||||
To add a new service to Coolify, please refer to our documentation:
|
||||
[Adding a New Service](https://coolify.io/docs/knowledge-base/add-a-service)
|
||||
|
||||
|
||||
## 9. Create a Pull Request
|
||||
## 8. Create a Pull Request
|
||||
|
||||
1. After making changes or adding a new service:
|
||||
- Commit your changes to your forked repository.
|
||||
@@ -179,11 +186,26 @@ To add a new service to Coolify, please refer to our documentation:
|
||||
- In the description, explain the changes you've made.
|
||||
- Reference any related issues by using keywords like "Fixes #123" or "Closes #456".
|
||||
|
||||
4. Important note:
|
||||
Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
|
||||
> [!IMPORTANT]
|
||||
> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
|
||||
|
||||
5. Submit your PR:
|
||||
4. Submit your PR:
|
||||
- Review your changes one last time.
|
||||
- Click "Create pull request" to submit.
|
||||
|
||||
> [!NOTE]
|
||||
> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers.
|
||||
|
||||
After submission, maintainers will review your PR and may request changes or provide feedback.
|
||||
|
||||
## Additional Contribution Guidelines
|
||||
|
||||
### Contributing a New Service
|
||||
|
||||
To add a new service to Coolify, please refer to our documentation:
|
||||
[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service)
|
||||
|
||||
### Contributing to Documentation
|
||||
|
||||
To contribute to the Coolify documentation, please refer to this guide:
|
||||
[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md)
|
||||
|
||||
@@ -54,7 +54,7 @@ Special thanks to our biggest sponsors!
|
||||
* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses.
|
||||
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
|
||||
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
|
||||
* [Hostinger](https://hostinger.com?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
|
||||
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
|
||||
* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services.
|
||||
* [Ubicloud](https://ubicloud.com/?ref=coolify.io) - An open-source alternative to hyperscale cloud providers, offering high-performance cloud computing services.
|
||||
* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
|
||||
|
||||
45
RELEASE.md
Normal file
45
RELEASE.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Coolify Release Guide
|
||||
|
||||
This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed.
|
||||
|
||||
## Release Process
|
||||
|
||||
1. **Development on `next` or separate branches**
|
||||
- Changes, fixes and new features are developed on the `next` or even separate branches.
|
||||
|
||||
2. **Merging to `main`**
|
||||
- Once changes are ready, they are merged from `next` into the `main` branch.
|
||||
|
||||
3. **Building the release**
|
||||
- After merging to `main`, a new release is built.
|
||||
- Note: A push to `main` does not automatically mean a new version is released.
|
||||
|
||||
4. **Creating a GitHub release**
|
||||
- A new release is created on GitHub with the new version details.
|
||||
|
||||
5. **Updating the CDN**
|
||||
- The final step is updating the version information on the CDN:
|
||||
[https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
|
||||
|
||||
> [!NOTE]
|
||||
> The CDN update may not occur immediately after the GitHub release. It can happen hours or even days later due to additional testing, stability checks, or potential hotfixes.
|
||||
|
||||
|
||||
## Version Availability
|
||||
|
||||
It's important to understand that a new version released on GitHub may not immediately become available for users to update (through manual or auto-update).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you see a new release on GitHub but haven't received the update, it's likely because the CDN hasn't been updated yet. This is intentional and ensures stability and allows for hotfixes before the new version is officially released.
|
||||
|
||||
## Manually Update to Specific Versions
|
||||
|
||||
> [!CAUTION]
|
||||
> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk!
|
||||
|
||||
To update your Coolify instance to a specific (unreleased) version, use the following command:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s <version>
|
||||
```
|
||||
-> Replace `<version>` with the version you want to update to (for example `4.0.0-beta.332`).
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\Application;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -9,44 +10,35 @@ class StopApplication
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Application $application, bool $previewDeployments = false)
|
||||
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
|
||||
{
|
||||
if ($application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$servers = collect([]);
|
||||
$servers->push($application->destination->server);
|
||||
$application->additional_servers->map(function ($server) use ($servers) {
|
||||
$servers->push($server);
|
||||
});
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
$server = $application->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
if ($previewDeployments) {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
}
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
if ($containerName) {
|
||||
instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
|
||||
}
|
||||
}
|
||||
ray('Stopping application: '.$application->name);
|
||||
|
||||
if ($server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$containersToStop = $application->getContainersToStop($previewDeployments);
|
||||
$application->stopContainers($containersToStop, $server);
|
||||
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
// remove network
|
||||
$uuid = $application->uuid;
|
||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||
$application->delete_connected_networks($application->uuid);
|
||||
}
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Actions\CoolifyTask;
|
||||
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Process\ProcessResult;
|
||||
@@ -137,7 +138,7 @@ class RunRemoteProcess
|
||||
$command = $this->activity->getExtraProperty('command');
|
||||
$server = Server::whereUuid($server_uuid)->firstOrFail();
|
||||
|
||||
return generateSshCommand($server, $command);
|
||||
return SshMultiplexingHelper::generateSshCommand($server, $command);
|
||||
}
|
||||
|
||||
protected function handleOutput(string $type, string $output)
|
||||
|
||||
@@ -23,7 +23,7 @@ class StartDragonfly
|
||||
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@@ -75,7 +75,7 @@ class StartDragonfly
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
@@ -118,10 +118,10 @@ class StartDragonfly
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class StartDragonfly
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class StartKeydb
|
||||
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@@ -74,7 +74,7 @@ class StartKeydb
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
@@ -94,10 +94,10 @@ class StartKeydb
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) {
|
||||
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/keydb.conf',
|
||||
'source' => $this->configuration_dir.'/keydb.conf',
|
||||
'target' => '/etc/keydb/keydb.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
@@ -125,10 +125,10 @@ class StartKeydb
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class StartKeydb
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class StartMariadb
|
||||
$this->database = $database;
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@@ -69,7 +69,7 @@ class StartMariadb
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
@@ -89,10 +89,10 @@ class StartMariadb
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) {
|
||||
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/custom-config.cnf',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
];
|
||||
@@ -120,10 +120,10 @@ class StartMariadb
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,18 +154,18 @@ class StartMariadb
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
|
||||
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
|
||||
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
|
||||
}
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class StartMongodb
|
||||
$startCommand = 'mongod';
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@@ -77,7 +77,7 @@ class StartMongodb
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
@@ -97,19 +97,19 @@ class StartMongodb
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) {
|
||||
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/mongod.conf',
|
||||
'source' => $this->configuration_dir.'/mongod.conf',
|
||||
'target' => '/etc/mongo/mongod.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
|
||||
$docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
|
||||
}
|
||||
$this->add_default_database();
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
|
||||
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
|
||||
'target' => '/docker-entrypoint-initdb.d',
|
||||
'read_only' => true,
|
||||
];
|
||||
@@ -136,10 +136,10 @@ class StartMongodb
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,15 +170,15 @@ class StartMongodb
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
|
||||
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
|
||||
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class StartMysql
|
||||
$this->database = $database;
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@@ -69,7 +69,7 @@ class StartMysql
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
@@ -89,10 +89,10 @@ class StartMysql
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) {
|
||||
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/custom-config.cnf',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
];
|
||||
@@ -120,10 +120,10 @@ class StartMysql
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,18 +154,18 @@ class StartMysql
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
|
||||
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
|
||||
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
|
||||
}
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ class StartPostgresql
|
||||
$this->generate_init_scripts();
|
||||
$this->add_custom_conf();
|
||||
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
|
||||
@@ -24,7 +24,7 @@ class StartRedis
|
||||
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@@ -78,7 +78,7 @@ class StartRedis
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
@@ -98,10 +98,10 @@ class StartRedis
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) {
|
||||
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/redis.conf',
|
||||
'source' => $this->configuration_dir.'/redis.conf',
|
||||
'target' => '/usr/local/etc/redis/redis.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
@@ -130,10 +130,10 @@ class StartRedis
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ class StartRedis
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopDatabase
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
|
||||
instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
|
||||
$this->stopContainer($database, $database->uuid, 300);
|
||||
if (! $isDeleteOperation) {
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
}
|
||||
|
||||
if ($database->is_public) {
|
||||
StopDatabaseProxy::run($database);
|
||||
}
|
||||
|
||||
return 'Database stopped successfully';
|
||||
}
|
||||
|
||||
private function stopContainer($database, string $containerName, int $timeout = 300): void
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
|
||||
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
|
||||
$startTime = time();
|
||||
while ($process->running()) {
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->forceStopContainer($containerName, $server);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
|
||||
private function forceStopContainer(string $containerName, $server): void
|
||||
{
|
||||
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName, $server): void
|
||||
{
|
||||
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
private function deleteConnectedNetworks($uuid, $server)
|
||||
{
|
||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class CheckProxy
|
||||
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
||||
return false;
|
||||
}
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
|
||||
if (! $uptime) {
|
||||
throw new \Exception($error);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -21,10 +22,16 @@ class CleanupDocker
|
||||
|
||||
private function getCommands(): array
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
$helperImageVersion = data_get($settings, 'helper_version');
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion;
|
||||
|
||||
$commonCommands = [
|
||||
'docker container prune -f --filter "label=coolify.managed=true"',
|
||||
'docker image prune -af',
|
||||
'docker image prune -af --filter "label!=coolify.managed=true"',
|
||||
'docker builder prune -af',
|
||||
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi",
|
||||
];
|
||||
|
||||
return $commonCommands;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Events\CloudflareTunnelConfigured;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -40,12 +41,17 @@ class ConfigureCloudflared
|
||||
instant_remote_process($commands, $server);
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
$server->settings->is_cloudflare_tunnel = false;
|
||||
$server->settings->save();
|
||||
throw $e;
|
||||
} finally {
|
||||
CloudflareTunnelConfigured::dispatch($server->team_id);
|
||||
|
||||
$commands = collect([
|
||||
'rm -fr /tmp/cloudflared',
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -55,6 +56,13 @@ class UpdateCoolify
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$all_servers = Server::all();
|
||||
$servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
|
||||
foreach ($servers as $server) {
|
||||
PullHelperImageJob::dispatch($server);
|
||||
}
|
||||
|
||||
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
|
||||
|
||||
remote_process([
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -9,11 +10,11 @@ class DeleteService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service)
|
||||
public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
|
||||
{
|
||||
try {
|
||||
$server = data_get($service, 'server');
|
||||
if ($server->isFunctional()) {
|
||||
if ($deleteVolumes && $server->isFunctional()) {
|
||||
$storagesToDelete = collect([]);
|
||||
|
||||
$service->environment_variables()->delete();
|
||||
@@ -33,13 +34,29 @@ class DeleteService
|
||||
foreach ($storagesToDelete as $storage) {
|
||||
$commands[] = "docker volume rm -f $storage->name";
|
||||
}
|
||||
$commands[] = "docker rm -f $service->uuid";
|
||||
|
||||
instant_remote_process($commands, $server, false);
|
||||
// Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
|
||||
if (! empty($commands)) {
|
||||
foreach ($commands as $command) {
|
||||
$result = instant_remote_process([$command], $server, false);
|
||||
if ($result !== 0) {
|
||||
ray("Failed to execute: $command");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteConnectedNetworks) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
}
|
||||
|
||||
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
} finally {
|
||||
if ($deleteConfigurations) {
|
||||
$service->delete_configurations();
|
||||
}
|
||||
foreach ($service->applications()->get() as $application) {
|
||||
$application->forceDelete();
|
||||
}
|
||||
@@ -50,6 +67,11 @@ class DeleteService
|
||||
$task->delete();
|
||||
}
|
||||
$service->tags()->detach();
|
||||
$service->forceDelete();
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ class StartService
|
||||
$service->saveComposeConfigs();
|
||||
$commands[] = 'cd '.$service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||
if ($service->networks()->count() > 0) {
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||
}
|
||||
$commands[] = 'echo Starting service.';
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = 'docker compose pull';
|
||||
@@ -29,7 +31,7 @@ class StartService
|
||||
$network = $service->destination->network;
|
||||
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
|
||||
foreach ($serviceNames as $serviceName => $serviceConfig) {
|
||||
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
|
||||
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
|
||||
}
|
||||
}
|
||||
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -9,40 +10,27 @@ class StopService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service)
|
||||
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
$server = $service->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
ray('Stopping service: '.$service->name);
|
||||
$applications = $service->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
if ($applications->count() < 6) {
|
||||
instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
}
|
||||
instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($dbs as $db) {
|
||||
if ($dbs->count() < 6) {
|
||||
|
||||
instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
$containersToStop = $service->getContainersToStop();
|
||||
$service->stopContainers($containersToStop, $server);
|
||||
|
||||
if (! $isDeleteOperation) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
|
||||
instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CleanupQueue extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:queue';
|
||||
|
||||
protected $description = 'Cleanup Queue';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running queue cleanup...\n";
|
||||
$prefix = config('database.redis.options.prefix');
|
||||
$keys = Redis::connection()->keys('*:laravel*');
|
||||
foreach ($keys as $key) {
|
||||
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
||||
Redis::connection()->del($keyWithoutPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
app/Console/Commands/CleanupRedis.php
Normal file
31
app/Console/Commands/CleanupRedis.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CleanupRedis extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:redis';
|
||||
|
||||
protected $description = 'Cleanup Redis';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Cleanup Redis keys.\n";
|
||||
$prefix = config('database.redis.options.prefix');
|
||||
|
||||
$keys = Redis::connection()->keys('*:laravel*');
|
||||
collect($keys)->each(function ($key) use ($prefix) {
|
||||
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
||||
Redis::connection()->del($keyWithoutPrefix);
|
||||
});
|
||||
|
||||
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
|
||||
collect($queueOverlaps)->each(function ($key) {
|
||||
Redis::connection()->del($key);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CleanupHelperContainersJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -35,6 +37,16 @@ class CleanupStuckedResources extends Command
|
||||
private function cleanup_stucked_resources()
|
||||
{
|
||||
|
||||
try {
|
||||
$servers = Server::all()->filter(function ($server) {
|
||||
return $server->isFunctional();
|
||||
});
|
||||
foreach ($servers as $server) {
|
||||
CleanupHelperContainersJob::dispatch($server);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($applications as $application) {
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Console\Commands;
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CleanupHelperContainersJob;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
@@ -18,7 +17,7 @@ use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Init extends Command
|
||||
{
|
||||
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
|
||||
protected $signature = 'app:init {--force-cloud}';
|
||||
|
||||
protected $description = 'Cleanup instance related stuffs';
|
||||
|
||||
@@ -26,9 +25,63 @@ class Init extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (isCloud() && ! $this->option('force-cloud')) {
|
||||
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->servers = Server::all();
|
||||
$this->alive();
|
||||
get_public_ips();
|
||||
if (isCloud()) {
|
||||
|
||||
} else {
|
||||
$this->send_alive_signal();
|
||||
get_public_ips();
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
$this->disable_metrics();
|
||||
$this->replace_slash_in_environment_name();
|
||||
$this->restore_coolify_db_backup();
|
||||
//
|
||||
$this->update_traefik_labels();
|
||||
if (! isCloud() || $this->option('force-cloud')) {
|
||||
$this->cleanup_unused_network_from_coolify_proxy();
|
||||
}
|
||||
if (isCloud()) {
|
||||
$this->cleanup_unnecessary_dynamic_proxy_configuration();
|
||||
} else {
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
}
|
||||
$this->call('cleanup:redis');
|
||||
$this->call('cleanup:stucked-resources');
|
||||
|
||||
if (isCloud()) {
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
$services = $response->json();
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$localhost = $this->servers->where('id', 0)->first();
|
||||
$localhost->setupDynamicProxyConfiguration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
$settings = InstanceSettings::get();
|
||||
if (! is_null(env('AUTOUPDATE', null))) {
|
||||
if (env('AUTOUPDATE') == true) {
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function disable_metrics()
|
||||
{
|
||||
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
|
||||
foreach ($this->servers as $server) {
|
||||
if ($server->settings->is_metrics_enabled === true) {
|
||||
@@ -39,62 +92,6 @@ class Init extends Command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$full_cleanup = $this->option('full-cleanup');
|
||||
$cleanup_deployments = $this->option('cleanup-deployments');
|
||||
$cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
|
||||
$this->replace_slash_in_environment_name();
|
||||
if ($cleanup_deployments) {
|
||||
echo "Running cleanup deployments.\n";
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
|
||||
return;
|
||||
}
|
||||
if ($cleanup_proxy_networks) {
|
||||
echo "Running cleanup proxy networks.\n";
|
||||
$this->cleanup_unused_network_from_coolify_proxy();
|
||||
|
||||
return;
|
||||
}
|
||||
if ($full_cleanup) {
|
||||
// Required for falsely deleted coolify db
|
||||
$this->restore_coolify_db_backup();
|
||||
$this->update_traefik_labels();
|
||||
$this->cleanup_unused_network_from_coolify_proxy();
|
||||
$this->cleanup_unnecessary_dynamic_proxy_configuration();
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
$this->call('cleanup:queue');
|
||||
$this->call('cleanup:stucked-resources');
|
||||
if (! isCloud()) {
|
||||
try {
|
||||
$localhost = $this->servers->where('id', 0)->first();
|
||||
$localhost->setupDynamicProxyConfiguration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
$settings = InstanceSettings::get();
|
||||
if (! is_null(env('AUTOUPDATE', null))) {
|
||||
if (env('AUTOUPDATE') == true) {
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
}
|
||||
if (isCloud()) {
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
$services = $response->json();
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
$this->call('cleanup:stucked-resources');
|
||||
}
|
||||
|
||||
private function update_traefik_labels()
|
||||
@@ -108,33 +105,28 @@ class Init extends Command
|
||||
|
||||
private function cleanup_unnecessary_dynamic_proxy_configuration()
|
||||
{
|
||||
if (isCloud()) {
|
||||
foreach ($this->servers as $server) {
|
||||
try {
|
||||
if (! $server->isFunctional()) {
|
||||
continue;
|
||||
}
|
||||
if ($server->id === 0) {
|
||||
continue;
|
||||
}
|
||||
$file = $server->proxyPath().'/dynamic/coolify.yaml';
|
||||
|
||||
return instant_remote_process([
|
||||
"rm -f $file",
|
||||
], $server, false);
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
|
||||
foreach ($this->servers as $server) {
|
||||
try {
|
||||
if (! $server->isFunctional()) {
|
||||
continue;
|
||||
}
|
||||
if ($server->id === 0) {
|
||||
continue;
|
||||
}
|
||||
$file = $server->proxyPath().'/dynamic/coolify.yaml';
|
||||
|
||||
return instant_remote_process([
|
||||
"rm -f $file",
|
||||
], $server, false);
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup_unused_network_from_coolify_proxy()
|
||||
{
|
||||
if (isCloud()) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->servers as $server) {
|
||||
if (! $server->isFunctional()) {
|
||||
continue;
|
||||
@@ -175,39 +167,32 @@ class Init extends Command
|
||||
|
||||
private function restore_coolify_db_backup()
|
||||
{
|
||||
try {
|
||||
$database = StandalonePostgresql::withTrashed()->find(0);
|
||||
if ($database && $database->trashed()) {
|
||||
echo "Restoring coolify db backup\n";
|
||||
$database->restore();
|
||||
$scheduledBackup = ScheduledDatabaseBackup::find(0);
|
||||
if (! $scheduledBackup) {
|
||||
ScheduledDatabaseBackup::create([
|
||||
'id' => 0,
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
'frequency' => '0 0 * * *',
|
||||
'database_id' => $database->id,
|
||||
'database_type' => 'App\Models\StandalonePostgresql',
|
||||
'team_id' => 0,
|
||||
]);
|
||||
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
|
||||
try {
|
||||
$database = StandalonePostgresql::withTrashed()->find(0);
|
||||
if ($database && $database->trashed()) {
|
||||
echo "Restoring coolify db backup\n";
|
||||
$database->restore();
|
||||
$scheduledBackup = ScheduledDatabaseBackup::find(0);
|
||||
if (! $scheduledBackup) {
|
||||
ScheduledDatabaseBackup::create([
|
||||
'id' => 0,
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
'frequency' => '0 0 * * *',
|
||||
'database_id' => $database->id,
|
||||
'database_type' => 'App\Models\StandalonePostgresql',
|
||||
'team_id' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup_stucked_helper_containers()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
if ($server->isFunctional()) {
|
||||
CleanupHelperContainersJob::dispatch($server);
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function alive()
|
||||
private function send_alive_signal()
|
||||
{
|
||||
$id = config('app.id');
|
||||
$version = config('version');
|
||||
@@ -225,23 +210,7 @@ class Init extends Command
|
||||
echo "Error in alive: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
// private function cleanup_ssh()
|
||||
// {
|
||||
|
||||
// TODO: it will cleanup id.root@host.docker.internal
|
||||
// try {
|
||||
// $files = Storage::allFiles('ssh/keys');
|
||||
// foreach ($files as $file) {
|
||||
// Storage::delete($file);
|
||||
// }
|
||||
// $files = Storage::allFiles('ssh/mux');
|
||||
// foreach ($files as $file) {
|
||||
// Storage::delete($file);
|
||||
// }
|
||||
// } catch (\Throwable $e) {
|
||||
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
|
||||
// }
|
||||
// }
|
||||
private function cleanup_in_progress_application_deployments()
|
||||
{
|
||||
// Cleanup any failed deployments
|
||||
@@ -263,11 +232,13 @@ class Init extends Command
|
||||
|
||||
private function replace_slash_in_environment_name()
|
||||
{
|
||||
$environments = Environment::all();
|
||||
foreach ($environments as $environment) {
|
||||
if (str_contains($environment->name, '/')) {
|
||||
$environment->name = str_replace('/', '-', $environment->name);
|
||||
$environment->save();
|
||||
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
|
||||
$environments = Environment::all();
|
||||
foreach ($environments as $environment) {
|
||||
if (str_contains($environment->name, '/')) {
|
||||
$environment->name = str_replace('/', '-', $environment->name);
|
||||
$environment->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Console;
|
||||
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\CleanupStaleMultiplexedConnections;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
@@ -29,7 +30,8 @@ class Kernel extends ConsoleKernel
|
||||
$this->all_servers = Server::all();
|
||||
$settings = InstanceSettings::get();
|
||||
|
||||
$schedule->command('telescope:prune')->daily();
|
||||
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyMinute();
|
||||
@@ -39,6 +41,10 @@ class Kernel extends ConsoleKernel
|
||||
$this->check_resources($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
$schedule->command('uploads:clear')->everyTwoMinutes();
|
||||
|
||||
$schedule->command('telescope:prune')->daily();
|
||||
|
||||
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
@@ -73,11 +79,11 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
||||
}
|
||||
$schedule->job(new PullHelperImageJob($server))
|
||||
->cron($settings->update_check_frequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
->onOneServer();
|
||||
}
|
||||
$schedule->job(new PullHelperImageJob)
|
||||
->cron($settings->update_check_frequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
->onOneServer();
|
||||
}
|
||||
|
||||
private function schedule_updates($schedule)
|
||||
|
||||
34
app/Events/CloudflareTunnelConfigured.php
Normal file
34
app/Events/CloudflareTunnelConfigured.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CloudflareTunnelConfigured implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
184
app/Helpers/SshMultiplexingHelper.php
Normal file
184
app/Helpers/SshMultiplexingHelper.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class SshMultiplexingHelper
|
||||
{
|
||||
public static function serverSshConfiguration(Server $server)
|
||||
{
|
||||
$privateKey = PrivateKey::findOrFail($server->private_key_id);
|
||||
$sshKeyLocation = $privateKey->getKeyLocation();
|
||||
$muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
|
||||
|
||||
return [
|
||||
'sshKeyLocation' => $sshKeyLocation,
|
||||
'muxFilename' => $muxFilename,
|
||||
];
|
||||
}
|
||||
|
||||
public static function ensureMultiplexedConnection(Server $server)
|
||||
{
|
||||
if (! self::isMultiplexingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
|
||||
self::validateSshKey($sshKeyLocation);
|
||||
|
||||
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
$checkCommand .= "{$server->user}@{$server->ip}";
|
||||
$process = Process::run($checkCommand);
|
||||
|
||||
if ($process->exitCode() !== 0) {
|
||||
self::establishNewMultiplexedConnection($server);
|
||||
}
|
||||
}
|
||||
|
||||
public static function establishNewMultiplexedConnection(Server $server)
|
||||
{
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$connectionTimeout = config('constants.ssh.connection_timeout');
|
||||
$serverInterval = config('constants.ssh.server_interval');
|
||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||
|
||||
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
|
||||
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
|
||||
$establishCommand .= "{$server->user}@{$server->ip}";
|
||||
|
||||
$establishProcess = Process::run($establishCommand);
|
||||
|
||||
if ($establishProcess->exitCode() !== 0) {
|
||||
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
public static function removeMuxFile(Server $server)
|
||||
{
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
$closeCommand .= "{$server->user}@{$server->ip}";
|
||||
Process::run($closeCommand);
|
||||
}
|
||||
|
||||
public static function generateScpCommand(Server $server, string $source, string $dest)
|
||||
{
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$timeout = config('constants.ssh.command_timeout');
|
||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||
|
||||
$scp_command = "timeout $timeout scp ";
|
||||
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
self::ensureMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
|
||||
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
|
||||
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
|
||||
|
||||
return $scp_command;
|
||||
}
|
||||
|
||||
public static function generateSshCommand(Server $server, string $command)
|
||||
{
|
||||
if ($server->settings->force_disabled) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$timeout = config('constants.ssh.command_timeout');
|
||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||
|
||||
$ssh_command = "timeout $timeout ssh ";
|
||||
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
self::ensureMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
|
||||
}
|
||||
|
||||
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
|
||||
|
||||
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
|
||||
$delimiter = Hash::make($command);
|
||||
$command = str_replace($delimiter, '', $command);
|
||||
|
||||
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
|
||||
.$command.PHP_EOL
|
||||
.$delimiter;
|
||||
|
||||
return $ssh_command;
|
||||
}
|
||||
|
||||
private static function isMultiplexingEnabled(): bool
|
||||
{
|
||||
return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
|
||||
}
|
||||
|
||||
private static function validateSshKey(string $sshKeyLocation): void
|
||||
{
|
||||
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
|
||||
$keyCheckProcess = Process::run($checkKeyCommand);
|
||||
|
||||
if ($keyCheckProcess->exitCode() !== 0) {
|
||||
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
|
||||
}
|
||||
}
|
||||
|
||||
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
|
||||
{
|
||||
$options = "-i {$sshKeyLocation} "
|
||||
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
|
||||
.'-o PasswordAuthentication=no '
|
||||
."-o ConnectTimeout=$connectionTimeout "
|
||||
."-o ServerAliveInterval=$serverInterval "
|
||||
.'-o RequestTTY=no '
|
||||
.'-o LogLevel=ERROR ';
|
||||
|
||||
// Bruh
|
||||
if ($isScp) {
|
||||
$options .= "-P {$server->port} ";
|
||||
} else {
|
||||
$options .= "-p {$server->port} ";
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -2529,6 +2529,131 @@ class ApplicationsController extends Controller
|
||||
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Execute Command',
|
||||
description: "Execute a command on the application's current container.",
|
||||
path: '/applications/{uuid}/execute',
|
||||
operationId: 'execute-command-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Command to execute.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: "Execute a command on the application's current container.",
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Command executed.'],
|
||||
'response' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function execute_command_by_uuid(Request $request)
|
||||
{
|
||||
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
|
||||
$allowedFields = ['command'];
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'command' => 'string|required',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
|
||||
$status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
|
||||
if ($status !== 'running') {
|
||||
return response()->json([
|
||||
'message' => 'Application is not running.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$commands = collect([
|
||||
executeInDocker($container['Names'], $request->command),
|
||||
]);
|
||||
|
||||
$res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Command executed.',
|
||||
'response' => $res,
|
||||
]);
|
||||
}
|
||||
|
||||
private function validateDataApplications(Request $request, Server $server)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
@@ -86,7 +86,7 @@ class DeployController extends Controller
|
||||
],
|
||||
tags: ['Deployments'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -150,7 +150,7 @@ class DeployController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get deployment(s) Uuid\'s',
|
||||
description: 'Get deployment(s) UUID\'s',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
|
||||
@@ -11,7 +11,7 @@ class ProjectController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'list projects.',
|
||||
description: 'List projects.',
|
||||
path: '/projects',
|
||||
operationId: 'list-projects',
|
||||
security: [
|
||||
@@ -47,7 +47,7 @@ class ProjectController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
|
||||
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'description', 'uuid')->get();
|
||||
|
||||
return response()->json(serializeApiResponse($projects),
|
||||
);
|
||||
@@ -55,7 +55,7 @@ class ProjectController extends Controller
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get',
|
||||
description: 'Get project by Uuid.',
|
||||
description: 'Get project by UUID.',
|
||||
path: '/projects/{uuid}',
|
||||
operationId: 'get-project-by-uuid',
|
||||
security: [
|
||||
@@ -139,7 +139,7 @@ class ProjectController extends Controller
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
return response()->json(['message' => 'UUID is required.'], 422);
|
||||
}
|
||||
if (! $request->environment_name) {
|
||||
return response()->json(['message' => 'Environment name is required.'], 422);
|
||||
@@ -341,7 +341,7 @@ class ProjectController extends Controller
|
||||
}
|
||||
$uuid = $request->uuid;
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
return response()->json(['message' => 'UUID is required.'], 422);
|
||||
}
|
||||
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($uuid)->first();
|
||||
@@ -417,7 +417,7 @@ class ProjectController extends Controller
|
||||
}
|
||||
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
return response()->json(['message' => 'UUID is required.'], 422);
|
||||
}
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $project) {
|
||||
|
||||
@@ -75,7 +75,7 @@ class SecurityController extends Controller
|
||||
],
|
||||
tags: ['Private Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -323,7 +323,7 @@ class SecurityController extends Controller
|
||||
],
|
||||
tags: ['Private Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
|
||||
@@ -107,7 +107,7 @@ class ServersController extends Controller
|
||||
],
|
||||
tags: ['Servers'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -185,7 +185,7 @@ class ServersController extends Controller
|
||||
],
|
||||
tags: ['Servers'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -263,7 +263,7 @@ class ServersController extends Controller
|
||||
],
|
||||
tags: ['Servers'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
|
||||
@@ -378,7 +378,7 @@ class ServicesController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get a service by Uuid.',
|
||||
description: 'Get a service by UUID.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
@@ -436,7 +436,7 @@ class ServicesController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Delete a service by Uuid',
|
||||
description: 'Delete a service by UUID',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
|
||||
@@ -27,6 +27,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
@@ -210,7 +211,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
ray('New container name: ', $this->container_name)->green();
|
||||
|
||||
savePrivateKeyToFs($this->server);
|
||||
$this->saved_outputs = collect();
|
||||
|
||||
// Set preview fqdn
|
||||
@@ -514,7 +514,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'hidden' => true,
|
||||
'ignore_errors' => true,
|
||||
], [
|
||||
"docker network connect {$networkId} coolify-proxy || true",
|
||||
"docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true",
|
||||
'hidden' => true,
|
||||
'ignore_errors' => true,
|
||||
]);
|
||||
@@ -919,10 +919,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$envs->push("COOLIFY_BRANCH={$local_branch}");
|
||||
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
||||
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -978,10 +978,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$envs->push("COOLIFY_BRANCH={$local_branch}");
|
||||
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
||||
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1066,15 +1066,55 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->environment_variables = $envs;
|
||||
}
|
||||
|
||||
private function elixir_finetunes()
|
||||
{
|
||||
if ($this->pull_request_id === 0) {
|
||||
$envType = 'environment_variables';
|
||||
} else {
|
||||
$envType = 'environment_variables_preview';
|
||||
}
|
||||
$mix_env = $this->application->{$envType}->where('key', 'MIX_ENV')->first();
|
||||
if ($mix_env) {
|
||||
if ($mix_env->is_build_time === false) {
|
||||
$this->application_deployment_queue->addLogEntry('MIX_ENV environment variable is not set as build time.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please set MIX_ENV environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry('MIX_ENV environment variable not found.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please add MIX_ENV environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
$secret_key_base = $this->application->{$envType}->where('key', 'SECRET_KEY_BASE')->first();
|
||||
if ($secret_key_base) {
|
||||
if ($secret_key_base->is_build_time === false) {
|
||||
$this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable is not set as build time.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please set SECRET_KEY_BASE environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable not found.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please add SECRET_KEY_BASE environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
$database_url = $this->application->{$envType}->where('key', 'DATABASE_URL')->first();
|
||||
if ($database_url) {
|
||||
if ($database_url->is_build_time === false) {
|
||||
$this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable is not set as build time.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please set DATABASE_URL environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable not found.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please add DATABASE_URL environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
}
|
||||
|
||||
private function laravel_finetunes()
|
||||
{
|
||||
if ($this->pull_request_id === 0) {
|
||||
$nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
$envType = 'environment_variables';
|
||||
} else {
|
||||
$nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
$envType = 'environment_variables_preview';
|
||||
}
|
||||
$nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
|
||||
if (! $nixpacks_php_fallback_path) {
|
||||
$nixpacks_php_fallback_path = new EnvironmentVariable;
|
||||
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
|
||||
@@ -1402,21 +1442,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$local_branch = "pull/{$this->pull_request_id}/head";
|
||||
}
|
||||
$private_key = data_get($this->application, 'private_key.private_key');
|
||||
$private_key = $this->application->privateKey?->getKeyLocation();
|
||||
if ($private_key) {
|
||||
$private_key = base64_encode($private_key);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'),
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
|
||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$private_key}\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
|
||||
'hidden' => true,
|
||||
'save' => 'git_commit_sha',
|
||||
],
|
||||
@@ -1533,6 +1563,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value);
|
||||
data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value);
|
||||
}
|
||||
if ($this->nixpacks_type === 'elixir') {
|
||||
$this->elixir_finetunes();
|
||||
}
|
||||
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
|
||||
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
|
||||
if ($this->nixpacks_type === 'rust') {
|
||||
@@ -2006,6 +2039,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
@@ -2025,6 +2062,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
@@ -2067,6 +2108,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
@@ -2086,6 +2131,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
@@ -2114,6 +2163,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
@@ -2133,6 +2186,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
@@ -2144,20 +2201,40 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $timeout in seconds
|
||||
*/
|
||||
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
|
||||
private function graceful_shutdown_container(string $containerName, int $timeout = 300)
|
||||
{
|
||||
try {
|
||||
$this->execute_remote_command(
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
|
||||
$startTime = time();
|
||||
while ($process->running()) {
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->execute_remote_command(
|
||||
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$isRunning = $this->execute_remote_command(
|
||||
["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
) === 'true';
|
||||
|
||||
if ($isRunning) {
|
||||
$this->execute_remote_command(
|
||||
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
}
|
||||
} catch (\Exception $error) {
|
||||
// report error if needed
|
||||
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
|
||||
}
|
||||
|
||||
$this->remove_container($containerName);
|
||||
}
|
||||
|
||||
private function remove_container(string $containerName)
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
|
||||
@@ -9,8 +9,8 @@ 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\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
|
||||
@@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
|
||||
{
|
||||
try {
|
||||
ray('Cleaning up helper containers on '.$this->server->name);
|
||||
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerId = data_get($container, 'ID');
|
||||
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||
$containerIds = collect(json_decode($containers))->pluck('ID');
|
||||
if ($containerIds->count() > 0) {
|
||||
foreach ($containerIds as $containerId) {
|
||||
ray('Removing container '.$containerId);
|
||||
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
|
||||
}
|
||||
|
||||
82
app/Jobs/CleanupStaleMultiplexedConnections.php
Normal file
82
app/Jobs/CleanupStaleMultiplexedConnections.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CleanupStaleMultiplexedConnections implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->cleanupStaleConnections();
|
||||
$this->cleanupNonExistentServerConnections();
|
||||
}
|
||||
|
||||
private function cleanupStaleConnections()
|
||||
{
|
||||
$muxFiles = Storage::disk('ssh-mux')->files();
|
||||
|
||||
foreach ($muxFiles as $muxFile) {
|
||||
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
|
||||
$server = Server::where('uuid', $serverUuid)->first();
|
||||
|
||||
if (! $server) {
|
||||
$this->removeMultiplexFile($muxFile);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
|
||||
$checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
|
||||
$checkProcess = Process::run($checkCommand);
|
||||
|
||||
if ($checkProcess->exitCode() !== 0) {
|
||||
$this->removeMultiplexFile($muxFile);
|
||||
} else {
|
||||
$muxContent = Storage::disk('ssh-mux')->get($muxFile);
|
||||
$establishedAt = Carbon::parse(substr($muxContent, 37));
|
||||
$expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
|
||||
|
||||
if (Carbon::now()->isAfter($expirationTime)) {
|
||||
$this->removeMultiplexFile($muxFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupNonExistentServerConnections()
|
||||
{
|
||||
$muxFiles = Storage::disk('ssh-mux')->files();
|
||||
$existingServerUuids = Server::pluck('uuid')->toArray();
|
||||
|
||||
foreach ($muxFiles as $muxFile) {
|
||||
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
|
||||
if (! in_array($serverUuid, $existingServerUuids)) {
|
||||
$this->removeMultiplexFile($muxFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function extractServerUuidFromMuxFile($muxFile)
|
||||
{
|
||||
return substr($muxFile, 4);
|
||||
}
|
||||
|
||||
private function removeMultiplexFile($muxFile)
|
||||
{
|
||||
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
|
||||
$closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
|
||||
Process::run($closeCommand);
|
||||
Storage::disk('ssh-mux')->delete($muxFile);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
GetContainersStatus::run($this->server);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
||||
|
||||
use App\Actions\Database\StopDatabase;
|
||||
use App\Events\BackupCreated;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledDatabaseBackupExecution;
|
||||
@@ -22,10 +23,9 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\InstanceSettings;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
@@ -79,16 +79,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [new WithoutOverlapping($this->backup->id)];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->backup->id;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
@@ -399,6 +389,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
|
||||
|
||||
$commands[] = $backupCommand;
|
||||
ray($commands);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
@@ -477,6 +468,34 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// private function upload_to_s3(): void
|
||||
// {
|
||||
// try {
|
||||
// if (is_null($this->s3)) {
|
||||
// return;
|
||||
// }
|
||||
// $key = $this->s3->key;
|
||||
// $secret = $this->s3->secret;
|
||||
// // $region = $this->s3->region;
|
||||
// $bucket = $this->s3->bucket;
|
||||
// $endpoint = $this->s3->endpoint;
|
||||
// $this->s3->testConnection(shouldSave: true);
|
||||
// $configName = new Cuid2;
|
||||
|
||||
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
|
||||
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
|
||||
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
|
||||
// instant_remote_process($commands, $this->server);
|
||||
// $this->add_to_backup_output('Uploaded to S3.');
|
||||
// } catch (\Throwable $e) {
|
||||
// $this->add_to_backup_output($e->getMessage());
|
||||
// throw $e;
|
||||
// } finally {
|
||||
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
|
||||
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
|
||||
// instant_remote_process($removeConfigCommands, $this->server, false);
|
||||
// }
|
||||
// }
|
||||
private function upload_to_s3(): void
|
||||
{
|
||||
try {
|
||||
@@ -518,7 +537,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
$imageExists = $this->checkImageExists($fullImageName);
|
||||
|
||||
if (!$imageExists) {
|
||||
if (! $imageExists) {
|
||||
$this->pullHelperImage($fullImageName);
|
||||
}
|
||||
}
|
||||
@@ -526,6 +545,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
private function checkImageExists(string $fullImageName): bool
|
||||
{
|
||||
$result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
|
||||
|
||||
return trim($result) === 'exists';
|
||||
}
|
||||
|
||||
@@ -534,7 +554,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
try {
|
||||
instant_remote_process(["docker pull {$fullImageName}"], $this->server);
|
||||
} catch (\Exception $e) {
|
||||
$errorMessage = "Failed to pull helper image: " . $e->getMessage();
|
||||
$errorMessage = 'Failed to pull helper image: '.$e->getMessage();
|
||||
$this->add_to_backup_output($errorMessage);
|
||||
throw new \RuntimeException($errorMessage);
|
||||
}
|
||||
@@ -545,6 +565,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$settings = InstanceSettings::get();
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$latestVersion = $settings->helper_version;
|
||||
|
||||
return "{$helperImage}:{$latestVersion}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
||||
|
||||
use App\Actions\Application\StopApplication;
|
||||
use App\Actions\Database\StopDatabase;
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Actions\Service\DeleteService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Models\Application;
|
||||
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(
|
||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
|
||||
public bool $deleteConfigurations = false,
|
||||
public bool $deleteVolumes = false) {}
|
||||
public bool $deleteConfigurations,
|
||||
public bool $deleteVolumes,
|
||||
public bool $dockerCleanup,
|
||||
public bool $deleteConnectedNetworks
|
||||
) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
case 'standalone-dragonfly':
|
||||
case 'standalone-clickhouse':
|
||||
$persistentStorages = $this->resource?->persistentStorages()?->get();
|
||||
StopDatabase::run($this->resource);
|
||||
StopDatabase::run($this->resource, true);
|
||||
break;
|
||||
case 'service':
|
||||
StopService::run($this->resource);
|
||||
DeleteService::run($this->resource);
|
||||
StopService::run($this->resource, true);
|
||||
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->deleteConfigurations) {
|
||||
$this->resource?->delete_configurations();
|
||||
}
|
||||
|
||||
$isDatabase = $this->resource instanceof StandalonePostgresql
|
||||
|| $this->resource instanceof StandaloneRedis
|
||||
|| $this->resource instanceof StandaloneMongodb
|
||||
|| $this->resource instanceof StandaloneMysql
|
||||
|| $this->resource instanceof StandaloneMariadb
|
||||
|| $this->resource instanceof StandaloneKeydb
|
||||
|| $this->resource instanceof StandaloneDragonfly
|
||||
|| $this->resource instanceof StandaloneClickhouse;
|
||||
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
|
||||
if (($this->dockerCleanup || $isDatabase) && $server) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
|
||||
if ($this->deleteConnectedNetworks && ! $isDatabase) {
|
||||
$this->resource?->delete_connected_networks($this->resource->uuid);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e->getMessage());
|
||||
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->resource->forceDelete();
|
||||
if ($this->dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
Artisan::queue('cleanup:stucked-resources');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -26,16 +25,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [new WithoutOverlapping($this->server->id)];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->server->id;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
@@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public GithubApp $github_app) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->github_app->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->github_app->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
@@ -19,17 +18,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
@@ -42,8 +31,8 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$current_version = $settings->helper_version;
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// New version available
|
||||
$helperImage = config('coolify.helper_image');
|
||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
|
||||
// $helperImage = config('coolify.helper_image');
|
||||
// instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
|
||||
$settings->update(['helper_version' => $latest_version]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle(): void
|
||||
|
||||
@@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ScheduledTaskJob implements ShouldQueue
|
||||
@@ -56,24 +55,17 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
{
|
||||
if ($this->resource instanceof Application) {
|
||||
$timezone = $this->resource->destination->server->settings->server_timezone;
|
||||
|
||||
return $timezone;
|
||||
} elseif ($this->resource instanceof Service) {
|
||||
$timezone = $this->resource->server->settings->server_timezone;
|
||||
|
||||
return $timezone;
|
||||
}
|
||||
|
||||
return 'UTC';
|
||||
}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [new WithoutOverlapping($this->task->id)];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->task->id;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
|
||||
@@ -94,12 +86,12 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
} elseif ($this->resource->type() == 'service') {
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
if (str(data_get($application, 'status'))->contains('running')) {
|
||||
$this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
|
||||
$this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid');
|
||||
}
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
if (str(data_get($database, 'status'))->contains('running')) {
|
||||
$this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
|
||||
$this->containers[] = data_get($database, 'name').'-'.data_get($this->resource, 'uuid');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -112,8 +104,8 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
}
|
||||
|
||||
foreach ($this->containers as $containerName) {
|
||||
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
|
||||
$cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
|
||||
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) {
|
||||
$cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'";
|
||||
$exec = "docker exec {$containerName} {$cmd}";
|
||||
$this->task_output = instant_remote_process([$exec], $this->server, true);
|
||||
$this->task_log->update([
|
||||
|
||||
@@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
@@ -24,7 +23,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 3;
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
|
||||
public $containers;
|
||||
|
||||
@@ -43,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
// public function middleware(): array
|
||||
// {
|
||||
// return [(new WithoutOverlapping($this->server->uuid))];
|
||||
// }
|
||||
|
||||
// public function uniqueId(): int
|
||||
// {
|
||||
// return $this->server->uuid;
|
||||
// }
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
@@ -91,7 +82,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private function serverStatus()
|
||||
{
|
||||
['uptime' => $uptime] = $this->server->validateConnection();
|
||||
['uptime' => $uptime] = $this->server->validateConnection(false);
|
||||
if ($uptime) {
|
||||
if ($this->server->unreachable_notification_sent === true) {
|
||||
$this->server->update(['unreachable_notification_sent' => false]);
|
||||
@@ -124,7 +115,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private function checkLogDrainContainer()
|
||||
{
|
||||
if(! $this->server->isLogDrainEnabled()) {
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
return;
|
||||
}
|
||||
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
|
||||
|
||||
@@ -10,7 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\Middleware\;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -26,16 +26,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Team $team) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->team->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->team->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (! $this->server->isServerReady($this->tries)) {
|
||||
|
||||
@@ -73,6 +73,8 @@ class Index extends Component
|
||||
}
|
||||
$this->privateKeyName = generate_random_name();
|
||||
$this->remoteServerName = generate_random_name();
|
||||
$this->remoteServerPort = $this->remoteServerPort;
|
||||
$this->remoteServerUser = $this->remoteServerUser;
|
||||
if (isDev()) {
|
||||
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
@@ -139,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
if (! $this->createdServer) {
|
||||
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
|
||||
}
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
|
||||
|
||||
return $this->validateServer('localhost');
|
||||
} elseif ($this->selectedServerType === 'remote') {
|
||||
@@ -154,6 +156,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
if ($this->servers->count() > 0) {
|
||||
$this->selectedExistingServer = $this->servers->first()->id;
|
||||
$this->updateServerDetails();
|
||||
$this->currentState = 'select-existing-server';
|
||||
|
||||
return;
|
||||
@@ -172,10 +175,19 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
return;
|
||||
}
|
||||
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
|
||||
$this->updateServerDetails();
|
||||
$this->currentState = 'validate-server';
|
||||
}
|
||||
|
||||
private function updateServerDetails()
|
||||
{
|
||||
if ($this->createdServer) {
|
||||
$this->remoteServerPort = $this->createdServer->port;
|
||||
$this->remoteServerUser = $this->createdServer->user;
|
||||
}
|
||||
}
|
||||
|
||||
public function getProxyType()
|
||||
{
|
||||
// Set Default Proxy Type
|
||||
@@ -219,27 +231,35 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
public function savePrivateKey()
|
||||
{
|
||||
$this->validate([
|
||||
'privateKeyName' => 'required',
|
||||
'privateKey' => 'required',
|
||||
'privateKeyName' => 'required|string|max:255',
|
||||
'privateKeyDescription' => 'nullable|string|max:255',
|
||||
'privateKey' => 'required|string',
|
||||
]);
|
||||
$this->createdPrivateKey = PrivateKey::create([
|
||||
'name' => $this->privateKeyName,
|
||||
'description' => $this->privateKeyDescription,
|
||||
'private_key' => $this->privateKey,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$this->createdPrivateKey->save();
|
||||
$this->currentState = 'create-server';
|
||||
|
||||
try {
|
||||
$privateKey = PrivateKey::createAndStore([
|
||||
'name' => $this->privateKeyName,
|
||||
'description' => $this->privateKeyDescription,
|
||||
'private_key' => $this->privateKey,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
||||
$this->createdPrivateKey = $privateKey;
|
||||
$this->currentState = 'create-server';
|
||||
} catch (\Exception $e) {
|
||||
$this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function saveServer()
|
||||
{
|
||||
$this->validate([
|
||||
'remoteServerName' => 'required',
|
||||
'remoteServerHost' => 'required',
|
||||
'remoteServerName' => 'required|string',
|
||||
'remoteServerHost' => 'required|string',
|
||||
'remoteServerPort' => 'required|integer',
|
||||
'remoteServerUser' => 'required',
|
||||
'remoteServerUser' => 'required|string',
|
||||
]);
|
||||
|
||||
$this->privateKey = formatPrivateKey($this->privateKey);
|
||||
$foundServer = Server::whereIp($this->remoteServerHost)->first();
|
||||
if ($foundServer) {
|
||||
@@ -269,7 +289,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
public function validateServer()
|
||||
{
|
||||
try {
|
||||
config()->set('coolify.mux_enabled', false);
|
||||
config()->set('constants.ssh.mux_enabled', false);
|
||||
|
||||
// EC2 does not have `uptime` command, lol
|
||||
instant_remote_process(['ls /'], $this->createdServer, true);
|
||||
@@ -277,9 +297,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$this->createdServer->settings()->update([
|
||||
'is_reachable' => true,
|
||||
]);
|
||||
$this->serverReachable = true;
|
||||
} catch (\Throwable $e) {
|
||||
$this->serverReachable = false;
|
||||
$this->createdServer->delete();
|
||||
$this->createdServer->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
|
||||
return handleError(error: $e, livewire: $this);
|
||||
}
|
||||
@@ -296,6 +319,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
]);
|
||||
$this->getProxyType();
|
||||
} catch (\Throwable $e) {
|
||||
$this->createdServer->settings()->update([
|
||||
'is_usable' => false,
|
||||
]);
|
||||
|
||||
return handleError(error: $e, livewire: $this);
|
||||
}
|
||||
}
|
||||
@@ -349,6 +376,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
);
|
||||
}
|
||||
|
||||
public function saveAndValidateServer()
|
||||
{
|
||||
$this->validate([
|
||||
'remoteServerPort' => 'required|integer|min:1|max:65535',
|
||||
'remoteServerUser' => 'required|string',
|
||||
]);
|
||||
|
||||
$this->createdServer->update([
|
||||
'port' => $this->remoteServerPort,
|
||||
'user' => $this->remoteServerUser,
|
||||
'timezone' => 'UTC',
|
||||
]);
|
||||
$this->validateServer();
|
||||
}
|
||||
|
||||
private function createNewPrivateKey()
|
||||
{
|
||||
$this->privateKeyName = generate_random_name();
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\CommandCenter;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
public $servers = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->servers = Server::isReachable()->get();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.command-center.index');
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ class Dashboard extends Component
|
||||
|
||||
public function cleanup_queue()
|
||||
{
|
||||
$this->dispatch('success', 'Cleanup started.');
|
||||
Artisan::queue('cleanup:application-deployment-queue', [
|
||||
'--team-id' => currentTeam()->id,
|
||||
]);
|
||||
|
||||
@@ -38,7 +38,7 @@ class Form extends Component
|
||||
}
|
||||
$this->destination->delete();
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
return redirect()->route('destination.all');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,28 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class NavbarDeleteTeam extends Component
|
||||
{
|
||||
public function delete()
|
||||
public $team;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->team = currentTeam()->name;
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$currentTeam = currentTeam();
|
||||
$currentTeam->delete();
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application\Deployment;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
|
||||
@@ -21,6 +21,8 @@ class Heading extends Component
|
||||
|
||||
protected string $deploymentUuid;
|
||||
|
||||
public bool $docker_cleanup = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
@@ -102,7 +104,7 @@ class Heading extends Component
|
||||
|
||||
public function stop()
|
||||
{
|
||||
StopApplication::run($this->application);
|
||||
StopApplication::run($this->application, false, $this->docker_cleanup);
|
||||
$this->application->status = 'exited';
|
||||
$this->application->save();
|
||||
if ($this->application->additional_servers->count() > 0) {
|
||||
@@ -135,4 +137,13 @@ class Heading extends Component
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.heading', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace App\Livewire\Project\Application;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -184,17 +186,20 @@ class Previews extends Component
|
||||
public function stop(int $pull_request_id)
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$timeout = 300;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
|
||||
foreach ($containers as $container) {
|
||||
$name = str_replace('/', '', $container['Names']);
|
||||
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
|
||||
}
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||
$this->stopContainers($containers, $server, $timeout);
|
||||
}
|
||||
GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
|
||||
$this->dispatch('reloadWindow');
|
||||
|
||||
GetContainersStatus::run($server);
|
||||
$this->application->refresh();
|
||||
$this->dispatch('containerStatusUpdated');
|
||||
$this->dispatch('success', 'Preview Deployment stopped.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -203,16 +208,21 @@ class Previews extends Component
|
||||
public function delete(int $pull_request_id)
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$timeout = 300;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
|
||||
foreach ($containers as $container) {
|
||||
$name = str_replace('/', '', $container['Names']);
|
||||
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
|
||||
}
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||
$this->stopContainers($containers, $server, $timeout);
|
||||
}
|
||||
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
|
||||
|
||||
ApplicationPreview::where('application_id', $this->application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first()
|
||||
->delete();
|
||||
|
||||
$this->application->refresh();
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Preview deleted.');
|
||||
@@ -220,4 +230,49 @@ class Previews extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainers(array $containers, $server, int $timeout)
|
||||
{
|
||||
$processes = [];
|
||||
foreach ($containers as $container) {
|
||||
$containerName = str_replace('/', '', $container['Names']);
|
||||
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
|
||||
}
|
||||
|
||||
$startTime = time();
|
||||
while (count($processes) > 0) {
|
||||
$finishedProcesses = array_filter($processes, function ($process) {
|
||||
return ! $process->running();
|
||||
});
|
||||
foreach (array_keys($finishedProcesses) as $containerName) {
|
||||
unset($processes[$containerName]);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->forceStopRemainingContainers(array_keys($processes), $server);
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName, $server)
|
||||
{
|
||||
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
|
||||
}
|
||||
|
||||
private function forceStopRemainingContainers(array $containerNames, $server)
|
||||
{
|
||||
foreach ($containerNames as $containerName) {
|
||||
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
@@ -12,6 +14,12 @@ class BackupEdit extends Component
|
||||
|
||||
public $s3s;
|
||||
|
||||
public bool $delete_associated_backups_locally = false;
|
||||
|
||||
public bool $delete_associated_backups_s3 = false;
|
||||
|
||||
public bool $delete_associated_backups_sftp = false;
|
||||
|
||||
public ?string $status = null;
|
||||
|
||||
public array $parameters;
|
||||
@@ -46,10 +54,24 @@ class BackupEdit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->delete_associated_backups_locally) {
|
||||
$this->deleteAssociatedBackupsLocally();
|
||||
}
|
||||
if ($this->delete_associated_backups_s3) {
|
||||
$this->deleteAssociatedBackupsS3();
|
||||
}
|
||||
|
||||
$this->backup->delete();
|
||||
|
||||
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$previousUrl = url()->previous();
|
||||
$url = Url::fromString($previousUrl);
|
||||
@@ -104,4 +126,66 @@ class BackupEdit extends Component
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsLocally()
|
||||
{
|
||||
$executions = $this->backup->executions;
|
||||
$backupFolder = null;
|
||||
|
||||
foreach ($executions as $execution) {
|
||||
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$server = $this->backup->database->service->destination->server;
|
||||
} else {
|
||||
$server = $this->backup->database->destination->server;
|
||||
}
|
||||
|
||||
if (! $backupFolder) {
|
||||
$backupFolder = dirname($execution->filename);
|
||||
}
|
||||
|
||||
delete_backup_locally($execution->filename, $server);
|
||||
$execution->delete();
|
||||
}
|
||||
|
||||
if ($backupFolder) {
|
||||
$this->deleteEmptyBackupFolder($backupFolder, $server);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsS3()
|
||||
{
|
||||
//Add function to delete backups from S3
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsSftp()
|
||||
{
|
||||
//Add function to delete backups from SFTP
|
||||
}
|
||||
|
||||
private function deleteEmptyBackupFolder($folderPath, $server)
|
||||
{
|
||||
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
|
||||
|
||||
if (trim($checkEmpty) === 'empty') {
|
||||
instant_remote_process(["rmdir '$folderPath'"], $server);
|
||||
|
||||
$parentFolder = dirname($folderPath);
|
||||
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
|
||||
|
||||
if (trim($checkParentEmpty) === 'empty') {
|
||||
instant_remote_process(["rmdir '$parentFolder'"], $server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.backup-edit', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,28 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
{
|
||||
public ?ScheduledDatabaseBackup $backup = null;
|
||||
|
||||
public $database;
|
||||
|
||||
public $executions = [];
|
||||
|
||||
public $setDeletableBackup;
|
||||
|
||||
public $delete_backup_s3 = true;
|
||||
|
||||
public $delete_backup_sftp = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
|
||||
@@ -31,19 +41,36 @@ class BackupExecutions extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteBackup($exeuctionId)
|
||||
#[On('deleteBackup')]
|
||||
public function deleteBackup($executionId, $password)
|
||||
{
|
||||
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$execution = $this->backup->executions()->where('id', $executionId)->first();
|
||||
if (is_null($execution)) {
|
||||
$this->dispatch('error', 'Backup execution not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
|
||||
} else {
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
|
||||
}
|
||||
|
||||
if ($this->delete_backup_s3) {
|
||||
// Add logic to delete from S3
|
||||
}
|
||||
|
||||
if ($this->delete_backup_sftp) {
|
||||
// Add logic to delete from SFTP
|
||||
}
|
||||
|
||||
$execution->delete();
|
||||
$this->dispatch('success', 'Backup deleted.');
|
||||
$this->refreshBackupExecutions();
|
||||
@@ -82,16 +109,18 @@ class BackupExecutions extends Component
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getServerTimezone()
|
||||
{
|
||||
$server = $this->server();
|
||||
if (!$server) {
|
||||
if (! $server) {
|
||||
return 'UTC';
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
|
||||
return $serverTimezone;
|
||||
}
|
||||
|
||||
@@ -104,6 +133,17 @@ class BackupExecutions extends Component
|
||||
} catch (\Exception $e) {
|
||||
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
return $dateObj->format('Y-m-d H:i:s T');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.backup-executions', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
|
||||
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class General extends Component
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
if (!$this->server->isLogDrainEnabled()) {
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
@@ -73,14 +73,14 @@ class General extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->database->is_public && !$this->database->public_port) {
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if (!str($this->database->status)->startsWith('running')) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
@@ -95,7 +95,7 @@ class General extends Component
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class General extends Component
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
if (!$this->server->isLogDrainEnabled()) {
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
@@ -88,14 +88,14 @@ class General extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->database->is_public && !$this->database->public_port) {
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if (!str($this->database->status)->startsWith('running')) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
@@ -110,7 +110,7 @@ class General extends Component
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ class Heading extends Component
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
@@ -54,7 +56,7 @@ class Heading extends Component
|
||||
|
||||
public function stop()
|
||||
{
|
||||
StopDatabase::run($this->database);
|
||||
StopDatabase::run($this->database, false, $this->docker_cleanup);
|
||||
$this->database->status = 'exited';
|
||||
$this->database->save();
|
||||
$this->check_status();
|
||||
@@ -71,4 +73,13 @@ class Heading extends Component
|
||||
$activity = StartDatabase::run($this->database);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.heading', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class General extends Component
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
if (!$this->server->isLogDrainEnabled()) {
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
@@ -94,14 +94,14 @@ class General extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->database->is_public && !$this->database->public_port) {
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if (!str($this->database->status)->startsWith('running')) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
@@ -116,7 +116,7 @@ class General extends Component
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class General extends Component
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
if (!$this->server->isLogDrainEnabled()) {
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
@@ -100,14 +100,14 @@ class General extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->database->is_public && !$this->database->public_port) {
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if (!str($this->database->status)->startsWith('running')) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
@@ -122,7 +122,7 @@ class General extends Component
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class General extends Component
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
if (!$this->server->isLogDrainEnabled()) {
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
@@ -101,14 +101,14 @@ class General extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->database->is_public && !$this->database->public_port) {
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if (!str($this->database->status)->startsWith('running')) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
@@ -123,7 +123,7 @@ class General extends Component
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class General extends Component
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
if (!$this->server->isLogDrainEnabled()) {
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
@@ -99,14 +99,14 @@ class General extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->database->is_public && !$this->database->public_port) {
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if (!str($this->database->status)->startsWith('running')) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
@@ -121,7 +121,7 @@ class General extends Component
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class General extends Component
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
if (!$this->server->isLogDrainEnabled()) {
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
@@ -88,14 +88,14 @@ class General extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->database->is_public && !$this->database->public_port) {
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if (!str($this->database->status)->startsWith('running')) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
|
||||
@@ -110,7 +110,7 @@ class General extends Component
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
|
||||
|
||||
public bool $disabled = false;
|
||||
|
||||
public string $environmentName = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
|
||||
@@ -13,9 +13,12 @@ class DeleteProject extends Component
|
||||
|
||||
public bool $disabled = false;
|
||||
|
||||
public string $projectName = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->projectName = Project::findOrFail($this->project_id)->name;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
|
||||
@@ -52,7 +52,7 @@ class Configuration extends Component
|
||||
$application = $this->service->applications->find($id);
|
||||
if ($application) {
|
||||
$application->restart();
|
||||
$this->dispatch('success', 'Application restarted successfully.');
|
||||
$this->dispatch('success', 'Service application restarted successfully.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -65,7 +65,7 @@ class Configuration extends Component
|
||||
$database = $this->service->databases->find($id);
|
||||
if ($database) {
|
||||
$database->restart();
|
||||
$this->dispatch('success', 'Database restarted successfully.');
|
||||
$this->dispatch('success', 'Service database restarted successfully.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class FileStorage extends Component
|
||||
@@ -83,8 +85,14 @@ class FileStorage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$message = 'File deleted.';
|
||||
if ($this->fileStorage->is_directory) {
|
||||
@@ -129,6 +137,13 @@ class FileStorage extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.file-storage');
|
||||
return view('livewire.project.service.file-storage', [
|
||||
'directoryDeletionCheckboxes' => [
|
||||
['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
|
||||
],
|
||||
'fileDeletionCheckboxes' => [
|
||||
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ class Navbar extends Component
|
||||
|
||||
public $isDeploymentProgress = false;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public $title = 'Configuration';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
|
||||
@@ -40,7 +44,7 @@ class Navbar extends Component
|
||||
|
||||
public function serviceStarted()
|
||||
{
|
||||
$this->dispatch('success', 'Service status changed.');
|
||||
// $this->dispatch('success', 'Service status changed.');
|
||||
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
|
||||
$this->service->isConfigurationChanged(true);
|
||||
$this->dispatch('configurationChanged');
|
||||
@@ -60,11 +64,6 @@ class Navbar extends Component
|
||||
$this->dispatch('success', 'Service status updated.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.navbar');
|
||||
}
|
||||
|
||||
public function checkDeployments()
|
||||
{
|
||||
try {
|
||||
@@ -95,14 +94,9 @@ class Navbar extends Component
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function stop(bool $forceCleanup = false)
|
||||
public function stop()
|
||||
{
|
||||
StopService::run($this->service);
|
||||
if ($forceCleanup) {
|
||||
$this->dispatch('success', 'Containers cleaned up.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service stopped.');
|
||||
}
|
||||
StopService::run($this->service, false, $this->docker_cleanup);
|
||||
ServiceStatusChanged::dispatch();
|
||||
}
|
||||
|
||||
@@ -121,4 +115,13 @@ class Navbar extends Component
|
||||
$activity = StartService::run($this->service);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.navbar', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class ServiceApplicationView extends Component
|
||||
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
|
||||
|
||||
public $parameters;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public $delete_volumes = true;
|
||||
|
||||
protected $rules = [
|
||||
'application.human_name' => 'nullable',
|
||||
'application.description' => 'nullable',
|
||||
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
|
||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
||||
];
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.service-application-view');
|
||||
}
|
||||
|
||||
public function updatedApplicationFqdn()
|
||||
{
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->application->delete();
|
||||
$this->dispatch('success', 'Application deleted.');
|
||||
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
|
||||
$this->dispatch('generateDockerCompose');
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.service-application-view', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -10,6 +15,8 @@ class Danger extends Component
|
||||
{
|
||||
public $resource;
|
||||
|
||||
public $resourceName;
|
||||
|
||||
public $projectUuid;
|
||||
|
||||
public $environmentName;
|
||||
@@ -18,22 +25,93 @@ class Danger extends Component
|
||||
|
||||
public bool $delete_volumes = true;
|
||||
|
||||
public bool $docker_cleanup = true;
|
||||
|
||||
public bool $delete_connected_networks = true;
|
||||
|
||||
public ?string $modalId = null;
|
||||
|
||||
public string $resourceDomain = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->modalId = new Cuid2;
|
||||
$parameters = get_route_parameters();
|
||||
$this->modalId = new Cuid2;
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentName = data_get($parameters, 'environment_name');
|
||||
|
||||
if ($this->resource === null) {
|
||||
if (isset($parameters['service_uuid'])) {
|
||||
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
|
||||
} elseif (isset($parameters['stack_service_uuid'])) {
|
||||
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
|
||||
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->resource === null) {
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! method_exists($this->resource, 'type')) {
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$this->resourceName = $this->resource->name ?? 'Application';
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
case 'standalone-redis':
|
||||
case 'standalone-mongodb':
|
||||
case 'standalone-mysql':
|
||||
case 'standalone-mariadb':
|
||||
case 'standalone-keydb':
|
||||
case 'standalone-dragonfly':
|
||||
case 'standalone-clickhouse':
|
||||
$this->resourceName = $this->resource->name ?? 'Database';
|
||||
break;
|
||||
case 'service':
|
||||
$this->resourceName = $this->resource->name ?? 'Service';
|
||||
break;
|
||||
case 'service-application':
|
||||
$this->resourceName = $this->resource->name ?? 'Service Application';
|
||||
break;
|
||||
case 'service-database':
|
||||
$this->resourceName = $this->resource->name ?? 'Service Database';
|
||||
break;
|
||||
default:
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->resource) {
|
||||
$this->addError('resource', 'Resource not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// $this->authorize('delete', $this->resource);
|
||||
$this->resource->delete();
|
||||
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
|
||||
DeleteResourceJob::dispatch(
|
||||
$this->resource,
|
||||
$this->delete_configurations,
|
||||
$this->delete_volumes,
|
||||
$this->docker_cleanup,
|
||||
$this->delete_connected_networks
|
||||
);
|
||||
|
||||
return redirect()->route('project.resource.index', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
@@ -43,4 +121,19 @@ class Danger extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.danger', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
|
||||
['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
|
||||
['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use App\Events\ApplicationStatusChanged;
|
||||
use App\Jobs\ContainerStatusJob;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -115,8 +117,14 @@ class Destination extends Component
|
||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||
}
|
||||
|
||||
public function removeServer(int $network_id, int $server_id)
|
||||
public function removeServer(int $network_id, int $server_id, $password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
|
||||
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Actions\Server\RunCommand;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class ExecuteContainerCommand extends Component
|
||||
{
|
||||
public string $command;
|
||||
|
||||
public string $container;
|
||||
public $container;
|
||||
|
||||
public Collection $containers;
|
||||
|
||||
@@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component
|
||||
|
||||
public string $type;
|
||||
|
||||
public string $workDir = '';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public Collection $servers;
|
||||
@@ -33,11 +29,13 @@ class ExecuteContainerCommand extends Component
|
||||
'server' => 'required',
|
||||
'container' => 'required',
|
||||
'command' => 'required',
|
||||
'workDir' => 'nullable',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (! auth()->user()->isAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->containers = collect();
|
||||
$this->servers = collect();
|
||||
@@ -62,24 +60,13 @@ class ExecuteContainerCommand extends Component
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
$this->container = $this->resource->uuid;
|
||||
$this->containers->push($this->container);
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
$this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
$this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
|
||||
});
|
||||
if ($this->resource->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->server);
|
||||
}
|
||||
}
|
||||
if ($this->containers->count() > 0) {
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function loadContainers()
|
||||
@@ -102,44 +89,65 @@ class ExecuteContainerCommand extends Component
|
||||
];
|
||||
$this->containers = $this->containers->push($payload);
|
||||
}
|
||||
} elseif (data_get($this->parameters, 'database_uuid')) {
|
||||
if ($this->resource->isRunning()) {
|
||||
$this->containers = $this->containers->push([
|
||||
'server' => $server,
|
||||
'container' => [
|
||||
'Names' => $this->resource->uuid,
|
||||
],
|
||||
]);
|
||||
}
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
ray($application);
|
||||
if ($application->isRunning()) {
|
||||
$this->containers->push([
|
||||
'server' => $this->resource->server,
|
||||
'container' => [
|
||||
'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
if ($database->isRunning()) {
|
||||
$this->containers->push([
|
||||
'server' => $this->resource->server,
|
||||
'container' => [
|
||||
'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
if ($this->containers->count() > 0) {
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->container = data_get($this->containers->first(), 'container.Names');
|
||||
} elseif (data_get($this->parameters, 'database_uuid')) {
|
||||
$this->container = $this->containers->first();
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function runCommand()
|
||||
#[On('connectToContainer')]
|
||||
public function connectToContainer()
|
||||
{
|
||||
try {
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$container = $this->containers->where('container.Names', $this->container)->first();
|
||||
$container_name = data_get($container, 'container.Names');
|
||||
if (is_null($container)) {
|
||||
throw new \RuntimeException('Container not found.');
|
||||
}
|
||||
$server = data_get($container, 'server');
|
||||
} else {
|
||||
$container_name = $this->container;
|
||||
$server = $this->servers->first();
|
||||
$container_name = data_get($this->container, 'container.Names');
|
||||
if (is_null($container_name)) {
|
||||
throw new \RuntimeException('Container not found.');
|
||||
}
|
||||
$server = data_get($this->container, 'server');
|
||||
|
||||
if ($server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
$cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
|
||||
if (! empty($this->workDir)) {
|
||||
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
|
||||
} else {
|
||||
$exec = "docker exec {$container_name} {$cmd}";
|
||||
}
|
||||
$activity = RunCommand::run(server: $server, command: $exec);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
|
||||
$this->dispatch('send-terminal-command',
|
||||
true,
|
||||
$container_name,
|
||||
$server->uuid,
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
@@ -108,14 +109,14 @@ class GetLogs extends Component
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
} else {
|
||||
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
}
|
||||
} else {
|
||||
if ($this->server->isSwarm()) {
|
||||
@@ -124,14 +125,14 @@ class GetLogs extends Component
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
} else {
|
||||
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
}
|
||||
}
|
||||
if ($refresh) {
|
||||
|
||||
@@ -7,7 +7,9 @@ use Livewire\Component;
|
||||
class Executions extends Component
|
||||
{
|
||||
public $executions = [];
|
||||
|
||||
public $selectedKey;
|
||||
|
||||
public $task;
|
||||
|
||||
public function getListeners()
|
||||
@@ -29,7 +31,7 @@ class Executions extends Component
|
||||
|
||||
public function server()
|
||||
{
|
||||
if (!$this->task) {
|
||||
if (! $this->task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -42,16 +44,18 @@ class Executions extends Component
|
||||
return $this->task->service->destination->server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getServerTimezone()
|
||||
{
|
||||
$server = $this->server();
|
||||
if (!$server) {
|
||||
if (! $server) {
|
||||
return 'UTC';
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
|
||||
return $serverTimezone;
|
||||
}
|
||||
|
||||
@@ -64,6 +68,7 @@ class Executions extends Component
|
||||
} catch (\Exception $e) {
|
||||
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
return $dateObj->format('Y-m-d H:i:s T');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ class Show extends Component
|
||||
|
||||
public string $type;
|
||||
|
||||
public string $scheduledTaskName;
|
||||
|
||||
protected $rules = [
|
||||
'task.enabled' => 'required|boolean',
|
||||
'task.name' => 'required|string',
|
||||
@@ -49,6 +51,7 @@ class Show extends Component
|
||||
|
||||
$this->modalId = new Cuid2;
|
||||
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
|
||||
$this->scheduledTaskName = $this->task->name;
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
@@ -75,9 +78,9 @@ class Show extends Component
|
||||
$this->task->delete();
|
||||
|
||||
if ($this->type == 'application') {
|
||||
return redirect()->route('project.application.configuration', $this->parameters);
|
||||
return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
|
||||
} else {
|
||||
return redirect()->route('project.service.configuration', $this->parameters);
|
||||
return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Shared\Storages;
|
||||
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
@@ -36,8 +38,14 @@ class Show extends Component
|
||||
$this->dispatch('success', 'Storage updated successfully');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->storage->delete();
|
||||
$this->dispatch('refreshStorages');
|
||||
}
|
||||
|
||||
44
app/Livewire/Project/Shared/Terminal.php
Normal file
44
app/Livewire/Project/Shared/Terminal.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Server;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class Terminal extends Component
|
||||
{
|
||||
#[On('send-terminal-command')]
|
||||
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
|
||||
{
|
||||
|
||||
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
|
||||
|
||||
if ($isContainer) {
|
||||
$status = getContainerStatus($server, $identifier);
|
||||
if ($status !== 'running') {
|
||||
return;
|
||||
}
|
||||
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
|
||||
} else {
|
||||
$command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
|
||||
}
|
||||
|
||||
// ssh command is sent back to frontend then to websocket
|
||||
// this is done because the websocket connection is not available here
|
||||
// a better solution would be to remove websocket on NodeJS and work with something like
|
||||
// 1. Laravel Pusher/Echo connection (not possible without a sdk)
|
||||
// 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
|
||||
// 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
|
||||
// 4. Follow-up discussions here:
|
||||
// - https://github.com/coollabsio/coolify/issues/2298
|
||||
// - https://github.com/coollabsio/coolify/discussions/3362
|
||||
$this->dispatch('send-back-command', $command);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.terminal');
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Actions\Server\RunCommand as ServerRunCommand;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class RunCommand extends Component
|
||||
{
|
||||
public string $command;
|
||||
|
||||
public $server;
|
||||
|
||||
public $servers = [];
|
||||
|
||||
protected $rules = [
|
||||
'server' => 'required',
|
||||
'command' => 'required',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'server' => 'server',
|
||||
'command' => 'command',
|
||||
];
|
||||
|
||||
public function mount($servers)
|
||||
{
|
||||
$this->servers = $servers;
|
||||
$this->server = $servers[0]->uuid;
|
||||
}
|
||||
|
||||
public function runCommand()
|
||||
{
|
||||
$this->validate();
|
||||
try {
|
||||
$activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,13 @@
|
||||
namespace App\Livewire\Security\PrivateKey;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Livewire\Component;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
class Create extends Component
|
||||
{
|
||||
use WithRateLimiting;
|
||||
public string $name = '';
|
||||
|
||||
public string $name;
|
||||
|
||||
public string $value;
|
||||
public string $value = '';
|
||||
|
||||
public ?string $from = null;
|
||||
|
||||
@@ -26,72 +22,69 @@ class Create extends Component
|
||||
'value' => 'required|string',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'name' => 'name',
|
||||
'value' => 'private Key',
|
||||
];
|
||||
|
||||
public function generateNewRSAKey()
|
||||
{
|
||||
try {
|
||||
$this->rateLimit(10);
|
||||
$this->name = generate_random_name();
|
||||
$this->description = 'Created by Coolify';
|
||||
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->generateNewKey('rsa');
|
||||
}
|
||||
|
||||
public function generateNewEDKey()
|
||||
{
|
||||
try {
|
||||
$this->rateLimit(10);
|
||||
$this->name = generate_random_name();
|
||||
$this->description = 'Created by Coolify';
|
||||
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->generateNewKey('ed25519');
|
||||
}
|
||||
|
||||
public function updated($updateProperty)
|
||||
private function generateNewKey($type)
|
||||
{
|
||||
if ($updateProperty === 'value') {
|
||||
try {
|
||||
$this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->$updateProperty === '') {
|
||||
$this->publicKey = '';
|
||||
} else {
|
||||
$this->publicKey = 'Invalid private key';
|
||||
}
|
||||
}
|
||||
$keyData = PrivateKey::generateNewKeyPair($type);
|
||||
$this->setKeyData($keyData);
|
||||
}
|
||||
|
||||
public function updated($property)
|
||||
{
|
||||
if ($property === 'value') {
|
||||
$this->validatePrivateKey();
|
||||
}
|
||||
$this->validateOnly($updateProperty);
|
||||
}
|
||||
|
||||
public function createPrivateKey()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
$this->value = trim($this->value);
|
||||
if (! str_ends_with($this->value, "\n")) {
|
||||
$this->value .= "\n";
|
||||
}
|
||||
$private_key = PrivateKey::create([
|
||||
$privateKey = PrivateKey::createAndStore([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'private_key' => $this->value,
|
||||
'private_key' => trim($this->value)."\n",
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
if ($this->from === 'server') {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
|
||||
return $this->redirectAfterCreation($privateKey);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function setKeyData(array $keyData)
|
||||
{
|
||||
$this->name = $keyData['name'];
|
||||
$this->description = $keyData['description'];
|
||||
$this->value = $keyData['private_key'];
|
||||
$this->publicKey = $keyData['public_key'];
|
||||
}
|
||||
|
||||
private function validatePrivateKey()
|
||||
{
|
||||
$validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
|
||||
$this->publicKey = $validationResult['publicKey'];
|
||||
|
||||
if (! $validationResult['isValid']) {
|
||||
$this->addError('value', 'Invalid private key');
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectAfterCreation(PrivateKey $privateKey)
|
||||
{
|
||||
return $this->from === 'server'
|
||||
? redirect()->route('dashboard')
|
||||
: redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
|
||||
}
|
||||
}
|
||||
|
||||
24
app/Livewire/Security/PrivateKey/Index.php
Normal file
24
app/Livewire/Security/PrivateKey/Index.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Security\PrivateKey;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
$privateKeys = PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get();
|
||||
|
||||
return view('livewire.security.private-key.index', [
|
||||
'privateKeys' => $privateKeys,
|
||||
])->layout('components.layout');
|
||||
}
|
||||
|
||||
public function cleanupUnusedKeys()
|
||||
{
|
||||
PrivateKey::cleanupUnusedKeys();
|
||||
$this->dispatch('success', 'Unused keys have been cleaned up.');
|
||||
}
|
||||
}
|
||||
@@ -29,25 +29,27 @@ class Show extends Component
|
||||
try {
|
||||
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadPublicKey()
|
||||
{
|
||||
$this->public_key = $this->private_key->publicKey();
|
||||
$this->public_key = $this->private_key->getPublicKey();
|
||||
if ($this->public_key === 'Error loading private key') {
|
||||
$this->dispatch('error', 'Failed to load public key. The private key may be invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
try {
|
||||
if ($this->private_key->isEmpty()) {
|
||||
$this->private_key->delete();
|
||||
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
|
||||
$this->private_key->safeDelete();
|
||||
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
|
||||
|
||||
return redirect()->route('security.private-key.index');
|
||||
}
|
||||
$this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
|
||||
return redirect()->route('security.private-key.index');
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -56,8 +58,9 @@ class Show extends Component
|
||||
public function changePrivateKey()
|
||||
{
|
||||
try {
|
||||
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
|
||||
$this->private_key->save();
|
||||
$this->private_key->updatePrivateKey([
|
||||
'private_key' => formatPrivateKey($this->private_key->private_key),
|
||||
]);
|
||||
refresh_server_connection($this->private_key);
|
||||
$this->dispatch('success', 'Private key updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component
|
||||
{
|
||||
try {
|
||||
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
|
||||
ConfigureCloudflared::run($server, $this->cloudflare_token);
|
||||
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
|
||||
$server->settings->is_cloudflare_tunnel = true;
|
||||
$server->ip = $this->ssh_domain;
|
||||
$server->save();
|
||||
$server->settings->save();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Delete extends Component
|
||||
@@ -11,8 +13,13 @@ class Delete extends Component
|
||||
|
||||
public $server;
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->authorize('delete', $this->server);
|
||||
if ($this->server->hasDefinedResources()) {
|
||||
|
||||
@@ -24,7 +24,16 @@ class Form extends Component
|
||||
|
||||
public $timezones;
|
||||
|
||||
protected $listeners = ['serverInstalled', 'revalidate' => '$refresh'];
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
|
||||
'refreshServerShow' => 'serverInstalled',
|
||||
'revalidate' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'server.name' => 'required',
|
||||
@@ -92,6 +101,12 @@ class Form extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function cloudflareTunnelConfigured()
|
||||
{
|
||||
$this->serverInstalled();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
|
||||
}
|
||||
|
||||
public function serverInstalled()
|
||||
{
|
||||
$this->server->refresh();
|
||||
@@ -234,4 +249,12 @@ class Form extends Component
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server timezone updated.');
|
||||
}
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Livewire\Server\New;
|
||||
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class ByIp extends Component
|
||||
@@ -40,7 +40,7 @@ class ByIp extends Component
|
||||
|
||||
public bool $is_build_server = false;
|
||||
|
||||
public $swarm_managers = [];
|
||||
public Collection $swarm_managers;
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|string',
|
||||
@@ -102,11 +102,6 @@ class ByIp extends Component
|
||||
'port' => $this->port,
|
||||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
'proxy' => [
|
||||
// set default proxy type to traefik v2
|
||||
'type' => ProxyTypes::TRAEFIK->value,
|
||||
'status' => ProxyStatus::EXITED->value,
|
||||
],
|
||||
];
|
||||
if ($this->is_swarm_worker) {
|
||||
$payload['swarm_cluster'] = $this->selected_swarm_cluster;
|
||||
@@ -115,6 +110,9 @@ class ByIp extends Component
|
||||
data_forget($payload, 'proxy');
|
||||
}
|
||||
$server = Server::create($payload);
|
||||
$server->proxy->set('status', 'exited');
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
if ($this->is_build_server) {
|
||||
$this->is_swarm_manager = false;
|
||||
$this->is_swarm_worker = false;
|
||||
|
||||
@@ -39,6 +39,7 @@ class Proxy extends Component
|
||||
{
|
||||
$this->server->proxy = null;
|
||||
$this->server->save();
|
||||
$this->dispatch('proxyChanged');
|
||||
}
|
||||
|
||||
public function selectProxy($proxy_type)
|
||||
@@ -47,7 +48,7 @@ class Proxy extends Component
|
||||
$this->server->proxy->set('type', $proxy_type);
|
||||
$this->server->save();
|
||||
$this->selectedProxy = $this->server->proxy->type;
|
||||
if ($this->selectedProxy !== 'NONE') {
|
||||
if ($this->server->proxySet()) {
|
||||
StartProxy::run($this->server, false);
|
||||
}
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
|
||||
@@ -6,6 +6,8 @@ use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Component;
|
||||
|
||||
class Deploy extends Component
|
||||
@@ -29,6 +31,7 @@ class Deploy extends Component
|
||||
'serverRefresh' => 'proxyStatusUpdated',
|
||||
'checkProxy',
|
||||
'startProxy',
|
||||
'proxyChanged' => 'proxyStatusUpdated',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -94,21 +97,43 @@ class Deploy extends Component
|
||||
public function stop(bool $forceStop = true)
|
||||
{
|
||||
try {
|
||||
if ($this->server->isSwarm()) {
|
||||
instant_remote_process([
|
||||
'docker service rm coolify-proxy_traefik',
|
||||
], $this->server);
|
||||
} else {
|
||||
instant_remote_process([
|
||||
'docker rm -f coolify-proxy',
|
||||
], $this->server);
|
||||
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$timeout = 30;
|
||||
|
||||
$process = $this->stopContainer($containerName, $timeout);
|
||||
|
||||
$startTime = time();
|
||||
while ($process->running()) {
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->forceStopContainer($containerName);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->proxy->force_stop = $forceStop;
|
||||
$this->server->save();
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
|
||||
$this->removeContainer($containerName);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->server->proxy->force_stop = $forceStop;
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
private function forceStopContainer(string $containerName)
|
||||
{
|
||||
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName)
|
||||
{
|
||||
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user