Merge branch 'next' into next
This commit is contained in:
		| @@ -6,13 +6,7 @@ APP_KEY= | |||||||
| APP_URL=http://localhost | APP_URL=http://localhost | ||||||
| APP_PORT=8000 | APP_PORT=8000 | ||||||
| APP_DEBUG=true | APP_DEBUG=true | ||||||
| MUX_ENABLED=false | SSH_MUX_ENABLED=true | ||||||
|  |  | ||||||
| # Enable Laravel Telescope for debugging |  | ||||||
| TELESCOPE_ENABLED=false |  | ||||||
|  |  | ||||||
| # Selenium Driver URL for Dusk |  | ||||||
| DUSK_DRIVER_URL=http://selenium:4444 |  | ||||||
|  |  | ||||||
| # PostgreSQL Database Configuration | # PostgreSQL Database Configuration | ||||||
| DB_DATABASE=coolify | DB_DATABASE=coolify | ||||||
| @@ -25,7 +19,13 @@ DB_PORT=5432 | |||||||
| # Set to true to enable Ray | # Set to true to enable Ray | ||||||
| RAY_ENABLED=false | RAY_ENABLED=false | ||||||
| # Set custom ray port | # 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 | # Special Keys for Andras | ||||||
| # For cache purging | # 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 | name: 🐞 Bug Report | ||||||
| description: "Create a new bug report." | description: "File a new bug report." | ||||||
| title: "[Bug]: " | title: "[Bug]: " | ||||||
|  | labels: ["🐛 Bug", "🔍 Triage"] | ||||||
| body: | body: | ||||||
|   - type: markdown |   - type: markdown | ||||||
|     attributes: |     attributes: | ||||||
|       value: >- |       value: | | ||||||
|         # 💎 Bounty program (with |         > [!IMPORTANT] | ||||||
|         [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) |         > **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 |   - type: textarea | ||||||
|     attributes: |     attributes: | ||||||
|       label: Description |       label: Error Message and Logs | ||||||
|       description: A clear and concise description of the problem |       description: Provide a detailed description of the error or exception you encountered, along with any relevant log output. | ||||||
|   - type: textarea |  | ||||||
|     attributes: |  | ||||||
|       label: Minimal Reproduction (if possible, example repository) |  | ||||||
|       description: Please provide a step by step guide to reproduce the issue. |  | ||||||
|     validations: |     validations: | ||||||
|       required: true |       required: true | ||||||
|  |  | ||||||
|   - type: textarea |   - type: textarea | ||||||
|     attributes: |     attributes: | ||||||
|       label: Exception or Error |       label: Steps to Reproduce | ||||||
|       description: Please provide error logs if possible. |       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 |   - type: input | ||||||
|     attributes: |     attributes: | ||||||
|       label: Version |       label: Example Repository URL | ||||||
|       description: Coolify's version (see top of your screen). |       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: |     validations: | ||||||
|       required: true |       required: true | ||||||
|   - type: checkboxes |  | ||||||
|  |   - type: dropdown | ||||||
|     attributes: |     attributes: | ||||||
|       label: Cloud? |       label: Are you using Coolify Cloud? | ||||||
|       description: "Are you using the cloud version of Coolify?" |  | ||||||
|       options: |       options: | ||||||
|         - label: 'Yes' |         - "No (self-hosted)" | ||||||
|           required: false |         - "Yes (Coolify Cloud)" | ||||||
|         - label: 'No' |     validations: | ||||||
|           required: false |       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 | blank_issues_enabled: false | ||||||
|  |  | ||||||
| contact_links: | contact_links: | ||||||
|   - name: 🤔 Community Support (Chat) |   - name: 🤔 Questions and Community Support | ||||||
|     url: https://coollabs.io/discord |     url: https://coollabs.io/discord | ||||||
|     about: Reach out to us on Discord. |     about: If you have any questions, reach out to us on Discord inside the "#support" channel. | ||||||
|   - name: 🙋♂️ Feature Requests |  | ||||||
|     url: https://github.com/coollabsio/coolify/discussions/categories/new-features |   - name: 💡 Feature Request | ||||||
|     about: All feature requests will be discussed here. |     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 |           platforms: linux/amd64 | ||||||
|           push: true |           push: true | ||||||
|           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next |           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next | ||||||
|  |           labels: | | ||||||
|  |             coolify.managed=true | ||||||
|   aarch64: |   aarch64: | ||||||
|     runs-on: [ self-hosted, arm64 ] |     runs-on: [ self-hosted, arm64 ] | ||||||
|     permissions: |     permissions: | ||||||
| @@ -64,6 +66,8 @@ jobs: | |||||||
|           platforms: linux/aarch64 |           platforms: linux/aarch64 | ||||||
|           push: true |           push: true | ||||||
|           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 |           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 | ||||||
|  |           labels: | | ||||||
|  |             coolify.managed=true | ||||||
|   merge-manifest: |   merge-manifest: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     permissions: |     permissions: | ||||||
| @@ -94,3 +98,4 @@ jobs: | |||||||
|         if: always() |         if: always() | ||||||
|         with: |         with: | ||||||
|           webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL  }} |           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 |           platforms: linux/amd64 | ||||||
|           push: true |           push: true | ||||||
|           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} |           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} | ||||||
|  |           labels: | | ||||||
|  |             coolify.managed=true | ||||||
|   aarch64: |   aarch64: | ||||||
|     runs-on: [ self-hosted, arm64 ] |     runs-on: [ self-hosted, arm64 ] | ||||||
|     permissions: |     permissions: | ||||||
| @@ -64,6 +66,8 @@ jobs: | |||||||
|           platforms: linux/aarch64 |           platforms: linux/aarch64 | ||||||
|           push: true |           push: true | ||||||
|           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 |           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 | ||||||
|  |           labels: | | ||||||
|  |             coolify.managed=true | ||||||
|   merge-manifest: |   merge-manifest: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     permissions: |     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: | jobs: | ||||||
|   amd64: |   amd64: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |     permissions: | ||||||
|  |       contents: read | ||||||
|  |       packages: write | ||||||
|  |       attestations: write | ||||||
|  |       id-token: write | ||||||
|  |       actions: write | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|       - name: Login to ghcr.io |       - name: Login to ghcr.io | ||||||
| @@ -37,6 +43,9 @@ jobs: | |||||||
|     permissions: |     permissions: | ||||||
|       contents: read |       contents: read | ||||||
|       packages: write |       packages: write | ||||||
|  |       attestations: write | ||||||
|  |       id-token: write | ||||||
|  |       actions: write | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|       - name: Login to ghcr.io |       - name: Login to ghcr.io | ||||||
| @@ -58,6 +67,9 @@ jobs: | |||||||
|     permissions: |     permissions: | ||||||
|       contents: read |       contents: read | ||||||
|       packages: write |       packages: write | ||||||
|  |       attestations: write | ||||||
|  |       id-token: write | ||||||
|  |       actions: write | ||||||
|     needs: [amd64, aarch64] |     needs: [amd64, aarch64] | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - 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) | > "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. | 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: | Follow the steps below for your operating system: | ||||||
| 
 | 
 | ||||||
| ### Windows | <details> | ||||||
|  | <summary><strong>Windows</strong></summary> | ||||||
| 
 | 
 | ||||||
| 1. Install `docker-ce`, Docker Desktop (or similar): | 1. Install `docker-ce`, Docker Desktop (or similar): | ||||||
|    - Docker CE (recommended): |    - Docker CE (recommended): | ||||||
| @@ -25,7 +39,10 @@ Follow the steps below for your operating system: | |||||||
| 2. Install Spin: | 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) |    - 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): | 1. Install Orbstack, Docker Desktop (or similar): | ||||||
|    - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop): |    - 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: | 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) |    - 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): | 1. Install Docker Engine, Docker Desktop (or similar): | ||||||
|    - Docker Engine (recommended, as there is no VM overhead): |    - Docker Engine (recommended, as there is no VM overhead): | ||||||
| @@ -47,8 +67,9 @@ Follow the steps below for your operating system: | |||||||
| 2. Install Spin: | 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) |    - 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: | 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. |    You should see version information for both Docker and Spin. | ||||||
| 
 | 
 | ||||||
| 
 | ## 3. Fork and Setup Local Repository | ||||||
| ## 3. Fork the Coolify repository and setup your local repository |  | ||||||
| 
 | 
 | ||||||
| 1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account. | 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): |    | Editor | Platform | Download Link | | ||||||
|      - Windows/macOS/Linux: Download and install from [https://code.visualstudio.com/download](https://code.visualstudio.com/download) |    |--------|----------|---------------| | ||||||
| 
 |    | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) | | ||||||
|    - Cursor (recommended but paid for getting the full benefits): |    | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) | | ||||||
|      - Windows/macOS/Linux: Download and install from [https://www.cursor.com/](https://www.cursor.com/) |    | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) | | ||||||
| 
 |  | ||||||
|    - Zed (very fast code editor): |  | ||||||
|      - macOS/Linux: Download and install from [https://zed.dev/download](https://zed.dev/download) |  | ||||||
|      - Windows: Not available yet |  | ||||||
| 
 | 
 | ||||||
| 3. Clone the Coolify Repository from your fork to your local machine | 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): |    - Use GitHub Desktop (recommended): | ||||||
|      - Download and install from [https://desktop.github.com/](https://desktop.github.com/) |      - Download and install from [https://desktop.github.com/](https://desktop.github.com/) | ||||||
|      - Open GitHub Desktop and login with your GitHub account |      - 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. Open the cloned Coolify Repository in your chosen code editor. | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| ## 4. Set up Environment Variables | ## 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. | 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`. | 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. | 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`. | 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. Save the changes to your `.env` file. | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| ## 5. Start Coolify | ## 5. Start Coolify | ||||||
| 
 | 
 | ||||||
| 1. Open a terminal in the local Coolify directory. | 1. Open a terminal in the local Coolify directory. | ||||||
| 
 |  | ||||||
| 2. Run the following command in the terminal (leave that terminal open): | 2. Run the following command in the terminal (leave that terminal open): | ||||||
|    ``` |    ```bash | ||||||
|    spin up |    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: | 3. If you encounter permission errors, especially on macOS, use: | ||||||
|    ``` |    ```bash | ||||||
|    sudo spin up |    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 | ## 6. Start Development | ||||||
| 
 | 
 | ||||||
| @@ -126,15 +137,17 @@ Note: If you change environment variables afterwards or anything seems broken, p | |||||||
|    - Password: `password` |    - Password: `password` | ||||||
| 
 | 
 | ||||||
| 2. Additional development tools: | 2. Additional development tools: | ||||||
|    - Laravel Horizon (scheduler): `http://localhost:8000/horizon` |    | Tool | URL | Note | | ||||||
|      Note: Only accessible when logged in as root user |    |------|-----|------| | ||||||
|    - Mailpit (email catcher): `http://localhost:8025` |    | Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user | | ||||||
|    - Telescope (debugging tool): `http://localhost:8000/telescope`  |    | Mailpit (email catcher) | `http://localhost:8025` | | | ||||||
|      Note: Disabled by default (so the database is not overloaded), enable by adding the following environment variable to your `.env` file: |    | Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default | | ||||||
|      ```env |  | ||||||
|      TELESCOPE_ENABLED=true |  | ||||||
|      ``` |  | ||||||
| 
 | 
 | ||||||
|  | > [!NOTE] | ||||||
|  | > To enable Telescope, add the following to your `.env` file: | ||||||
|  | > ```env | ||||||
|  | > TELESCOPE_ENABLED=true | ||||||
|  | > ``` | ||||||
| 
 | 
 | ||||||
| ## 7. Development Notes | ## 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 |    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. Create a Pull Request | ||||||
| ## 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 |  | ||||||
| 
 | 
 | ||||||
| 1. After making changes or adding a new service: | 1. After making changes or adding a new service: | ||||||
|    - Commit your changes to your forked repository. |    - 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. |    - In the description, explain the changes you've made. | ||||||
|    - Reference any related issues by using keywords like "Fixes #123" or "Closes #456". |    - Reference any related issues by using keywords like "Fixes #123" or "Closes #456". | ||||||
| 
 | 
 | ||||||
| 4. Important note: | > [!IMPORTANT] | ||||||
|    Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. | > 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. |    - Review your changes one last time. | ||||||
|    - Click "Create pull request" to submit. |    - 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. | 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. | * [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. | * [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. | * [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. | * [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. | * [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. | * [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; | namespace App\Actions\Application; | ||||||
| 
 | 
 | ||||||
|  | use App\Actions\Server\CleanupDocker; | ||||||
| use App\Models\Application; | use App\Models\Application; | ||||||
| use Lorisleiva\Actions\Concerns\AsAction; | use Lorisleiva\Actions\Concerns\AsAction; | ||||||
| 
 | 
 | ||||||
| @@ -9,44 +10,35 @@ class StopApplication | |||||||
| { | { | ||||||
|     use AsAction; |     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()) { |         try { | ||||||
|             instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server); |             $server = $application->destination->server; | ||||||
|  |             if (! $server->isFunctional()) { | ||||||
|  |                 return 'Server is not functional'; | ||||||
|  |             } | ||||||
|  |             ray('Stopping application: '.$application->name); | ||||||
|  | 
 | ||||||
|  |             if ($server->isSwarm()) { | ||||||
|  |                 instant_remote_process(["docker stack rm {$application->uuid}"], $server); | ||||||
| 
 | 
 | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         $servers = collect([]); |             $containersToStop = $application->getContainersToStop($previewDeployments); | ||||||
|         $servers->push($application->destination->server); |             $application->stopContainers($containersToStop, $server); | ||||||
|         $application->additional_servers->map(function ($server) use ($servers) { | 
 | ||||||
|             $servers->push($server); |  | ||||||
|         }); |  | ||||||
|         foreach ($servers as $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); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             if ($application->build_pack === 'dockercompose') { |             if ($application->build_pack === 'dockercompose') { | ||||||
|                 // remove network
 |                 $application->delete_connected_networks($application->uuid); | ||||||
|                 $uuid = $application->uuid; |             } | ||||||
|                 instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); | 
 | ||||||
|                 instant_remote_process(["docker network rm {$uuid}"], $server, false); |             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\ActivityTypes; | ||||||
| use App\Enums\ProcessStatus; | use App\Enums\ProcessStatus; | ||||||
|  | use App\Helpers\SshMultiplexingHelper; | ||||||
| use App\Jobs\ApplicationDeploymentJob; | use App\Jobs\ApplicationDeploymentJob; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use Illuminate\Process\ProcessResult; | use Illuminate\Process\ProcessResult; | ||||||
| @@ -137,7 +138,7 @@ class RunRemoteProcess | |||||||
|         $command = $this->activity->getExtraProperty('command'); |         $command = $this->activity->getExtraProperty('command'); | ||||||
|         $server = Server::whereUuid($server_uuid)->firstOrFail(); |         $server = Server::whereUuid($server_uuid)->firstOrFail(); | ||||||
| 
 | 
 | ||||||
|         return generateSshCommand($server, $command); |         return SshMultiplexingHelper::generateSshCommand($server, $command); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected function handleOutput(string $type, string $output) |     protected function handleOutput(string $type, string $output) | ||||||
|   | |||||||
| @@ -37,7 +37,6 @@ class StartPostgresql | |||||||
|         $this->generate_init_scripts(); |         $this->generate_init_scripts(); | ||||||
|         $this->add_custom_conf(); |         $this->add_custom_conf(); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         $docker_compose = [ |         $docker_compose = [ | ||||||
|             'services' => [ |             'services' => [ | ||||||
|                 $container_name => [ |                 $container_name => [ | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Actions\Database; | namespace App\Actions\Database; | ||||||
| 
 | 
 | ||||||
|  | use App\Actions\Server\CleanupDocker; | ||||||
| use App\Models\StandaloneClickhouse; | use App\Models\StandaloneClickhouse; | ||||||
| use App\Models\StandaloneDragonfly; | use App\Models\StandaloneDragonfly; | ||||||
| use App\Models\StandaloneKeydb; | use App\Models\StandaloneKeydb; | ||||||
| @@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb; | |||||||
| use App\Models\StandaloneMysql; | use App\Models\StandaloneMysql; | ||||||
| use App\Models\StandalonePostgresql; | use App\Models\StandalonePostgresql; | ||||||
| use App\Models\StandaloneRedis; | use App\Models\StandaloneRedis; | ||||||
|  | use Illuminate\Support\Facades\Process; | ||||||
| use Lorisleiva\Actions\Concerns\AsAction; | use Lorisleiva\Actions\Concerns\AsAction; | ||||||
| 
 | 
 | ||||||
| class StopDatabase | class StopDatabase | ||||||
| { | { | ||||||
|     use AsAction; |     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; |         $server = $database->destination->server; | ||||||
|         if (! $server->isFunctional()) { |         if (! $server->isFunctional()) { | ||||||
|             return 'Server is not functional'; |             return 'Server is not functional'; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false); |         $this->stopContainer($database, $database->uuid, 300); | ||||||
|         instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false); |         if (! $isDeleteOperation) { | ||||||
|         instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false); |             if ($dockerCleanup) { | ||||||
|  |                 CleanupDocker::dispatch($server, true); | ||||||
|  |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         if ($database->is_public) { |         if ($database->is_public) { | ||||||
|             StopDatabaseProxy::run($database); |             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) { |         if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|         ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(); |         ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false); | ||||||
|         if (! $uptime) { |         if (! $uptime) { | ||||||
|             throw new \Exception($error); |             throw new \Exception($error); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Actions\Server; | namespace App\Actions\Server; | ||||||
| 
 | 
 | ||||||
|  | use App\Models\InstanceSettings; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use Lorisleiva\Actions\Concerns\AsAction; | use Lorisleiva\Actions\Concerns\AsAction; | ||||||
| 
 | 
 | ||||||
| @@ -21,10 +22,16 @@ class CleanupDocker | |||||||
| 
 | 
 | ||||||
|     private function getCommands(): array |     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 = [ |         $commonCommands = [ | ||||||
|             'docker container prune -f --filter "label=coolify.managed=true"', |             '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 builder prune -af', | ||||||
|  |             "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi", | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         return $commonCommands; |         return $commonCommands; | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Actions\Server; | namespace App\Actions\Server; | ||||||
| 
 | 
 | ||||||
|  | use App\Events\CloudflareTunnelConfigured; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use Lorisleiva\Actions\Concerns\AsAction; | use Lorisleiva\Actions\Concerns\AsAction; | ||||||
| use Symfony\Component\Yaml\Yaml; | use Symfony\Component\Yaml\Yaml; | ||||||
| @@ -40,12 +41,17 @@ class ConfigureCloudflared | |||||||
|             instant_remote_process($commands, $server); |             instant_remote_process($commands, $server); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             ray($e); |             ray($e); | ||||||
|  |             $server->settings->is_cloudflare_tunnel = false; | ||||||
|  |             $server->settings->save(); | ||||||
|             throw $e; |             throw $e; | ||||||
|         } finally { |         } finally { | ||||||
|  |             CloudflareTunnelConfigured::dispatch($server->team_id); | ||||||
|  | 
 | ||||||
|             $commands = collect([ |             $commands = collect([ | ||||||
|                 'rm -fr /tmp/cloudflared', |                 'rm -fr /tmp/cloudflared', | ||||||
|             ]); |             ]); | ||||||
|             instant_remote_process($commands, $server); |             instant_remote_process($commands, $server); | ||||||
|  | 
 | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Actions\Server; | namespace App\Actions\Server; | ||||||
| 
 | 
 | ||||||
|  | use App\Jobs\PullHelperImageJob; | ||||||
| use App\Models\InstanceSettings; | use App\Models\InstanceSettings; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use Lorisleiva\Actions\Concerns\AsAction; | use Lorisleiva\Actions\Concerns\AsAction; | ||||||
| @@ -55,6 +56,13 @@ class UpdateCoolify | |||||||
| 
 | 
 | ||||||
|             return; |             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); |         instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false); | ||||||
| 
 | 
 | ||||||
|         remote_process([ |         remote_process([ | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Actions\Service; | namespace App\Actions\Service; | ||||||
| 
 | 
 | ||||||
|  | use App\Actions\Server\CleanupDocker; | ||||||
| use App\Models\Service; | use App\Models\Service; | ||||||
| use Lorisleiva\Actions\Concerns\AsAction; | use Lorisleiva\Actions\Concerns\AsAction; | ||||||
| 
 | 
 | ||||||
| @@ -9,11 +10,11 @@ class DeleteService | |||||||
| { | { | ||||||
|     use AsAction; |     use AsAction; | ||||||
| 
 | 
 | ||||||
|     public function handle(Service $service) |     public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks) | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             $server = data_get($service, 'server'); |             $server = data_get($service, 'server'); | ||||||
|             if ($server->isFunctional()) { |             if ($deleteVolumes && $server->isFunctional()) { | ||||||
|                 $storagesToDelete = collect([]); |                 $storagesToDelete = collect([]); | ||||||
| 
 | 
 | ||||||
|                 $service->environment_variables()->delete(); |                 $service->environment_variables()->delete(); | ||||||
| @@ -33,13 +34,29 @@ class DeleteService | |||||||
|                 foreach ($storagesToDelete as $storage) { |                 foreach ($storagesToDelete as $storage) { | ||||||
|                     $commands[] = "docker volume rm -f $storage->name"; |                     $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) { |         } catch (\Exception $e) { | ||||||
|             throw new \Exception($e->getMessage()); |             throw new \Exception($e->getMessage()); | ||||||
|         } finally { |         } finally { | ||||||
|  |             if ($deleteConfigurations) { | ||||||
|  |                 $service->delete_configurations(); | ||||||
|  |             } | ||||||
|             foreach ($service->applications()->get() as $application) { |             foreach ($service->applications()->get() as $application) { | ||||||
|                 $application->forceDelete(); |                 $application->forceDelete(); | ||||||
|             } |             } | ||||||
| @@ -50,6 +67,11 @@ class DeleteService | |||||||
|                 $task->delete(); |                 $task->delete(); | ||||||
|             } |             } | ||||||
|             $service->tags()->detach(); |             $service->tags()->detach(); | ||||||
|  |             $service->forceDelete(); | ||||||
|  | 
 | ||||||
|  |             if ($dockerCleanup) { | ||||||
|  |                 CleanupDocker::dispatch($server, true); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,8 +16,10 @@ class StartService | |||||||
|         $service->saveComposeConfigs(); |         $service->saveComposeConfigs(); | ||||||
|         $commands[] = 'cd '.$service->workdir(); |         $commands[] = 'cd '.$service->workdir(); | ||||||
|         $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; |         $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; | ||||||
|  |         if ($service->networks()->count() > 0) { | ||||||
|             $commands[] = "echo 'Creating Docker network.'"; |             $commands[] = "echo 'Creating Docker network.'"; | ||||||
|             $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; |             $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; | ||||||
|  |         } | ||||||
|         $commands[] = 'echo Starting service.'; |         $commands[] = 'echo Starting service.'; | ||||||
|         $commands[] = "echo 'Pulling images.'"; |         $commands[] = "echo 'Pulling images.'"; | ||||||
|         $commands[] = 'docker compose pull'; |         $commands[] = 'docker compose pull'; | ||||||
| @@ -29,7 +31,7 @@ class StartService | |||||||
|             $network = $service->destination->network; |             $network = $service->destination->network; | ||||||
|             $serviceNames = data_get(Yaml::parse($compose), 'services', []); |             $serviceNames = data_get(Yaml::parse($compose), 'services', []); | ||||||
|             foreach ($serviceNames as $serviceName => $serviceConfig) { |             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'); |         $activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Actions\Service; | namespace App\Actions\Service; | ||||||
| 
 | 
 | ||||||
|  | use App\Actions\Server\CleanupDocker; | ||||||
| use App\Models\Service; | use App\Models\Service; | ||||||
| use Lorisleiva\Actions\Concerns\AsAction; | use Lorisleiva\Actions\Concerns\AsAction; | ||||||
| 
 | 
 | ||||||
| @@ -9,40 +10,27 @@ class StopService | |||||||
| { | { | ||||||
|     use AsAction; |     use AsAction; | ||||||
| 
 | 
 | ||||||
|     public function handle(Service $service) |     public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true) | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             $server = $service->destination->server; |             $server = $service->destination->server; | ||||||
|             if (! $server->isFunctional()) { |             if (! $server->isFunctional()) { | ||||||
|                 return 'Server is not functional'; |                 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) { |         } catch (\Exception $e) { | ||||||
|             ray($e->getMessage()); |             ray($e->getMessage()); | ||||||
| 
 | 
 | ||||||
|             return $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; | namespace App\Console\Commands; | ||||||
| 
 | 
 | ||||||
|  | use App\Jobs\CleanupHelperContainersJob; | ||||||
| use App\Models\Application; | use App\Models\Application; | ||||||
| use App\Models\ApplicationPreview; | use App\Models\ApplicationPreview; | ||||||
| use App\Models\ScheduledDatabaseBackup; | use App\Models\ScheduledDatabaseBackup; | ||||||
| use App\Models\ScheduledTask; | use App\Models\ScheduledTask; | ||||||
|  | use App\Models\Server; | ||||||
| use App\Models\Service; | use App\Models\Service; | ||||||
| use App\Models\ServiceApplication; | use App\Models\ServiceApplication; | ||||||
| use App\Models\ServiceDatabase; | use App\Models\ServiceDatabase; | ||||||
| @@ -35,6 +37,16 @@ class CleanupStuckedResources extends Command | |||||||
|     private function cleanup_stucked_resources() |     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 { |         try { | ||||||
|             $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); |             $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); | ||||||
|             foreach ($applications as $application) { |             foreach ($applications as $application) { | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ namespace App\Console\Commands; | |||||||
| use App\Actions\Server\StopSentinel; | use App\Actions\Server\StopSentinel; | ||||||
| use App\Enums\ActivityTypes; | use App\Enums\ActivityTypes; | ||||||
| use App\Enums\ApplicationDeploymentStatus; | use App\Enums\ApplicationDeploymentStatus; | ||||||
| use App\Jobs\CleanupHelperContainersJob; |  | ||||||
| use App\Models\ApplicationDeploymentQueue; | use App\Models\ApplicationDeploymentQueue; | ||||||
| use App\Models\Environment; | use App\Models\Environment; | ||||||
| use App\Models\InstanceSettings; | use App\Models\InstanceSettings; | ||||||
| @@ -18,7 +17,7 @@ use Illuminate\Support\Facades\Http; | |||||||
| 
 | 
 | ||||||
| class Init extends Command | 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'; |     protected $description = 'Cleanup instance related stuffs'; | ||||||
| 
 | 
 | ||||||
| @@ -26,9 +25,63 @@ class Init extends Command | |||||||
| 
 | 
 | ||||||
|     public function handle() |     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->servers = Server::all(); | ||||||
|         $this->alive(); |         if (isCloud()) { | ||||||
|  | 
 | ||||||
|  |         } else { | ||||||
|  |             $this->send_alive_signal(); | ||||||
|             get_public_ips(); |             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'), '<=')) { |         if (version_compare('4.0.0-beta.312', config('version'), '<=')) { | ||||||
|             foreach ($this->servers as $server) { |             foreach ($this->servers as $server) { | ||||||
|                 if ($server->settings->is_metrics_enabled === true) { |                 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() |     private function update_traefik_labels() | ||||||
| @@ -108,7 +105,6 @@ class Init extends Command | |||||||
| 
 | 
 | ||||||
|     private function cleanup_unnecessary_dynamic_proxy_configuration() |     private function cleanup_unnecessary_dynamic_proxy_configuration() | ||||||
|     { |     { | ||||||
|         if (isCloud()) { |  | ||||||
|         foreach ($this->servers as $server) { |         foreach ($this->servers as $server) { | ||||||
|             try { |             try { | ||||||
|                 if (! $server->isFunctional()) { |                 if (! $server->isFunctional()) { | ||||||
| @@ -128,13 +124,9 @@ class Init extends Command | |||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     private function cleanup_unused_network_from_coolify_proxy() |     private function cleanup_unused_network_from_coolify_proxy() | ||||||
|     { |     { | ||||||
|         if (isCloud()) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         foreach ($this->servers as $server) { |         foreach ($this->servers as $server) { | ||||||
|             if (! $server->isFunctional()) { |             if (! $server->isFunctional()) { | ||||||
|                 continue; |                 continue; | ||||||
| @@ -175,6 +167,7 @@ class Init extends Command | |||||||
| 
 | 
 | ||||||
|     private function restore_coolify_db_backup() |     private function restore_coolify_db_backup() | ||||||
|     { |     { | ||||||
|  |         if (version_compare('4.0.0-beta.179', config('version'), '<=')) { | ||||||
|             try { |             try { | ||||||
|                 $database = StandalonePostgresql::withTrashed()->find(0); |                 $database = StandalonePostgresql::withTrashed()->find(0); | ||||||
|                 if ($database && $database->trashed()) { |                 if ($database && $database->trashed()) { | ||||||
| @@ -197,17 +190,9 @@ class Init extends Command | |||||||
|                 echo "Error in restoring coolify db backup: {$e->getMessage()}\n"; |                 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); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function alive() |     private function send_alive_signal() | ||||||
|     { |     { | ||||||
|         $id = config('app.id'); |         $id = config('app.id'); | ||||||
|         $version = config('version'); |         $version = config('version'); | ||||||
| @@ -225,23 +210,7 @@ class Init extends Command | |||||||
|             echo "Error in alive: {$e->getMessage()}\n"; |             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() |     private function cleanup_in_progress_application_deployments() | ||||||
|     { |     { | ||||||
|         // Cleanup any failed deployments
 |         // Cleanup any failed deployments
 | ||||||
| @@ -263,6 +232,7 @@ class Init extends Command | |||||||
| 
 | 
 | ||||||
|     private function replace_slash_in_environment_name() |     private function replace_slash_in_environment_name() | ||||||
|     { |     { | ||||||
|  |         if (version_compare('4.0.0-beta.298', config('version'), '<=')) { | ||||||
|             $environments = Environment::all(); |             $environments = Environment::all(); | ||||||
|             foreach ($environments as $environment) { |             foreach ($environments as $environment) { | ||||||
|                 if (str_contains($environment->name, '/')) { |                 if (str_contains($environment->name, '/')) { | ||||||
| @@ -272,3 +242,4 @@ class Init extends Command | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ namespace App\Console; | |||||||
| 
 | 
 | ||||||
| use App\Jobs\CheckForUpdatesJob; | use App\Jobs\CheckForUpdatesJob; | ||||||
| use App\Jobs\CleanupInstanceStuffsJob; | use App\Jobs\CleanupInstanceStuffsJob; | ||||||
|  | use App\Jobs\CleanupStaleMultiplexedConnections; | ||||||
| use App\Jobs\DatabaseBackupJob; | use App\Jobs\DatabaseBackupJob; | ||||||
| use App\Jobs\DockerCleanupJob; | use App\Jobs\DockerCleanupJob; | ||||||
| use App\Jobs\PullHelperImageJob; | use App\Jobs\PullHelperImageJob; | ||||||
| @@ -29,7 +30,8 @@ class Kernel extends ConsoleKernel | |||||||
|         $this->all_servers = Server::all(); |         $this->all_servers = Server::all(); | ||||||
|         $settings = InstanceSettings::get(); |         $settings = InstanceSettings::get(); | ||||||
| 
 | 
 | ||||||
|         $schedule->command('telescope:prune')->daily(); |         $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); | ||||||
|  | 
 | ||||||
|         if (isDev()) { |         if (isDev()) { | ||||||
|             // Instance Jobs
 |             // Instance Jobs
 | ||||||
|             $schedule->command('horizon:snapshot')->everyMinute(); |             $schedule->command('horizon:snapshot')->everyMinute(); | ||||||
| @@ -39,6 +41,10 @@ class Kernel extends ConsoleKernel | |||||||
|             $this->check_resources($schedule); |             $this->check_resources($schedule); | ||||||
|             $this->check_scheduled_tasks($schedule); |             $this->check_scheduled_tasks($schedule); | ||||||
|             $schedule->command('uploads:clear')->everyTwoMinutes(); |             $schedule->command('uploads:clear')->everyTwoMinutes(); | ||||||
|  | 
 | ||||||
|  |             $schedule->command('telescope:prune')->daily(); | ||||||
|  | 
 | ||||||
|  |             $schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer(); | ||||||
|         } else { |         } else { | ||||||
|             // Instance Jobs
 |             // Instance Jobs
 | ||||||
|             $schedule->command('horizon:snapshot')->everyFiveMinutes(); |             $schedule->command('horizon:snapshot')->everyFiveMinutes(); | ||||||
| @@ -73,12 +79,12 @@ class Kernel extends ConsoleKernel | |||||||
|                     } |                     } | ||||||
|                 })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); |                 })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); | ||||||
|             } |             } | ||||||
|             $schedule->job(new PullHelperImageJob($server)) |         } | ||||||
|  |         $schedule->job(new PullHelperImageJob) | ||||||
|             ->cron($settings->update_check_frequency) |             ->cron($settings->update_check_frequency) | ||||||
|             ->timezone($settings->instance_timezone) |             ->timezone($settings->instance_timezone) | ||||||
|             ->onOneServer(); |             ->onOneServer(); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     private function schedule_updates($schedule) |     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) |     private function validateDataApplications(Request $request, Server $server) | ||||||
|     { |     { | ||||||
|         $teamId = getTeamIdFromToken(); |         $teamId = getTeamIdFromToken(); | ||||||
|   | |||||||
| @@ -86,7 +86,7 @@ class DeployController extends Controller | |||||||
|         ], |         ], | ||||||
|         tags: ['Deployments'], |         tags: ['Deployments'], | ||||||
|         parameters: [ |         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: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
| @@ -150,7 +150,7 @@ class DeployController extends Controller | |||||||
|         responses: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
|                 response: 200, |                 response: 200, | ||||||
|                 description: 'Get deployment(s) Uuid\'s', |                 description: 'Get deployment(s) UUID\'s', | ||||||
|                 content: [ |                 content: [ | ||||||
|                     new OA\MediaType( |                     new OA\MediaType( | ||||||
|                         mediaType: 'application/json', |                         mediaType: 'application/json', | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ class ProjectController extends Controller | |||||||
| { | { | ||||||
|     #[OA\Get(
 |     #[OA\Get(
 | ||||||
|         summary: 'List', |         summary: 'List', | ||||||
|         description: 'list projects.', |         description: 'List projects.', | ||||||
|         path: '/projects', |         path: '/projects', | ||||||
|         operationId: 'list-projects', |         operationId: 'list-projects', | ||||||
|         security: [ |         security: [ | ||||||
| @@ -47,7 +47,7 @@ class ProjectController extends Controller | |||||||
|         if (is_null($teamId)) { |         if (is_null($teamId)) { | ||||||
|             return invalidTokenResponse(); |             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), |         return response()->json(serializeApiResponse($projects), | ||||||
|         ); |         ); | ||||||
| @@ -55,7 +55,7 @@ class ProjectController extends Controller | |||||||
| 
 | 
 | ||||||
|     #[OA\Get(
 |     #[OA\Get(
 | ||||||
|         summary: 'Get', |         summary: 'Get', | ||||||
|         description: 'Get project by Uuid.', |         description: 'Get project by UUID.', | ||||||
|         path: '/projects/{uuid}', |         path: '/projects/{uuid}', | ||||||
|         operationId: 'get-project-by-uuid', |         operationId: 'get-project-by-uuid', | ||||||
|         security: [ |         security: [ | ||||||
| @@ -139,7 +139,7 @@ class ProjectController extends Controller | |||||||
|             return invalidTokenResponse(); |             return invalidTokenResponse(); | ||||||
|         } |         } | ||||||
|         if (! $request->uuid) { |         if (! $request->uuid) { | ||||||
|             return response()->json(['message' => 'Uuid is required.'], 422); |             return response()->json(['message' => 'UUID is required.'], 422); | ||||||
|         } |         } | ||||||
|         if (! $request->environment_name) { |         if (! $request->environment_name) { | ||||||
|             return response()->json(['message' => 'Environment name is required.'], 422); |             return response()->json(['message' => 'Environment name is required.'], 422); | ||||||
| @@ -341,7 +341,7 @@ class ProjectController extends Controller | |||||||
|         } |         } | ||||||
|         $uuid = $request->uuid; |         $uuid = $request->uuid; | ||||||
|         if (! $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(); |         $project = Project::whereTeamId($teamId)->whereUuid($uuid)->first(); | ||||||
| @@ -417,7 +417,7 @@ class ProjectController extends Controller | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (! $request->uuid) { |         if (! $request->uuid) { | ||||||
|             return response()->json(['message' => 'Uuid is required.'], 422); |             return response()->json(['message' => 'UUID is required.'], 422); | ||||||
|         } |         } | ||||||
|         $project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first(); |         $project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first(); | ||||||
|         if (! $project) { |         if (! $project) { | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ class SecurityController extends Controller | |||||||
|         ], |         ], | ||||||
|         tags: ['Private Keys'], |         tags: ['Private Keys'], | ||||||
|         parameters: [ |         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: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
| @@ -323,7 +323,7 @@ class SecurityController extends Controller | |||||||
|         ], |         ], | ||||||
|         tags: ['Private Keys'], |         tags: ['Private Keys'], | ||||||
|         parameters: [ |         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: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
|   | |||||||
| @@ -107,7 +107,7 @@ class ServersController extends Controller | |||||||
|         ], |         ], | ||||||
|         tags: ['Servers'], |         tags: ['Servers'], | ||||||
|         parameters: [ |         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: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
| @@ -185,7 +185,7 @@ class ServersController extends Controller | |||||||
|         ], |         ], | ||||||
|         tags: ['Servers'], |         tags: ['Servers'], | ||||||
|         parameters: [ |         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: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
| @@ -263,7 +263,7 @@ class ServersController extends Controller | |||||||
|         ], |         ], | ||||||
|         tags: ['Servers'], |         tags: ['Servers'], | ||||||
|         parameters: [ |         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: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
|   | |||||||
| @@ -378,7 +378,7 @@ class ServicesController extends Controller | |||||||
|         responses: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
|                 response: 200, |                 response: 200, | ||||||
|                 description: 'Get a service by Uuid.', |                 description: 'Get a service by UUID.', | ||||||
|                 content: [ |                 content: [ | ||||||
|                     new OA\MediaType( |                     new OA\MediaType( | ||||||
|                         mediaType: 'application/json', |                         mediaType: 'application/json', | ||||||
| @@ -436,7 +436,7 @@ class ServicesController extends Controller | |||||||
|         responses: [ |         responses: [ | ||||||
|             new OA\Response( |             new OA\Response( | ||||||
|                 response: 200, |                 response: 200, | ||||||
|                 description: 'Delete a service by Uuid', |                 description: 'Delete a service by UUID', | ||||||
|                 content: [ |                 content: [ | ||||||
|                     new OA\MediaType( |                     new OA\MediaType( | ||||||
|                         mediaType: 'application/json', |                         mediaType: 'application/json', | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ use Illuminate\Foundation\Bus\Dispatchable; | |||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| use Illuminate\Support\Collection; | use Illuminate\Support\Collection; | ||||||
|  | use Illuminate\Support\Facades\Process; | ||||||
| use Illuminate\Support\Sleep; | use Illuminate\Support\Sleep; | ||||||
| use Illuminate\Support\Str; | use Illuminate\Support\Str; | ||||||
| use RuntimeException; | use RuntimeException; | ||||||
| @@ -210,7 +211,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|         } |         } | ||||||
|         ray('New container name: ', $this->container_name)->green(); |         ray('New container name: ', $this->container_name)->green(); | ||||||
| 
 | 
 | ||||||
|         savePrivateKeyToFs($this->server); |  | ||||||
|         $this->saved_outputs = collect(); |         $this->saved_outputs = collect(); | ||||||
| 
 | 
 | ||||||
|         // Set preview fqdn
 |         // Set preview fqdn
 | ||||||
| @@ -514,7 +514,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|                 'hidden' => true, |                 'hidden' => true, | ||||||
|                 'ignore_errors' => true, |                 'ignore_errors' => true, | ||||||
|             ], [ |             ], [ | ||||||
|                 "docker network connect {$networkId} coolify-proxy || true", |                 "docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true", | ||||||
|                 'hidden' => true, |                 'hidden' => true, | ||||||
|                 'ignore_errors' => 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->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()) { |                 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()) { |                 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->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()) { |                 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()) { |                 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; |         $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() |     private function laravel_finetunes() | ||||||
|     { |     { | ||||||
|         if ($this->pull_request_id === 0) { |         if ($this->pull_request_id === 0) { | ||||||
|             $nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); |             $envType = 'environment_variables'; | ||||||
|             $nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); |  | ||||||
|         } else { |         } else { | ||||||
|             $nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); |             $envType = 'environment_variables_preview'; | ||||||
|             $nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); |  | ||||||
|         } |         } | ||||||
|  |         $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) { |         if (! $nixpacks_php_fallback_path) { | ||||||
|             $nixpacks_php_fallback_path = new EnvironmentVariable; |             $nixpacks_php_fallback_path = new EnvironmentVariable; | ||||||
|             $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; |             $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; | ||||||
| @@ -1402,21 +1442,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|         if ($this->pull_request_id !== 0) { |         if ($this->pull_request_id !== 0) { | ||||||
|             $local_branch = "pull/{$this->pull_request_id}/head"; |             $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) { |         if ($private_key) { | ||||||
|             $private_key = base64_encode($private_key); |  | ||||||
|             $this->execute_remote_command( |             $this->execute_remote_command( | ||||||
|                 [ |                 [ | ||||||
|                     executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'), |                     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}"), | ||||||
|                 ], |  | ||||||
|                 [ |  | ||||||
|                     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}"), |  | ||||||
|                     'hidden' => true, |                     'hidden' => true, | ||||||
|                     'save' => 'git_commit_sha', |                     '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_FALLBACK_PATH', $variables[0]->value); | ||||||
|                     data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->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->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); | ||||||
|                 $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); |                 $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); | ||||||
|                 if ($this->nixpacks_type === 'rust') { |                 if ($this->nixpacks_type === 'rust') { | ||||||
| @@ -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"), |                             executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), | ||||||
|                             'hidden' => true, |                             'hidden' => true, | ||||||
|                         ], |                         ], | ||||||
|  |                         [ | ||||||
|  |                             executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), | ||||||
|  |                             'hidden' => true, | ||||||
|  |                         ], | ||||||
|                         [ |                         [ | ||||||
|                             executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), |                             executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), | ||||||
|                             'hidden' => true, |                             '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"), |                             executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), | ||||||
|                             'hidden' => true, |                             'hidden' => true, | ||||||
|                         ], |                         ], | ||||||
|  |                         [ | ||||||
|  |                             executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), | ||||||
|  |                             'hidden' => true, | ||||||
|  |                         ], | ||||||
|                         [ |                         [ | ||||||
|                             executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), |                             executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), | ||||||
|                             'hidden' => true, |                             '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"), |                     executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), | ||||||
|                     'hidden' => true, |                     'hidden' => true, | ||||||
|                 ], |                 ], | ||||||
|  |                 [ | ||||||
|  |                     executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), | ||||||
|  |                     'hidden' => true, | ||||||
|  |                 ], | ||||||
|                 [ |                 [ | ||||||
|                     executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), |                     executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), | ||||||
|                     'hidden' => true, |                     '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"), |                         executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), | ||||||
|                         'hidden' => true, |                         'hidden' => true, | ||||||
|                     ], |                     ], | ||||||
|  |                     [ | ||||||
|  |                         executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), | ||||||
|  |                         'hidden' => true, | ||||||
|  |                     ], | ||||||
|                     [ |                     [ | ||||||
|                         executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), |                         executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), | ||||||
|                         'hidden' => true, |                         '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"), |                             executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), | ||||||
|                             'hidden' => true, |                             'hidden' => true, | ||||||
|                         ], |                         ], | ||||||
|  |                         [ | ||||||
|  |                             executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), | ||||||
|  |                             'hidden' => true, | ||||||
|  |                         ], | ||||||
|                         [ |                         [ | ||||||
|                             executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), |                             executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), | ||||||
|                             'hidden' => true, |                             '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"), |                             executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), | ||||||
|                             'hidden' => true, |                             'hidden' => true, | ||||||
|                         ], |                         ], | ||||||
|  |                         [ | ||||||
|  |                             executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), | ||||||
|  |                             'hidden' => true, | ||||||
|  |                         ], | ||||||
|                         [ |                         [ | ||||||
|                             executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), |                             executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), | ||||||
|                             'hidden' => true, |                             'hidden' => true, | ||||||
| @@ -2144,20 +2201,40 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); | |||||||
|         $this->application_deployment_queue->addLogEntry('Building docker image completed.'); |         $this->application_deployment_queue->addLogEntry('Building docker image completed.'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     private function graceful_shutdown_container(string $containerName, int $timeout = 300) | ||||||
|      * @param  int  $timeout  in seconds |  | ||||||
|      */ |  | ||||||
|     private function graceful_shutdown_container(string $containerName, int $timeout = 30) |  | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|  |             $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); | ||||||
|  | 
 | ||||||
|  |             $startTime = time(); | ||||||
|  |             while ($process->running()) { | ||||||
|  |                 if (time() - $startTime >= $timeout) { | ||||||
|                     $this->execute_remote_command( |                     $this->execute_remote_command( | ||||||
|                 ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], |                         ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true] | ||||||
|                 ["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true] |  | ||||||
|                     ); |                     ); | ||||||
|         } catch (\Exception $error) { |                     break; | ||||||
|             // report error if needed
 |                 } | ||||||
|  |                 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) { | ||||||
|  |             $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( |         $this->execute_remote_command( | ||||||
|             ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] |             ["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\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| use Illuminate\Support\Facades\Http; |  | ||||||
| use Illuminate\Support\Facades\File; | use Illuminate\Support\Facades\File; | ||||||
|  | use Illuminate\Support\Facades\Http; | ||||||
| 
 | 
 | ||||||
| class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue | class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue | ||||||
| { | { | ||||||
|   | |||||||
| @@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S | |||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             ray('Cleaning up helper containers on '.$this->server->name); |             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 = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false); | ||||||
|             $containers = format_docker_command_output_to_json($containers); |             $containerIds = collect(json_decode($containers))->pluck('ID'); | ||||||
|             if ($containers->count() > 0) { |             if ($containerIds->count() > 0) { | ||||||
|                 foreach ($containers as $container) { |                 foreach ($containerIds as $containerId) { | ||||||
|                     $containerId = data_get($container, 'ID'); |  | ||||||
|                     ray('Removing container '.$containerId); |                     ray('Removing container '.$containerId); | ||||||
|                     instant_remote_process(['docker container rm -f '.$containerId], $this->server, false); |                     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\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| 
 | 
 | ||||||
| class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue | class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue | ||||||
| @@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public function __construct(public Server $server) {} |     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() |     public function handle() | ||||||
|     { |     { | ||||||
|         GetContainersStatus::run($this->server); |         GetContainersStatus::run($this->server); | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ namespace App\Jobs; | |||||||
| 
 | 
 | ||||||
| use App\Actions\Database\StopDatabase; | use App\Actions\Database\StopDatabase; | ||||||
| use App\Events\BackupCreated; | use App\Events\BackupCreated; | ||||||
|  | use App\Models\InstanceSettings; | ||||||
| use App\Models\S3Storage; | use App\Models\S3Storage; | ||||||
| use App\Models\ScheduledDatabaseBackup; | use App\Models\ScheduledDatabaseBackup; | ||||||
| use App\Models\ScheduledDatabaseBackupExecution; | use App\Models\ScheduledDatabaseBackupExecution; | ||||||
| @@ -22,10 +23,9 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| use Illuminate\Support\Str; | use Illuminate\Support\Str; | ||||||
| use App\Models\InstanceSettings; | use Visus\Cuid2\Cuid2; | ||||||
| 
 | 
 | ||||||
| class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue | 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 |     public function handle(): void | ||||||
|     { |     { | ||||||
|         try { |         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"; |             $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; | ||||||
| 
 | 
 | ||||||
|             $commands[] = $backupCommand; |             $commands[] = $backupCommand; | ||||||
|  |             ray($commands); | ||||||
|             $this->backup_output = instant_remote_process($commands, $this->server); |             $this->backup_output = instant_remote_process($commands, $this->server); | ||||||
|             $this->backup_output = trim($this->backup_output); |             $this->backup_output = trim($this->backup_output); | ||||||
|             if ($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 |     private function upload_to_s3(): void | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
| @@ -526,6 +545,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|     private function checkImageExists(string $fullImageName): bool |     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); |         $result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false); | ||||||
|  | 
 | ||||||
|         return trim($result) === 'exists'; |         return trim($result) === 'exists'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -534,7 +554,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|         try { |         try { | ||||||
|             instant_remote_process(["docker pull {$fullImageName}"], $this->server); |             instant_remote_process(["docker pull {$fullImageName}"], $this->server); | ||||||
|         } catch (\Exception $e) { |         } 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); |             $this->add_to_backup_output($errorMessage); | ||||||
|             throw new \RuntimeException($errorMessage); |             throw new \RuntimeException($errorMessage); | ||||||
|         } |         } | ||||||
| @@ -545,6 +565,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|         $settings = InstanceSettings::get(); |         $settings = InstanceSettings::get(); | ||||||
|         $helperImage = config('coolify.helper_image'); |         $helperImage = config('coolify.helper_image'); | ||||||
|         $latestVersion = $settings->helper_version; |         $latestVersion = $settings->helper_version; | ||||||
|  | 
 | ||||||
|         return "{$helperImage}:{$latestVersion}"; |         return "{$helperImage}:{$latestVersion}"; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ namespace App\Jobs; | |||||||
| 
 | 
 | ||||||
| use App\Actions\Application\StopApplication; | use App\Actions\Application\StopApplication; | ||||||
| use App\Actions\Database\StopDatabase; | use App\Actions\Database\StopDatabase; | ||||||
|  | use App\Actions\Server\CleanupDocker; | ||||||
| use App\Actions\Service\DeleteService; | use App\Actions\Service\DeleteService; | ||||||
| use App\Actions\Service\StopService; | use App\Actions\Service\StopService; | ||||||
| use App\Models\Application; | use App\Models\Application; | ||||||
| @@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public function __construct( |     public function __construct( | ||||||
|         public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, |         public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, | ||||||
|         public bool $deleteConfigurations = false, |         public bool $deleteConfigurations, | ||||||
|         public bool $deleteVolumes = false) {} |         public bool $deleteVolumes, | ||||||
|  |         public bool $dockerCleanup, | ||||||
|  |         public bool $deleteConnectedNetworks | ||||||
|  |     ) {} | ||||||
| 
 | 
 | ||||||
|     public function handle() |     public function handle() | ||||||
|     { |     { | ||||||
| @@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|                 case 'standalone-dragonfly': |                 case 'standalone-dragonfly': | ||||||
|                 case 'standalone-clickhouse': |                 case 'standalone-clickhouse': | ||||||
|                     $persistentStorages = $this->resource?->persistentStorages()?->get(); |                     $persistentStorages = $this->resource?->persistentStorages()?->get(); | ||||||
|                     StopDatabase::run($this->resource); |                     StopDatabase::run($this->resource, true); | ||||||
|                     break; |                     break; | ||||||
|                 case 'service': |                 case 'service': | ||||||
|                     StopService::run($this->resource); |                     StopService::run($this->resource, true); | ||||||
|                     DeleteService::run($this->resource); |                     DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks); | ||||||
|                     break; |                     break; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|             if ($this->deleteConfigurations) { |             if ($this->deleteConfigurations) { | ||||||
|                 $this->resource?->delete_configurations(); |                 $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) { |         } catch (\Throwable $e) { | ||||||
|             ray($e->getMessage()); |  | ||||||
|             send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); |             send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); | ||||||
|             throw $e; |             throw $e; | ||||||
|         } finally { |         } finally { | ||||||
|             $this->resource->forceDelete(); |             $this->resource->forceDelete(); | ||||||
|  |             if ($this->dockerCleanup) { | ||||||
|  |                 CleanupDocker::dispatch($server, true); | ||||||
|  |             } | ||||||
|             Artisan::queue('cleanup:stucked-resources'); |             Artisan::queue('cleanup:stucked-resources'); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| use Illuminate\Support\Facades\Log; | use Illuminate\Support\Facades\Log; | ||||||
| 
 | 
 | ||||||
| @@ -26,16 +25,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public function __construct(public Server $server) {} |     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 |     public function handle(): void | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| use Illuminate\Support\Facades\Http; | use Illuminate\Support\Facades\Http; | ||||||
| 
 | 
 | ||||||
| @@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public function __construct(public GithubApp $github_app) {} |     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() |     public function handle() | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| use Illuminate\Support\Facades\Http; | use Illuminate\Support\Facades\Http; | ||||||
| 
 | 
 | ||||||
| @@ -19,17 +18,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public $timeout = 1000; |     public $timeout = 1000; | ||||||
| 
 | 
 | ||||||
|     public function middleware(): array |     public function __construct() {} | ||||||
|     { |  | ||||||
|         return [(new WithoutOverlapping($this->server->uuid))]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function uniqueId(): string |  | ||||||
|     { |  | ||||||
|         return $this->server->uuid; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function __construct(public Server $server) {} |  | ||||||
| 
 | 
 | ||||||
|     public function handle(): void |     public function handle(): void | ||||||
|     { |     { | ||||||
| @@ -42,8 +31,8 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue | |||||||
|                 $current_version = $settings->helper_version; |                 $current_version = $settings->helper_version; | ||||||
|                 if (version_compare($latest_version, $current_version, '>')) { |                 if (version_compare($latest_version, $current_version, '>')) { | ||||||
|                     // New version available
 |                     // New version available
 | ||||||
|                     $helperImage = config('coolify.helper_image'); |                     // $helperImage = config('coolify.helper_image');
 | ||||||
|                     instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server); |                     // instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
 | ||||||
|                     $settings->update(['helper_version' => $latest_version]); |                     $settings->update(['helper_version' => $latest_version]); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| 
 | 
 | ||||||
| class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue | class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue | ||||||
| @@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public $timeout = 1000; |     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 Server $server) {} | ||||||
| 
 | 
 | ||||||
|     public function handle(): void |     public function handle(): void | ||||||
|   | |||||||
| @@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| 
 | 
 | ||||||
| class ScheduledTaskJob implements ShouldQueue | class ScheduledTaskJob implements ShouldQueue | ||||||
| @@ -56,24 +55,17 @@ class ScheduledTaskJob implements ShouldQueue | |||||||
|     { |     { | ||||||
|         if ($this->resource instanceof Application) { |         if ($this->resource instanceof Application) { | ||||||
|             $timezone = $this->resource->destination->server->settings->server_timezone; |             $timezone = $this->resource->destination->server->settings->server_timezone; | ||||||
|  | 
 | ||||||
|             return $timezone; |             return $timezone; | ||||||
|         } elseif ($this->resource instanceof Service) { |         } elseif ($this->resource instanceof Service) { | ||||||
|             $timezone = $this->resource->server->settings->server_timezone; |             $timezone = $this->resource->server->settings->server_timezone; | ||||||
|  | 
 | ||||||
|             return $timezone; |             return $timezone; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         return 'UTC'; |         return 'UTC'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function middleware(): array |  | ||||||
|     { |  | ||||||
|         return [new WithoutOverlapping($this->task->id)]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function uniqueId(): int |  | ||||||
|     { |  | ||||||
|         return $this->task->id; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function handle(): void |     public function handle(): void | ||||||
|     { |     { | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| use Illuminate\Support\Arr; | use Illuminate\Support\Arr; | ||||||
| 
 | 
 | ||||||
| @@ -24,7 +23,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| { | { | ||||||
|     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; |     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; | ||||||
| 
 | 
 | ||||||
|     public $tries = 3; |     public $tries = 1; | ||||||
|  | 
 | ||||||
|  |     public $timeout = 60; | ||||||
| 
 | 
 | ||||||
|     public $containers; |     public $containers; | ||||||
| 
 | 
 | ||||||
| @@ -43,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public function __construct(public Server $server) {} |     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() |     public function handle() | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
| @@ -91,7 +82,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     private function serverStatus() |     private function serverStatus() | ||||||
|     { |     { | ||||||
|         ['uptime' => $uptime] = $this->server->validateConnection(); |         ['uptime' => $uptime] = $this->server->validateConnection(false); | ||||||
|         if ($uptime) { |         if ($uptime) { | ||||||
|             if ($this->server->unreachable_notification_sent === true) { |             if ($this->server->unreachable_notification_sent === true) { | ||||||
|                 $this->server->update(['unreachable_notification_sent' => false]); |                 $this->server->update(['unreachable_notification_sent' => false]); | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; | use Illuminate\Queue\Middleware\; | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| 
 | 
 | ||||||
| class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue | class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue | ||||||
| @@ -26,16 +26,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public function __construct(public Team $team) {} |     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() |     public function handle() | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; | |||||||
| use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
| use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
| use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||||
| use Illuminate\Queue\Middleware\WithoutOverlapping; |  | ||||||
| use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||||
| 
 | 
 | ||||||
| class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue | class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue | ||||||
| @@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue | |||||||
| 
 | 
 | ||||||
|     public function __construct(public Server $server) {} |     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() |     public function handle() | ||||||
|     { |     { | ||||||
|         if (! $this->server->isServerReady($this->tries)) { |         if (! $this->server->isServerReady($this->tries)) { | ||||||
|   | |||||||
| @@ -73,6 +73,8 @@ class Index extends Component | |||||||
|         } |         } | ||||||
|         $this->privateKeyName = generate_random_name(); |         $this->privateKeyName = generate_random_name(); | ||||||
|         $this->remoteServerName = generate_random_name(); |         $this->remoteServerName = generate_random_name(); | ||||||
|  |         $this->remoteServerPort = $this->remoteServerPort; | ||||||
|  |         $this->remoteServerUser = $this->remoteServerUser; | ||||||
|         if (isDev()) { |         if (isDev()) { | ||||||
|             $this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY----- |             $this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY----- | ||||||
| b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW | ||||||
| @@ -139,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== | |||||||
|             if (! $this->createdServer) { |             if (! $this->createdServer) { | ||||||
|                 return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.'); |                 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'); |             return $this->validateServer('localhost'); | ||||||
|         } elseif ($this->selectedServerType === 'remote') { |         } elseif ($this->selectedServerType === 'remote') { | ||||||
| @@ -154,6 +156,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== | |||||||
|             $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); |             $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); | ||||||
|             if ($this->servers->count() > 0) { |             if ($this->servers->count() > 0) { | ||||||
|                 $this->selectedExistingServer = $this->servers->first()->id; |                 $this->selectedExistingServer = $this->servers->first()->id; | ||||||
|  |                 $this->updateServerDetails(); | ||||||
|                 $this->currentState = 'select-existing-server'; |                 $this->currentState = 'select-existing-server'; | ||||||
| 
 | 
 | ||||||
|                 return; |                 return; | ||||||
| @@ -172,10 +175,19 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; |         $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; | ||||||
|         $this->serverPublicKey = $this->createdServer->privateKey->publicKey(); |         $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey(); | ||||||
|  |         $this->updateServerDetails(); | ||||||
|         $this->currentState = 'validate-server'; |         $this->currentState = 'validate-server'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private function updateServerDetails() | ||||||
|  |     { | ||||||
|  |         if ($this->createdServer) { | ||||||
|  |             $this->remoteServerPort = $this->createdServer->port; | ||||||
|  |             $this->remoteServerUser = $this->createdServer->user; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function getProxyType() |     public function getProxyType() | ||||||
|     { |     { | ||||||
|         // Set Default Proxy Type
 |         // Set Default Proxy Type
 | ||||||
| @@ -219,27 +231,35 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== | |||||||
|     public function savePrivateKey() |     public function savePrivateKey() | ||||||
|     { |     { | ||||||
|         $this->validate([ |         $this->validate([ | ||||||
|             'privateKeyName' => 'required', |             'privateKeyName' => 'required|string|max:255', | ||||||
|             'privateKey' => 'required', |             'privateKeyDescription' => 'nullable|string|max:255', | ||||||
|  |             'privateKey' => 'required|string', | ||||||
|         ]); |         ]); | ||||||
|         $this->createdPrivateKey = PrivateKey::create([ | 
 | ||||||
|  |         try { | ||||||
|  |             $privateKey = PrivateKey::createAndStore([ | ||||||
|                 'name' => $this->privateKeyName, |                 'name' => $this->privateKeyName, | ||||||
|                 'description' => $this->privateKeyDescription, |                 'description' => $this->privateKeyDescription, | ||||||
|                 'private_key' => $this->privateKey, |                 'private_key' => $this->privateKey, | ||||||
|                 'team_id' => currentTeam()->id, |                 'team_id' => currentTeam()->id, | ||||||
|             ]); |             ]); | ||||||
|         $this->createdPrivateKey->save(); | 
 | ||||||
|  |             $this->createdPrivateKey = $privateKey; | ||||||
|             $this->currentState = 'create-server'; |             $this->currentState = 'create-server'; | ||||||
|  |         } catch (\Exception $e) { | ||||||
|  |             $this->addError('privateKey', 'Failed to save private key: '.$e->getMessage()); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function saveServer() |     public function saveServer() | ||||||
|     { |     { | ||||||
|         $this->validate([ |         $this->validate([ | ||||||
|             'remoteServerName' => 'required', |             'remoteServerName' => 'required|string', | ||||||
|             'remoteServerHost' => 'required', |             'remoteServerHost' => 'required|string', | ||||||
|             'remoteServerPort' => 'required|integer', |             'remoteServerPort' => 'required|integer', | ||||||
|             'remoteServerUser' => 'required', |             'remoteServerUser' => 'required|string', | ||||||
|         ]); |         ]); | ||||||
|  | 
 | ||||||
|         $this->privateKey = formatPrivateKey($this->privateKey); |         $this->privateKey = formatPrivateKey($this->privateKey); | ||||||
|         $foundServer = Server::whereIp($this->remoteServerHost)->first(); |         $foundServer = Server::whereIp($this->remoteServerHost)->first(); | ||||||
|         if ($foundServer) { |         if ($foundServer) { | ||||||
| @@ -269,7 +289,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== | |||||||
|     public function validateServer() |     public function validateServer() | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             config()->set('coolify.mux_enabled', false); |             config()->set('constants.ssh.mux_enabled', false); | ||||||
| 
 | 
 | ||||||
|             // EC2 does not have `uptime` command, lol
 |             // EC2 does not have `uptime` command, lol
 | ||||||
|             instant_remote_process(['ls /'], $this->createdServer, true); |             instant_remote_process(['ls /'], $this->createdServer, true); | ||||||
| @@ -277,9 +297,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== | |||||||
|             $this->createdServer->settings()->update([ |             $this->createdServer->settings()->update([ | ||||||
|                 'is_reachable' => true, |                 'is_reachable' => true, | ||||||
|             ]); |             ]); | ||||||
|  |             $this->serverReachable = true; | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             $this->serverReachable = false; |             $this->serverReachable = false; | ||||||
|             $this->createdServer->delete(); |             $this->createdServer->settings()->update([ | ||||||
|  |                 'is_reachable' => false, | ||||||
|  |             ]); | ||||||
| 
 | 
 | ||||||
|             return handleError(error: $e, livewire: $this); |             return handleError(error: $e, livewire: $this); | ||||||
|         } |         } | ||||||
| @@ -296,6 +319,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== | |||||||
|             ]); |             ]); | ||||||
|             $this->getProxyType(); |             $this->getProxyType(); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|  |             $this->createdServer->settings()->update([ | ||||||
|  |                 'is_usable' => false, | ||||||
|  |             ]); | ||||||
|  | 
 | ||||||
|             return handleError(error: $e, livewire: $this); |             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() |     private function createNewPrivateKey() | ||||||
|     { |     { | ||||||
|         $this->privateKeyName = generate_random_name(); |         $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() |     public function cleanup_queue() | ||||||
|     { |     { | ||||||
|         $this->dispatch('success', 'Cleanup started.'); |  | ||||||
|         Artisan::queue('cleanup:application-deployment-queue', [ |         Artisan::queue('cleanup:application-deployment-queue', [ | ||||||
|             '--team-id' => currentTeam()->id, |             '--team-id' => currentTeam()->id, | ||||||
|         ]); |         ]); | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ class Form extends Component | |||||||
|             } |             } | ||||||
|             $this->destination->delete(); |             $this->destination->delete(); | ||||||
| 
 | 
 | ||||||
|             return redirect()->route('dashboard'); |             return redirect()->route('destination.all'); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             return handleError($e, $this); |             return handleError($e, $this); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -2,13 +2,28 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Livewire; | namespace App\Livewire; | ||||||
| 
 | 
 | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
| use Illuminate\Support\Facades\DB; | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class NavbarDeleteTeam extends 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 = currentTeam(); | ||||||
|         $currentTeam->delete(); |         $currentTeam->delete(); | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application\Deployment; | |||||||
| 
 | 
 | ||||||
| use App\Models\Application; | use App\Models\Application; | ||||||
| use App\Models\ApplicationDeploymentQueue; | use App\Models\ApplicationDeploymentQueue; | ||||||
| use Illuminate\Support\Collection; |  | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class Show extends Component | class Show extends Component | ||||||
|   | |||||||
| @@ -21,6 +21,8 @@ class Heading extends Component | |||||||
| 
 | 
 | ||||||
|     protected string $deploymentUuid; |     protected string $deploymentUuid; | ||||||
| 
 | 
 | ||||||
|  |     public bool $docker_cleanup = true; | ||||||
|  | 
 | ||||||
|     public function getListeners() |     public function getListeners() | ||||||
|     { |     { | ||||||
|         $teamId = auth()->user()->currentTeam()->id; |         $teamId = auth()->user()->currentTeam()->id; | ||||||
| @@ -102,7 +104,7 @@ class Heading extends Component | |||||||
| 
 | 
 | ||||||
|     public function stop() |     public function stop() | ||||||
|     { |     { | ||||||
|         StopApplication::run($this->application); |         StopApplication::run($this->application, false, $this->docker_cleanup); | ||||||
|         $this->application->status = 'exited'; |         $this->application->status = 'exited'; | ||||||
|         $this->application->save(); |         $this->application->save(); | ||||||
|         if ($this->application->additional_servers->count() > 0) { |         if ($this->application->additional_servers->count() > 0) { | ||||||
| @@ -135,4 +137,13 @@ class Heading extends Component | |||||||
|             'environment_name' => $this->parameters['environment_name'], |             '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\Actions\Docker\GetContainersStatus; | ||||||
| use App\Models\Application; | use App\Models\Application; | ||||||
| use App\Models\ApplicationPreview; | use App\Models\ApplicationPreview; | ||||||
|  | use Illuminate\Process\InvokedProcess; | ||||||
| use Illuminate\Support\Collection; | use Illuminate\Support\Collection; | ||||||
|  | use Illuminate\Support\Facades\Process; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| use Spatie\Url\Url; | use Spatie\Url\Url; | ||||||
| use Visus\Cuid2\Cuid2; | use Visus\Cuid2\Cuid2; | ||||||
| @@ -184,17 +186,20 @@ class Previews extends Component | |||||||
|     public function stop(int $pull_request_id) |     public function stop(int $pull_request_id) | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|  |             $server = $this->application->destination->server; | ||||||
|  |             $timeout = 300; | ||||||
|  | 
 | ||||||
|             if ($this->application->destination->server->isSwarm()) { |             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 { |             } else { | ||||||
|                 $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); |                 $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); | ||||||
|                 foreach ($containers as $container) { |                 $this->stopContainers($containers, $server, $timeout); | ||||||
|                     $name = str_replace('/', '', $container['Names']); |  | ||||||
|                     instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false); |  | ||||||
|             } |             } | ||||||
|             } | 
 | ||||||
|             GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high'); |             GetContainersStatus::run($server); | ||||||
|             $this->dispatch('reloadWindow'); |             $this->application->refresh(); | ||||||
|  |             $this->dispatch('containerStatusUpdated'); | ||||||
|  |             $this->dispatch('success', 'Preview Deployment stopped.'); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             return handleError($e, $this); |             return handleError($e, $this); | ||||||
|         } |         } | ||||||
| @@ -203,16 +208,21 @@ class Previews extends Component | |||||||
|     public function delete(int $pull_request_id) |     public function delete(int $pull_request_id) | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|  |             $server = $this->application->destination->server; | ||||||
|  |             $timeout = 300; | ||||||
|  | 
 | ||||||
|             if ($this->application->destination->server->isSwarm()) { |             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 { |             } else { | ||||||
|                 $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); |                 $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); | ||||||
|                 foreach ($containers as $container) { |                 $this->stopContainers($containers, $server, $timeout); | ||||||
|                     $name = str_replace('/', '', $container['Names']); |  | ||||||
|                     instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false); |  | ||||||
|             } |             } | ||||||
|             } | 
 | ||||||
|             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->application->refresh(); | ||||||
|             $this->dispatch('update_links'); |             $this->dispatch('update_links'); | ||||||
|             $this->dispatch('success', 'Preview deleted.'); |             $this->dispatch('success', 'Preview deleted.'); | ||||||
| @@ -220,4 +230,49 @@ class Previews extends Component | |||||||
|             return handleError($e, $this); |             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; | namespace App\Livewire\Project\Database; | ||||||
| 
 | 
 | ||||||
| use App\Models\ScheduledDatabaseBackup; | use App\Models\ScheduledDatabaseBackup; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| use Spatie\Url\Url; | use Spatie\Url\Url; | ||||||
| 
 | 
 | ||||||
| @@ -12,6 +14,12 @@ class BackupEdit extends Component | |||||||
| 
 | 
 | ||||||
|     public $s3s; |     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 ?string $status = null; | ||||||
| 
 | 
 | ||||||
|     public array $parameters; |     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 { |         try { | ||||||
|  |             if ($this->delete_associated_backups_locally) { | ||||||
|  |                 $this->deleteAssociatedBackupsLocally(); | ||||||
|  |             } | ||||||
|  |             if ($this->delete_associated_backups_s3) { | ||||||
|  |                 $this->deleteAssociatedBackupsS3(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             $this->backup->delete(); |             $this->backup->delete(); | ||||||
|  | 
 | ||||||
|             if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { |             if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { | ||||||
|                 $previousUrl = url()->previous(); |                 $previousUrl = url()->previous(); | ||||||
|                 $url = Url::fromString($previousUrl); |                 $url = Url::fromString($previousUrl); | ||||||
| @@ -104,4 +126,66 @@ class BackupEdit extends Component | |||||||
|             $this->dispatch('error', $e->getMessage()); |             $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; | namespace App\Livewire\Project\Database; | ||||||
| 
 | 
 | ||||||
| use App\Models\ScheduledDatabaseBackup; | use App\Models\ScheduledDatabaseBackup; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
|  | use Livewire\Attributes\On; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class BackupExecutions extends Component | class BackupExecutions extends Component | ||||||
| { | { | ||||||
|     public ?ScheduledDatabaseBackup $backup = null; |     public ?ScheduledDatabaseBackup $backup = null; | ||||||
|  | 
 | ||||||
|     public $database; |     public $database; | ||||||
|  | 
 | ||||||
|     public $executions = []; |     public $executions = []; | ||||||
|  | 
 | ||||||
|     public $setDeletableBackup; |     public $setDeletableBackup; | ||||||
| 
 | 
 | ||||||
|  |     public $delete_backup_s3 = true; | ||||||
|  | 
 | ||||||
|  |     public $delete_backup_sftp = true; | ||||||
|  | 
 | ||||||
|     public function getListeners() |     public function getListeners() | ||||||
|     { |     { | ||||||
|         $userId = auth()->user()->id; |         $userId = Auth::id(); | ||||||
| 
 | 
 | ||||||
|         return [ |         return [ | ||||||
|             "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', |             "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)) { |         if (is_null($execution)) { | ||||||
|             $this->dispatch('error', 'Backup execution not found.'); |             $this->dispatch('error', 'Backup execution not found.'); | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { |         if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { | ||||||
|             delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); |             delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); | ||||||
|         } else { |         } else { | ||||||
|             delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); |             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(); |         $execution->delete(); | ||||||
|         $this->dispatch('success', 'Backup deleted.'); |         $this->dispatch('success', 'Backup deleted.'); | ||||||
|         $this->refreshBackupExecutions(); |         $this->refreshBackupExecutions(); | ||||||
| @@ -82,6 +109,7 @@ class BackupExecutions extends Component | |||||||
|                 return $server; |                 return $server; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -92,6 +120,7 @@ class BackupExecutions extends Component | |||||||
|             return 'UTC'; |             return 'UTC'; | ||||||
|         } |         } | ||||||
|         $serverTimezone = $server->settings->server_timezone; |         $serverTimezone = $server->settings->server_timezone; | ||||||
|  | 
 | ||||||
|         return $serverTimezone; |         return $serverTimezone; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -104,6 +133,17 @@ class BackupExecutions extends Component | |||||||
|         } catch (\Exception $e) { |         } catch (\Exception $e) { | ||||||
|             $dateObj->setTimezone(new \DateTimeZone('UTC')); |             $dateObj->setTimezone(new \DateTimeZone('UTC')); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         return $dateObj->format('Y-m-d H:i:s T'); |         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'], | ||||||
|  |             ], | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ class Heading extends Component | |||||||
| 
 | 
 | ||||||
|     public array $parameters; |     public array $parameters; | ||||||
| 
 | 
 | ||||||
|  |     public $docker_cleanup = true; | ||||||
|  | 
 | ||||||
|     public function getListeners() |     public function getListeners() | ||||||
|     { |     { | ||||||
|         $userId = auth()->user()->id; |         $userId = auth()->user()->id; | ||||||
| @@ -54,7 +56,7 @@ class Heading extends Component | |||||||
| 
 | 
 | ||||||
|     public function stop() |     public function stop() | ||||||
|     { |     { | ||||||
|         StopDatabase::run($this->database); |         StopDatabase::run($this->database, false, $this->docker_cleanup); | ||||||
|         $this->database->status = 'exited'; |         $this->database->status = 'exited'; | ||||||
|         $this->database->save(); |         $this->database->save(); | ||||||
|         $this->check_status(); |         $this->check_status(); | ||||||
| @@ -71,4 +73,13 @@ class Heading extends Component | |||||||
|         $activity = StartDatabase::run($this->database); |         $activity = StartDatabase::run($this->database); | ||||||
|         $this->dispatch('activityMonitor', $activity->id); |         $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).'], | ||||||
|  |             ], | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,9 +13,12 @@ class DeleteEnvironment extends Component | |||||||
| 
 | 
 | ||||||
|     public bool $disabled = false; |     public bool $disabled = false; | ||||||
| 
 | 
 | ||||||
|  |     public string $environmentName = ''; | ||||||
|  | 
 | ||||||
|     public function mount() |     public function mount() | ||||||
|     { |     { | ||||||
|         $this->parameters = get_route_parameters(); |         $this->parameters = get_route_parameters(); | ||||||
|  |         $this->environmentName = Environment::findOrFail($this->environment_id)->name; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function delete() |     public function delete() | ||||||
|   | |||||||
| @@ -13,9 +13,12 @@ class DeleteProject extends Component | |||||||
| 
 | 
 | ||||||
|     public bool $disabled = false; |     public bool $disabled = false; | ||||||
| 
 | 
 | ||||||
|  |     public string $projectName = ''; | ||||||
|  | 
 | ||||||
|     public function mount() |     public function mount() | ||||||
|     { |     { | ||||||
|         $this->parameters = get_route_parameters(); |         $this->parameters = get_route_parameters(); | ||||||
|  |         $this->projectName = Project::findOrFail($this->project_id)->name; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function delete() |     public function delete() | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ class Configuration extends Component | |||||||
|             $application = $this->service->applications->find($id); |             $application = $this->service->applications->find($id); | ||||||
|             if ($application) { |             if ($application) { | ||||||
|                 $application->restart(); |                 $application->restart(); | ||||||
|                 $this->dispatch('success', 'Application restarted successfully.'); |                 $this->dispatch('success', 'Service application restarted successfully.'); | ||||||
|             } |             } | ||||||
|         } catch (\Exception $e) { |         } catch (\Exception $e) { | ||||||
|             return handleError($e, $this); |             return handleError($e, $this); | ||||||
| @@ -65,7 +65,7 @@ class Configuration extends Component | |||||||
|             $database = $this->service->databases->find($id); |             $database = $this->service->databases->find($id); | ||||||
|             if ($database) { |             if ($database) { | ||||||
|                 $database->restart(); |                 $database->restart(); | ||||||
|                 $this->dispatch('success', 'Database restarted successfully.'); |                 $this->dispatch('success', 'Service database restarted successfully.'); | ||||||
|             } |             } | ||||||
|         } catch (\Exception $e) { |         } catch (\Exception $e) { | ||||||
|             return handleError($e, $this); |             return handleError($e, $this); | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb; | |||||||
| use App\Models\StandaloneMysql; | use App\Models\StandaloneMysql; | ||||||
| use App\Models\StandalonePostgresql; | use App\Models\StandalonePostgresql; | ||||||
| use App\Models\StandaloneRedis; | use App\Models\StandaloneRedis; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class FileStorage extends 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 { |         try { | ||||||
|             $message = 'File deleted.'; |             $message = 'File deleted.'; | ||||||
|             if ($this->fileStorage->is_directory) { |             if ($this->fileStorage->is_directory) { | ||||||
| @@ -129,6 +137,13 @@ class FileStorage extends Component | |||||||
| 
 | 
 | ||||||
|     public function render() |     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 $isDeploymentProgress = false; | ||||||
| 
 | 
 | ||||||
|  |     public $docker_cleanup = true; | ||||||
|  | 
 | ||||||
|  |     public $title = 'Configuration'; | ||||||
|  | 
 | ||||||
|     public function mount() |     public function mount() | ||||||
|     { |     { | ||||||
|         if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { |         if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { | ||||||
| @@ -40,7 +44,7 @@ class Navbar extends Component | |||||||
| 
 | 
 | ||||||
|     public function serviceStarted() |     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()) { |         if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) { | ||||||
|             $this->service->isConfigurationChanged(true); |             $this->service->isConfigurationChanged(true); | ||||||
|             $this->dispatch('configurationChanged'); |             $this->dispatch('configurationChanged'); | ||||||
| @@ -60,11 +64,6 @@ class Navbar extends Component | |||||||
|         $this->dispatch('success', 'Service status updated.'); |         $this->dispatch('success', 'Service status updated.'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function render() |  | ||||||
|     { |  | ||||||
|         return view('livewire.project.service.navbar'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function checkDeployments() |     public function checkDeployments() | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
| @@ -95,14 +94,9 @@ class Navbar extends Component | |||||||
|         $this->dispatch('activityMonitor', $activity->id); |         $this->dispatch('activityMonitor', $activity->id); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function stop(bool $forceCleanup = false) |     public function stop() | ||||||
|     { |     { | ||||||
|         StopService::run($this->service); |         StopService::run($this->service, false, $this->docker_cleanup); | ||||||
|         if ($forceCleanup) { |  | ||||||
|             $this->dispatch('success', 'Containers cleaned up.'); |  | ||||||
|         } else { |  | ||||||
|             $this->dispatch('success', 'Service stopped.'); |  | ||||||
|         } |  | ||||||
|         ServiceStatusChanged::dispatch(); |         ServiceStatusChanged::dispatch(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -121,4 +115,13 @@ class Navbar extends Component | |||||||
|         $activity = StartService::run($this->service); |         $activity = StartService::run($this->service); | ||||||
|         $this->dispatch('activityMonitor', $activity->id); |         $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; | namespace App\Livewire\Project\Service; | ||||||
| 
 | 
 | ||||||
| use App\Models\ServiceApplication; | use App\Models\ServiceApplication; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class ServiceApplicationView extends Component | class ServiceApplicationView extends Component | ||||||
| @@ -11,6 +13,10 @@ class ServiceApplicationView extends Component | |||||||
| 
 | 
 | ||||||
|     public $parameters; |     public $parameters; | ||||||
| 
 | 
 | ||||||
|  |     public $docker_cleanup = true; | ||||||
|  | 
 | ||||||
|  |     public $delete_volumes = true; | ||||||
|  | 
 | ||||||
|     protected $rules = [ |     protected $rules = [ | ||||||
|         'application.human_name' => 'nullable', |         'application.human_name' => 'nullable', | ||||||
|         'application.description' => 'nullable', |         'application.description' => 'nullable', | ||||||
| @@ -23,11 +29,6 @@ class ServiceApplicationView extends Component | |||||||
|         'application.is_stripprefix_enabled' => 'nullable|boolean', |         'application.is_stripprefix_enabled' => 'nullable|boolean', | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     public function render() |  | ||||||
|     { |  | ||||||
|         return view('livewire.project.service.service-application-view'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function updatedApplicationFqdn() |     public function updatedApplicationFqdn() | ||||||
|     { |     { | ||||||
|         $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); |         $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.'); |         $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 { |         try { | ||||||
|             $this->application->delete(); |             $this->application->delete(); | ||||||
|             $this->dispatch('success', 'Application deleted.'); |             $this->dispatch('success', 'Application deleted.'); | ||||||
| @@ -91,4 +98,17 @@ class ServiceApplicationView extends Component | |||||||
|             $this->dispatch('generateDockerCompose'); |             $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; | namespace App\Livewire\Project\Shared; | ||||||
| 
 | 
 | ||||||
| use App\Jobs\DeleteResourceJob; | 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 Livewire\Component; | ||||||
| use Visus\Cuid2\Cuid2; | use Visus\Cuid2\Cuid2; | ||||||
| 
 | 
 | ||||||
| @@ -10,6 +15,8 @@ class Danger extends Component | |||||||
| { | { | ||||||
|     public $resource; |     public $resource; | ||||||
| 
 | 
 | ||||||
|  |     public $resourceName; | ||||||
|  | 
 | ||||||
|     public $projectUuid; |     public $projectUuid; | ||||||
| 
 | 
 | ||||||
|     public $environmentName; |     public $environmentName; | ||||||
| @@ -18,22 +25,93 @@ class Danger extends Component | |||||||
| 
 | 
 | ||||||
|     public bool $delete_volumes = true; |     public bool $delete_volumes = true; | ||||||
| 
 | 
 | ||||||
|  |     public bool $docker_cleanup = true; | ||||||
|  | 
 | ||||||
|  |     public bool $delete_connected_networks = true; | ||||||
|  | 
 | ||||||
|     public ?string $modalId = null; |     public ?string $modalId = null; | ||||||
| 
 | 
 | ||||||
|  |     public string $resourceDomain = ''; | ||||||
|  | 
 | ||||||
|     public function mount() |     public function mount() | ||||||
|     { |     { | ||||||
|         $this->modalId = new Cuid2; |  | ||||||
|         $parameters = get_route_parameters(); |         $parameters = get_route_parameters(); | ||||||
|  |         $this->modalId = new Cuid2; | ||||||
|         $this->projectUuid = data_get($parameters, 'project_uuid'); |         $this->projectUuid = data_get($parameters, 'project_uuid'); | ||||||
|         $this->environmentName = data_get($parameters, 'environment_name'); |         $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(); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     public function delete() |         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($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 { |         try { | ||||||
|             // $this->authorize('delete', $this->resource);
 |  | ||||||
|             $this->resource->delete(); |             $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', [ |             return redirect()->route('project.resource.index', [ | ||||||
|                 'project_uuid' => $this->projectUuid, |                 'project_uuid' => $this->projectUuid, | ||||||
| @@ -43,4 +121,19 @@ class Danger extends Component | |||||||
|             return handleError($e, $this); |             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\Jobs\ContainerStatusJob; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use App\Models\StandaloneDocker; | use App\Models\StandaloneDocker; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| use Visus\Cuid2\Cuid2; | use Visus\Cuid2\Cuid2; | ||||||
| 
 | 
 | ||||||
| @@ -115,8 +117,14 @@ class Destination extends Component | |||||||
|         ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); |         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) { |         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.'); |             $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; | namespace App\Livewire\Project\Shared; | ||||||
| 
 | 
 | ||||||
| use App\Actions\Server\RunCommand; |  | ||||||
| use App\Models\Application; | use App\Models\Application; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use App\Models\Service; | use App\Models\Service; | ||||||
| use Illuminate\Support\Collection; | use Illuminate\Support\Collection; | ||||||
|  | use Livewire\Attributes\On; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class ExecuteContainerCommand extends Component | class ExecuteContainerCommand extends Component | ||||||
| { | { | ||||||
|     public string $command; |     public $container; | ||||||
| 
 |  | ||||||
|     public string $container; |  | ||||||
| 
 | 
 | ||||||
|     public Collection $containers; |     public Collection $containers; | ||||||
| 
 | 
 | ||||||
| @@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component | |||||||
| 
 | 
 | ||||||
|     public string $type; |     public string $type; | ||||||
| 
 | 
 | ||||||
|     public string $workDir = ''; |  | ||||||
| 
 |  | ||||||
|     public Server $server; |     public Server $server; | ||||||
| 
 | 
 | ||||||
|     public Collection $servers; |     public Collection $servers; | ||||||
| @@ -33,11 +29,13 @@ class ExecuteContainerCommand extends Component | |||||||
|         'server' => 'required', |         'server' => 'required', | ||||||
|         'container' => 'required', |         'container' => 'required', | ||||||
|         'command' => 'required', |         'command' => 'required', | ||||||
|         'workDir' => 'nullable', |  | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     public function mount() |     public function mount() | ||||||
|     { |     { | ||||||
|  |         if (! auth()->user()->isAdmin()) { | ||||||
|  |             abort(403); | ||||||
|  |         } | ||||||
|         $this->parameters = get_route_parameters(); |         $this->parameters = get_route_parameters(); | ||||||
|         $this->containers = collect(); |         $this->containers = collect(); | ||||||
|         $this->servers = collect(); |         $this->servers = collect(); | ||||||
| @@ -62,24 +60,13 @@ class ExecuteContainerCommand extends Component | |||||||
|             if ($this->resource->destination->server->isFunctional()) { |             if ($this->resource->destination->server->isFunctional()) { | ||||||
|                 $this->servers = $this->servers->push($this->resource->destination->server); |                 $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')) { |         } elseif (data_get($this->parameters, 'service_uuid')) { | ||||||
|             $this->type = 'service'; |             $this->type = 'service'; | ||||||
|             $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); |             $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()) { |             if ($this->resource->server->isFunctional()) { | ||||||
|                 $this->servers = $this->servers->push($this->resource->server); |                 $this->servers = $this->servers->push($this->resource->server); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if ($this->containers->count() > 0) { |  | ||||||
|             $this->container = $this->containers->first(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function loadContainers() |     public function loadContainers() | ||||||
| @@ -102,44 +89,65 @@ class ExecuteContainerCommand extends Component | |||||||
|                     ]; |                     ]; | ||||||
|                     $this->containers = $this->containers->push($payload); |                     $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 ($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(); |             $this->container = $this->containers->first(); | ||||||
|             } elseif (data_get($this->parameters, 'service_uuid')) { |  | ||||||
|                 $this->container = $this->containers->first(); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function runCommand() |     #[On('connectToContainer')]
 | ||||||
|  |     public function connectToContainer() | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             if (data_get($this->parameters, 'application_uuid')) { |             $container_name = data_get($this->container, 'container.Names'); | ||||||
|                 $container = $this->containers->where('container.Names', $this->container)->first(); |             if (is_null($container_name)) { | ||||||
|                 $container_name = data_get($container, 'container.Names'); |  | ||||||
|                 if (is_null($container)) { |  | ||||||
|                 throw new \RuntimeException('Container not found.'); |                 throw new \RuntimeException('Container not found.'); | ||||||
|             } |             } | ||||||
|                 $server = data_get($container, 'server'); |             $server = data_get($this->container, 'server'); | ||||||
|             } else { | 
 | ||||||
|                 $container_name = $this->container; |  | ||||||
|                 $server = $this->servers->first(); |  | ||||||
|             } |  | ||||||
|             if ($server->isForceDisabled()) { |             if ($server->isForceDisabled()) { | ||||||
|                 throw new \RuntimeException('Server is disabled.'); |                 throw new \RuntimeException('Server is disabled.'); | ||||||
|             } |             } | ||||||
|             $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'"; | 
 | ||||||
|             if (! empty($this->workDir)) { |             $this->dispatch('send-terminal-command', | ||||||
|                 $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}"; |                 true, | ||||||
|             } else { |                 $container_name, | ||||||
|                 $exec = "docker exec {$container_name} {$cmd}"; |                 $server->uuid, | ||||||
|             } |             ); | ||||||
|             $activity = RunCommand::run(server: $server, command: $exec); | 
 | ||||||
|             $this->dispatch('activityMonitor', $activity->id); |  | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             return handleError($e, $this); |             return handleError($e, $this); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Livewire\Project\Shared; | namespace App\Livewire\Project\Shared; | ||||||
| 
 | 
 | ||||||
|  | use App\Helpers\SshMultiplexingHelper; | ||||||
| use App\Models\Application; | use App\Models\Application; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use App\Models\Service; | use App\Models\Service; | ||||||
| @@ -108,14 +109,14 @@ class GetLogs extends Component | |||||||
|                         $command = parseCommandsByLineForSudo(collect($command), $this->server); |                         $command = parseCommandsByLineForSudo(collect($command), $this->server); | ||||||
|                         $command = $command[0]; |                         $command = $command[0]; | ||||||
|                     } |                     } | ||||||
|                     $sshCommand = generateSshCommand($this->server, $command); |                     $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); | ||||||
|                 } else { |                 } else { | ||||||
|                     $command = "docker logs -n {$this->numberOfLines} -t {$this->container}"; |                     $command = "docker logs -n {$this->numberOfLines} -t {$this->container}"; | ||||||
|                     if ($this->server->isNonRoot()) { |                     if ($this->server->isNonRoot()) { | ||||||
|                         $command = parseCommandsByLineForSudo(collect($command), $this->server); |                         $command = parseCommandsByLineForSudo(collect($command), $this->server); | ||||||
|                         $command = $command[0]; |                         $command = $command[0]; | ||||||
|                     } |                     } | ||||||
|                     $sshCommand = generateSshCommand($this->server, $command); |                     $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 if ($this->server->isSwarm()) { |                 if ($this->server->isSwarm()) { | ||||||
| @@ -124,14 +125,14 @@ class GetLogs extends Component | |||||||
|                         $command = parseCommandsByLineForSudo(collect($command), $this->server); |                         $command = parseCommandsByLineForSudo(collect($command), $this->server); | ||||||
|                         $command = $command[0]; |                         $command = $command[0]; | ||||||
|                     } |                     } | ||||||
|                     $sshCommand = generateSshCommand($this->server, $command); |                     $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); | ||||||
|                 } else { |                 } else { | ||||||
|                     $command = "docker logs -n {$this->numberOfLines} {$this->container}"; |                     $command = "docker logs -n {$this->numberOfLines} {$this->container}"; | ||||||
|                     if ($this->server->isNonRoot()) { |                     if ($this->server->isNonRoot()) { | ||||||
|                         $command = parseCommandsByLineForSudo(collect($command), $this->server); |                         $command = parseCommandsByLineForSudo(collect($command), $this->server); | ||||||
|                         $command = $command[0]; |                         $command = $command[0]; | ||||||
|                     } |                     } | ||||||
|                     $sshCommand = generateSshCommand($this->server, $command); |                     $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             if ($refresh) { |             if ($refresh) { | ||||||
|   | |||||||
| @@ -7,7 +7,9 @@ use Livewire\Component; | |||||||
| class Executions extends Component | class Executions extends Component | ||||||
| { | { | ||||||
|     public $executions = []; |     public $executions = []; | ||||||
|  | 
 | ||||||
|     public $selectedKey; |     public $selectedKey; | ||||||
|  | 
 | ||||||
|     public $task; |     public $task; | ||||||
| 
 | 
 | ||||||
|     public function getListeners() |     public function getListeners() | ||||||
| @@ -42,6 +44,7 @@ class Executions extends Component | |||||||
|                 return $this->task->service->destination->server; |                 return $this->task->service->destination->server; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -52,6 +55,7 @@ class Executions extends Component | |||||||
|             return 'UTC'; |             return 'UTC'; | ||||||
|         } |         } | ||||||
|         $serverTimezone = $server->settings->server_timezone; |         $serverTimezone = $server->settings->server_timezone; | ||||||
|  | 
 | ||||||
|         return $serverTimezone; |         return $serverTimezone; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -64,6 +68,7 @@ class Executions extends Component | |||||||
|         } catch (\Exception $e) { |         } catch (\Exception $e) { | ||||||
|             $dateObj->setTimezone(new \DateTimeZone('UTC')); |             $dateObj->setTimezone(new \DateTimeZone('UTC')); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         return $dateObj->format('Y-m-d H:i:s T'); |         return $dateObj->format('Y-m-d H:i:s T'); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,6 +20,8 @@ class Show extends Component | |||||||
| 
 | 
 | ||||||
|     public string $type; |     public string $type; | ||||||
| 
 | 
 | ||||||
|  |     public string $scheduledTaskName; | ||||||
|  | 
 | ||||||
|     protected $rules = [ |     protected $rules = [ | ||||||
|         'task.enabled' => 'required|boolean', |         'task.enabled' => 'required|boolean', | ||||||
|         'task.name' => 'required|string', |         'task.name' => 'required|string', | ||||||
| @@ -49,6 +51,7 @@ class Show extends Component | |||||||
| 
 | 
 | ||||||
|         $this->modalId = new Cuid2; |         $this->modalId = new Cuid2; | ||||||
|         $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); |         $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); | ||||||
|  |         $this->scheduledTaskName = $this->task->name; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function instantSave() |     public function instantSave() | ||||||
| @@ -75,9 +78,9 @@ class Show extends Component | |||||||
|             $this->task->delete(); |             $this->task->delete(); | ||||||
| 
 | 
 | ||||||
|             if ($this->type == 'application') { |             if ($this->type == 'application') { | ||||||
|                 return redirect()->route('project.application.configuration', $this->parameters); |                 return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName); | ||||||
|             } else { |             } else { | ||||||
|                 return redirect()->route('project.service.configuration', $this->parameters); |                 return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName); | ||||||
|             } |             } | ||||||
|         } catch (\Exception $e) { |         } catch (\Exception $e) { | ||||||
|             return handleError($e); |             return handleError($e); | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ | |||||||
| namespace App\Livewire\Project\Shared\Storages; | namespace App\Livewire\Project\Shared\Storages; | ||||||
| 
 | 
 | ||||||
| use App\Models\LocalPersistentVolume; | use App\Models\LocalPersistentVolume; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class Show extends Component | class Show extends Component | ||||||
| @@ -36,8 +38,14 @@ class Show extends Component | |||||||
|         $this->dispatch('success', 'Storage updated successfully'); |         $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->storage->delete(); | ||||||
|         $this->dispatch('refreshStorages'); |         $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; | namespace App\Livewire\Security\PrivateKey; | ||||||
| 
 | 
 | ||||||
| use App\Models\PrivateKey; | use App\Models\PrivateKey; | ||||||
| use DanHarrin\LivewireRateLimiting\WithRateLimiting; |  | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| use phpseclib3\Crypt\PublicKeyLoader; |  | ||||||
| 
 | 
 | ||||||
| class Create extends Component | class Create extends Component | ||||||
| { | { | ||||||
|     use WithRateLimiting; |     public string $name = ''; | ||||||
| 
 | 
 | ||||||
|     public string $name; |     public string $value = ''; | ||||||
| 
 |  | ||||||
|     public string $value; |  | ||||||
| 
 | 
 | ||||||
|     public ?string $from = null; |     public ?string $from = null; | ||||||
| 
 | 
 | ||||||
| @@ -26,72 +22,69 @@ class Create extends Component | |||||||
|         'value' => 'required|string', |         'value' => 'required|string', | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     protected $validationAttributes = [ |  | ||||||
|         'name' => 'name', |  | ||||||
|         'value' => 'private Key', |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     public function generateNewRSAKey() |     public function generateNewRSAKey() | ||||||
|     { |     { | ||||||
|         try { |         $this->generateNewKey('rsa'); | ||||||
|             $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); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function generateNewEDKey() |     public function generateNewEDKey() | ||||||
|     { |     { | ||||||
|         try { |         $this->generateNewKey('ed25519'); | ||||||
|             $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); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function updated($updateProperty) |     private function generateNewKey($type) | ||||||
|     { |     { | ||||||
|         if ($updateProperty === 'value') { |         $keyData = PrivateKey::generateNewKeyPair($type); | ||||||
|             try { |         $this->setKeyData($keyData); | ||||||
|                 $this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']); |  | ||||||
|             } catch (\Throwable $e) { |  | ||||||
|                 if ($this->$updateProperty === '') { |  | ||||||
|                     $this->publicKey = ''; |  | ||||||
|                 } else { |  | ||||||
|                     $this->publicKey = 'Invalid private key'; |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public function updated($property) | ||||||
|  |     { | ||||||
|  |         if ($property === 'value') { | ||||||
|  |             $this->validatePrivateKey(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|         $this->validateOnly($updateProperty); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     public function createPrivateKey() |     public function createPrivateKey() | ||||||
|     { |     { | ||||||
|         $this->validate(); |         $this->validate(); | ||||||
|  | 
 | ||||||
|         try { |         try { | ||||||
|             $this->value = trim($this->value); |             $privateKey = PrivateKey::createAndStore([ | ||||||
|             if (! str_ends_with($this->value, "\n")) { |  | ||||||
|                 $this->value .= "\n"; |  | ||||||
|             } |  | ||||||
|             $private_key = PrivateKey::create([ |  | ||||||
|                 'name' => $this->name, |                 'name' => $this->name, | ||||||
|                 'description' => $this->description, |                 'description' => $this->description, | ||||||
|                 'private_key' => $this->value, |                 'private_key' => trim($this->value)."\n", | ||||||
|                 'team_id' => currentTeam()->id, |                 '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) { |         } catch (\Throwable $e) { | ||||||
|             return handleError($e, $this); |             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 { |         try { | ||||||
|             $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail(); |             $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail(); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             return handleError($e, $this); |             abort(404); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function loadPublicKey() |     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() |     public function delete() | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             if ($this->private_key->isEmpty()) { |             $this->private_key->safeDelete(); | ||||||
|                 $this->private_key->delete(); |  | ||||||
|             currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); |             currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); | ||||||
| 
 | 
 | ||||||
|             return redirect()->route('security.private-key.index'); |             return redirect()->route('security.private-key.index'); | ||||||
|             } |         } catch (\Exception $e) { | ||||||
|             $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.'); |             $this->dispatch('error', $e->getMessage()); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             return handleError($e, $this); |             return handleError($e, $this); | ||||||
|         } |         } | ||||||
| @@ -56,8 +58,9 @@ class Show extends Component | |||||||
|     public function changePrivateKey() |     public function changePrivateKey() | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             $this->private_key->private_key = formatPrivateKey($this->private_key->private_key); |             $this->private_key->updatePrivateKey([ | ||||||
|             $this->private_key->save(); |                 'private_key' => formatPrivateKey($this->private_key->private_key), | ||||||
|  |             ]); | ||||||
|             refresh_server_connection($this->private_key); |             refresh_server_connection($this->private_key); | ||||||
|             $this->dispatch('success', 'Private key updated.'); |             $this->dispatch('success', 'Private key updated.'); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|   | |||||||
| @@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component | |||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); |             $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->settings->is_cloudflare_tunnel = true; | ||||||
|             $server->ip = $this->ssh_domain; |             $server->ip = $this->ssh_domain; | ||||||
|             $server->save(); |             $server->save(); | ||||||
|             $server->settings->save(); |             $server->settings->save(); | ||||||
|             $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); |             $this->dispatch('warning', 'Cloudflare Tunnels configuration started.'); | ||||||
|             $this->dispatch('refreshServerShow'); |  | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             return handleError($e, $this); |             return handleError($e, $this); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ | |||||||
| namespace App\Livewire\Server; | namespace App\Livewire\Server; | ||||||
| 
 | 
 | ||||||
| use Illuminate\Foundation\Auth\Access\AuthorizesRequests; | use Illuminate\Foundation\Auth\Access\AuthorizesRequests; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class Delete extends Component | class Delete extends Component | ||||||
| @@ -11,8 +13,13 @@ class Delete extends Component | |||||||
| 
 | 
 | ||||||
|     public $server; |     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 { |         try { | ||||||
|             $this->authorize('delete', $this->server); |             $this->authorize('delete', $this->server); | ||||||
|             if ($this->server->hasDefinedResources()) { |             if ($this->server->hasDefinedResources()) { | ||||||
|   | |||||||
| @@ -24,7 +24,16 @@ class Form extends Component | |||||||
| 
 | 
 | ||||||
|     public $timezones; |     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 = [ |     protected $rules = [ | ||||||
|         'server.name' => 'required', |         '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() |     public function serverInstalled() | ||||||
|     { |     { | ||||||
|         $this->server->refresh(); |         $this->server->refresh(); | ||||||
| @@ -234,4 +249,12 @@ class Form extends Component | |||||||
|         $this->server->settings->save(); |         $this->server->settings->save(); | ||||||
|         $this->dispatch('success', 'Server timezone updated.'); |         $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; | namespace App\Livewire\Server\New; | ||||||
| 
 | 
 | ||||||
| use App\Enums\ProxyStatus; |  | ||||||
| use App\Enums\ProxyTypes; | use App\Enums\ProxyTypes; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use App\Models\Team; | use App\Models\Team; | ||||||
|  | use Illuminate\Support\Collection; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class ByIp extends Component | class ByIp extends Component | ||||||
| @@ -40,7 +40,7 @@ class ByIp extends Component | |||||||
| 
 | 
 | ||||||
|     public bool $is_build_server = false; |     public bool $is_build_server = false; | ||||||
| 
 | 
 | ||||||
|     public $swarm_managers = []; |     public Collection $swarm_managers; | ||||||
| 
 | 
 | ||||||
|     protected $rules = [ |     protected $rules = [ | ||||||
|         'name' => 'required|string', |         'name' => 'required|string', | ||||||
| @@ -102,11 +102,6 @@ class ByIp extends Component | |||||||
|                 'port' => $this->port, |                 'port' => $this->port, | ||||||
|                 'team_id' => currentTeam()->id, |                 'team_id' => currentTeam()->id, | ||||||
|                 'private_key_id' => $this->private_key_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) { |             if ($this->is_swarm_worker) { | ||||||
|                 $payload['swarm_cluster'] = $this->selected_swarm_cluster; |                 $payload['swarm_cluster'] = $this->selected_swarm_cluster; | ||||||
| @@ -115,6 +110,9 @@ class ByIp extends Component | |||||||
|                 data_forget($payload, 'proxy'); |                 data_forget($payload, 'proxy'); | ||||||
|             } |             } | ||||||
|             $server = Server::create($payload); |             $server = Server::create($payload); | ||||||
|  |             $server->proxy->set('status', 'exited'); | ||||||
|  |             $server->proxy->set('type', ProxyTypes::TRAEFIK->value); | ||||||
|  |             $server->save(); | ||||||
|             if ($this->is_build_server) { |             if ($this->is_build_server) { | ||||||
|                 $this->is_swarm_manager = false; |                 $this->is_swarm_manager = false; | ||||||
|                 $this->is_swarm_worker = false; |                 $this->is_swarm_worker = false; | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ class Proxy extends Component | |||||||
|     { |     { | ||||||
|         $this->server->proxy = null; |         $this->server->proxy = null; | ||||||
|         $this->server->save(); |         $this->server->save(); | ||||||
|  |         $this->dispatch('proxyChanged'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function selectProxy($proxy_type) |     public function selectProxy($proxy_type) | ||||||
| @@ -47,7 +48,7 @@ class Proxy extends Component | |||||||
|         $this->server->proxy->set('type', $proxy_type); |         $this->server->proxy->set('type', $proxy_type); | ||||||
|         $this->server->save(); |         $this->server->save(); | ||||||
|         $this->selectedProxy = $this->server->proxy->type; |         $this->selectedProxy = $this->server->proxy->type; | ||||||
|         if ($this->selectedProxy !== 'NONE') { |         if ($this->server->proxySet()) { | ||||||
|             StartProxy::run($this->server, false); |             StartProxy::run($this->server, false); | ||||||
|         } |         } | ||||||
|         $this->dispatch('proxyStatusUpdated'); |         $this->dispatch('proxyStatusUpdated'); | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ use App\Actions\Proxy\CheckProxy; | |||||||
| use App\Actions\Proxy\StartProxy; | use App\Actions\Proxy\StartProxy; | ||||||
| use App\Events\ProxyStatusChanged; | use App\Events\ProxyStatusChanged; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
|  | use Illuminate\Process\InvokedProcess; | ||||||
|  | use Illuminate\Support\Facades\Process; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class Deploy extends Component | class Deploy extends Component | ||||||
| @@ -29,6 +31,7 @@ class Deploy extends Component | |||||||
|             'serverRefresh' => 'proxyStatusUpdated', |             'serverRefresh' => 'proxyStatusUpdated', | ||||||
|             'checkProxy', |             'checkProxy', | ||||||
|             'startProxy', |             'startProxy', | ||||||
|  |             'proxyChanged' => 'proxyStatusUpdated', | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -94,21 +97,43 @@ class Deploy extends Component | |||||||
|     public function stop(bool $forceStop = true) |     public function stop(bool $forceStop = true) | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             if ($this->server->isSwarm()) { |             $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; | ||||||
|                 instant_remote_process([ |             $timeout = 30; | ||||||
|                     'docker service rm coolify-proxy_traefik', | 
 | ||||||
|                 ], $this->server); |             $process = $this->stopContainer($containerName, $timeout); | ||||||
|             } else { | 
 | ||||||
|                 instant_remote_process([ |             $startTime = time(); | ||||||
|                     'docker rm -f coolify-proxy', |             while ($process->running()) { | ||||||
|                 ], $this->server); |                 if (time() - $startTime >= $timeout) { | ||||||
|  |                     $this->forceStopContainer($containerName); | ||||||
|  |                     break; | ||||||
|                 } |                 } | ||||||
|             $this->server->proxy->status = 'exited'; |                 usleep(100000); | ||||||
|             $this->server->proxy->force_stop = $forceStop; |             } | ||||||
|             $this->server->save(); | 
 | ||||||
|             $this->dispatch('proxyStatusUpdated'); |             $this->removeContainer($containerName); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             return handleError($e, $this); |             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); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ class Show extends Component | |||||||
| 
 | 
 | ||||||
|     public $parameters = []; |     public $parameters = []; | ||||||
| 
 | 
 | ||||||
|     protected $listeners = ['proxyStatusUpdated']; |     protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated']; | ||||||
| 
 | 
 | ||||||
|     public function proxyStatusUpdated() |     public function proxyStatusUpdated() | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ class Show extends Component | |||||||
| 
 | 
 | ||||||
|     public $parameters = []; |     public $parameters = []; | ||||||
| 
 | 
 | ||||||
|     protected $listeners = ['refreshServerShow' => '$refresh']; |     protected $listeners = ['refreshServerShow']; | ||||||
| 
 | 
 | ||||||
|     public function mount() |     public function mount() | ||||||
|     { |     { | ||||||
| @@ -29,6 +29,12 @@ class Show extends Component | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function refreshServerShow() | ||||||
|  |     { | ||||||
|  |         $this->server->refresh(); | ||||||
|  |         $this->dispatch('$refresh'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function submit() |     public function submit() | ||||||
|     { |     { | ||||||
|         $this->dispatch('serverRefresh', false); |         $this->dispatch('serverRefresh', false); | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Livewire\Server; | namespace App\Livewire\Server; | ||||||
| 
 | 
 | ||||||
|  | use App\Models\PrivateKey; | ||||||
| use App\Models\Server; | use App\Models\Server; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| @@ -13,25 +14,15 @@ class ShowPrivateKey extends Component | |||||||
| 
 | 
 | ||||||
|     public $parameters; |     public $parameters; | ||||||
| 
 | 
 | ||||||
|     public function setPrivateKey($newPrivateKeyId) |     public function setPrivateKey($privateKeyId) | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             $oldPrivateKeyId = $this->server->private_key_id; |             $privateKey = PrivateKey::findOrFail($privateKeyId); | ||||||
|             refresh_server_connection($this->server->privateKey); |             $this->server->update(['private_key_id' => $privateKey->id]); | ||||||
|             $this->server->update([ |  | ||||||
|                 'private_key_id' => $newPrivateKeyId, |  | ||||||
|             ]); |  | ||||||
|             $this->server->refresh(); |             $this->server->refresh(); | ||||||
|             refresh_server_connection($this->server->privateKey); |             $this->dispatch('success', 'Private key updated successfully.'); | ||||||
|             $this->checkConnection(); |         } catch (\Exception $e) { | ||||||
|         } catch (\Throwable $e) { |             $this->dispatch('error', 'Failed to update private key: '.$e->getMessage()); | ||||||
|             $this->server->update([ |  | ||||||
|                 'private_key_id' => $oldPrivateKeyId, |  | ||||||
|             ]); |  | ||||||
|             $this->server->refresh(); |  | ||||||
|             refresh_server_connection($this->server->privateKey); |  | ||||||
| 
 |  | ||||||
|             return handleError($e, $this); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -43,7 +34,7 @@ class ShowPrivateKey extends Component | |||||||
|                 $this->dispatch('success', 'Server is reachable.'); |                 $this->dispatch('success', 'Server is reachable.'); | ||||||
|             } else { |             } else { | ||||||
|                 ray($error); |                 ray($error); | ||||||
|                 $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.'); |                 $this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error); | ||||||
| 
 | 
 | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ namespace App\Livewire\Team; | |||||||
| 
 | 
 | ||||||
| use App\Models\Team; | use App\Models\Team; | ||||||
| use App\Models\User; | use App\Models\User; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| 
 | 
 | ||||||
| class AdminView extends Component | class AdminView extends Component | ||||||
| @@ -73,8 +75,13 @@ class AdminView extends Component | |||||||
|         $team->delete(); |         $team->delete(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function delete($id) |     public function delete($id, $password) | ||||||
|     { |     { | ||||||
|  |         if (! Hash::check($password, Auth::user()->password)) { | ||||||
|  |             $this->addError('password', 'The provided password is incorrect.'); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         if (! auth()->user()->isInstanceAdmin()) { |         if (! auth()->user()->isInstanceAdmin()) { | ||||||
|             return $this->dispatch('error', 'You are not authorized to delete users'); |             return $this->dispatch('error', 'You are not authorized to delete users'); | ||||||
|         } |         } | ||||||
|   | |||||||
							
								
								
									
										76
									
								
								app/Livewire/Terminal/Index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								app/Livewire/Terminal/Index.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Livewire\Terminal; | ||||||
|  | 
 | ||||||
|  | use App\Models\Server; | ||||||
|  | use Livewire\Attributes\On; | ||||||
|  | use Livewire\Component; | ||||||
|  | 
 | ||||||
|  | class Index extends Component | ||||||
|  | { | ||||||
|  |     public $selected_uuid = 'default'; | ||||||
|  | 
 | ||||||
|  |     public $servers = []; | ||||||
|  | 
 | ||||||
|  |     public $containers = []; | ||||||
|  | 
 | ||||||
|  |     public function mount() | ||||||
|  |     { | ||||||
|  |         if (! auth()->user()->isAdmin()) { | ||||||
|  |             abort(403); | ||||||
|  |         } | ||||||
|  |         $this->servers = Server::isReachable()->get(); | ||||||
|  |         $this->containers = $this->getAllActiveContainers(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function getAllActiveContainers() | ||||||
|  |     { | ||||||
|  |         return collect($this->servers)->flatMap(function ($server) { | ||||||
|  |             if (! $server->isFunctional()) { | ||||||
|  |                 return []; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return $server->loadAllContainers()->map(function ($container) use ($server) { | ||||||
|  |                 $state = data_get_str($container, 'State')->lower(); | ||||||
|  |                 if ($state->contains('running')) { | ||||||
|  |                     return [ | ||||||
|  |                         'name' => data_get($container, 'Names'), | ||||||
|  |                         'connection_name' => data_get($container, 'Names'), | ||||||
|  |                         'uuid' => data_get($container, 'Names'), | ||||||
|  |                         'status' => data_get_str($container, 'State')->lower(), | ||||||
|  |                         'server' => $server, | ||||||
|  |                         'server_uuid' => $server->uuid, | ||||||
|  |                     ]; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return null; | ||||||
|  |             })->filter(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function updatedSelectedUuid() | ||||||
|  |     { | ||||||
|  |         $this->connectToContainer(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[On('connectToContainer')]
 | ||||||
|  |     public function connectToContainer() | ||||||
|  |     { | ||||||
|  |         if ($this->selected_uuid === 'default') { | ||||||
|  |             $this->dispatch('error', 'Please select a server or a container.'); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         $container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid); | ||||||
|  |         $this->dispatch('send-terminal-command', | ||||||
|  |             isset($container), | ||||||
|  |             $container['connection_name'] ?? $this->selected_uuid, | ||||||
|  |             $container['server_uuid'] ?? $this->selected_uuid | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function render() | ||||||
|  |     { | ||||||
|  |         return view('livewire.terminal.index'); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -6,7 +6,9 @@ use App\Enums\ApplicationDeploymentStatus; | |||||||
| use Illuminate\Database\Eloquent\Casts\Attribute; | use Illuminate\Database\Eloquent\Casts\Attribute; | ||||||
| use Illuminate\Database\Eloquent\Relations\HasMany; | use Illuminate\Database\Eloquent\Relations\HasMany; | ||||||
| use Illuminate\Database\Eloquent\SoftDeletes; | use Illuminate\Database\Eloquent\SoftDeletes; | ||||||
|  | use Illuminate\Process\InvokedProcess; | ||||||
| use Illuminate\Support\Collection; | use Illuminate\Support\Collection; | ||||||
|  | use Illuminate\Support\Facades\Process; | ||||||
| use Illuminate\Support\Str; | use Illuminate\Support\Str; | ||||||
| use OpenApi\Attributes as OA; | use OpenApi\Attributes as OA; | ||||||
| use RuntimeException; | use RuntimeException; | ||||||
| @@ -149,12 +151,64 @@ class Application extends BaseModel | |||||||
|         return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); |         return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function getContainersToStop(bool $previewDeployments = false): array | ||||||
|  |     { | ||||||
|  |         $containers = $previewDeployments | ||||||
|  |             ? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true) | ||||||
|  |             : getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0); | ||||||
|  | 
 | ||||||
|  |         return $containers->pluck('Names')->toArray(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function stopContainers(array $containerNames, $server, int $timeout = 600) | ||||||
|  |     { | ||||||
|  |         $processes = []; | ||||||
|  |         foreach ($containerNames as $containerName) { | ||||||
|  |             $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $startTime = time(); | ||||||
|  |         while (count($processes) > 0) { | ||||||
|  |             $finishedProcesses = array_filter($processes, function ($process) { | ||||||
|  |                 return ! $process->running(); | ||||||
|  |             }); | ||||||
|  |             foreach ($finishedProcesses as $containerName => $process) { | ||||||
|  |                 unset($processes[$containerName]); | ||||||
|  |                 $this->removeContainer($containerName, $server); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (time() - $startTime >= $timeout) { | ||||||
|  |                 $this->forceStopRemainingContainers(array_keys($processes), $server); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             usleep(100000); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess | ||||||
|  |     { | ||||||
|  |         return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function removeContainer(string $containerName, $server) | ||||||
|  |     { | ||||||
|  |         instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function forceStopRemainingContainers(array $containerNames, $server) | ||||||
|  |     { | ||||||
|  |         foreach ($containerNames as $containerName) { | ||||||
|  |             instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); | ||||||
|  |             $this->removeContainer($containerName, $server); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function delete_configurations() |     public function delete_configurations() | ||||||
|     { |     { | ||||||
|         $server = data_get($this, 'destination.server'); |         $server = data_get($this, 'destination.server'); | ||||||
|         $workdir = $this->workdir(); |         $workdir = $this->workdir(); | ||||||
|         if (str($workdir)->endsWith($this->uuid)) { |         if (str($workdir)->endsWith($this->uuid)) { | ||||||
|             ray('Deleting workdir'); |  | ||||||
|             instant_remote_process(['rm -rf '.$this->workdir()], $server, false); |             instant_remote_process(['rm -rf '.$this->workdir()], $server, false); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -176,6 +230,13 @@ class Application extends BaseModel | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function delete_connected_networks($uuid) | ||||||
|  |     { | ||||||
|  |         $server = data_get($this, 'destination.server'); | ||||||
|  |         instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); | ||||||
|  |         instant_remote_process(["docker network rm {$uuid}"], $server, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function additional_servers() |     public function additional_servers() | ||||||
|     { |     { | ||||||
|         return $this->belongsToMany(Server::class, 'additional_destinations') |         return $this->belongsToMany(Server::class, 'additional_destinations') | ||||||
| @@ -1034,6 +1095,7 @@ class Application extends BaseModel | |||||||
|             throw new \Exception($e->getMessage()); |             throw new \Exception($e->getMessage()); | ||||||
|         } |         } | ||||||
|         $services = data_get($yaml, 'services'); |         $services = data_get($yaml, 'services'); | ||||||
|  | 
 | ||||||
|         $commands = collect([]); |         $commands = collect([]); | ||||||
|         $services = collect($services)->map(function ($service) use ($commands) { |         $services = collect($services)->map(function ($service) use ($commands) { | ||||||
|             $serviceVolumes = collect(data_get($service, 'volumes', [])); |             $serviceVolumes = collect(data_get($service, 'volumes', [])); | ||||||
| @@ -1166,7 +1228,6 @@ class Application extends BaseModel | |||||||
|         } else { |         } else { | ||||||
|             throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name."); |             throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name."); | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function parseContainerLabels(?ApplicationPreview $preview = null) |     public function parseContainerLabels(?ApplicationPreview $preview = null) | ||||||
|   | |||||||
| @@ -2,6 +2,9 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Models; | namespace App\Models; | ||||||
| 
 | 
 | ||||||
|  | use DanHarrin\LivewireRateLimiting\WithRateLimiting; | ||||||
|  | use Illuminate\Support\Facades\Storage; | ||||||
|  | use Illuminate\Validation\ValidationException; | ||||||
| use OpenApi\Attributes as OA; | use OpenApi\Attributes as OA; | ||||||
| use phpseclib3\Crypt\PublicKeyLoader; | use phpseclib3\Crypt\PublicKeyLoader; | ||||||
| 
 | 
 | ||||||
| @@ -22,48 +25,144 @@ use phpseclib3\Crypt\PublicKeyLoader; | |||||||
| )] | )] | ||||||
| class PrivateKey extends BaseModel | class PrivateKey extends BaseModel | ||||||
| { | { | ||||||
|  |     use WithRateLimiting; | ||||||
|  | 
 | ||||||
|     protected $fillable = [ |     protected $fillable = [ | ||||||
|         'name', |         'name', | ||||||
|         'description', |         'description', | ||||||
|         'private_key', |         'private_key', | ||||||
|         'is_git_related', |         'is_git_related', | ||||||
|         'team_id', |         'team_id', | ||||||
|  |         'fingerprint', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     protected $casts = [ | ||||||
|  |         'private_key' => 'encrypted', | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     protected static function booted() |     protected static function booted() | ||||||
|     { |     { | ||||||
|         static::saving(function ($key) { |         static::saving(function ($key) { | ||||||
|             $privateKey = data_get($key, 'private_key'); |             $key->private_key = formatPrivateKey($key->private_key); | ||||||
|             if (substr($privateKey, -1) !== "\n") { | 
 | ||||||
|                 $key->private_key = $privateKey."\n"; |             if (! self::validatePrivateKey($key->private_key)) { | ||||||
|  |                 throw ValidationException::withMessages([ | ||||||
|  |                     'private_key' => ['The private key is invalid.'], | ||||||
|  |                 ]); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $key->fingerprint = self::generateFingerprint($key->private_key); | ||||||
|  |             if (self::fingerprintExists($key->fingerprint, $key->id)) { | ||||||
|  |                 throw ValidationException::withMessages([ | ||||||
|  |                     'private_key' => ['This private key already exists.'], | ||||||
|  |                 ]); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |         static::deleted(function ($key) { | ||||||
|  |             self::deleteFromStorage($key); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getPublicKey() | ||||||
|  |     { | ||||||
|  |         return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static function ownedByCurrentTeam(array $select = ['*']) |     public static function ownedByCurrentTeam(array $select = ['*']) | ||||||
|     { |     { | ||||||
|         $selectArray = collect($select)->concat(['id']); |         $selectArray = collect($select)->concat(['id']); | ||||||
| 
 | 
 | ||||||
|         return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all()); |         return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function publicKey() |     public static function validatePrivateKey($privateKey) | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']); |             PublicKeyLoader::load($privateKey); | ||||||
|         } catch (\Throwable $e) { |  | ||||||
|             return 'Error loading private key'; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     public function isEmpty() |  | ||||||
|     { |  | ||||||
|         if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) { |  | ||||||
|             return true; |             return true; | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         return false; |     public static function createAndStore(array $data) | ||||||
|  |     { | ||||||
|  |         $privateKey = new self($data); | ||||||
|  |         $privateKey->save(); | ||||||
|  |         $privateKey->storeInFileSystem(); | ||||||
|  | 
 | ||||||
|  |         return $privateKey; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function generateNewKeyPair($type = 'rsa') | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             $instance = new self; | ||||||
|  |             $instance->rateLimit(10); | ||||||
|  |             $name = generate_random_name(); | ||||||
|  |             $description = 'Created by Coolify'; | ||||||
|  |             $keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa'); | ||||||
|  | 
 | ||||||
|  |             return [ | ||||||
|  |                 'name' => $name, | ||||||
|  |                 'description' => $description, | ||||||
|  |                 'private_key' => $keyPair['private'], | ||||||
|  |                 'public_key' => $keyPair['public'], | ||||||
|  |             ]; | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function extractPublicKeyFromPrivate($privateKey) | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             $key = PublicKeyLoader::load($privateKey); | ||||||
|  | 
 | ||||||
|  |             return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function validateAndExtractPublicKey($privateKey) | ||||||
|  |     { | ||||||
|  |         $isValid = self::validatePrivateKey($privateKey); | ||||||
|  |         $publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : ''; | ||||||
|  | 
 | ||||||
|  |         return [ | ||||||
|  |             'isValid' => $isValid, | ||||||
|  |             'publicKey' => $publicKey, | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function storeInFileSystem() | ||||||
|  |     { | ||||||
|  |         $filename = "ssh_key@{$this->uuid}"; | ||||||
|  |         Storage::disk('ssh-keys')->put($filename, $this->private_key); | ||||||
|  | 
 | ||||||
|  |         return "/var/www/html/storage/app/ssh/keys/{$filename}"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function deleteFromStorage(self $privateKey) | ||||||
|  |     { | ||||||
|  |         $filename = "ssh_key@{$privateKey->uuid}"; | ||||||
|  |         Storage::disk('ssh-keys')->delete($filename); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getKeyLocation() | ||||||
|  |     { | ||||||
|  |         return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function updatePrivateKey(array $data) | ||||||
|  |     { | ||||||
|  |         $this->update($data); | ||||||
|  |         $this->storeInFileSystem(); | ||||||
|  | 
 | ||||||
|  |         return $this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function servers() |     public function servers() | ||||||
| @@ -85,4 +184,53 @@ class PrivateKey extends BaseModel | |||||||
|     { |     { | ||||||
|         return $this->hasMany(GitlabApp::class); |         return $this->hasMany(GitlabApp::class); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public function isInUse() | ||||||
|  |     { | ||||||
|  |         return $this->servers()->exists() | ||||||
|  |             || $this->applications()->exists() | ||||||
|  |             || $this->githubApps()->exists() | ||||||
|  |             || $this->gitlabApps()->exists(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function safeDelete() | ||||||
|  |     { | ||||||
|  |         if (! $this->isInUse()) { | ||||||
|  |             $this->delete(); | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function generateFingerprint($privateKey) | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             $key = PublicKeyLoader::load($privateKey); | ||||||
|  |             $publicKey = $key->getPublicKey(); | ||||||
|  | 
 | ||||||
|  |             return $publicKey->getFingerprint('sha256'); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static function fingerprintExists($fingerprint, $excludeId = null) | ||||||
|  |     { | ||||||
|  |         $query = self::where('fingerprint', $fingerprint); | ||||||
|  | 
 | ||||||
|  |         if (! is_null($excludeId)) { | ||||||
|  |             $query->where('id', '!=', $excludeId); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $query->exists(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function cleanupUnusedKeys() | ||||||
|  |     { | ||||||
|  |         self::ownedByCurrentTeam()->each(function ($privateKey) { | ||||||
|  |             $privateKey->safeDelete(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ use OpenApi\Attributes as OA; | |||||||
|         'id' => ['type' => 'integer'], |         'id' => ['type' => 'integer'], | ||||||
|         'uuid' => ['type' => 'string'], |         'uuid' => ['type' => 'string'], | ||||||
|         'name' => ['type' => 'string'], |         'name' => ['type' => 'string'], | ||||||
|  |         'description' => ['type' => 'string'], | ||||||
|         'environments' => new OA\Property( |         'environments' => new OA\Property( | ||||||
|             property: 'environments', |             property: 'environments', | ||||||
|             type: 'array', |             type: 'array', | ||||||
|   | |||||||
| @@ -35,14 +35,17 @@ class ScheduledDatabaseBackup extends BaseModel | |||||||
|     { |     { | ||||||
|         return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); |         return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     public function server() |     public function server() | ||||||
|     { |     { | ||||||
|         if ($this->database) { |         if ($this->database) { | ||||||
|             if ($this->database->destination && $this->database->destination->server) { |             if ($this->database->destination && $this->database->destination->server) { | ||||||
|                 $server = $this->database->destination->server; |                 $server = $this->database->destination->server; | ||||||
|  | 
 | ||||||
|                 return $server; |                 return $server; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,8 +4,6 @@ namespace App\Models; | |||||||
| 
 | 
 | ||||||
| use Illuminate\Database\Eloquent\Relations\HasMany; | use Illuminate\Database\Eloquent\Relations\HasMany; | ||||||
| use Illuminate\Database\Eloquent\Relations\HasOne; | use Illuminate\Database\Eloquent\Relations\HasOne; | ||||||
| use App\Models\Service; |  | ||||||
| use App\Models\Application; |  | ||||||
| 
 | 
 | ||||||
| class ScheduledTask extends BaseModel | class ScheduledTask extends BaseModel | ||||||
| { | { | ||||||
| @@ -37,19 +35,23 @@ class ScheduledTask extends BaseModel | |||||||
|         if ($this->application) { |         if ($this->application) { | ||||||
|             if ($this->application->destination && $this->application->destination->server) { |             if ($this->application->destination && $this->application->destination->server) { | ||||||
|                 $server = $this->application->destination->server; |                 $server = $this->application->destination->server; | ||||||
|  | 
 | ||||||
|                 return $server; |                 return $server; | ||||||
|             } |             } | ||||||
|         } elseif ($this->service) { |         } elseif ($this->service) { | ||||||
|             if ($this->service->destination && $this->service->destination->server) { |             if ($this->service->destination && $this->service->destination->server) { | ||||||
|                 $server = $this->service->destination->server; |                 $server = $this->service->destination->server; | ||||||
|  | 
 | ||||||
|                 return $server; |                 return $server; | ||||||
|             } |             } | ||||||
|         } elseif ($this->database) { |         } elseif ($this->database) { | ||||||
|             if ($this->database->destination && $this->database->destination->server) { |             if ($this->database->destination && $this->database->destination->server) { | ||||||
|                 $server = $this->database->destination->server; |                 $server = $this->database->destination->server; | ||||||
|  | 
 | ||||||
|                 return $server; |                 return $server; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ namespace App\Models; | |||||||
| use App\Actions\Server\InstallDocker; | use App\Actions\Server\InstallDocker; | ||||||
| use App\Enums\ProxyTypes; | use App\Enums\ProxyTypes; | ||||||
| use App\Jobs\PullSentinelImageJob; | use App\Jobs\PullSentinelImageJob; | ||||||
| use App\Notifications\Server\Revived; |  | ||||||
| use Illuminate\Database\Eloquent\Builder; | use Illuminate\Database\Eloquent\Builder; | ||||||
| use Illuminate\Database\Eloquent\Casts\Attribute; | use Illuminate\Database\Eloquent\Casts\Attribute; | ||||||
| use Illuminate\Support\Collection; | use Illuminate\Support\Collection; | ||||||
| @@ -112,6 +111,16 @@ class Server extends BaseModel | |||||||
|         'proxy', |         'proxy', | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'name', | ||||||
|  |         'ip', | ||||||
|  |         'port', | ||||||
|  |         'user', | ||||||
|  |         'description', | ||||||
|  |         'private_key_id', | ||||||
|  |         'team_id', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|     protected $guarded = []; |     protected $guarded = []; | ||||||
| 
 | 
 | ||||||
|     public static function isReachable() |     public static function isReachable() | ||||||
| @@ -146,6 +155,11 @@ class Server extends BaseModel | |||||||
|         return $this->hasOne(ServerSetting::class); |         return $this->hasOne(ServerSetting::class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function proxySet() | ||||||
|  |     { | ||||||
|  |         return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function setupDefault404Redirect() |     public function setupDefault404Redirect() | ||||||
|     { |     { | ||||||
|         $dynamic_conf_path = $this->proxyPath().'/dynamic'; |         $dynamic_conf_path = $this->proxyPath().'/dynamic'; | ||||||
| @@ -153,11 +167,11 @@ class Server extends BaseModel | |||||||
|         $redirect_url = $this->proxy->redirect_url; |         $redirect_url = $this->proxy->redirect_url; | ||||||
|         if ($proxy_type === ProxyTypes::TRAEFIK->value) { |         if ($proxy_type === ProxyTypes::TRAEFIK->value) { | ||||||
|             $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; |             $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; | ||||||
|         } elseif ($proxy_type === 'CADDY') { |         } elseif ($proxy_type === ProxyTypes::CADDY->value) { | ||||||
|             $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; |             $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; | ||||||
|         } |         } | ||||||
|         if (empty($redirect_url)) { |         if (empty($redirect_url)) { | ||||||
|             if ($proxy_type === 'CADDY') { |             if ($proxy_type === ProxyTypes::CADDY->value) { | ||||||
|                 $conf = ':80, :443 { |                 $conf = ':80, :443 { | ||||||
| respond 404 | respond 404 | ||||||
| }'; | }'; | ||||||
| @@ -227,7 +241,7 @@ respond 404 | |||||||
|                 $conf; |                 $conf; | ||||||
| 
 | 
 | ||||||
|             $base64 = base64_encode($conf); |             $base64 = base64_encode($conf); | ||||||
|         } elseif ($proxy_type === 'CADDY') { |         } elseif ($proxy_type === ProxyTypes::CADDY->value) { | ||||||
|             $conf = ":80, :443 { |             $conf = ":80, :443 { | ||||||
|     redir $redirect_url |     redir $redirect_url | ||||||
| }";
 | }";
 | ||||||
| @@ -243,9 +257,6 @@ respond 404 | |||||||
|             "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", |             "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", | ||||||
|         ], $this); |         ], $this); | ||||||
| 
 | 
 | ||||||
|         if (config('app.env') == 'local') { |  | ||||||
|             ray($conf); |  | ||||||
|         } |  | ||||||
|         if ($proxy_type === 'CADDY') { |         if ($proxy_type === 'CADDY') { | ||||||
|             $this->reloadCaddy(); |             $this->reloadCaddy(); | ||||||
|         } |         } | ||||||
| @@ -295,6 +306,13 @@ respond 404 | |||||||
|                                 'service' => 'coolify-realtime', |                                 'service' => 'coolify-realtime', | ||||||
|                                 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", |                                 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", | ||||||
|                             ], |                             ], | ||||||
|  |                             'coolify-terminal-ws' => [ | ||||||
|  |                                 'entryPoints' => [ | ||||||
|  |                                     0 => 'http', | ||||||
|  |                                 ], | ||||||
|  |                                 'service' => 'coolify-terminal', | ||||||
|  |                                 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)", | ||||||
|  |                             ], | ||||||
|                         ], |                         ], | ||||||
|                         'services' => [ |                         'services' => [ | ||||||
|                             'coolify' => [ |                             'coolify' => [ | ||||||
| @@ -315,6 +333,15 @@ respond 404 | |||||||
|                                     ], |                                     ], | ||||||
|                                 ], |                                 ], | ||||||
|                             ], |                             ], | ||||||
|  |                             'coolify-terminal' => [ | ||||||
|  |                                 'loadBalancer' => [ | ||||||
|  |                                     'servers' => [ | ||||||
|  |                                         0 => [ | ||||||
|  |                                             'url' => 'http://coolify-realtime:6002', | ||||||
|  |                                         ], | ||||||
|  |                                     ], | ||||||
|  |                                 ], | ||||||
|  |                             ], | ||||||
|                         ], |                         ], | ||||||
|                     ], |                     ], | ||||||
|                 ]; |                 ]; | ||||||
| @@ -344,6 +371,16 @@ respond 404 | |||||||
|                             'certresolver' => 'letsencrypt', |                             'certresolver' => 'letsencrypt', | ||||||
|                         ], |                         ], | ||||||
|                     ]; |                     ]; | ||||||
|  |                     $traefik_dynamic_conf['http']['routers']['coolify-terminal-wss'] = [ | ||||||
|  |                         'entryPoints' => [ | ||||||
|  |                             0 => 'https', | ||||||
|  |                         ], | ||||||
|  |                         'service' => 'coolify-terminal', | ||||||
|  |                         'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)", | ||||||
|  |                         'tls' => [ | ||||||
|  |                             'certresolver' => 'letsencrypt', | ||||||
|  |                         ], | ||||||
|  |                     ]; | ||||||
|                 } |                 } | ||||||
|                 $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); |                 $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); | ||||||
|                 $yaml = |                 $yaml = | ||||||
| @@ -377,6 +414,9 @@ $schema://$host { | |||||||
|     handle /app/* { |     handle /app/* { | ||||||
|         reverse_proxy coolify-realtime:6001 |         reverse_proxy coolify-realtime:6001 | ||||||
|     } |     } | ||||||
|  |     handle /terminal/ws { | ||||||
|  |         reverse_proxy coolify-realtime:6002 | ||||||
|  |     } | ||||||
|     reverse_proxy coolify:80 |     reverse_proxy coolify:80 | ||||||
| }";
 | }";
 | ||||||
|                 $base64 = base64_encode($caddy_file); |                 $base64 = base64_encode($caddy_file); | ||||||
| @@ -736,6 +776,18 @@ $schema://$host { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function loadAllContainers(): Collection | ||||||
|  |     { | ||||||
|  |         if ($this->isFunctional()) { | ||||||
|  |             $containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this); | ||||||
|  |             $containers = format_docker_command_output_to_json($containers); | ||||||
|  | 
 | ||||||
|  |             return collect($containers); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return collect([]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function loadUnmanagedContainers(): Collection |     public function loadUnmanagedContainers(): Collection | ||||||
|     { |     { | ||||||
|         if ($this->isFunctional()) { |         if ($this->isFunctional()) { | ||||||
| @@ -782,9 +834,9 @@ $schema://$host { | |||||||
|             $clickhouses = data_get($standaloneDocker, 'clickhouses', collect([])); |             $clickhouses = data_get($standaloneDocker, 'clickhouses', collect([])); | ||||||
| 
 | 
 | ||||||
|             return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); |             return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); | ||||||
|         })->filter(function ($item) { |         })->flatten()->filter(function ($item) { | ||||||
|             return data_get($item, 'name') !== 'coolify-db'; |             return data_get($item, 'name') !== 'coolify-db'; | ||||||
|         })->flatten(); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function applications() |     public function applications() | ||||||
| @@ -828,6 +880,35 @@ $schema://$host { | |||||||
|         return $this->hasMany(Service::class); |         return $this->hasMany(Service::class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function port(): Attribute | ||||||
|  |     { | ||||||
|  |         return Attribute::make( | ||||||
|  |             get: function ($value) { | ||||||
|  |                 return preg_replace('/[^0-9]/', '', $value); | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function user(): Attribute | ||||||
|  |     { | ||||||
|  |         return Attribute::make( | ||||||
|  |             get: function ($value) { | ||||||
|  |                 $sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value); | ||||||
|  | 
 | ||||||
|  |                 return $sanitizedValue; | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function ip(): Attribute | ||||||
|  |     { | ||||||
|  |         return Attribute::make( | ||||||
|  |             get: function ($value) { | ||||||
|  |                 return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value); | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function getIp(): Attribute |     public function getIp(): Attribute | ||||||
|     { |     { | ||||||
|         return Attribute::make( |         return Attribute::make( | ||||||
| @@ -900,10 +981,9 @@ $schema://$host { | |||||||
|     public function isFunctional() |     public function isFunctional() | ||||||
|     { |     { | ||||||
|         $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; |         $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; | ||||||
|         ['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this); | 
 | ||||||
|         if (! $isFunctional) { |         if (! $isFunctional) { | ||||||
|             Storage::disk('ssh-keys')->delete($private_key_filename); |             Storage::disk('ssh-mux')->delete($this->muxFilename()); | ||||||
|             Storage::disk('ssh-mux')->delete($mux_filename); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return $isFunctional; |         return $isFunctional; | ||||||
| @@ -955,9 +1035,10 @@ $schema://$host { | |||||||
|         return data_get($this, 'settings.is_swarm_worker'); |         return data_get($this, 'settings.is_swarm_worker'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function validateConnection() |     public function validateConnection($isManualCheck = true) | ||||||
|     { |     { | ||||||
|         config()->set('coolify.mux_enabled', false); |         config()->set('constants.ssh.mux_enabled', ! $isManualCheck); | ||||||
|  |         // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
 | ||||||
| 
 | 
 | ||||||
|         $server = Server::find($this->id); |         $server = Server::find($this->id); | ||||||
|         if (! $server) { |         if (! $server) { | ||||||
| @@ -967,7 +1048,6 @@ $schema://$host { | |||||||
|             return ['uptime' => false, 'error' => 'Server skipped.']; |             return ['uptime' => false, 'error' => 'Server skipped.']; | ||||||
|         } |         } | ||||||
|         try { |         try { | ||||||
|             // EC2 does not have `uptime` command, lol
 |  | ||||||
|             instant_remote_process(['ls /'], $server); |             instant_remote_process(['ls /'], $server); | ||||||
|             $server->settings()->update([ |             $server->settings()->update([ | ||||||
|                 'is_reachable' => true, |                 'is_reachable' => true, | ||||||
| @@ -976,7 +1056,6 @@ $schema://$host { | |||||||
|                 'unreachable_count' => 0, |                 'unreachable_count' => 0, | ||||||
|             ]); |             ]); | ||||||
|             if (data_get($server, 'unreachable_notification_sent') === true) { |             if (data_get($server, 'unreachable_notification_sent') === true) { | ||||||
|                 // $server->team?->notify(new Revived($server));
 |  | ||||||
|                 $server->update(['unreachable_notification_sent' => false]); |                 $server->update(['unreachable_notification_sent' => false]); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @@ -1105,4 +1184,24 @@ $schema://$host { | |||||||
|     { |     { | ||||||
|         return $this->settings->is_build_server; |         return $this->settings->is_build_server; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public static function createWithPrivateKey(array $data, PrivateKey $privateKey) | ||||||
|  |     { | ||||||
|  |         $server = new self($data); | ||||||
|  |         $server->privateKey()->associate($privateKey); | ||||||
|  |         $server->save(); | ||||||
|  | 
 | ||||||
|  |         return $server; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null) | ||||||
|  |     { | ||||||
|  |         $this->update($data); | ||||||
|  |         if ($privateKey) { | ||||||
|  |             $this->privateKey()->associate($privateKey); | ||||||
|  |             $this->save(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute; | |||||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
| use Illuminate\Database\Eloquent\Relations\HasMany; | use Illuminate\Database\Eloquent\Relations\HasMany; | ||||||
| use Illuminate\Database\Eloquent\SoftDeletes; | use Illuminate\Database\Eloquent\SoftDeletes; | ||||||
|  | use Illuminate\Process\InvokedProcess; | ||||||
| use Illuminate\Support\Collection; | use Illuminate\Support\Collection; | ||||||
|  | use Illuminate\Support\Facades\Process; | ||||||
| use Illuminate\Support\Facades\Storage; | use Illuminate\Support\Facades\Storage; | ||||||
| use OpenApi\Attributes as OA; | use OpenApi\Attributes as OA; | ||||||
| use Spatie\Url\Url; | use Spatie\Url\Url; | ||||||
| @@ -131,15 +133,81 @@ class Service extends BaseModel | |||||||
|         return $this->morphToMany(Tag::class, 'taggable'); |         return $this->morphToMany(Tag::class, 'taggable'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function getContainersToStop(): array | ||||||
|  |     { | ||||||
|  |         $containersToStop = []; | ||||||
|  |         $applications = $this->applications()->get(); | ||||||
|  |         foreach ($applications as $application) { | ||||||
|  |             $containersToStop[] = "{$application->name}-{$this->uuid}"; | ||||||
|  |         } | ||||||
|  |         $dbs = $this->databases()->get(); | ||||||
|  |         foreach ($dbs as $db) { | ||||||
|  |             $containersToStop[] = "{$db->name}-{$this->uuid}"; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $containersToStop; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function stopContainers(array $containerNames, $server, int $timeout = 300) | ||||||
|  |     { | ||||||
|  |         $processes = []; | ||||||
|  |         foreach ($containerNames as $containerName) { | ||||||
|  |             $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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function stopContainer(string $containerName, int $timeout): InvokedProcess | ||||||
|  |     { | ||||||
|  |         return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function removeContainer(string $containerName, $server) | ||||||
|  |     { | ||||||
|  |         instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function forceStopRemainingContainers(array $containerNames, $server) | ||||||
|  |     { | ||||||
|  |         foreach ($containerNames as $containerName) { | ||||||
|  |             instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); | ||||||
|  |             $this->removeContainer($containerName, $server); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function delete_configurations() |     public function delete_configurations() | ||||||
|     { |     { | ||||||
|         $server = data_get($this, 'server'); |         $server = data_get($this, 'destination.server'); | ||||||
|         $workdir = $this->workdir(); |         $workdir = $this->workdir(); | ||||||
|         if (str($workdir)->endsWith($this->uuid)) { |         if (str($workdir)->endsWith($this->uuid)) { | ||||||
|             instant_remote_process(['rm -rf '.$this->workdir()], $server, false); |             instant_remote_process(['rm -rf '.$this->workdir()], $server, false); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function delete_connected_networks($uuid) | ||||||
|  |     { | ||||||
|  |         $server = data_get($this, 'destination.server'); | ||||||
|  |         instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); | ||||||
|  |         instant_remote_process(["docker network rm {$uuid}"], $server, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function status() |     public function status() | ||||||
|     { |     { | ||||||
|         $applications = $this->applications; |         $applications = $this->applications; | ||||||
| @@ -667,7 +735,7 @@ class Service extends BaseModel | |||||||
|                     } |                     } | ||||||
|                     $data = $data->merge([ |                     $data = $data->merge([ | ||||||
|                         'Root User' => [ |                         'Root User' => [ | ||||||
|                             'key' => 'N/A', |                             'key' => 'GITLAB_ROOT_USER', | ||||||
|                             'value' => 'root', |                             'value' => 'root', | ||||||
|                             'rules' => 'required', |                             'rules' => 'required', | ||||||
|                             'isPassword' => true, |                             'isPassword' => true, | ||||||
| @@ -1016,10 +1084,20 @@ class Service extends BaseModel | |||||||
|         $commands[] = 'rm -f .env || true'; |         $commands[] = 'rm -f .env || true'; | ||||||
| 
 | 
 | ||||||
|         $envs_from_coolify = $this->environment_variables()->get(); |         $envs_from_coolify = $this->environment_variables()->get(); | ||||||
|         foreach ($envs_from_coolify as $env) { |         $sorted = $envs_from_coolify->sortBy(function ($env) { | ||||||
|  |             if (str($env->key)->startsWith('SERVICE_')) { | ||||||
|  |                 return 1; | ||||||
|  |             } | ||||||
|  |             if (str($env->value)->startsWith('$SERVICE_') || str($env->value)->startsWith('${SERVICE_')) { | ||||||
|  |                 return 2; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return 3; | ||||||
|  |         }); | ||||||
|  |         foreach ($sorted as $env) { | ||||||
|             $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; |             $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; | ||||||
|         } |         } | ||||||
|         if ($envs_from_coolify->count() === 0) { |         if ($sorted->count() === 0) { | ||||||
|             $commands[] = 'touch .env'; |             $commands[] = 'touch .env'; | ||||||
|         } |         } | ||||||
|         instant_remote_process($commands, $this->server); |         instant_remote_process($commands, $this->server); | ||||||
|   | |||||||
| @@ -32,6 +32,16 @@ class ServiceApplication extends BaseModel | |||||||
|         return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); |         return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function isRunning() | ||||||
|  |     { | ||||||
|  |         return str($this->status)->contains('running'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function isExited() | ||||||
|  |     { | ||||||
|  |         return str($this->status)->contains('exited'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function isLogDrainEnabled() |     public function isLogDrainEnabled() | ||||||
|     { |     { | ||||||
|         return data_get($this, 'is_log_drain_enabled', false); |         return data_get($this, 'is_log_drain_enabled', false); | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Leonardo Cabeza
					Leonardo Cabeza