Merge remote-tracking branch 'origin/next' into api-sensitive-data-scrubber
This commit is contained in:
commit
ff976a134f
136 changed files with 6142 additions and 1733 deletions
|
|
@ -27,12 +27,6 @@ DB_PORT=5432
|
||||||
# DB_WRITE_PASSWORD=
|
# DB_WRITE_PASSWORD=
|
||||||
# DB_STICKY=true
|
# DB_STICKY=true
|
||||||
|
|
||||||
# Ray Configuration
|
|
||||||
# Set to true to enable Ray
|
|
||||||
RAY_ENABLED=false
|
|
||||||
# Set custom ray port
|
|
||||||
# RAY_PORT=
|
|
||||||
|
|
||||||
# Enable Laravel Telescope for debugging
|
# Enable Laravel Telescope for debugging
|
||||||
TELESCOPE_ENABLED=false
|
TELESCOPE_ENABLED=false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,4 @@ ROOT_USERNAME=
|
||||||
ROOT_USER_EMAIL=
|
ROOT_USER_EMAIL=
|
||||||
ROOT_USER_PASSWORD=
|
ROOT_USER_PASSWORD=
|
||||||
|
|
||||||
REGISTRY_URL=ghcr.io
|
REGISTRY_URL=docker.io
|
||||||
|
|
|
||||||
24
.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
vendored
24
.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
name: 🐞 Bug Report
|
name: 🐞 Bug Report
|
||||||
description: "File a new bug report."
|
description: "File a new bug report."
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["🐛 Bug", "🔍 Triage"]
|
labels: ["🔍 Triage"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -11,10 +11,22 @@ body:
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Error Message and Logs
|
label: Description and Error Message
|
||||||
description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
|
description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: Please describe what you expected to happen instead of the issue. Be as detailed as possible.
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
4.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -37,7 +49,7 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: Coolify Version
|
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.
|
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"
|
placeholder: "v4.1.2"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|
@ -55,6 +67,12 @@ body:
|
||||||
label: Operating System and Version (self-hosted)
|
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.
|
description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version.
|
||||||
placeholder: "Ubuntu 22.04"
|
placeholder: "Ubuntu 22.04"
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots / Visuals
|
||||||
|
description: If possible, provide screenshots, screen recordings, or diagrams to help illustrate the issue.
|
||||||
|
placeholder: "Attach images or provide links to recordings demonstrating the problem."
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
||||||
438
CONTRIBUTING.md
438
CONTRIBUTING.md
|
|
@ -1,289 +1,231 @@
|
||||||
# Contributing to Coolify
|
# Contributing to Coolify
|
||||||
|
We’re happy that you’re interested in contributing to Coolify!
|
||||||
|
|
||||||
> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
|
There are many ways to help:
|
||||||
|
- Answer questions in GitHub Discussions or Discord
|
||||||
|
- Report reproducible bugs
|
||||||
|
- Submit pull requests to fix issues
|
||||||
|
- Add new one-click services
|
||||||
|
- Improve documentation
|
||||||
|
|
||||||
You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
|
Coolify is a PaaS used by 400,000+ people worldwide and maintained by two active maintainers. Contributions are welcome — but **alignment matters more than quantity**.
|
||||||
|
|
||||||
To understand the tech stack, please refer to the [Tech Stack](TECH_STACK.md) document.
|
This guide explains **what kind of contributions are likely to be accepted** and how to submit them properly. Following it saves time for both you and the maintainers.
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
1. [Setup Development Environment](#1-setup-development-environment)
|
|
||||||
2. [Verify Installation](#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. [Create a Pull Request](#7-create-a-pull-request)
|
|
||||||
8. [Development Notes](#development-notes)
|
|
||||||
9. [Resetting Development Environment](#resetting-development-environment)
|
|
||||||
10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
|
|
||||||
|
|
||||||
## 1. Setup Development Environment
|
|
||||||
|
|
||||||
Follow the steps below for your operating system:
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Windows</strong></summary>
|
|
||||||
|
|
||||||
1. Install `docker-ce`, Docker Desktop (or similar):
|
|
||||||
- Docker CE (recommended):
|
|
||||||
- Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
|
|
||||||
- After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
|
|
||||||
- Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
|
|
||||||
- Install Docker Desktop (easier):
|
|
||||||
- Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
|
|
||||||
- Ensure WSL2 backend is enabled in Docker Desktop settings
|
|
||||||
|
|
||||||
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?ref=coolify)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>MacOS</strong></summary>
|
|
||||||
|
|
||||||
1. Install Orbstack, Docker Desktop (or similar):
|
|
||||||
- Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
|
|
||||||
- Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
|
|
||||||
- Docker Desktop:
|
|
||||||
- Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
|
|
||||||
|
|
||||||
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?ref=coolify)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Linux</strong></summary>
|
|
||||||
|
|
||||||
1. Install Docker Engine, Docker Desktop (or similar):
|
|
||||||
- Docker Engine (recommended, as there is no VM overhead):
|
|
||||||
- Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
|
|
||||||
- Docker Desktop:
|
|
||||||
- If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
|
|
||||||
|
|
||||||
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?ref=coolify)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 2. Verify Installation (Optional)
|
|
||||||
|
|
||||||
After installing Docker (or Orbstack) and Spin, verify the installation:
|
|
||||||
|
|
||||||
1. Open a terminal or command prompt
|
|
||||||
2. Run the following commands:
|
|
||||||
```bash
|
|
||||||
docker --version
|
|
||||||
spin --version
|
|
||||||
```
|
|
||||||
You should see version information for both Docker and Spin.
|
|
||||||
|
|
||||||
## 3. Fork and Setup Local Repository
|
|
||||||
|
|
||||||
1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account.
|
|
||||||
|
|
||||||
2. Install a code editor on your machine (choose one):
|
|
||||||
|
|
||||||
| Editor | Platform | Download Link |
|
|
||||||
|--------|----------|---------------|
|
|
||||||
| Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) |
|
|
||||||
| Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) |
|
|
||||||
| Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download?ref=coolify) |
|
|
||||||
|
|
||||||
3. Clone the Coolify Repository from your fork to your local machine
|
|
||||||
- Use `git clone` in the command line, or
|
|
||||||
- Use GitHub Desktop (recommended):
|
|
||||||
- Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify)
|
|
||||||
- Open GitHub Desktop and login with your GitHub account
|
|
||||||
- Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
|
|
||||||
|
|
||||||
4. Open the cloned Coolify Repository in your chosen code editor.
|
|
||||||
|
|
||||||
## 4. Set up Environment Variables
|
|
||||||
|
|
||||||
1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository.
|
|
||||||
2. Duplicate the `.env.development.example` file and rename the copy to `.env`.
|
|
||||||
3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup.
|
|
||||||
4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`.
|
|
||||||
5. Save the changes to your `.env` file.
|
|
||||||
|
|
||||||
## 5. Start Coolify
|
|
||||||
|
|
||||||
1. Open a terminal in the local Coolify directory.
|
|
||||||
2. Run the following command in the terminal (leave that terminal open):
|
|
||||||
```bash
|
|
||||||
spin up
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> You may see some errors, but don't worry; this is expected.
|
|
||||||
|
|
||||||
3. If you encounter permission errors, especially on macOS, use:
|
|
||||||
```bash
|
|
||||||
sudo spin up
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
|
|
||||||
|
|
||||||
## 6. Start Development
|
|
||||||
|
|
||||||
1. Access your Coolify instance:
|
|
||||||
- URL: `http://localhost:8000`
|
|
||||||
- Login: `test@example.com`
|
|
||||||
- Password: `password`
|
|
||||||
|
|
||||||
2. Additional development tools:
|
|
||||||
|
|
||||||
| Tool | URL | Note |
|
|
||||||
|------|-----|------|
|
|
||||||
| Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
|
|
||||||
| Mailpit (email catcher) | `http://localhost:8025` | |
|
|
||||||
| Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default |
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> To enable Telescope, add the following to your `.env` file:
|
|
||||||
> ```env
|
|
||||||
> TELESCOPE_ENABLED=true
|
|
||||||
> ```
|
|
||||||
|
|
||||||
## 7. Create a Pull Request
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Please read the [Pull Request Guidelines](#pull-request-guidelines) carefully before creating your PR.
|
> These guidelines may feel stricter than in many open-source projects. That is intentional.
|
||||||
|
> Clear structure and boundaries prevent maintainer burnout and keep the project sustainable long-term.
|
||||||
|
|
||||||
1. After making changes or adding a new service:
|
|
||||||
- Commit your changes to your forked repository.
|
|
||||||
- Push the changes to your GitHub account.
|
|
||||||
|
|
||||||
2. Creating the Pull Request (PR):
|
## High-Level Expectations
|
||||||
- Navigate to the main Coolify repository on GitHub.
|
- Coolify has a clear product direction.
|
||||||
- Click the "Pull requests" tab.
|
- Ownership and decisions are centralized.
|
||||||
- Click the green "New pull request" button.
|
- Review capacity is limited.
|
||||||
- Choose your fork and `next` branch as the compare branch.
|
- Not every contribution will be accepted — even if technically correct.
|
||||||
- Click "Create pull request".
|
|
||||||
|
|
||||||
3. Filling out the PR details:
|
This is normal for a two-maintainer project.
|
||||||
- Give your PR a descriptive title.
|
|
||||||
- Use the Pull Request Template provided and fill in the details.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `v4.x` branch.
|
|
||||||
|
|
||||||
4. Submit your PR:
|
## State of the Project
|
||||||
- Review your changes one last time.
|
Coolify is currently at v4. While v4 is stable, it has some limitations, including:
|
||||||
- Click "Create pull request" to submit.
|
- Limited scaling support
|
||||||
|
- A more complex user experience
|
||||||
|
- Other smaller issues that need refinement
|
||||||
|
|
||||||
> [!NOTE]
|
These limitations will be addressed in Coolify v5, which is in the planning stage. Because of this, major features, architectural changes, or significant UI changes will not be accepted for v4 at this stage.
|
||||||
> 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.
|
We welcome contributions that help stabilize v4 for a bug free experience.
|
||||||
|
|
||||||
#### Pull Request Guidelines
|
|
||||||
To maintain high-quality contributions and efficient review process:
|
|
||||||
- **Target Branch**: Always target the `next` branch, never `v4.x` or any other branch. PRs targeting incorrect branches will be closed without review.
|
|
||||||
- **Descriptive Titles**: Use clear, concise PR titles that describe the change (e.g., "fix: one click postgresql database stuck in restart loop" instead of "Fix database").
|
|
||||||
- **PR Descriptions**: Provide detailed, meaningful descriptions. Avoid generic or AI-generated fluff. Include:
|
|
||||||
- What the change does
|
|
||||||
- Why it's needed
|
|
||||||
- How to test it
|
|
||||||
- Any breaking changes
|
|
||||||
- Screenshot or video recording of your changes working without any issues
|
|
||||||
- Links to related issues
|
|
||||||
- **Link to Issues**: All PRs must link to an existing GitHub issue. If no issue exists, create one first. Unrelated PRs may be closed.
|
|
||||||
- **Single Responsibility**: Each PR should address one issue or feature. Do not bundle unrelated changes.
|
|
||||||
- **Draft Mode**: Use draft PRs for work-in-progress. Convert to ready-for-review only when complete and tested.
|
|
||||||
- **Review Readiness**: Ensure your PR is ready for review within a reasonable timeframe (max 7 days in draft). Stale drafts may be closed.
|
|
||||||
- **Current Focus**: We are currently prioritizing stability and bug fixes over new features. PRs adding new features may not be reviewed, or may be closed without review to maintain focus.
|
|
||||||
- **Language Translations**: Coolify currently supports only English. Pull requests for new language translations will not be accepted. Multi-language support may be considered in the next major version (v5).
|
|
||||||
- **AI Usage Policy**: We are not against AI tools—we use them ourselves. However, AI discourse is mandatory: You must fully understand the changes in your PR and be able to explain them clearly. Many PRs using AI lack this understanding, leading to untested or incorrect submissions. If you use AI, ensure you can articulate what the code does, why it was changed, and how it was tested.
|
|
||||||
|
|
||||||
#### Review Process
|
## What Makes a Strong Contribution
|
||||||
- **Response Time**: Maintainers will review PRs promptly, but complex changes may take time. Be patient and responsive to feedback.
|
The following types of contributions are most likely to be accepted:
|
||||||
- **Revisions**: Address all review comments. Unresolved feedback may lead to PR closure.
|
|
||||||
- **Merge Criteria**: PRs are merged only after:
|
|
||||||
- All tests pass (including CI)
|
|
||||||
- Code review approval
|
|
||||||
- **Closing PRs**: PRs may be closed for:
|
|
||||||
- Inactivity (>7 days without response)
|
|
||||||
- Failure to meet guidelines
|
|
||||||
- Duplicate or superseded work
|
|
||||||
- Security or quality concerns
|
|
||||||
|
|
||||||
#### Code Quality and Testing
|
#### Code Quality and Testing
|
||||||
All contributions must adhere to the highest standards of code quality and testing:
|
All contributions must adhere to the highest standards of code quality and testing:
|
||||||
|
|
||||||
- **Testing Required**: Every PR must include steps to test your changes. Untested code will not be reviewed or merged.
|
If your change is small and obvious (typo fix, small bug, minor docs update), you may open a pull request directly.
|
||||||
- **Local Verification**: Ensure your changes work in the development environment. Test all affected features thoroughly.
|
|
||||||
- **Code Standards**: Follow the existing code style, conventions, and patterns in the codebase.
|
|
||||||
- **No AI-Generated Code**: Do not submit code generated by AI tools without fully understanding and verifying it. AI-generated submissions that are untested or incorrect will be rejected immediately.
|
|
||||||
|
|
||||||
|
|
||||||
## Development Notes
|
If you are fixing a bug in `file.yaml`, do not:
|
||||||
|
- Reformat unrelated files
|
||||||
|
- Refactor unrelated code
|
||||||
|
- Fix style issues elsewhere
|
||||||
|
- Combine multiple unrelated changes
|
||||||
|
|
||||||
When working on Coolify, keep the following in mind:
|
Even “improvements” increase review complexity.
|
||||||
|
|
||||||
1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
|
**One pull request = one logical change.**
|
||||||
```bash
|
|
||||||
docker exec -it coolify php artisan migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
|
If you want to refactor or clean up code, discuss it first and submit it separately.
|
||||||
```bash
|
|
||||||
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 environment-specific issues.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
## Discussion Is Required for Larger Changes
|
||||||
> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
|
For anything beyond a small fix, you must discuss it before opening a pull request.
|
||||||
|
|
||||||
## Resetting Development Environment
|
This includes:
|
||||||
|
- New features
|
||||||
|
- UI/UX changes
|
||||||
|
- Changes to default behavior
|
||||||
|
- Refactors or cleanup work
|
||||||
|
- Performance rewrites
|
||||||
|
- Architectural changes
|
||||||
|
- Changes touching many files
|
||||||
|
|
||||||
If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
|
Discussion happens in GitHub Discussions: https://github.com/coollabsio/coolify/discussions/categories/general
|
||||||
|
|
||||||
1. Stop all running containers `ctrl + c`.
|
Pull requests introducing major changes without prior discussion will be closed without review.
|
||||||
|
|
||||||
2. Remove all Coolify containers:
|
This ensures alignment before significant work is done.
|
||||||
```bash
|
|
||||||
docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
|
|
||||||
```bash
|
|
||||||
docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Remove unused images:
|
## What This Project Is Not
|
||||||
```bash
|
To set clear expectations:
|
||||||
docker image prune -a
|
- Coolify is not optimized for first-time open-source contributors
|
||||||
```
|
- We do not provide beginner-focused mentorship issues
|
||||||
|
- Large unsolicited changes are unlikely to be accepted
|
||||||
|
- Broad refactors or style rewrites are not helpful
|
||||||
|
- Low-effort AI-generated pull requests will be closed
|
||||||
|
|
||||||
5. Start Coolify again:
|
AI usage is allowed. However, contributors must fully understand what their changes do and why.
|
||||||
```bash
|
|
||||||
spin up
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Run database migrations and seeders:
|
Clear expectations help everyone use their time effectively.
|
||||||
```bash
|
|
||||||
docker exec -it coolify php artisan migrate:fresh --seed
|
|
||||||
```
|
|
||||||
|
|
||||||
After completing these steps, you'll have a fresh development setup.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
# Ways to Contribute
|
||||||
> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
|
## 1. Support Contributions
|
||||||
|
We use Discord for most support requests and GitHub Discussions for help.
|
||||||
|
|
||||||
## Additional Contribution Guidelines
|
### Requesting Support
|
||||||
|
If you need help:
|
||||||
|
- Provide complete and detailed information
|
||||||
|
- Include logs, screenshots, and steps to reproduce
|
||||||
|
- Be respectful — support is voluntary
|
||||||
|
|
||||||
### Contributing a New Service
|
Do not ping people for attention. They respond when available.
|
||||||
|
|
||||||
To add a new service to Coolify, please refer to our documentation:
|
### Providing Support
|
||||||
[Adding a New Service](https://coolify.io/docs/get-started/contribute/service)
|
If you help others:
|
||||||
|
- Verify your information before sharing
|
||||||
|
- Be patient and respectful
|
||||||
|
- Remember that not everyone has the same experience level
|
||||||
|
|
||||||
### Contributing to Documentation
|
|
||||||
|
|
||||||
To contribute to the Coolify documentation, please refer to this guide:
|
## 2. Bug Report Contributions
|
||||||
[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/readme.md)
|
Create a GitHub issue **only** if:
|
||||||
|
- The bug is reproducible
|
||||||
|
- You have confirmed no existing issue already covers it
|
||||||
|
|
||||||
|
For questions or general help, use GitHub Discussions or the Discord support channel.
|
||||||
|
|
||||||
|
Bug reports must include:
|
||||||
|
- Clear reproduction steps
|
||||||
|
- Expected result
|
||||||
|
- Actual result
|
||||||
|
|
||||||
|
Incomplete reports and reports generated using AI may be closed.
|
||||||
|
|
||||||
|
|
||||||
|
## 3. Code Contributions
|
||||||
|
Maintainers may close pull requests at their discretion, without explanation.
|
||||||
|
|
||||||
|
### Issue Requirement
|
||||||
|
Every pull request should reference and close an Issue or Discussion.
|
||||||
|
|
||||||
|
If none exists, create one first.
|
||||||
|
|
||||||
|
Pull requests without linked issue or discussions may not be reviewed and can be closed at any time.
|
||||||
|
|
||||||
|
|
||||||
|
## Commit Message Format
|
||||||
|
All commits must start with an action and category:
|
||||||
|
- `fix(ui):` — UI-related fixes
|
||||||
|
- `feat(api):` — API-related changes
|
||||||
|
- `feat(service):` — One-click service changes
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `fix(api): version endpoint returns wrong data`
|
||||||
|
- `feat(service): add supabase`
|
||||||
|
|
||||||
|
Use the commit description only for concise context.
|
||||||
|
|
||||||
|
Walls of text listing every change in description will be rejected.
|
||||||
|
|
||||||
|
|
||||||
|
## Pull Request Title Format
|
||||||
|
Pull request titles follow the same format:
|
||||||
|
- `fix(ui):`
|
||||||
|
- `feat(api):`
|
||||||
|
- `feat(service):`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `fix(api): version endpoint returns wrong data`
|
||||||
|
- `feat(service): add supabase`
|
||||||
|
|
||||||
|
|
||||||
|
## AI Usage Disclosure
|
||||||
|
If AI tools were used at any stage, mention it in the pull request description.
|
||||||
|
|
||||||
|
AI is allowed.
|
||||||
|
|
||||||
|
However:
|
||||||
|
- You must understand every change
|
||||||
|
- You must verify correctness
|
||||||
|
- You must ensure it follows project patterns
|
||||||
|
|
||||||
|
AI-generated pull requests without clear understanding will be closed.
|
||||||
|
|
||||||
|
|
||||||
|
## Test Before Submitting
|
||||||
|
Before submitting a pull request:
|
||||||
|
- Manually test your changes thoroughly
|
||||||
|
- Verify they work in a clean environment
|
||||||
|
- Provide detailed testing steps in the PR description
|
||||||
|
|
||||||
|
If maintainers cannot reproduce working behavior, the PR will be closed without further review.
|
||||||
|
|
||||||
|
|
||||||
|
## Submitting a Pull Request
|
||||||
|
- GitHub will auto-populate the PR template
|
||||||
|
- The contributor agreement in PR description must remain intact
|
||||||
|
- Pull requests without the contributor agreement will be closed
|
||||||
|
- All pull requests must target the `next` branch
|
||||||
|
- PRs targeting other branches will be closed without review
|
||||||
|
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
**Q: Should I ask before fixing a typo or a small bug?**
|
||||||
|
A: No, small, obvious fixes like typos or narrowly-scoped bug fixes can be submitted as a PR directly.
|
||||||
|
|
||||||
|
**Q: I have an idea for a new feature.**
|
||||||
|
A: Awesome! Discuss it first in GitHub Discussions or Discord. **Do not** open a PR for new features without prior alignment.
|
||||||
|
|
||||||
|
**Q: My PR was closed without detailed feedback.**
|
||||||
|
A: This usually means it didn’t align with the project’s direction, required more review bandwidth than available, or targeted major changes not allowed in v4.
|
||||||
|
|
||||||
|
**Q: Can I work on an open issue?**
|
||||||
|
A: Comment on the issue first to confirm it’s still relevant and that no one else is actively working on it. For anything beyond a small fix, discuss your approach before implementing.
|
||||||
|
|
||||||
|
**Q: I noticed code that could be cleaned up while working on my change.**
|
||||||
|
A: Focus only on your stated goal. Cleanups or refactors should be submitted as separate PRs after discussion.
|
||||||
|
|
||||||
|
**Q: Can I use AI to help with my PR?**
|
||||||
|
A: Yes, AI-assisted contributions are allowed. But you must fully understand and verify the changes. PRs that appear to be generated by AI without context understanding will be closed.
|
||||||
|
|
||||||
|
**Q: My PR was closed without review. Can I submit a new one?**
|
||||||
|
A: Yes, but keep in mind a PR closure is feedback, not a rejection of your effort. It usually means the PR didn’t match the project goals or guidelines. Address these issues first — repeating the same approach may hurt your standing with maintainers.
|
||||||
|
|
||||||
|
|
||||||
|
# Development Guides
|
||||||
|
## Local Development
|
||||||
|
To build and run Coolify locally, see: [Development](./DEVELOPMENT.md)
|
||||||
|
|
||||||
|
### macOS Development with Lima
|
||||||
|
Mac users can use [Lima](https://lima-vm.io/) to run a lightweight Linux virtual machine for local Coolify development. This is useful if you prefer a Linux-based Docker environment on macOS.
|
||||||
|
|
||||||
|
After creating and starting a Lima VM, run the normal local development commands from inside the VM as described in [Development](./DEVELOPMENT.md).
|
||||||
|
|
||||||
|
## Adding a New Service
|
||||||
|
To add a new one-click service, follow: https://coolify.io/docs/get-started/contribute/service
|
||||||
|
|
||||||
|
## Contributing to Documentation
|
||||||
|
To contribute to documentation, see: https://coolify.io/docs/get-started/contribute/documentation
|
||||||
|
|
|
||||||
212
DEVELOPMENT.md
Normal file
212
DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# Contributing to Coolify
|
||||||
|
> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
|
||||||
|
|
||||||
|
You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
|
||||||
|
|
||||||
|
To understand the tech stack, please refer to the [Tech Stack](TECH_STACK.md) document.
|
||||||
|
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
1. [Setup Development Environment](#1-setup-development-environment)
|
||||||
|
2. [Verify Installation](#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. [Create a Pull Request](#7-create-a-pull-request)
|
||||||
|
8. [Development Notes](#development-notes)
|
||||||
|
9. [Resetting Development Environment](#resetting-development-environment)
|
||||||
|
10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
|
||||||
|
|
||||||
|
|
||||||
|
## 1. Setup Development Environment
|
||||||
|
Follow the steps below for your operating system:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Windows</strong></summary>
|
||||||
|
|
||||||
|
1. Install `docker-ce`, Docker Desktop (or similar):
|
||||||
|
- Docker CE (recommended):
|
||||||
|
- Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
|
||||||
|
- After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
|
||||||
|
- Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
|
||||||
|
- Install Docker Desktop (easier):
|
||||||
|
- Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
|
||||||
|
- Ensure WSL2 backend is enabled in Docker Desktop settings
|
||||||
|
|
||||||
|
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?ref=coolify)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>MacOS</strong></summary>
|
||||||
|
|
||||||
|
1. Install Orbstack, Docker Desktop (or similar):
|
||||||
|
- Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
|
||||||
|
- Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
|
||||||
|
- Docker Desktop:
|
||||||
|
- Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
|
||||||
|
|
||||||
|
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?ref=coolify)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Linux</strong></summary>
|
||||||
|
|
||||||
|
1. Install Docker Engine, Docker Desktop (or similar):
|
||||||
|
- Docker Engine (recommended, as there is no VM overhead):
|
||||||
|
- Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
|
||||||
|
- Docker Desktop:
|
||||||
|
- If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
|
||||||
|
|
||||||
|
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?ref=coolify)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
## 2. Verify Installation (Optional)
|
||||||
|
After installing Docker (or Orbstack) and Spin, verify the installation:
|
||||||
|
|
||||||
|
1. Open a terminal or command prompt
|
||||||
|
2. Run the following commands:
|
||||||
|
```bash
|
||||||
|
docker --version
|
||||||
|
spin --version
|
||||||
|
```
|
||||||
|
You should see version information for both Docker and Spin.
|
||||||
|
|
||||||
|
|
||||||
|
## 3. Fork and Setup Local Repository
|
||||||
|
1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account.
|
||||||
|
|
||||||
|
2. Install a code editor on your machine (choose one):
|
||||||
|
|
||||||
|
| Editor | Platform | Download Link |
|
||||||
|
|--------|----------|---------------|
|
||||||
|
| Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) |
|
||||||
|
| Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) |
|
||||||
|
| Zed (very fast) | Windows/macOS/Linux | [Download](https://zed.dev/download?ref=coolify) |
|
||||||
|
|
||||||
|
3. Clone the Coolify Repository from your fork to your local machine
|
||||||
|
- Use `git clone` in the command line, or
|
||||||
|
- Use GitHub Desktop (recommended):
|
||||||
|
- Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify)
|
||||||
|
- Open GitHub Desktop and login with your GitHub account
|
||||||
|
- Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
|
||||||
|
|
||||||
|
4. Open the cloned Coolify Repository in your chosen code editor.
|
||||||
|
|
||||||
|
|
||||||
|
## 4. Set up Environment Variables
|
||||||
|
1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository.
|
||||||
|
2. Duplicate the `.env.development.example` file and rename the copy to `.env`.
|
||||||
|
3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup.
|
||||||
|
4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`.
|
||||||
|
5. Save the changes to your `.env` file.
|
||||||
|
|
||||||
|
|
||||||
|
## 5. Start Coolify
|
||||||
|
1. Open a terminal in the local Coolify directory.
|
||||||
|
2. Run the following command in the terminal (leave that terminal open):
|
||||||
|
```bash
|
||||||
|
spin up
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> You may see some errors, but don't worry; this is expected.
|
||||||
|
|
||||||
|
3. If you encounter permission errors, especially on macOS, use:
|
||||||
|
```bash
|
||||||
|
sudo spin up
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
|
||||||
|
|
||||||
|
|
||||||
|
## 6. Start Development
|
||||||
|
1. Access your Coolify instance:
|
||||||
|
- URL: `http://localhost:8000`
|
||||||
|
- Login: `test@example.com`
|
||||||
|
- Password: `password`
|
||||||
|
|
||||||
|
2. Additional development tools:
|
||||||
|
|
||||||
|
| Tool | URL | Note |
|
||||||
|
|------|-----|------|
|
||||||
|
| Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
|
||||||
|
| Mailpit (email catcher) | `http://localhost:8025` | |
|
||||||
|
| Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> To enable Telescope, add the following to your `.env` file:
|
||||||
|
> ```env
|
||||||
|
> TELESCOPE_ENABLED=true
|
||||||
|
> ```
|
||||||
|
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
When working on Coolify, keep the following in mind:
|
||||||
|
|
||||||
|
1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
|
||||||
|
```bash
|
||||||
|
docker exec -it coolify php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
|
||||||
|
```bash
|
||||||
|
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 environment-specific issues.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
|
||||||
|
|
||||||
|
|
||||||
|
## Resetting Development Environment
|
||||||
|
If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
|
||||||
|
|
||||||
|
1. Stop all running containers `ctrl + c`.
|
||||||
|
|
||||||
|
2. Remove all Coolify containers:
|
||||||
|
```bash
|
||||||
|
docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
|
||||||
|
```bash
|
||||||
|
docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Remove unused images:
|
||||||
|
```bash
|
||||||
|
docker image prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Start Coolify again:
|
||||||
|
```bash
|
||||||
|
spin up
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run database migrations and seeders:
|
||||||
|
```bash
|
||||||
|
docker exec -it coolify php artisan migrate:fresh --seed
|
||||||
|
```
|
||||||
|
|
||||||
|
After completing these steps, you'll have a fresh development setup.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
|
||||||
|
|
||||||
|
|
||||||
|
## Additional Development Guidelines
|
||||||
|
### Adding a New Service
|
||||||
|
To add a new service to Coolify, please refer to our documentation: [Adding a New Service](https://coolify.io/docs/get-started/contribute/service)
|
||||||
|
|
||||||
|
### Development for Documentation
|
||||||
|
To contribute to the Coolify documentation, please refer to this guide: [Contributing to the Coolify Documentation](https://coolify.io/docs/get-started/contribute/documentation)
|
||||||
|
|
@ -143,8 +143,6 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
ray("Database proxy for {$database->name} disabled due to non-transient error: {$e->getMessage()}");
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
||||||
$realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion";
|
$realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion";
|
||||||
|
|
||||||
$helperImageVersion = getHelperVersion();
|
$helperImageVersion = getHelperVersion();
|
||||||
$helperImage = config('constants.coolify.helper_image');
|
$helperImage = coolifyHelperImage();
|
||||||
$helperImageWithVersion = "$helperImage:$helperImageVersion";
|
$helperImageWithVersion = "$helperImage:$helperImageVersion";
|
||||||
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
|
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
|
||||||
$helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion";
|
$helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion";
|
||||||
|
|
|
||||||
|
|
@ -26,22 +26,14 @@ public function handle(int $serverId, bool $deleteFromHetzner = false, ?int $het
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ray($server ? 'Deleting server from Coolify' : 'Server already deleted from Coolify, skipping Coolify deletion');
|
|
||||||
|
|
||||||
// If server is already deleted from Coolify, skip this part
|
// If server is already deleted from Coolify, skip this part
|
||||||
if (! $server) {
|
if (! $server) {
|
||||||
return; // Server already force deleted from Coolify
|
return; // Server already force deleted from Coolify
|
||||||
}
|
}
|
||||||
|
|
||||||
ray('force deleting server from Coolify', ['server_id' => $server->id]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$server->forceDelete();
|
$server->forceDelete();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
ray('Failed to force delete server from Coolify', [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'server_id' => $server->id,
|
|
||||||
]);
|
|
||||||
logger()->error('Failed to force delete server from Coolify', [
|
logger()->error('Failed to force delete server from Coolify', [
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'server_id' => $server->id,
|
'server_id' => $server->id,
|
||||||
|
|
@ -66,10 +58,6 @@ private function deleteFromHetznerById(int $hetznerServerId, ?int $cloudProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $token) {
|
if (! $token) {
|
||||||
ray('No Hetzner token found for team, skipping Hetzner deletion', [
|
|
||||||
'team_id' => $teamId,
|
|
||||||
'hetzner_server_id' => $hetznerServerId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -77,16 +65,7 @@ private function deleteFromHetznerById(int $hetznerServerId, ?int $cloudProvider
|
||||||
$hetznerService = new HetznerService($token->token);
|
$hetznerService = new HetznerService($token->token);
|
||||||
$hetznerService->deleteServer($hetznerServerId);
|
$hetznerService->deleteServer($hetznerServerId);
|
||||||
|
|
||||||
ray('Deleted server from Hetzner', [
|
|
||||||
'hetzner_server_id' => $hetznerServerId,
|
|
||||||
'team_id' => $teamId,
|
|
||||||
]);
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
ray('Failed to delete server from Hetzner', [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'hetzner_server_id' => $hetznerServerId,
|
|
||||||
'team_id' => $teamId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Log the error but don't prevent the server from being deleted from Coolify
|
// Log the error but don't prevent the server from being deleted from Coolify
|
||||||
logger()->error('Failed to delete server from Hetzner', [
|
logger()->error('Failed to delete server from Hetzner', [
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public function handle(Server $server, bool $restart = false, ?string $latestVer
|
||||||
$endpoint = data_get($server, 'settings.sentinel_custom_url');
|
$endpoint = data_get($server, 'settings.sentinel_custom_url');
|
||||||
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
||||||
$mountDir = '/data/coolify/sentinel';
|
$mountDir = '/data/coolify/sentinel';
|
||||||
$image = config('constants.coolify.registry_url').'/coollabsio/sentinel:'.$version;
|
$image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version;
|
||||||
if (! $endpoint) {
|
if (! $endpoint) {
|
||||||
throw new \RuntimeException('You should set FQDN in Instance Settings.');
|
throw new \RuntimeException('You should set FQDN in Instance Settings.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,10 +118,14 @@ private function update()
|
||||||
{
|
{
|
||||||
$latestHelperImageVersion = getHelperVersion();
|
$latestHelperImageVersion = getHelperVersion();
|
||||||
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
||||||
|
$registryUrl = coolifyRegistryUrl();
|
||||||
|
|
||||||
remote_process([
|
remote_process([
|
||||||
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
||||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion",
|
'bash /data/coolify/source/upgrade.sh '.
|
||||||
|
escapeshellarg($this->latestVersion).' '.
|
||||||
|
escapeshellarg($latestHelperImageVersion).' '.
|
||||||
|
escapeshellarg($registryUrl),
|
||||||
], $this->server);
|
], $this->server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,7 @@ public function applications(Request $request)
|
||||||
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
||||||
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
||||||
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
||||||
|
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
|
||||||
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
|
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
|
||||||
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
||||||
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
||||||
|
|
@ -382,6 +383,7 @@ public function create_public_application(Request $request)
|
||||||
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
||||||
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
||||||
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
||||||
|
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
|
||||||
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
|
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
|
||||||
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
||||||
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
||||||
|
|
@ -548,6 +550,7 @@ public function create_private_gh_app_application(Request $request)
|
||||||
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
||||||
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
||||||
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
||||||
|
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
|
||||||
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
|
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
|
||||||
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
||||||
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
||||||
|
|
@ -741,6 +744,7 @@ public function create_private_deploy_key_application(Request $request)
|
||||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||||
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
||||||
|
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
|
||||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||||
|
|
@ -875,6 +879,7 @@ public function create_dockerfile_application(Request $request)
|
||||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||||
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
||||||
|
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
|
||||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||||
|
|
@ -959,7 +964,7 @@ private function create_application(Request $request, $type)
|
||||||
if ($return instanceof JsonResponse) {
|
if ($return instanceof JsonResponse) {
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
|
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
|
||||||
|
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
|
|
@ -997,6 +1002,10 @@ private function create_application(Request $request, $type)
|
||||||
}
|
}
|
||||||
$serverUuid = $request->server_uuid;
|
$serverUuid = $request->server_uuid;
|
||||||
$fqdn = $request->domains;
|
$fqdn = $request->domains;
|
||||||
|
if ($request->has('domains') && is_string($request->domains)) {
|
||||||
|
$fqdn = ValidationPatterns::normalizeApplicationDomains($request->domains);
|
||||||
|
$request->offsetSet('domains', $fqdn);
|
||||||
|
}
|
||||||
$autogenerateDomain = $request->boolean('autogenerate_domain', true);
|
$autogenerateDomain = $request->boolean('autogenerate_domain', true);
|
||||||
$instantDeploy = $request->instant_deploy;
|
$instantDeploy = $request->instant_deploy;
|
||||||
$githubAppUuid = $request->github_app_uuid;
|
$githubAppUuid = $request->github_app_uuid;
|
||||||
|
|
@ -1005,6 +1014,7 @@ private function create_application(Request $request, $type)
|
||||||
$isSpa = $request->is_spa;
|
$isSpa = $request->is_spa;
|
||||||
$isAutoDeployEnabled = $request->is_auto_deploy_enabled;
|
$isAutoDeployEnabled = $request->is_auto_deploy_enabled;
|
||||||
$isForceHttpsEnabled = $request->is_force_https_enabled;
|
$isForceHttpsEnabled = $request->is_force_https_enabled;
|
||||||
|
$isPreviewDeploymentsEnabled = $request->is_preview_deployments_enabled;
|
||||||
$connectToDockerNetwork = $request->connect_to_docker_network;
|
$connectToDockerNetwork = $request->connect_to_docker_network;
|
||||||
$customNginxConfiguration = $request->custom_nginx_configuration;
|
$customNginxConfiguration = $request->custom_nginx_configuration;
|
||||||
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
|
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
|
||||||
|
|
@ -1076,7 +1086,7 @@ private function create_application(Request $request, $type)
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_domains.*' => 'array:name,domain',
|
'docker_compose_domains.*' => 'array:name,domain',
|
||||||
'docker_compose_domains.*.name' => 'string|required',
|
'docker_compose_domains.*.name' => 'string|required',
|
||||||
'docker_compose_domains.*.domain' => 'string|nullable',
|
'docker_compose_domains.*.domain' => ValidationPatterns::applicationDomainRules(),
|
||||||
];
|
];
|
||||||
// ports_exposes is not required for dockercompose
|
// ports_exposes is not required for dockercompose
|
||||||
if ($request->build_pack === 'dockercompose') {
|
if ($request->build_pack === 'dockercompose') {
|
||||||
|
|
@ -1132,7 +1142,7 @@ private function create_application(Request $request, $type)
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$urls = $urls->map(function ($url) use (&$errors) {
|
$urls = $urls->map(function ($url) use (&$errors) {
|
||||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
if (! isValidDomainUrl($url)) {
|
||||||
$errors[] = "Invalid URL: {$url}";
|
$errors[] = "Invalid URL: {$url}";
|
||||||
|
|
||||||
return $url;
|
return $url;
|
||||||
|
|
@ -1182,15 +1192,15 @@ private function create_application(Request $request, $type)
|
||||||
$request->offsetUnset('docker_compose_domains');
|
$request->offsetUnset('docker_compose_domains');
|
||||||
}
|
}
|
||||||
if ($dockerComposeDomainsJson->count() > 0) {
|
if ($dockerComposeDomainsJson->count() > 0) {
|
||||||
$application->docker_compose_domains = $dockerComposeDomainsJson;
|
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
|
||||||
}
|
}
|
||||||
$repository_url_parsed = Url::fromString($request->git_repository);
|
$repository_url_parsed = Url::fromString($request->git_repository);
|
||||||
$git_host = $repository_url_parsed->getHost();
|
$git_host = $repository_url_parsed->getHost();
|
||||||
if ($git_host === 'github.com') {
|
if ($git_host === 'github.com') {
|
||||||
$application->source_type = GithubApp::class;
|
$application->source_type = GithubApp::class;
|
||||||
$application->source_id = GithubApp::find(0)->id;
|
$application->source_id = GithubApp::find(0)->id;
|
||||||
|
$application->git_repository = str($repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2))->trim()->toString();
|
||||||
}
|
}
|
||||||
$application->git_repository = str($repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2))->trim()->toString();
|
|
||||||
$application->fqdn = $fqdn;
|
$application->fqdn = $fqdn;
|
||||||
$application->destination_id = $destination->id;
|
$application->destination_id = $destination->id;
|
||||||
$application->destination_type = $destination->getMorphClass();
|
$application->destination_type = $destination->getMorphClass();
|
||||||
|
|
@ -1212,6 +1222,10 @@ private function create_application(Request $request, $type)
|
||||||
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
}
|
}
|
||||||
|
if (isset($isPreviewDeploymentsEnabled)) {
|
||||||
|
$application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled;
|
||||||
|
$application->settings->save();
|
||||||
|
}
|
||||||
if (isset($connectToDockerNetwork)) {
|
if (isset($connectToDockerNetwork)) {
|
||||||
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -1284,7 +1298,7 @@ private function create_application(Request $request, $type)
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_domains.*' => 'array:name,domain',
|
'docker_compose_domains.*' => 'array:name,domain',
|
||||||
'docker_compose_domains.*.name' => 'string|required',
|
'docker_compose_domains.*.name' => 'string|required',
|
||||||
'docker_compose_domains.*.domain' => 'string|nullable',
|
'docker_compose_domains.*.domain' => ValidationPatterns::applicationDomainRules(),
|
||||||
];
|
];
|
||||||
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
||||||
$validationMessages = [
|
$validationMessages = [
|
||||||
|
|
@ -1373,7 +1387,7 @@ private function create_application(Request $request, $type)
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$urls = $urls->map(function ($url) use (&$errors) {
|
$urls = $urls->map(function ($url) use (&$errors) {
|
||||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
if (! isValidDomainUrl($url)) {
|
||||||
$errors[] = "Invalid URL: {$url}";
|
$errors[] = "Invalid URL: {$url}";
|
||||||
|
|
||||||
return $url;
|
return $url;
|
||||||
|
|
@ -1423,7 +1437,7 @@ private function create_application(Request $request, $type)
|
||||||
$request->offsetUnset('docker_compose_domains');
|
$request->offsetUnset('docker_compose_domains');
|
||||||
}
|
}
|
||||||
if ($dockerComposeDomainsJson->count() > 0) {
|
if ($dockerComposeDomainsJson->count() > 0) {
|
||||||
$application->docker_compose_domains = $dockerComposeDomainsJson;
|
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
|
||||||
}
|
}
|
||||||
$application->fqdn = $fqdn;
|
$application->fqdn = $fqdn;
|
||||||
$application->git_repository = str($gitRepository)->trim()->toString();
|
$application->git_repository = str($gitRepository)->trim()->toString();
|
||||||
|
|
@ -1457,6 +1471,10 @@ private function create_application(Request $request, $type)
|
||||||
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
}
|
}
|
||||||
|
if (isset($isPreviewDeploymentsEnabled)) {
|
||||||
|
$application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled;
|
||||||
|
$application->settings->save();
|
||||||
|
}
|
||||||
if (isset($connectToDockerNetwork)) {
|
if (isset($connectToDockerNetwork)) {
|
||||||
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -1524,7 +1542,7 @@ private function create_application(Request $request, $type)
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_domains.*' => 'array:name,domain',
|
'docker_compose_domains.*' => 'array:name,domain',
|
||||||
'docker_compose_domains.*.name' => 'string|required',
|
'docker_compose_domains.*.name' => 'string|required',
|
||||||
'docker_compose_domains.*.domain' => 'string|nullable',
|
'docker_compose_domains.*.domain' => ValidationPatterns::applicationDomainRules(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
||||||
|
|
@ -1586,7 +1604,7 @@ private function create_application(Request $request, $type)
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$urls = $urls->map(function ($url) use (&$errors) {
|
$urls = $urls->map(function ($url) use (&$errors) {
|
||||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
if (! isValidDomainUrl($url)) {
|
||||||
$errors[] = "Invalid URL: {$url}";
|
$errors[] = "Invalid URL: {$url}";
|
||||||
|
|
||||||
return $url;
|
return $url;
|
||||||
|
|
@ -1636,7 +1654,7 @@ private function create_application(Request $request, $type)
|
||||||
$request->offsetUnset('docker_compose_domains');
|
$request->offsetUnset('docker_compose_domains');
|
||||||
}
|
}
|
||||||
if ($dockerComposeDomainsJson->count() > 0) {
|
if ($dockerComposeDomainsJson->count() > 0) {
|
||||||
$application->docker_compose_domains = $dockerComposeDomainsJson;
|
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
|
||||||
}
|
}
|
||||||
$application->fqdn = $fqdn;
|
$application->fqdn = $fqdn;
|
||||||
$application->private_key_id = $privateKey->id;
|
$application->private_key_id = $privateKey->id;
|
||||||
|
|
@ -1666,6 +1684,10 @@ private function create_application(Request $request, $type)
|
||||||
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
}
|
}
|
||||||
|
if (isset($isPreviewDeploymentsEnabled)) {
|
||||||
|
$application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled;
|
||||||
|
$application->settings->save();
|
||||||
|
}
|
||||||
if (isset($connectToDockerNetwork)) {
|
if (isset($connectToDockerNetwork)) {
|
||||||
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -1790,6 +1812,10 @@ private function create_application(Request $request, $type)
|
||||||
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
}
|
}
|
||||||
|
if (isset($isPreviewDeploymentsEnabled)) {
|
||||||
|
$application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled;
|
||||||
|
$application->settings->save();
|
||||||
|
}
|
||||||
if (isset($connectToDockerNetwork)) {
|
if (isset($connectToDockerNetwork)) {
|
||||||
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -1909,6 +1935,10 @@ private function create_application(Request $request, $type)
|
||||||
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
$application->settings->is_force_https_enabled = $isForceHttpsEnabled;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
}
|
}
|
||||||
|
if (isset($isPreviewDeploymentsEnabled)) {
|
||||||
|
$application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled;
|
||||||
|
$application->settings->save();
|
||||||
|
}
|
||||||
if (isset($connectToDockerNetwork)) {
|
if (isset($connectToDockerNetwork)) {
|
||||||
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -2058,6 +2088,13 @@ public function application_by_uuid(Request $request)
|
||||||
default: 100,
|
default: 100,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'show_timestamps',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Show timestamps in the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(type: 'boolean', default: false),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
responses: [
|
responses: [
|
||||||
new OA\Response(
|
new OA\Response(
|
||||||
|
|
@ -2121,8 +2158,9 @@ public function logs_by_uuid(Request $request)
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$lines = $request->query->get('lines', 100) ?: 100;
|
$lines = normalizeLogLines($request->query('lines'));
|
||||||
$logs = getContainerLogs($application->destination->server, $container['ID'], $lines);
|
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
|
||||||
|
$logs = getContainerLogs($application->destination->server, $container['ID'], $lines, $showTimestamps);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'logs' => $logs,
|
'logs' => $logs,
|
||||||
|
|
@ -2269,6 +2307,7 @@ public function delete_by_uuid(Request $request)
|
||||||
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
||||||
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
||||||
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
||||||
|
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
|
||||||
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
||||||
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
||||||
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
|
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
|
||||||
|
|
@ -2328,6 +2367,7 @@ public function delete_by_uuid(Request $request)
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
|
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
|
||||||
'is_preserve_repository_enabled' => ['type' => 'boolean', 'description' => 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'],
|
'is_preserve_repository_enabled' => ['type' => 'boolean', 'description' => 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'],
|
||||||
|
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include source commit information in the build. Default is false.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -2413,7 +2453,7 @@ public function update_by_uuid(Request $request)
|
||||||
$this->authorize('update', $application);
|
$this->authorize('update', $application);
|
||||||
|
|
||||||
$server = $application->destination->server;
|
$server = $application->destination->server;
|
||||||
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
|
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled', 'include_source_commit_in_build'];
|
||||||
|
|
||||||
$validationRules = [
|
$validationRules = [
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
|
|
@ -2423,11 +2463,13 @@ public function update_by_uuid(Request $request)
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_domains.*' => 'array:name,domain',
|
'docker_compose_domains.*' => 'array:name,domain',
|
||||||
'docker_compose_domains.*.name' => 'string|required',
|
'docker_compose_domains.*.name' => 'string|required',
|
||||||
'docker_compose_domains.*.domain' => 'string|nullable',
|
'docker_compose_domains.*.domain' => ValidationPatterns::applicationDomainRules(),
|
||||||
'custom_nginx_configuration' => 'string|nullable',
|
'custom_nginx_configuration' => 'string|nullable',
|
||||||
'is_http_basic_auth_enabled' => 'boolean|nullable',
|
'is_http_basic_auth_enabled' => 'boolean|nullable',
|
||||||
|
'is_preview_deployments_enabled' => 'boolean|nullable',
|
||||||
'http_basic_auth_username' => 'string',
|
'http_basic_auth_username' => 'string',
|
||||||
'http_basic_auth_password' => 'string',
|
'http_basic_auth_password' => 'string',
|
||||||
|
'include_source_commit_in_build' => 'boolean',
|
||||||
];
|
];
|
||||||
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
||||||
$validationMessages = [
|
$validationMessages = [
|
||||||
|
|
@ -2527,29 +2569,7 @@ public function update_by_uuid(Request $request)
|
||||||
$requestHasDomains = $request->has('domains');
|
$requestHasDomains = $request->has('domains');
|
||||||
if ($requestHasDomains && $server->isProxyShouldRun()) {
|
if ($requestHasDomains && $server->isProxyShouldRun()) {
|
||||||
$uuid = $request->uuid;
|
$uuid = $request->uuid;
|
||||||
$urls = $request->domains;
|
$errors = ValidationPatterns::validateApplicationDomains($request->domains);
|
||||||
$urls = str($urls)->replaceStart(',', '')->replaceEnd(',', '')->trim();
|
|
||||||
$errors = [];
|
|
||||||
$urls = str($urls)->trim()->explode(',')->map(function ($url) use (&$errors) {
|
|
||||||
$url = trim($url);
|
|
||||||
|
|
||||||
// If "domains" is empty clear all URLs from the fqdn column
|
|
||||||
if (blank($url)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
|
||||||
$errors[] = 'Invalid URL: '.$url;
|
|
||||||
|
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
|
|
||||||
if (! in_array(strtolower($scheme), ['http', 'https'])) {
|
|
||||||
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return str($url)->lower();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count($errors) > 0) {
|
if (count($errors) > 0) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
@ -2557,6 +2577,9 @@ public function update_by_uuid(Request $request)
|
||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
$domains = ValidationPatterns::normalizeApplicationDomains($request->domains);
|
||||||
|
$request->offsetSet('domains', $domains);
|
||||||
|
$urls = collect(ValidationPatterns::applicationDomainList($domains));
|
||||||
// Check for domain conflicts
|
// Check for domain conflicts
|
||||||
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $uuid);
|
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $uuid);
|
||||||
if (isset($result['error'])) {
|
if (isset($result['error'])) {
|
||||||
|
|
@ -2601,7 +2624,7 @@ public function update_by_uuid(Request $request)
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$urls = $urls->map(function ($url) use (&$errors) {
|
$urls = $urls->map(function ($url) use (&$errors) {
|
||||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
if (! isValidDomainUrl($url)) {
|
||||||
$errors[] = "Invalid URL: {$url}";
|
$errors[] = "Invalid URL: {$url}";
|
||||||
|
|
||||||
return $url;
|
return $url;
|
||||||
|
|
@ -2660,10 +2683,12 @@ public function update_by_uuid(Request $request)
|
||||||
$isSpa = $request->is_spa;
|
$isSpa = $request->is_spa;
|
||||||
$isAutoDeployEnabled = $request->is_auto_deploy_enabled;
|
$isAutoDeployEnabled = $request->is_auto_deploy_enabled;
|
||||||
$isForceHttpsEnabled = $request->is_force_https_enabled;
|
$isForceHttpsEnabled = $request->is_force_https_enabled;
|
||||||
|
$isPreviewDeploymentsEnabled = $request->is_preview_deployments_enabled;
|
||||||
$connectToDockerNetwork = $request->connect_to_docker_network;
|
$connectToDockerNetwork = $request->connect_to_docker_network;
|
||||||
$useBuildServer = $request->use_build_server;
|
$useBuildServer = $request->use_build_server;
|
||||||
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled');
|
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled');
|
||||||
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled');
|
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled');
|
||||||
|
$includeSourceCommitInBuild = $request->boolean('include_source_commit_in_build');
|
||||||
if (isset($useBuildServer)) {
|
if (isset($useBuildServer)) {
|
||||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -2689,6 +2714,11 @@ public function update_by_uuid(Request $request)
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($isPreviewDeploymentsEnabled)) {
|
||||||
|
$application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled;
|
||||||
|
$application->settings->save();
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($connectToDockerNetwork)) {
|
if (isset($connectToDockerNetwork)) {
|
||||||
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
|
|
@ -2702,6 +2732,10 @@ public function update_by_uuid(Request $request)
|
||||||
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
|
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
|
||||||
$application->settings->save();
|
$application->settings->save();
|
||||||
}
|
}
|
||||||
|
if ($request->has('include_source_commit_in_build')) {
|
||||||
|
$application->settings->include_source_commit_in_build = $includeSourceCommitInBuild;
|
||||||
|
$application->settings->save();
|
||||||
|
}
|
||||||
removeUnnecessaryFieldsFromRequest($request);
|
removeUnnecessaryFieldsFromRequest($request);
|
||||||
|
|
||||||
$data = $request->only($allowedFields);
|
$data = $request->only($allowedFields);
|
||||||
|
|
@ -3669,7 +3703,7 @@ public function action_deploy(Request $request)
|
||||||
'team_id' => $teamId,
|
'team_id' => $teamId,
|
||||||
'application_uuid' => $application->uuid,
|
'application_uuid' => $application->uuid,
|
||||||
'application_name' => $application->name,
|
'application_name' => $application->name,
|
||||||
'deployment_uuid' => $deployment_uuid->toString(),
|
'deployment_uuid' => $deployment_uuid,
|
||||||
'force_rebuild' => $force,
|
'force_rebuild' => $force,
|
||||||
'instant_deploy' => $instant_deploy,
|
'instant_deploy' => $instant_deploy,
|
||||||
]);
|
]);
|
||||||
|
|
@ -3677,7 +3711,7 @@ public function action_deploy(Request $request)
|
||||||
return response()->json(
|
return response()->json(
|
||||||
[
|
[
|
||||||
'message' => 'Deployment request queued.',
|
'message' => 'Deployment request queued.',
|
||||||
'deployment_uuid' => $deployment_uuid->toString(),
|
'deployment_uuid' => $deployment_uuid,
|
||||||
],
|
],
|
||||||
200
|
200
|
||||||
);
|
);
|
||||||
|
|
@ -3863,13 +3897,13 @@ public function action_restart(Request $request)
|
||||||
'team_id' => $teamId,
|
'team_id' => $teamId,
|
||||||
'application_uuid' => $application->uuid,
|
'application_uuid' => $application->uuid,
|
||||||
'application_name' => $application->name,
|
'application_name' => $application->name,
|
||||||
'deployment_uuid' => $deployment_uuid->toString(),
|
'deployment_uuid' => $deployment_uuid,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json(
|
return response()->json(
|
||||||
[
|
[
|
||||||
'message' => 'Restart request queued.',
|
'message' => 'Restart request queued.',
|
||||||
'deployment_uuid' => $deployment_uuid->toString(),
|
'deployment_uuid' => $deployment_uuid,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -3916,36 +3950,16 @@ private function validateDataApplications(Request $request, Server $server)
|
||||||
}
|
}
|
||||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||||
$uuid = $request->uuid;
|
$uuid = $request->uuid;
|
||||||
$urls = $request->domains;
|
$errors = ValidationPatterns::validateApplicationDomains($request->domains);
|
||||||
$urls = str($urls)->replaceEnd(',', '')->trim();
|
|
||||||
$urls = str($urls)->replaceStart(',', '')->trim();
|
|
||||||
$errors = [];
|
|
||||||
$urls = str($urls)->trim()->explode(',')->map(function ($url) use (&$errors) {
|
|
||||||
$url = trim($url);
|
|
||||||
|
|
||||||
// If "domains" is empty clear all URLs from the fqdn column
|
|
||||||
if (blank($url)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
|
||||||
$errors[] = 'Invalid URL: '.$url;
|
|
||||||
|
|
||||||
return str($url)->lower();
|
|
||||||
}
|
|
||||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
|
|
||||||
if (! in_array(strtolower($scheme), ['http', 'https'])) {
|
|
||||||
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return str($url)->lower();
|
|
||||||
});
|
|
||||||
if (count($errors) > 0) {
|
if (count($errors) > 0) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Validation failed.',
|
'message' => 'Validation failed.',
|
||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
$normalizedDomains = ValidationPatterns::normalizeApplicationDomains($request->domains);
|
||||||
|
$request->offsetSet('domains', $normalizedDomains);
|
||||||
|
$urls = collect(ValidationPatterns::applicationDomainList($normalizedDomains));
|
||||||
// Check for domain conflicts
|
// Check for domain conflicts
|
||||||
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $uuid);
|
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $uuid);
|
||||||
if (isset($result['error'])) {
|
if (isset($result['error'])) {
|
||||||
|
|
|
||||||
|
|
@ -2335,6 +2335,116 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
return response()->json(['message' => 'Invalid database type requested.'], 400);
|
return response()->json(['message' => 'Invalid database type requested.'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'Get database logs.',
|
||||||
|
description: 'Get database logs by UUID.',
|
||||||
|
path: '/databases/{uuid}/logs',
|
||||||
|
operationId: 'get-database-logs-by-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Databases'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the database.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'lines',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Number of lines to show from the end of the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'integer',
|
||||||
|
format: 'int32',
|
||||||
|
default: 100,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'show_timestamps',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Show timestamps in the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(type: 'boolean', default: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Get database logs by UUID.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'logs' => ['type' => 'string'],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
ref: '#/components/responses/401',
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 400,
|
||||||
|
ref: '#/components/responses/400',
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 404,
|
||||||
|
ref: '#/components/responses/404',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function logs_by_uuid(Request $request)
|
||||||
|
{
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
$uuid = $request->route('uuid');
|
||||||
|
if (! $uuid) {
|
||||||
|
return response()->json(['message' => 'UUID is required.'], 400);
|
||||||
|
}
|
||||||
|
$database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
|
||||||
|
if (! $database) {
|
||||||
|
return response()->json(['message' => 'Database not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$containers = getCurrentDatabaseContainerStatus($database->destination->server, $database->id);
|
||||||
|
|
||||||
|
if ($containers->count() == 0) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Database is not running.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$container = $containers->first();
|
||||||
|
|
||||||
|
$status = getContainerStatus($database->destination->server, $container['Names']);
|
||||||
|
if ($status !== 'running') {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Database is not running.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = normalizeLogLines($request->query('lines'));
|
||||||
|
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
|
||||||
|
$logs = getContainerLogs($database->destination->server, $container['ID'], $lines, $showTimestamps);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'logs' => $logs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[OA\Delete(
|
#[OA\Delete(
|
||||||
summary: 'Delete',
|
summary: 'Delete',
|
||||||
description: 'Delete database by UUID.',
|
description: 'Delete database by UUID.',
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@ public function deploy(Request $request)
|
||||||
|
|
||||||
$uuids = $request->input('uuid');
|
$uuids = $request->input('uuid');
|
||||||
$tags = $request->input('tag');
|
$tags = $request->input('tag');
|
||||||
$force = $request->input('force') ?? false;
|
$force = $request->boolean('force');
|
||||||
$pullRequestId = $request->input('pull_request_id', $request->input('pr'));
|
$pullRequestId = $request->input('pull_request_id', $request->input('pr'));
|
||||||
$pr = $pullRequestId ? max((int) $pullRequestId, 0) : 0;
|
$pr = $pullRequestId ? max((int) $pullRequestId, 0) : 0;
|
||||||
$dockerTag = $request->string('docker_tag')->trim()->value() ?: null;
|
$dockerTag = $request->string('docker_tag')->trim()->value() ?: null;
|
||||||
|
|
@ -429,7 +429,7 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false, int $p
|
||||||
}
|
}
|
||||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $result;
|
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $result;
|
||||||
if ($deployment_uuid) {
|
if ($deployment_uuid) {
|
||||||
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
|
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid]);
|
||||||
} else {
|
} else {
|
||||||
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
|
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
|
||||||
}
|
}
|
||||||
|
|
@ -475,7 +475,7 @@ public function by_tags(string $tags, int $team_id, bool $force = false)
|
||||||
}
|
}
|
||||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $result;
|
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $result;
|
||||||
if ($deployment_uuid) {
|
if ($deployment_uuid) {
|
||||||
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
|
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid]);
|
||||||
}
|
}
|
||||||
$message = $message->merge($return_message);
|
$message = $message->merge($return_message);
|
||||||
}
|
}
|
||||||
|
|
@ -533,7 +533,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0, ?st
|
||||||
'resource_type' => 'application',
|
'resource_type' => 'application',
|
||||||
'application_uuid' => $resource->uuid,
|
'application_uuid' => $resource->uuid,
|
||||||
'application_name' => $resource->name,
|
'application_name' => $resource->name,
|
||||||
'deployment_uuid' => $deployment_uuid?->toString(),
|
'deployment_uuid' => $deployment_uuid,
|
||||||
'force_rebuild' => $force,
|
'force_rebuild' => $force,
|
||||||
'pull_request_id' => $pr,
|
'pull_request_id' => $pr,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ public function list_github_apps(Request $request)
|
||||||
'private_key_uuid' => ['type' => 'string', 'description' => 'UUID of an existing private key for GitHub App authentication.'],
|
'private_key_uuid' => ['type' => 'string', 'description' => 'UUID of an existing private key for GitHub App authentication.'],
|
||||||
'is_system_wide' => ['type' => 'boolean', 'description' => 'Is this app system-wide (cloud only).'],
|
'is_system_wide' => ['type' => 'boolean', 'description' => 'Is this app system-wide (cloud only).'],
|
||||||
],
|
],
|
||||||
required: ['name', 'api_url', 'html_url', 'app_id', 'installation_id', 'client_id', 'client_secret', 'private_key_uuid'],
|
required: ['name', 'html_url', 'app_id', 'installation_id', 'client_id', 'client_secret', 'private_key_uuid'],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -212,10 +212,14 @@ public function create_github_app(Request $request)
|
||||||
'is_system_wide',
|
'is_system_wide',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$request->merge([
|
||||||
|
'organization' => normalizeGithubOrganization($request->input('organization')),
|
||||||
|
]);
|
||||||
|
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'organization' => 'nullable|string|max:255',
|
'organization' => ['nullable', 'string', 'max:255', 'regex:/\A[^\s\/?#]+\z/'],
|
||||||
'api_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
'api_url' => ['nullable', 'string', 'url', new SafeExternalUrl],
|
||||||
'html_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
'html_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||||
'custom_user' => 'nullable|string|max:255',
|
'custom_user' => 'nullable|string|max:255',
|
||||||
'custom_port' => 'nullable|integer|min:1|max:65535',
|
'custom_port' => 'nullable|integer|min:1|max:65535',
|
||||||
|
|
@ -259,7 +263,9 @@ public function create_github_app(Request $request)
|
||||||
'uuid' => Str::uuid(),
|
'uuid' => Str::uuid(),
|
||||||
'name' => $request->input('name'),
|
'name' => $request->input('name'),
|
||||||
'organization' => $request->input('organization'),
|
'organization' => $request->input('organization'),
|
||||||
'api_url' => $request->input('api_url'),
|
'api_url' => filled($request->input('api_url'))
|
||||||
|
? $request->input('api_url')
|
||||||
|
: githubApiUrlFromHtmlUrl($request->input('html_url')),
|
||||||
'html_url' => $request->input('html_url'),
|
'html_url' => $request->input('html_url'),
|
||||||
'custom_user' => $request->input('custom_user', 'git'),
|
'custom_user' => $request->input('custom_user', 'git'),
|
||||||
'custom_port' => $request->input('custom_port', 22),
|
'custom_port' => $request->input('custom_port', 22),
|
||||||
|
|
@ -596,13 +602,17 @@ public function update_github_app(Request $request, $github_app_id)
|
||||||
|
|
||||||
$payload = $request->only($allowedFields);
|
$payload = $request->only($allowedFields);
|
||||||
|
|
||||||
|
if (array_key_exists('organization', $payload)) {
|
||||||
|
$payload['organization'] = normalizeGithubOrganization($payload['organization']);
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the request
|
// Validate the request
|
||||||
$rules = [];
|
$rules = [];
|
||||||
if (isset($payload['name'])) {
|
if (isset($payload['name'])) {
|
||||||
$rules['name'] = 'string';
|
$rules['name'] = 'string';
|
||||||
}
|
}
|
||||||
if (isset($payload['organization'])) {
|
if (isset($payload['organization'])) {
|
||||||
$rules['organization'] = 'nullable|string';
|
$rules['organization'] = ['nullable', 'string', 'regex:/\A[^\s\/?#]+\z/'];
|
||||||
}
|
}
|
||||||
if (isset($payload['api_url'])) {
|
if (isset($payload['api_url'])) {
|
||||||
$rules['api_url'] = ['url', new SafeExternalUrl];
|
$rules['api_url'] = ['url', new SafeExternalUrl];
|
||||||
|
|
@ -646,6 +656,13 @@ public function update_github_app(Request $request, $github_app_id)
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('organization', $payload)) {
|
||||||
|
$payload['organization'] = normalizeGithubOrganization($payload['organization']);
|
||||||
|
}
|
||||||
|
if (isset($payload['html_url']) && ! filled($payload['api_url'] ?? null)) {
|
||||||
|
$payload['api_url'] = githubApiUrlFromHtmlUrl($payload['html_url']);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle private_key_uuid -> private_key_id conversion
|
// Handle private_key_uuid -> private_key_id conversion
|
||||||
if (isset($payload['private_key_uuid'])) {
|
if (isset($payload['private_key_uuid'])) {
|
||||||
$privateKey = PrivateKey::where('team_id', $teamId)
|
$privateKey = PrivateKey::where('team_id', $teamId)
|
||||||
|
|
|
||||||
|
|
@ -112,19 +112,10 @@ private function applyServiceUrls(Service $service, array $urlsArray, string $te
|
||||||
return str($urlValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter();
|
return str($urlValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter();
|
||||||
});
|
});
|
||||||
|
|
||||||
$urls = $urls->map(function ($url) use (&$errors) {
|
$errors = ValidationPatterns::validateApplicationDomains($urls->implode(','));
|
||||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
$urls = collect(ValidationPatterns::applicationDomainList(
|
||||||
$errors[] = "Invalid URL: {$url}";
|
ValidationPatterns::normalizeApplicationDomains($urls->implode(','))
|
||||||
|
));
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
|
|
||||||
if (! in_array(strtolower($scheme), ['http', 'https'])) {
|
|
||||||
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $url;
|
|
||||||
});
|
|
||||||
|
|
||||||
$duplicates = $urls->duplicates()->unique()->values();
|
$duplicates = $urls->duplicates()->unique()->values();
|
||||||
if ($duplicates->isNotEmpty() && ! $forceDomainOverride) {
|
if ($duplicates->isNotEmpty() && ! $forceDomainOverride) {
|
||||||
|
|
@ -153,10 +144,10 @@ private function applyServiceUrls(Service $service, array $urlsArray, string $te
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filled($containerUrls)) {
|
if (filled($containerUrls)) {
|
||||||
$containerUrls = str($containerUrls)->replaceStart(',', '')->replaceEnd(',', '')->trim();
|
$containerUrls = ValidationPatterns::normalizeApplicationDomains($containerUrls);
|
||||||
$containerUrls = str($containerUrls)->explode(',')->map(fn ($url) => str(trim($url))->lower());
|
$containerUrlCollection = collect(ValidationPatterns::applicationDomainList($containerUrls));
|
||||||
|
|
||||||
$result = checkIfDomainIsAlreadyUsedViaAPI($containerUrls, $teamId, $application->uuid);
|
$result = checkIfDomainIsAlreadyUsedViaAPI($containerUrlCollection, $teamId, $application->uuid);
|
||||||
if (isset($result['error'])) {
|
if (isset($result['error'])) {
|
||||||
$errors[] = $result['error'];
|
$errors[] = $result['error'];
|
||||||
|
|
||||||
|
|
@ -168,8 +159,6 @@ private function applyServiceUrls(Service $service, array $urlsArray, string $te
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$containerUrls = $containerUrls->filter(fn ($u) => filled($u))->unique()->implode(',');
|
|
||||||
} else {
|
} else {
|
||||||
$containerUrls = null;
|
$containerUrls = null;
|
||||||
}
|
}
|
||||||
|
|
@ -803,6 +792,125 @@ public function service_by_uuid(Request $request)
|
||||||
return response()->json($this->removeSensitiveData($service));
|
return response()->json($this->removeSensitiveData($service));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'Get service logs.',
|
||||||
|
description: 'Get logs for a specific service sub-resource by service UUID. The `sub_service_name` query parameter must match the `name` field of one of the service applications or databases returned by `GET /services/{uuid}`.',
|
||||||
|
path: '/services/{uuid}/logs',
|
||||||
|
operationId: 'get-service-logs-by-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Services'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the service.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'sub_service_name',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Sub-service name from `GET /services/{uuid}` under `applications[].name` or `databases[].name`. Do not use `human_name` or the Docker container name with the service UUID suffix.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', example: 'appwrite-console'),
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'lines',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Number of lines to show from the end of the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'integer',
|
||||||
|
format: 'int32',
|
||||||
|
default: 100,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'show_timestamps',
|
||||||
|
in: 'query',
|
||||||
|
description: 'Show timestamps in the logs.',
|
||||||
|
required: false,
|
||||||
|
schema: new OA\Schema(type: 'boolean', default: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Get service logs by UUID.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'logs' => ['type' => 'string'],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
ref: '#/components/responses/401',
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 400,
|
||||||
|
ref: '#/components/responses/400',
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 404,
|
||||||
|
ref: '#/components/responses/404',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function logs_by_uuid(Request $request)
|
||||||
|
{
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
$uuid = $request->route('uuid');
|
||||||
|
if (! $uuid) {
|
||||||
|
return response()->json(['message' => 'UUID is required.'], 400);
|
||||||
|
}
|
||||||
|
$subServiceName = $request->query->get('sub_service_name');
|
||||||
|
if (! $subServiceName) {
|
||||||
|
return response()->json(['message' => 'Sub service name is required.'], 400);
|
||||||
|
}
|
||||||
|
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||||
|
if (! $service) {
|
||||||
|
return response()->json(['message' => 'Service not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = "{$subServiceName}-{$service->uuid}";
|
||||||
|
$containers = getCurrentServiceSubContainerStatus($service->destination->server, $service->id, $name);
|
||||||
|
$container = $containers->first();
|
||||||
|
|
||||||
|
if (! $container) {
|
||||||
|
return response()->json(['message' => 'Container not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = getContainerStatus($service->destination->server, $container['Names']);
|
||||||
|
if ($status !== 'running') {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Container is not running.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = normalizeLogLines($request->query('lines'));
|
||||||
|
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
|
||||||
|
$logs = getContainerLogs($service->destination->server, $container['ID'], $lines, $showTimestamps);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'logs' => $logs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[OA\Delete(
|
#[OA\Delete(
|
||||||
summary: 'Delete',
|
summary: 'Delete',
|
||||||
description: 'Delete service by UUID.',
|
description: 'Delete service by UUID.',
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ public function forgot_password(Request $request)
|
||||||
public function link()
|
public function link()
|
||||||
{
|
{
|
||||||
$token = request()->get('token');
|
$token = request()->get('token');
|
||||||
if ($token) {
|
if (is_string($token) && $token !== '') {
|
||||||
try {
|
try {
|
||||||
$decrypted = Crypt::decryptString($token);
|
$decrypted = Crypt::decryptString($token);
|
||||||
} catch (DecryptException) {
|
} catch (DecryptException) {
|
||||||
|
|
@ -126,9 +126,8 @@ public function link()
|
||||||
$invitation = TeamInvitation::query()
|
$invitation = TeamInvitation::query()
|
||||||
->where('email', $email)
|
->where('email', $email)
|
||||||
->when($invitationUuid, fn ($query) => $query->where('uuid', $invitationUuid))
|
->when($invitationUuid, fn ($query) => $query->where('uuid', $invitationUuid))
|
||||||
->where('link', request()->fullUrl())
|
|
||||||
->first();
|
->first();
|
||||||
if (! $invitation || ! $invitation->isValid()) {
|
if (! $invitation || ! $this->invitationLinkMatchesToken($invitation, $token) || ! $invitation->isValid()) {
|
||||||
return redirect()->route('login')->with('error', 'Invitation has expired or been revoked.');
|
return redirect()->route('login')->with('error', 'Invitation has expired or been revoked.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,10 +138,11 @@ public function link()
|
||||||
}
|
}
|
||||||
$invitation->delete();
|
$invitation->delete();
|
||||||
|
|
||||||
Auth::login($user);
|
|
||||||
$user->forceFill([
|
$user->forceFill([
|
||||||
'password' => Hash::make(Str::random(64)),
|
'password' => Hash::make(Str::random(64)),
|
||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
session(['currentTeam' => $team]);
|
session(['currentTeam' => $team]);
|
||||||
|
|
||||||
return redirect()->route('dashboard');
|
return redirect()->route('dashboard');
|
||||||
|
|
@ -152,6 +152,19 @@ public function link()
|
||||||
return redirect()->route('login')->with('error', 'Invalid credentials.');
|
return redirect()->route('login')->with('error', 'Invalid credentials.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function invitationLinkMatchesToken(TeamInvitation $invitation, string $token): bool
|
||||||
|
{
|
||||||
|
$query = parse_url($invitation->link, PHP_URL_QUERY);
|
||||||
|
if (! is_string($query)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_str($query, $parameters);
|
||||||
|
$storedToken = $parameters['token'] ?? null;
|
||||||
|
|
||||||
|
return is_string($storedToken) && hash_equals($storedToken, $token);
|
||||||
|
}
|
||||||
|
|
||||||
public function showInvitation()
|
public function showInvitation()
|
||||||
{
|
{
|
||||||
$invitationUuid = request()->route('uuid');
|
$invitationUuid = request()->route('uuid');
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ public function manual(Request $request)
|
||||||
'mode' => 'manual',
|
'mode' => 'manual',
|
||||||
'application_uuid' => $application->uuid,
|
'application_uuid' => $application->uuid,
|
||||||
'application_name' => $application->name,
|
'application_name' => $application->name,
|
||||||
'deployment_uuid' => $deployment_uuid->toString(),
|
'deployment_uuid' => $deployment_uuid,
|
||||||
'commit' => $commit,
|
'commit' => $commit,
|
||||||
'repository' => $full_name ?? null,
|
'repository' => $full_name ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ public function manual(Request $request)
|
||||||
'mode' => 'manual',
|
'mode' => 'manual',
|
||||||
'application_uuid' => $application->uuid,
|
'application_uuid' => $application->uuid,
|
||||||
'application_name' => $application->name,
|
'application_name' => $application->name,
|
||||||
'deployment_uuid' => $deployment_uuid->toString(),
|
'deployment_uuid' => $deployment_uuid,
|
||||||
'commit' => data_get($payload, 'after'),
|
'commit' => data_get($payload, 'after'),
|
||||||
'repository' => $full_name ?? null,
|
'repository' => $full_name ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ public function manual(Request $request)
|
||||||
'mode' => 'manual',
|
'mode' => 'manual',
|
||||||
'application_uuid' => $application->uuid,
|
'application_uuid' => $application->uuid,
|
||||||
'application_name' => $application->name,
|
'application_name' => $application->name,
|
||||||
'deployment_uuid' => $deployment_uuid->toString(),
|
'deployment_uuid' => $deployment_uuid,
|
||||||
'commit' => data_get($payload, 'after'),
|
'commit' => data_get($payload, 'after'),
|
||||||
'repository' => $full_name ?? null,
|
'repository' => $full_name ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,21 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
|
|
||||||
private const RAILPACK_GENERATED_CONFIG_PATH = '.coolify/railpack.generated.json';
|
private const RAILPACK_GENERATED_CONFIG_PATH = '.coolify/railpack.generated.json';
|
||||||
|
|
||||||
|
private const DOCKER_CLIENT_ENV_KEYS = [
|
||||||
|
'BUILDKIT_HOST',
|
||||||
|
'BUILDX_BUILDER',
|
||||||
|
'BUILDX_CONFIG',
|
||||||
|
'DOCKER_API_VERSION',
|
||||||
|
'DOCKER_BUILDKIT',
|
||||||
|
'DOCKER_CERT_PATH',
|
||||||
|
'DOCKER_CLI_EXPERIMENTAL',
|
||||||
|
'DOCKER_CONFIG',
|
||||||
|
'DOCKER_CONTEXT',
|
||||||
|
'DOCKER_HOST',
|
||||||
|
'DOCKER_TLS',
|
||||||
|
'DOCKER_TLS_VERIFY',
|
||||||
|
];
|
||||||
|
|
||||||
public $tries = 1;
|
public $tries = 1;
|
||||||
|
|
||||||
public $timeout = 3600;
|
public $timeout = 3600;
|
||||||
|
|
@ -1701,6 +1716,10 @@ private function generate_buildtime_environment_variables()
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($sorted_environment_variables as $env) {
|
foreach ($sorted_environment_variables as $env) {
|
||||||
|
if ($this->build_pack === 'railpack' && $this->is_reserved_docker_client_env_key($env->key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
|
$resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
|
||||||
// For literal/multiline vars, real_value includes quotes that we need to remove
|
// For literal/multiline vars, real_value includes quotes that we need to remove
|
||||||
if ($env->is_literal || $env->is_multiline) {
|
if ($env->is_literal || $env->is_multiline) {
|
||||||
|
|
@ -1752,6 +1771,10 @@ private function generate_buildtime_environment_variables()
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($sorted_environment_variables as $env) {
|
foreach ($sorted_environment_variables as $env) {
|
||||||
|
if ($this->build_pack === 'railpack' && $this->is_reserved_docker_client_env_key($env->key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
|
$resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
|
||||||
// For literal/multiline vars, real_value includes quotes that we need to remove
|
// For literal/multiline vars, real_value includes quotes that we need to remove
|
||||||
if ($env->is_literal || $env->is_multiline) {
|
if ($env->is_literal || $env->is_multiline) {
|
||||||
|
|
@ -2124,7 +2147,7 @@ private function create_workdir()
|
||||||
private function prepare_builder_image(bool $firstTry = true)
|
private function prepare_builder_image(bool $firstTry = true)
|
||||||
{
|
{
|
||||||
$this->checkForCancellation();
|
$this->checkForCancellation();
|
||||||
$helperImage = config('constants.coolify.helper_image');
|
$helperImage = coolifyHelperImage();
|
||||||
$helperImage = "{$helperImage}:".getHelperVersion();
|
$helperImage = "{$helperImage}:".getHelperVersion();
|
||||||
// Get user home directory
|
// Get user home directory
|
||||||
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
|
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
|
||||||
|
|
@ -2229,7 +2252,7 @@ private function set_coolify_variables()
|
||||||
|
|
||||||
// Only include SOURCE_COMMIT in build context if enabled in settings
|
// Only include SOURCE_COMMIT in build context if enabled in settings
|
||||||
if ($this->application->settings->include_source_commit_in_build) {
|
if ($this->application->settings->include_source_commit_in_build) {
|
||||||
$this->coolify_variables .= "SOURCE_COMMIT={$this->commit} ";
|
$this->coolify_variables .= 'SOURCE_COMMIT='.escapeShellValue($this->commit).' ';
|
||||||
}
|
}
|
||||||
if ($this->pull_request_id === 0) {
|
if ($this->pull_request_id === 0) {
|
||||||
$fqdn = $this->application->fqdn;
|
$fqdn = $this->application->fqdn;
|
||||||
|
|
@ -2241,17 +2264,33 @@ private function set_coolify_variables()
|
||||||
$fqdn = $url->getHost();
|
$fqdn = $url->getHost();
|
||||||
$url = $url->withHost($fqdn)->withPort(null)->__toString();
|
$url = $url->withHost($fqdn)->withPort(null)->__toString();
|
||||||
if ((int) $this->application->compose_parsing_version >= 3) {
|
if ((int) $this->application->compose_parsing_version >= 3) {
|
||||||
$this->coolify_variables .= "COOLIFY_URL={$url} ";
|
$this->coolify_variables .= 'COOLIFY_URL='.escapeShellValue($url).' ';
|
||||||
$this->coolify_variables .= "COOLIFY_FQDN={$fqdn} ";
|
$this->coolify_variables .= 'COOLIFY_FQDN='.escapeShellValue($fqdn).' ';
|
||||||
} else {
|
} else {
|
||||||
$this->coolify_variables .= "COOLIFY_URL={$fqdn} ";
|
$this->coolify_variables .= 'COOLIFY_URL='.escapeShellValue($fqdn).' ';
|
||||||
$this->coolify_variables .= "COOLIFY_FQDN={$url} ";
|
$this->coolify_variables .= 'COOLIFY_FQDN='.escapeShellValue($url).' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isset($this->application->git_branch)) {
|
if (isset($this->application->git_branch)) {
|
||||||
$this->coolify_variables .= 'COOLIFY_BRANCH='.escapeShellValue($this->application->git_branch).' ';
|
$this->coolify_variables .= 'COOLIFY_BRANCH='.escapeShellValue($this->application->git_branch).' ';
|
||||||
}
|
}
|
||||||
$this->coolify_variables .= "COOLIFY_RESOURCE_UUID={$this->application->uuid} ";
|
$this->coolify_variables .= 'COOLIFY_RESOURCE_UUID='.escapeShellValue($this->application->uuid).' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shellAssignmentForDockerfileArg(string $assignment): string
|
||||||
|
{
|
||||||
|
[$key, $value] = array_pad(explode('=', $assignment, 2), 2, null);
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
|
return $assignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($value, "'") && str_ends_with($value, "'")) {
|
||||||
|
$value = substr($value, 1, -1);
|
||||||
|
$value = str_replace("'\\''", "'", $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{$key}={$value}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private function gitLsRemoteCommand(string $lsRemoteRef, ?string $identityFile = null): string
|
private function gitLsRemoteCommand(string $lsRemoteRef, ?string $identityFile = null): string
|
||||||
|
|
@ -2327,7 +2366,7 @@ private function check_git_if_build_needed()
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback) {
|
if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback && $this->shouldResolveBranchHeadCommit()) {
|
||||||
// Extract commit SHA from git ls-remote output, handling multi-line output (e.g., redirect warnings)
|
// Extract commit SHA from git ls-remote output, handling multi-line output (e.g., redirect warnings)
|
||||||
// Expected format: "commit_sha\trefs/heads/branch" possibly preceded by warning lines
|
// Expected format: "commit_sha\trefs/heads/branch" possibly preceded by warning lines
|
||||||
// Note: Git warnings can be on the same line as the result (no newline)
|
// Note: Git warnings can be on the same line as the result (no newline)
|
||||||
|
|
@ -2359,6 +2398,13 @@ private function check_git_if_build_needed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function shouldResolveBranchHeadCommit(): bool
|
||||||
|
{
|
||||||
|
$commit = trim($this->commit);
|
||||||
|
|
||||||
|
return $commit === '' || $commit === 'HEAD';
|
||||||
|
}
|
||||||
|
|
||||||
private function clone_repository()
|
private function clone_repository()
|
||||||
{
|
{
|
||||||
$importCommands = $this->generate_git_import_commands();
|
$importCommands = $this->generate_git_import_commands();
|
||||||
|
|
@ -2566,6 +2612,20 @@ private function generate_nixpacks_env_variables()
|
||||||
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
|
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function is_reserved_docker_client_env_key(?string $key): bool
|
||||||
|
{
|
||||||
|
if (blank($key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array(strtoupper($key), self::DOCKER_CLIENT_ENV_KEYS, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function without_reserved_docker_client_variables(Collection $variables): Collection
|
||||||
|
{
|
||||||
|
return $variables->reject(fn ($value, $key) => $this->is_reserved_docker_client_env_key((string) $key));
|
||||||
|
}
|
||||||
|
|
||||||
private function generate_railpack_env_variables(): Collection
|
private function generate_railpack_env_variables(): Collection
|
||||||
{
|
{
|
||||||
$variables = $this->railpack_build_variables();
|
$variables = $this->railpack_build_variables();
|
||||||
|
|
@ -2686,6 +2746,8 @@ private function railpack_build_secret_flags(Collection $variables): string
|
||||||
|
|
||||||
private function railpack_build_command(string $imageName, Collection $variables): string
|
private function railpack_build_command(string $imageName, Collection $variables): string
|
||||||
{
|
{
|
||||||
|
$variables = $this->without_reserved_docker_client_variables($variables);
|
||||||
|
|
||||||
$cacheArgs = '';
|
$cacheArgs = '';
|
||||||
if ($this->force_rebuild) {
|
if ($this->force_rebuild) {
|
||||||
$cacheArgs = '--no-cache';
|
$cacheArgs = '--no-cache';
|
||||||
|
|
@ -2712,7 +2774,7 @@ private function railpack_build_command(string $imageName, Collection $variables
|
||||||
$secretFlags = $this->railpack_build_secret_flags($variables);
|
$secretFlags = $this->railpack_build_secret_flags($variables);
|
||||||
$frontendImage = 'ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version');
|
$frontendImage = 'ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version');
|
||||||
|
|
||||||
$buildxBuildCommand = "{$environmentPrefix}docker buildx build --builder coolify-railpack"
|
$buildxBuildCommand = "{$environmentPrefix}DOCKER_CONFIG=/root/.docker docker buildx build --builder coolify-railpack"
|
||||||
." {$this->addHosts} --network host"
|
." {$this->addHosts} --network host"
|
||||||
." --build-arg BUILDKIT_SYNTAX=\"{$frontendImage}\""
|
." --build-arg BUILDKIT_SYNTAX=\"{$frontendImage}\""
|
||||||
." {$cacheArgs}"
|
." {$cacheArgs}"
|
||||||
|
|
@ -2723,7 +2785,7 @@ private function railpack_build_command(string $imageName, Collection $variables
|
||||||
." -t {$imageName}"
|
." -t {$imageName}"
|
||||||
." {$this->workdir}";
|
." {$this->workdir}";
|
||||||
|
|
||||||
return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
|
return 'DOCKER_CONFIG=/root/.docker docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
|
||||||
.' && '.$this->wrap_build_command_with_env_export($buildxBuildCommand);
|
.' && '.$this->wrap_build_command_with_env_export($buildxBuildCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2878,9 +2940,25 @@ private function ensure_docker_buildx_available_for_railpack(): void
|
||||||
throw new DeploymentException('Railpack deployments require the Docker buildx CLI plugin on the build server. Install or enable docker buildx and retry the deployment.');
|
throw new DeploymentException('Railpack deployments require the Docker buildx CLI plugin on the build server. Install or enable docker buildx and retry the deployment.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensure_helper_docker_buildx_available_for_railpack(): void
|
||||||
|
{
|
||||||
|
$this->execute_remote_command([
|
||||||
|
executeInDocker($this->deployment_uuid, 'DOCKER_CONFIG=/root/.docker docker buildx version >/dev/null 2>&1 && echo available || echo not-available'),
|
||||||
|
'hidden' => true,
|
||||||
|
'save' => 'railpack_helper_buildx_available',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (trim((string) $this->saved_outputs->get('railpack_helper_buildx_available')) === 'available') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DeploymentException('Railpack deployments require the Docker buildx CLI plugin inside the Coolify helper container. The helper could not find buildx at /root/.docker/cli-plugins/docker-buildx. Pull the latest helper image and retry the deployment.');
|
||||||
|
}
|
||||||
|
|
||||||
private function build_railpack_image(): void
|
private function build_railpack_image(): void
|
||||||
{
|
{
|
||||||
$this->ensure_docker_buildx_available_for_railpack();
|
$this->ensure_docker_buildx_available_for_railpack();
|
||||||
|
$this->ensure_helper_docker_buildx_available_for_railpack();
|
||||||
|
|
||||||
$railpackVariables = $this->generate_railpack_env_variables();
|
$railpackVariables = $this->generate_railpack_env_variables();
|
||||||
$railpackConfigPath = $this->generate_railpack_config_file();
|
$railpackConfigPath = $this->generate_railpack_config_file();
|
||||||
|
|
@ -4080,6 +4158,10 @@ private function generate_docker_env_flags_for_secrets()
|
||||||
|
|
||||||
$variables = $this->env_args;
|
$variables = $this->env_args;
|
||||||
|
|
||||||
|
if ($this->build_pack === 'railpack') {
|
||||||
|
$variables = $this->without_reserved_docker_client_variables($variables);
|
||||||
|
}
|
||||||
|
|
||||||
if ($variables->isEmpty()) {
|
if ($variables->isEmpty()) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -4220,7 +4302,7 @@ private function add_build_env_variables_to_dockerfile()
|
||||||
$coolify_vars = collect(explode(' ', trim($this->coolify_variables)))
|
$coolify_vars = collect(explode(' ', trim($this->coolify_variables)))
|
||||||
->filter()
|
->filter()
|
||||||
->map(function ($var) {
|
->map(function ($var) {
|
||||||
return "ARG {$var}";
|
return 'ARG '.$this->shellAssignmentForDockerfileArg($var);
|
||||||
});
|
});
|
||||||
$argsToInsert = $argsToInsert->merge($coolify_vars);
|
$argsToInsert = $argsToInsert->merge($coolify_vars);
|
||||||
}
|
}
|
||||||
|
|
@ -4242,7 +4324,7 @@ private function add_build_env_variables_to_dockerfile()
|
||||||
$coolify_vars = collect(explode(' ', trim($this->coolify_variables)))
|
$coolify_vars = collect(explode(' ', trim($this->coolify_variables)))
|
||||||
->filter()
|
->filter()
|
||||||
->map(function ($var) {
|
->map(function ($var) {
|
||||||
return "ARG {$var}";
|
return 'ARG '.$this->shellAssignmentForDockerfileArg($var);
|
||||||
});
|
});
|
||||||
$argsToInsert = $argsToInsert->merge($coolify_vars);
|
$argsToInsert = $argsToInsert->merge($coolify_vars);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public function handle(): void
|
||||||
'active_deployment_uuids' => $activeDeployments,
|
'active_deployment_uuids' => $activeDeployments,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.config('constants.coolify.registry_url').'/coollabsio/coolify-helper")))\''], $this->server, false);
|
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.coolifyRegistryUrl().'/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||||
$helperContainers = collect(json_decode($containers));
|
$helperContainers = collect(json_decode($containers));
|
||||||
|
|
||||||
if ($helperContainers->count() > 0) {
|
if ($helperContainers->count() > 0) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
use App\Notifications\Database\BackupFailed;
|
use App\Notifications\Database\BackupFailed;
|
||||||
use App\Notifications\Database\BackupSuccess;
|
use App\Notifications\Database\BackupSuccess;
|
||||||
use App\Notifications\Database\BackupSuccessWithS3Warning;
|
use App\Notifications\Database\BackupSuccessWithS3Warning;
|
||||||
|
use App\Rules\SafeWebhookUrl;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||||
|
|
@ -716,8 +717,12 @@ private function upload_to_s3(): void
|
||||||
$escapedSecret = escapeshellarg($secret);
|
$escapedSecret = escapeshellarg($secret);
|
||||||
$escapedBackupLocation = escapeshellarg($this->backup_location);
|
$escapedBackupLocation = escapeshellarg($this->backup_location);
|
||||||
$escapedS3Destination = escapeshellarg("temporary/{$bucket}{$this->backup_dir}/");
|
$escapedS3Destination = escapeshellarg("temporary/{$bucket}{$this->backup_dir}/");
|
||||||
|
$resolveOptions = collect(SafeWebhookUrl::minioClientResolveOptions($endpoint))
|
||||||
|
->map(fn (string $resolveOption): string => '--resolve '.escapeshellarg($resolveOption))
|
||||||
|
->implode(' ');
|
||||||
|
$resolveOptions = $resolveOptions === '' ? '' : ' '.$resolveOptions;
|
||||||
|
|
||||||
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set temporary {$escapedEndpoint} {$escapedKey} {$escapedSecret}";
|
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set{$resolveOptions} temporary {$escapedEndpoint} {$escapedKey} {$escapedSecret}";
|
||||||
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp {$escapedBackupLocation} {$escapedS3Destination}";
|
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp {$escapedBackupLocation} {$escapedS3Destination}";
|
||||||
instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||||
|
|
||||||
|
|
@ -734,7 +739,7 @@ private function upload_to_s3(): void
|
||||||
|
|
||||||
private function getFullImageName(): string
|
private function getFullImageName(): string
|
||||||
{
|
{
|
||||||
$helperImage = config('constants.coolify.helper_image');
|
$helperImage = coolifyHelperImage();
|
||||||
$latestVersion = getHelperVersion();
|
$latestVersion = getHelperVersion();
|
||||||
|
|
||||||
return "{$helperImage}:{$latestVersion}";
|
return "{$helperImage}:{$latestVersion}";
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,24 @@ public function handle(): void
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
Log::warning('SendMessageToDiscordJob: blocked unsafe webhook URL', [
|
Log::warning('SendMessageToDiscordJob: blocked unsafe webhook URL', [
|
||||||
'url' => $this->webhookUrl,
|
'url' => SafeWebhookUrl::redactedUrlForLog($this->webhookUrl),
|
||||||
'errors' => $validator->errors()->all(),
|
'errors' => $validator->errors()->all(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Http::withOptions(['allow_redirects' => false])->post($this->webhookUrl, $this->message->toPayload());
|
try {
|
||||||
|
$httpOptions = SafeWebhookUrl::httpClientOptions($this->webhookUrl);
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
Log::warning('SendMessageToDiscordJob: blocked unsafe webhook URL at send time', [
|
||||||
|
'url' => SafeWebhookUrl::redactedUrlForLog($this->webhookUrl),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Http::withOptions($httpOptions)->post($this->webhookUrl, $this->message->toPayload());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,15 +44,26 @@ public function handle(): void
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
Log::warning('SendMessageToSlackJob: blocked unsafe webhook URL', [
|
Log::warning('SendMessageToSlackJob: blocked unsafe webhook URL', [
|
||||||
'url' => $this->webhookUrl,
|
'url' => SafeWebhookUrl::redactedUrlForLog($this->webhookUrl),
|
||||||
'errors' => $validator->errors()->all(),
|
'errors' => $validator->errors()->all(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$httpOptions = SafeWebhookUrl::httpClientOptions($this->webhookUrl);
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
Log::warning('SendMessageToSlackJob: blocked unsafe webhook URL at send time', [
|
||||||
|
'url' => SafeWebhookUrl::redactedUrlForLog($this->webhookUrl),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isSlackWebhook()) {
|
if ($this->isSlackWebhook()) {
|
||||||
$this->sendToSlack();
|
$this->sendToSlack($httpOptions);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +73,7 @@ public function handle(): void
|
||||||
*
|
*
|
||||||
* @see https://github.com/coollabsio/coolify/pull/6139#issuecomment-3756777708
|
* @see https://github.com/coollabsio/coolify/pull/6139#issuecomment-3756777708
|
||||||
*/
|
*/
|
||||||
$this->sendToMattermost();
|
$this->sendToMattermost($httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isSlackWebhook(): bool
|
private function isSlackWebhook(): bool
|
||||||
|
|
@ -79,9 +90,12 @@ private function isSlackWebhook(): bool
|
||||||
return $scheme === 'https' && $host === 'hooks.slack.com';
|
return $scheme === 'https' && $host === 'hooks.slack.com';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendToSlack(): void
|
/**
|
||||||
|
* @param array<string, mixed> $httpOptions
|
||||||
|
*/
|
||||||
|
private function sendToSlack(array $httpOptions): void
|
||||||
{
|
{
|
||||||
Http::withOptions(['allow_redirects' => false])->post($this->webhookUrl, [
|
Http::withOptions($httpOptions)->post($this->webhookUrl, [
|
||||||
'text' => $this->message->title,
|
'text' => $this->message->title,
|
||||||
'blocks' => [
|
'blocks' => [
|
||||||
[
|
[
|
||||||
|
|
@ -119,11 +133,14 @@ private function sendToSlack(): void
|
||||||
/**
|
/**
|
||||||
* @todo v5 refactor: Extract this into a separate SendMessageToMattermostJob.php triggered via the "mattermost" notification channel type.
|
* @todo v5 refactor: Extract this into a separate SendMessageToMattermostJob.php triggered via the "mattermost" notification channel type.
|
||||||
*/
|
*/
|
||||||
private function sendToMattermost(): void
|
/**
|
||||||
|
* @param array<string, mixed> $httpOptions
|
||||||
|
*/
|
||||||
|
private function sendToMattermost(array $httpOptions): void
|
||||||
{
|
{
|
||||||
$username = config('app.name');
|
$username = config('app.name');
|
||||||
|
|
||||||
Http::withOptions(['allow_redirects' => false])->post($this->webhookUrl, [
|
Http::withOptions($httpOptions)->post($this->webhookUrl, [
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'attachments' => [
|
'attachments' => [
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -50,28 +50,24 @@ public function handle(): void
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
Log::warning('SendWebhookJob: blocked unsafe webhook URL', [
|
Log::warning('SendWebhookJob: blocked unsafe webhook URL', [
|
||||||
'url' => $this->webhookUrl,
|
'url' => SafeWebhookUrl::redactedUrlForLog($this->webhookUrl),
|
||||||
'errors' => $validator->errors()->all(),
|
'errors' => $validator->errors()->all(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDev()) {
|
try {
|
||||||
ray('Sending webhook notification', [
|
$httpOptions = SafeWebhookUrl::httpClientOptions($this->webhookUrl);
|
||||||
'url' => $this->webhookUrl,
|
} catch (\RuntimeException $e) {
|
||||||
'payload' => $this->payload,
|
Log::warning('SendWebhookJob: blocked unsafe webhook URL at send time', [
|
||||||
|
'url' => SafeWebhookUrl::redactedUrlForLog($this->webhookUrl),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = Http::withOptions(['allow_redirects' => false])->post($this->webhookUrl, $this->payload);
|
Http::withOptions($httpOptions)->post($this->webhookUrl, $this->payload);
|
||||||
|
|
||||||
if (isDev()) {
|
|
||||||
ray('Webhook response', [
|
|
||||||
'status' => $response->status(),
|
|
||||||
'body' => $response->body(),
|
|
||||||
'successful' => $response->successful(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,6 @@ private function checkHetznerStatus(): void
|
||||||
$this->server->update(['hetzner_server_status' => $status]);
|
$this->server->update(['hetzner_server_status' => $status]);
|
||||||
$this->server->hetzner_server_status = $status;
|
$this->server->hetzner_server_status = $status;
|
||||||
if ($status === 'off') {
|
if ($status === 'off') {
|
||||||
ray('Server is powered off, marking as unreachable');
|
|
||||||
throw new \Exception('Server is powered off');
|
throw new \Exception('Server is powered off');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,6 @@ private function loadSearchableItems()
|
||||||
$cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
|
$cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
|
||||||
|
|
||||||
$this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
|
$this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
|
||||||
ray()->showQueries();
|
|
||||||
$items = collect();
|
$items = collect();
|
||||||
$team = auth()->user()->currentTeam();
|
$team = auth()->user()->currentTeam();
|
||||||
|
|
||||||
|
|
@ -530,7 +529,6 @@ private function loadSearchableItems()
|
||||||
'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description.' server servers'),
|
'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description.' server servers'),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
ray($servers);
|
|
||||||
// Get all projects
|
// Get all projects
|
||||||
$projects = Project::ownedByCurrentTeam()
|
$projects = Project::ownedByCurrentTeam()
|
||||||
->withCount(['environments', 'applications', 'services'])
|
->withCount(['environments', 'applications', 'services'])
|
||||||
|
|
|
||||||
|
|
@ -168,13 +168,6 @@ public function saveModel()
|
||||||
$this->syncData(true);
|
$this->syncData(true);
|
||||||
refreshSession();
|
refreshSession();
|
||||||
|
|
||||||
if (isDev()) {
|
|
||||||
ray('Webhook settings saved', [
|
|
||||||
'webhook_enabled' => $this->settings->webhook_enabled,
|
|
||||||
'webhook_url' => $this->settings->webhook_url,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->dispatch('success', 'Settings saved.');
|
$this->dispatch('success', 'Settings saved.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,13 +176,6 @@ public function sendTestNotification()
|
||||||
try {
|
try {
|
||||||
$this->authorize('sendTest', $this->settings);
|
$this->authorize('sendTest', $this->settings);
|
||||||
|
|
||||||
if (isDev()) {
|
|
||||||
ray('Sending test webhook notification', [
|
|
||||||
'team_id' => $this->team->id,
|
|
||||||
'webhook_url' => $this->settings->webhook_url,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->team->notify(new Test(channel: 'webhook'));
|
$this->team->notify(new Test(channel: 'webhook'));
|
||||||
$this->dispatch('success', 'Test notification sent.');
|
$this->dispatch('success', 'Test notification sent.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\Features\SupportEvents\Event;
|
use Livewire\Features\SupportEvents\Event;
|
||||||
use Spatie\Url\Url;
|
|
||||||
|
|
||||||
class General extends Component
|
class General extends Component
|
||||||
{
|
{
|
||||||
|
|
@ -142,7 +141,8 @@ protected function rules(): array
|
||||||
return [
|
return [
|
||||||
'name' => ValidationPatterns::nameRules(),
|
'name' => ValidationPatterns::nameRules(),
|
||||||
'description' => ValidationPatterns::descriptionRules(),
|
'description' => ValidationPatterns::descriptionRules(),
|
||||||
'fqdn' => 'nullable',
|
'fqdn' => ValidationPatterns::applicationDomainRules(),
|
||||||
|
'parsedServiceDomains.*.domain' => ValidationPatterns::applicationDomainRules(),
|
||||||
'gitRepository' => 'required',
|
'gitRepository' => 'required',
|
||||||
'gitBranch' => ['required', 'string', new ValidGitBranch],
|
'gitBranch' => ['required', 'string', new ValidGitBranch],
|
||||||
'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
|
'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
|
||||||
|
|
@ -771,16 +771,7 @@ public function submit($showToaster = true)
|
||||||
$oldBaseDirectory = $this->application->base_directory;
|
$oldBaseDirectory = $this->application->base_directory;
|
||||||
|
|
||||||
// Process FQDN with intermediate variable to avoid Collection/string confusion
|
// Process FQDN with intermediate variable to avoid Collection/string confusion
|
||||||
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
$this->fqdn = ValidationPatterns::normalizeApplicationDomains($this->fqdn);
|
||||||
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
|
||||||
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
|
||||||
$domain = trim($domain);
|
|
||||||
Url::fromString($domain, ['http', 'https']);
|
|
||||||
|
|
||||||
return str($domain)->lower();
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->fqdn = $domains->unique()->implode(',');
|
|
||||||
$warning = sslipDomainWarning($this->fqdn);
|
$warning = sslipDomainWarning($this->fqdn);
|
||||||
if ($warning) {
|
if ($warning) {
|
||||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||||
|
|
@ -863,6 +854,9 @@ public function submit($showToaster = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($this->buildPack === 'dockercompose') {
|
if ($this->buildPack === 'dockercompose') {
|
||||||
|
foreach ($this->parsedServiceDomains as $serviceName => $service) {
|
||||||
|
$this->parsedServiceDomains[$serviceName]['domain'] = ValidationPatterns::normalizeApplicationDomains(data_get($service, 'domain'));
|
||||||
|
}
|
||||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||||
if ($this->application->isDirty('docker_compose_domains')) {
|
if ($this->application->isDirty('docker_compose_domains')) {
|
||||||
foreach ($this->parsedServiceDomains as $service) {
|
foreach ($this->parsedServiceDomains as $service) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Jobs\DeleteResourceJob;
|
use App\Jobs\DeleteResourceJob;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -117,12 +118,14 @@ public function save_preview($preview_id)
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($previewKey !== false && isset($this->previewFqdns[$previewKey])) {
|
if ($previewKey !== false && isset($this->previewFqdns[$previewKey])) {
|
||||||
|
$this->validate([
|
||||||
|
"previewFqdns.{$previewKey}" => ValidationPatterns::applicationDomainRules(),
|
||||||
|
]);
|
||||||
|
|
||||||
$fqdn = $this->previewFqdns[$previewKey];
|
$fqdn = $this->previewFqdns[$previewKey];
|
||||||
|
|
||||||
if (! empty($fqdn)) {
|
if (! empty($fqdn)) {
|
||||||
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
$fqdn = ValidationPatterns::normalizeApplicationDomains($fqdn);
|
||||||
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
|
||||||
$fqdn = str($fqdn)->trim()->lower();
|
|
||||||
$this->previewFqdns[$previewKey] = $fqdn;
|
$this->previewFqdns[$previewKey] = $fqdn;
|
||||||
|
|
||||||
if (! validateDNSEntry($fqdn, $this->application->destination->server)) {
|
if (! validateDNSEntry($fqdn, $this->application->destination->server)) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Livewire\Project\Application;
|
namespace App\Livewire\Project\Application;
|
||||||
|
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
|
|
@ -33,6 +34,11 @@ public function save()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->preview->application);
|
$this->authorize('update', $this->preview->application);
|
||||||
|
$this->validate([
|
||||||
|
'domain' => ValidationPatterns::applicationDomainRules(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->domain = ValidationPatterns::normalizeApplicationDomains($this->domain);
|
||||||
|
|
||||||
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
||||||
$docker_compose_domains = json_decode($docker_compose_domains, true) ?: [];
|
$docker_compose_domains = json_decode($docker_compose_domains, true) ?: [];
|
||||||
|
|
@ -73,9 +79,13 @@ public function generate()
|
||||||
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
||||||
$preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn;
|
$preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn;
|
||||||
} else {
|
} else {
|
||||||
|
foreach (ValidationPatterns::validateApplicationDomains($domain_string) as $error) {
|
||||||
|
throw new \InvalidArgumentException($error);
|
||||||
|
}
|
||||||
|
|
||||||
// Use the existing domain from the main application
|
// Use the existing domain from the main application
|
||||||
// Handle multiple domains separated by commas
|
// Handle multiple domains separated by commas
|
||||||
$domain_list = explode(',', $domain_string);
|
$domain_list = ValidationPatterns::applicationDomainList($domain_string);
|
||||||
$preview_fqdns = [];
|
$preview_fqdns = [];
|
||||||
$template = $this->preview->application->preview_url_template;
|
$template = $this->preview->application->preview_url_template;
|
||||||
$random = new_public_id();
|
$random = new_public_id();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
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 App\Rules\SafeWebhookUrl;
|
||||||
use App\Support\DatabaseBackupFileValidator;
|
use App\Support\DatabaseBackupFileValidator;
|
||||||
use App\Support\ValidationPatterns;
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
|
@ -598,6 +599,7 @@ public function checkS3File()
|
||||||
'bucket' => $s3Storage->bucket,
|
'bucket' => $s3Storage->bucket,
|
||||||
'endpoint' => $s3Storage->endpoint,
|
'endpoint' => $s3Storage->endpoint,
|
||||||
'use_path_style_endpoint' => true,
|
'use_path_style_endpoint' => true,
|
||||||
|
'http' => SafeWebhookUrl::httpClientOptions($s3Storage->endpoint),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
|
|
@ -678,7 +680,7 @@ public function restoreFromS3(string $password = ''): bool|string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get helper image
|
// Get helper image
|
||||||
$helperImage = config('constants.coolify.helper_image');
|
$helperImage = coolifyHelperImage();
|
||||||
$latestVersion = getHelperVersion();
|
$latestVersion = getHelperVersion();
|
||||||
$fullImageName = "{$helperImage}:{$latestVersion}";
|
$fullImageName = "{$helperImage}:{$latestVersion}";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,14 +112,17 @@ public function loadServices()
|
||||||
$default_logo = 'images/default.webp';
|
$default_logo = 'images/default.webp';
|
||||||
$logo = data_get($service, 'logo', $default_logo);
|
$logo = data_get($service, 'logo', $default_logo);
|
||||||
$local_logo_path = public_path($logo);
|
$local_logo_path = public_path($logo);
|
||||||
|
$serviceKey = (string) $key;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => str($key)->headline(),
|
'id' => $serviceKey,
|
||||||
|
'name' => str($serviceKey)->headline(),
|
||||||
|
'docsSlug' => str($serviceKey)->lower()->value(),
|
||||||
'logo' => asset($logo),
|
'logo' => asset($logo),
|
||||||
'logo_github_url' => file_exists($local_logo_path)
|
'logo_github_url' => file_exists($local_logo_path)
|
||||||
? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
|
? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
|
||||||
: asset($default_logo),
|
: asset($default_logo),
|
||||||
'templateLastUpdated' => $templateLastUpdatedMap[(string) $key] ?? null,
|
'templateLastUpdated' => $templateLastUpdatedMap[$serviceKey] ?? null,
|
||||||
] + (array) $service;
|
] + (array) $service;
|
||||||
})->all();
|
})->all();
|
||||||
|
|
||||||
|
|
@ -336,7 +339,10 @@ private function formatLastModified(string $path): ?string
|
||||||
|
|
||||||
public function setType(string $type)
|
public function setType(string $type)
|
||||||
{
|
{
|
||||||
$type = str($type)->lower()->slug()->value();
|
if (! str($type)->startsWith('one-click-service-')) {
|
||||||
|
$type = str($type)->lower()->slug()->value();
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->loading) {
|
if ($this->loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
namespace App\Livewire\Project\Service;
|
namespace App\Livewire\Project\Service;
|
||||||
|
|
||||||
use App\Models\ServiceApplication;
|
use App\Models\ServiceApplication;
|
||||||
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
|
||||||
|
|
||||||
class EditDomain extends Component
|
class EditDomain extends Component
|
||||||
{
|
{
|
||||||
|
|
@ -28,12 +28,15 @@ class EditDomain extends Component
|
||||||
|
|
||||||
public $requiredPort = null;
|
public $requiredPort = null;
|
||||||
|
|
||||||
#[Validate(['nullable'])]
|
#[Validate]
|
||||||
public ?string $fqdn = null;
|
public ?string $fqdn = null;
|
||||||
|
|
||||||
protected $rules = [
|
protected function rules(): array
|
||||||
'fqdn' => 'nullable',
|
{
|
||||||
];
|
return [
|
||||||
|
'fqdn' => ValidationPatterns::applicationDomainRules(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
|
@ -82,15 +85,9 @@ public function submit()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->application);
|
$this->authorize('update', $this->application);
|
||||||
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
$this->validate();
|
||||||
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
|
||||||
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
|
||||||
$domain = trim($domain);
|
|
||||||
Url::fromString($domain, ['http', 'https']);
|
|
||||||
|
|
||||||
return str($domain)->lower();
|
$this->fqdn = ValidationPatterns::normalizeApplicationDomains($this->fqdn);
|
||||||
});
|
|
||||||
$this->fqdn = $domains->unique()->implode(',');
|
|
||||||
$warning = sslipDomainWarning($this->fqdn);
|
$warning = sslipDomainWarning($this->fqdn);
|
||||||
if ($warning) {
|
if ($warning) {
|
||||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\ServiceApplication;
|
use App\Models\ServiceApplication;
|
||||||
use App\Models\ServiceDatabase;
|
use App\Models\ServiceDatabase;
|
||||||
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
|
||||||
|
|
||||||
class Index extends Component
|
class Index extends Component
|
||||||
{
|
{
|
||||||
|
|
@ -480,15 +480,11 @@ public function submitApplication()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->serviceApplication);
|
$this->authorize('update', $this->serviceApplication);
|
||||||
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
$this->validate([
|
||||||
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
'fqdn' => ValidationPatterns::applicationDomainRules(),
|
||||||
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
]);
|
||||||
$domain = trim($domain);
|
|
||||||
Url::fromString($domain, ['http', 'https']);
|
|
||||||
|
|
||||||
return str($domain)->lower();
|
$this->fqdn = ValidationPatterns::normalizeApplicationDomains($this->fqdn);
|
||||||
});
|
|
||||||
$this->fqdn = $domains->unique()->implode(',');
|
|
||||||
$warning = sslipDomainWarning($this->fqdn);
|
$warning = sslipDomainWarning($this->fqdn);
|
||||||
if ($warning) {
|
if ($warning) {
|
||||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,10 @@ public function getEnvironmentVariablesPreviewProperty()
|
||||||
|
|
||||||
private function getEnvironmentVariables(bool $isPreview, bool $withSearch = true): Collection
|
private function getEnvironmentVariables(bool $isPreview, bool $withSearch = true): Collection
|
||||||
{
|
{
|
||||||
|
if ($isPreview && ! $this->supportsPreviewEnvironmentVariables()) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
$query = $isPreview
|
$query = $isPreview
|
||||||
? $this->resource->environment_variables_preview()
|
? $this->resource->environment_variables_preview()
|
||||||
: $this->resource->environment_variables();
|
: $this->resource->environment_variables();
|
||||||
|
|
@ -119,12 +123,21 @@ private function searchTerm(): string
|
||||||
return trim($this->search);
|
return trim($this->search);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function supportsPreviewEnvironmentVariables(): bool
|
||||||
|
{
|
||||||
|
return $this->showPreview && $this->resource instanceof Application;
|
||||||
|
}
|
||||||
|
|
||||||
public function getHasEnvironmentVariablesProperty(): bool
|
public function getHasEnvironmentVariablesProperty(): bool
|
||||||
{
|
{
|
||||||
return $this->environmentVariables->isNotEmpty() ||
|
$hasPreviewEnvironmentVariables = $this->supportsPreviewEnvironmentVariables() && (
|
||||||
$this->environmentVariablesPreview->isNotEmpty() ||
|
$this->environmentVariablesPreview->isNotEmpty() ||
|
||||||
|
$this->hardcodedEnvironmentVariablesPreview->isNotEmpty()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->environmentVariables->isNotEmpty() ||
|
||||||
$this->hardcodedEnvironmentVariables->isNotEmpty() ||
|
$this->hardcodedEnvironmentVariables->isNotEmpty() ||
|
||||||
$this->hardcodedEnvironmentVariablesPreview->isNotEmpty();
|
$hasPreviewEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function nullLockedValues($envs)
|
private function nullLockedValues($envs)
|
||||||
|
|
@ -158,6 +171,10 @@ public function getHardcodedEnvironmentVariablesPreviewProperty()
|
||||||
|
|
||||||
protected function getHardcodedVariables(bool $isPreview)
|
protected function getHardcodedVariables(bool $isPreview)
|
||||||
{
|
{
|
||||||
|
if ($isPreview && ! $this->supportsPreviewEnvironmentVariables()) {
|
||||||
|
return collect([]);
|
||||||
|
}
|
||||||
|
|
||||||
// Only for services and docker-compose applications
|
// Only for services and docker-compose applications
|
||||||
if ($this->resource->type() !== 'service' &&
|
if ($this->resource->type() !== 'service' &&
|
||||||
($this->resourceClass !== 'App\Models\Application' ||
|
($this->resourceClass !== 'App\Models\Application' ||
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ private function getContainersForServer($server)
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Log error but don't fail the entire operation
|
// Log error but don't fail the entire operation
|
||||||
ray("Error loading containers for server {$server->name}: ".$e->getMessage());
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -537,6 +537,20 @@ public function loadHetznerTokens(): void
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function limaStartCommand(): ?string
|
||||||
|
{
|
||||||
|
if (! isDev()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($this->server->uuid) {
|
||||||
|
'lima-ubuntu-2404' => 'limactl start --yes --name=coolify-lima-ubuntu-2404 docker/lima/ubuntu-2404.yaml',
|
||||||
|
'lima-ubuntu-2604' => 'limactl start --yes --name=coolify-lima-ubuntu-2604 docker/lima/ubuntu-2604.yaml',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public function searchHetznerServer(): void
|
public function searchHetznerServer(): void
|
||||||
{
|
{
|
||||||
$this->hetznerSearchError = null;
|
$this->hetznerSearchError = null;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ class Advanced extends Component
|
||||||
#[Validate('boolean')]
|
#[Validate('boolean')]
|
||||||
public bool $is_mcp_server_enabled;
|
public bool $is_mcp_server_enabled;
|
||||||
|
|
||||||
|
public ?string $webhook_allowed_internal_hosts = null;
|
||||||
|
|
||||||
|
#[Validate('boolean')]
|
||||||
|
public bool $webhook_allow_localhost;
|
||||||
|
|
||||||
public function rules()
|
public function rules()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
@ -56,6 +61,8 @@ public function rules()
|
||||||
'disable_two_step_confirmation' => 'boolean',
|
'disable_two_step_confirmation' => 'boolean',
|
||||||
'is_wire_navigate_enabled' => 'boolean',
|
'is_wire_navigate_enabled' => 'boolean',
|
||||||
'is_mcp_server_enabled' => 'boolean',
|
'is_mcp_server_enabled' => 'boolean',
|
||||||
|
'webhook_allowed_internal_hosts' => 'nullable|string',
|
||||||
|
'webhook_allow_localhost' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +82,8 @@ public function mount()
|
||||||
$this->is_sponsorship_popup_enabled = $this->settings->is_sponsorship_popup_enabled;
|
$this->is_sponsorship_popup_enabled = $this->settings->is_sponsorship_popup_enabled;
|
||||||
$this->is_wire_navigate_enabled = $this->settings->is_wire_navigate_enabled ?? true;
|
$this->is_wire_navigate_enabled = $this->settings->is_wire_navigate_enabled ?? true;
|
||||||
$this->is_mcp_server_enabled = $this->settings->is_mcp_server_enabled ?? false;
|
$this->is_mcp_server_enabled = $this->settings->is_mcp_server_enabled ?? false;
|
||||||
|
$this->webhook_allowed_internal_hosts = collect($this->settings->webhook_allowed_internal_hosts ?? [])->implode(',');
|
||||||
|
$this->webhook_allow_localhost = $this->settings->webhook_allow_localhost ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function submit()
|
public function submit()
|
||||||
|
|
@ -141,13 +150,21 @@ public function submit()
|
||||||
$this->allowed_ips = implode(',', $validEntries);
|
$this->allowed_ips = implode(',', $validEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->instantSave();
|
$webhookAllowedInternalHosts = $this->normalizeWebhookAllowedInternalHosts();
|
||||||
|
if ($webhookAllowedInternalHosts === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->instantSave($webhookAllowedInternalHosts);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSave()
|
/**
|
||||||
|
* @param array<int, string>|null $webhookAllowedInternalHosts
|
||||||
|
*/
|
||||||
|
public function instantSave(?array $webhookAllowedInternalHosts = null)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->settings);
|
$this->authorize('update', $this->settings);
|
||||||
|
|
@ -161,6 +178,8 @@ public function instantSave()
|
||||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
|
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
|
||||||
$this->settings->is_wire_navigate_enabled = $this->is_wire_navigate_enabled;
|
$this->settings->is_wire_navigate_enabled = $this->is_wire_navigate_enabled;
|
||||||
$this->settings->is_mcp_server_enabled = $this->is_mcp_server_enabled;
|
$this->settings->is_mcp_server_enabled = $this->is_mcp_server_enabled;
|
||||||
|
$this->settings->webhook_allowed_internal_hosts = $webhookAllowedInternalHosts ?? $this->settings->webhook_allowed_internal_hosts ?? [];
|
||||||
|
$this->settings->webhook_allow_localhost = $this->webhook_allow_localhost;
|
||||||
$this->settings->save();
|
$this->settings->save();
|
||||||
$this->dispatch('success', 'Settings updated!');
|
$this->dispatch('success', 'Settings updated!');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
@ -168,6 +187,49 @@ public function instantSave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>|false
|
||||||
|
*/
|
||||||
|
private function normalizeWebhookAllowedInternalHosts(): array|false
|
||||||
|
{
|
||||||
|
$entries = collect(preg_split('/[,\r\n]+/', $this->webhook_allowed_internal_hosts ?? '') ?: [])
|
||||||
|
->map(fn (string $entry): string => rtrim(strtolower(trim($entry)), '.'))
|
||||||
|
->filter()
|
||||||
|
->unique()
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$invalidEntries = $entries->reject(fn (string $entry): bool => $this->isValidWebhookAllowlistEntry($entry));
|
||||||
|
if ($invalidEntries->isNotEmpty()) {
|
||||||
|
$this->dispatch('error', 'Invalid webhook internal allowlist entries: '.$invalidEntries->implode(', '));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->webhook_allowed_internal_hosts = $entries->implode(',');
|
||||||
|
|
||||||
|
return $entries->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidWebhookAllowlistEntry(string $entry): bool
|
||||||
|
{
|
||||||
|
if (filter_var($entry, FILTER_VALIDATE_IP)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($entry, '/')) {
|
||||||
|
[$ip, $mask] = array_pad(explode('/', $entry, 2), 2, null);
|
||||||
|
$isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||||
|
$maxMask = $isIpv6 ? 128 : 32;
|
||||||
|
|
||||||
|
return filter_var($ip, FILTER_VALIDATE_IP) !== false
|
||||||
|
&& is_numeric($mask)
|
||||||
|
&& (int) $mask >= 0
|
||||||
|
&& (int) $mask <= $maxMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter_var($entry, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
public function toggleRegistration($password): bool
|
public function toggleRegistration($password): bool
|
||||||
{
|
{
|
||||||
if (! verifyPasswordConfirmation($password, $this)) {
|
if (! verifyPasswordConfirmation($password, $this)) {
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,6 @@ class Index extends Component
|
||||||
|
|
||||||
public bool $forceSaveDomains = false;
|
public bool $forceSaveDomains = false;
|
||||||
|
|
||||||
public $buildActivityId = null;
|
|
||||||
|
|
||||||
protected array $messages = [
|
protected array $messages = [
|
||||||
'fqdn.url' => 'Invalid instance URL.',
|
'fqdn.url' => 'Invalid instance URL.',
|
||||||
'fqdn.max' => 'URL must not exceed 255 characters.',
|
'fqdn.max' => 'URL must not exceed 255 characters.',
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
|
|
@ -26,6 +28,9 @@ class Updates extends Component
|
||||||
#[Validate('boolean')]
|
#[Validate('boolean')]
|
||||||
public bool $is_auto_update_enabled;
|
public bool $is_auto_update_enabled;
|
||||||
|
|
||||||
|
#[Validate('required|string|in:docker.io,ghcr.io')]
|
||||||
|
public string $docker_registry_url;
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (! isInstanceAdmin()) {
|
if (! isInstanceAdmin()) {
|
||||||
|
|
@ -39,6 +44,7 @@ public function mount()
|
||||||
$this->auto_update_frequency = $this->settings->auto_update_frequency;
|
$this->auto_update_frequency = $this->settings->auto_update_frequency;
|
||||||
$this->update_check_frequency = $this->settings->update_check_frequency;
|
$this->update_check_frequency = $this->settings->update_check_frequency;
|
||||||
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
|
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
|
||||||
|
$this->docker_registry_url = $this->settings->docker_registry_url ?: 'docker.io';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
|
|
@ -50,16 +56,51 @@ public function instantSave()
|
||||||
'auto_update_frequency' => ['required', 'string'],
|
'auto_update_frequency' => ['required', 'string'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
$validated = $this->validate([
|
||||||
|
'docker_registry_url' => ['required', 'string', 'in:docker.io,ghcr.io'],
|
||||||
|
]);
|
||||||
$this->settings->auto_update_frequency = $this->auto_update_frequency;
|
$this->settings->auto_update_frequency = $this->auto_update_frequency;
|
||||||
$this->settings->update_check_frequency = $this->update_check_frequency;
|
$this->settings->update_check_frequency = $this->update_check_frequency;
|
||||||
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
|
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
|
||||||
|
$this->settings->docker_registry_url = $validated['docker_registry_url'];
|
||||||
|
$this->syncRegistryUrlToEnv($validated['docker_registry_url']);
|
||||||
$this->settings->save();
|
$this->settings->save();
|
||||||
$this->dispatch('success', 'Settings updated!');
|
$this->dispatch('success', 'Settings updated!');
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
throw $e;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function syncRegistryUrlToEnv(string $registryUrl): void
|
||||||
|
{
|
||||||
|
if (! $this->server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
instant_remote_process([
|
||||||
|
$this->registryEnvSyncCommand($registryUrl),
|
||||||
|
], $this->server);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::warning('Failed to sync REGISTRY_URL to .env', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new \RuntimeException('Failed to sync REGISTRY_URL to .env. Settings were not saved.', previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function registryEnvSyncCommand(string $registryUrl): string
|
||||||
|
{
|
||||||
|
$envFile = '/data/coolify/source/.env';
|
||||||
|
$sedExpression = escapeshellarg("s|^REGISTRY_URL=.*|REGISTRY_URL={$registryUrl}|");
|
||||||
|
$registryLine = escapeshellarg("REGISTRY_URL={$registryUrl}");
|
||||||
|
|
||||||
|
return "if grep -q '^REGISTRY_URL=' {$envFile}; then sed -i {$sedExpression} {$envFile}; else printf '%s\\n' {$registryLine} >> {$envFile}; fi";
|
||||||
|
}
|
||||||
|
|
||||||
public function submit()
|
public function submit()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -89,6 +130,8 @@ public function submit()
|
||||||
if ($this->server) {
|
if ($this->server) {
|
||||||
$this->server->setupDynamicProxyConfiguration();
|
$this->server->setupDynamicProxyConfiguration();
|
||||||
}
|
}
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
throw $e;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,8 @@
|
||||||
use App\Rules\SafeExternalUrl;
|
use App\Rules\SafeExternalUrl;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Lcobucci\JWT\Configuration;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
|
||||||
use Lcobucci\JWT\Signer\Rsa\Sha256;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Change extends Component
|
class Change extends Component
|
||||||
|
|
@ -82,11 +79,13 @@ class Change extends Component
|
||||||
|
|
||||||
public string $activeTab = 'general';
|
public string $activeTab = 'general';
|
||||||
|
|
||||||
|
private bool $shouldDeriveApiUrlAfterHtmlUrlUpdate = false;
|
||||||
|
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'required|string',
|
'name' => 'required|string',
|
||||||
'organization' => 'nullable|string',
|
'organization' => ['nullable', 'string', 'regex:/\A[^\s\/?#]+\z/'],
|
||||||
'apiUrl' => ['required', 'string', 'url', new SafeExternalUrl],
|
'apiUrl' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||||
'htmlUrl' => ['required', 'string', 'url', new SafeExternalUrl],
|
'htmlUrl' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||||
'customUser' => 'required|string',
|
'customUser' => 'required|string',
|
||||||
|
|
@ -107,6 +106,19 @@ protected function rules(): array
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatingHtmlUrl(): void
|
||||||
|
{
|
||||||
|
$this->shouldDeriveApiUrlAfterHtmlUrlUpdate = blank($this->apiUrl)
|
||||||
|
|| $this->apiUrl === githubApiUrlFromHtmlUrl($this->htmlUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedHtmlUrl(): void
|
||||||
|
{
|
||||||
|
if ($this->shouldDeriveApiUrlAfterHtmlUrlUpdate) {
|
||||||
|
$this->apiUrl = githubApiUrlFromHtmlUrl($this->htmlUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
if ($this->github_app) {
|
if ($this->github_app) {
|
||||||
|
|
@ -123,6 +135,11 @@ private function syncData(bool $toModel = false): void
|
||||||
{
|
{
|
||||||
if ($toModel) {
|
if ($toModel) {
|
||||||
// Sync TO model (before save)
|
// Sync TO model (before save)
|
||||||
|
$this->organization = normalizeGithubOrganization($this->organization);
|
||||||
|
$this->apiUrl = filled($this->apiUrl)
|
||||||
|
? $this->apiUrl
|
||||||
|
: githubApiUrlFromHtmlUrl($this->htmlUrl);
|
||||||
|
|
||||||
$this->github_app->name = $this->name;
|
$this->github_app->name = $this->name;
|
||||||
$this->github_app->organization = $this->organization;
|
$this->github_app->organization = $this->organization;
|
||||||
$this->github_app->api_url = $this->apiUrl;
|
$this->github_app->api_url = $this->apiUrl;
|
||||||
|
|
@ -208,6 +225,8 @@ public function checkPermissions()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncGithubAppName($this->github_app);
|
||||||
|
|
||||||
GithubAppPermissionJob::dispatchSync($this->github_app);
|
GithubAppPermissionJob::dispatchSync($this->github_app);
|
||||||
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
|
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||||
$this->syncData(false);
|
$this->syncData(false);
|
||||||
|
|
@ -296,31 +315,14 @@ public function mount()
|
||||||
|
|
||||||
public function getGithubAppNameUpdatePath()
|
public function getGithubAppNameUpdatePath()
|
||||||
{
|
{
|
||||||
if (str($this->github_app->organization)->isNotEmpty()) {
|
$name = encodeGithubPathSegment($this->github_app->name);
|
||||||
return "{$this->github_app->html_url}/organizations/{$this->github_app->organization}/settings/apps/{$this->github_app->name}";
|
$organization = normalizeGithubOrganization($this->github_app->organization);
|
||||||
|
|
||||||
|
if (filled($organization)) {
|
||||||
|
return rtrim($this->github_app->html_url, '/').'/organizations/'.encodeGithubPathSegment($organization)."/settings/apps/{$name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}";
|
return rtrim($this->github_app->html_url, '/')."/settings/apps/{$name}";
|
||||||
}
|
|
||||||
|
|
||||||
private function generateGithubJwt($private_key, $app_id): string
|
|
||||||
{
|
|
||||||
$configuration = Configuration::forAsymmetricSigner(
|
|
||||||
new Sha256,
|
|
||||||
InMemory::plainText($private_key),
|
|
||||||
InMemory::plainText($private_key)
|
|
||||||
);
|
|
||||||
|
|
||||||
$now = time();
|
|
||||||
|
|
||||||
return $configuration->builder()
|
|
||||||
->issuedBy((string) $app_id)
|
|
||||||
->permittedFor('https://api.github.com')
|
|
||||||
->identifiedBy((string) $now)
|
|
||||||
->issuedAt(new \DateTimeImmutable("@{$now}"))
|
|
||||||
->expiresAt(new \DateTimeImmutable('@'.($now + 600)))
|
|
||||||
->getToken($configuration->signer(), $configuration->signingKey())
|
|
||||||
->toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateGithubAppName()
|
public function updateGithubAppName()
|
||||||
|
|
@ -328,39 +330,29 @@ public function updateGithubAppName()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->github_app);
|
$this->authorize('update', $this->github_app);
|
||||||
|
|
||||||
$privateKey = PrivateKey::ownedByCurrentTeam()->find($this->github_app->private_key_id);
|
$this->github_app->app_id = $this->appId;
|
||||||
|
$this->github_app->private_key_id = $this->privateKeyId;
|
||||||
|
$this->github_app->unsetRelation('privateKey');
|
||||||
|
|
||||||
if (! $privateKey) {
|
if (! $this->appId) {
|
||||||
|
$this->dispatch('error', 'App ID is required before synchronizing the GitHub App name.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! PrivateKey::ownedByCurrentTeam()->find($this->privateKeyId)) {
|
||||||
$this->dispatch('error', 'No private key found for this GitHub App.');
|
$this->dispatch('error', 'No private key found for this GitHub App.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$jwt = $this->generateGithubJwt($privateKey->private_key, $this->github_app->app_id);
|
$appSlug = syncGithubAppName($this->github_app, true);
|
||||||
|
|
||||||
$response = Http::withHeaders([
|
if ($appSlug) {
|
||||||
'Accept' => 'application/vnd.github+json',
|
$this->name = str($appSlug)->kebab();
|
||||||
'X-GitHub-Api-Version' => '2022-11-28',
|
$this->dispatch('success', 'GitHub App name and private key name synchronized successfully.');
|
||||||
'Authorization' => "Bearer {$jwt}",
|
|
||||||
])->get("{$this->github_app->api_url}/app");
|
|
||||||
|
|
||||||
if ($response->successful()) {
|
|
||||||
$app_data = $response->json();
|
|
||||||
$app_slug = $app_data['slug'] ?? null;
|
|
||||||
|
|
||||||
if ($app_slug) {
|
|
||||||
$this->github_app->name = $app_slug;
|
|
||||||
$this->name = str($app_slug)->kebab();
|
|
||||||
$privateKey->name = "github-app-{$app_slug}";
|
|
||||||
$privateKey->save();
|
|
||||||
$this->github_app->save();
|
|
||||||
$this->dispatch('success', 'GitHub App name and SSH key name synchronized successfully.');
|
|
||||||
} else {
|
|
||||||
$this->dispatch('info', 'Could not find App Name (slug) in GitHub response.');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$error_message = $response->json()['message'] ?? 'Unknown error';
|
$this->dispatch('info', 'Could not find App Name (slug) in GitHub response.');
|
||||||
$this->dispatch('error', "Failed to fetch GitHub App information: {$error_message}");
|
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|
@ -373,11 +365,17 @@ public function submit()
|
||||||
$this->authorize('update', $this->github_app);
|
$this->authorize('update', $this->github_app);
|
||||||
|
|
||||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||||
|
$this->organization = normalizeGithubOrganization($this->organization);
|
||||||
|
$this->apiUrl = filled($this->apiUrl)
|
||||||
|
? $this->apiUrl
|
||||||
|
: githubApiUrlFromHtmlUrl($this->htmlUrl);
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
$this->syncData(true);
|
$this->syncData(true);
|
||||||
$this->github_app->save();
|
$this->github_app->save();
|
||||||
$this->dispatch('success', 'Github App updated.');
|
$this->dispatch('success', 'Github App updated.');
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
throw $e;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Models\GithubApp;
|
use App\Models\GithubApp;
|
||||||
use App\Rules\SafeExternalUrl;
|
use App\Rules\SafeExternalUrl;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Create extends Component
|
class Create extends Component
|
||||||
|
|
@ -25,19 +26,39 @@ class Create extends Component
|
||||||
|
|
||||||
public bool $is_system_wide = false;
|
public bool $is_system_wide = false;
|
||||||
|
|
||||||
|
private bool $shouldDeriveApiUrlAfterHtmlUrlUpdate = false;
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->name = substr(generate_random_name(), 0, 30);
|
$this->name = substr(generate_random_name(), 0, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatingHtmlUrl(): void
|
||||||
|
{
|
||||||
|
$this->shouldDeriveApiUrlAfterHtmlUrlUpdate = blank($this->api_url)
|
||||||
|
|| $this->api_url === githubApiUrlFromHtmlUrl($this->html_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedHtmlUrl(): void
|
||||||
|
{
|
||||||
|
if ($this->shouldDeriveApiUrlAfterHtmlUrlUpdate) {
|
||||||
|
$this->api_url = githubApiUrlFromHtmlUrl($this->html_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function createGitHubApp()
|
public function createGitHubApp()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('createAnyResource');
|
$this->authorize('createAnyResource');
|
||||||
|
|
||||||
|
$this->organization = normalizeGithubOrganization($this->organization);
|
||||||
|
$this->api_url = filled($this->api_url)
|
||||||
|
? $this->api_url
|
||||||
|
: githubApiUrlFromHtmlUrl($this->html_url);
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'name' => 'required|string',
|
'name' => 'required|string',
|
||||||
'organization' => 'nullable|string',
|
'organization' => ['nullable', 'string', 'regex:/\A[^\s\/?#]+\z/'],
|
||||||
'api_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
'api_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||||
'html_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
'html_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||||
'custom_user' => 'required|string',
|
'custom_user' => 'required|string',
|
||||||
|
|
@ -60,6 +81,8 @@ public function createGitHubApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirectRoute($this, 'source.github.show', ['github_app_uuid' => $github_app->uuid]);
|
return redirectRoute($this, 'source.github.show', ['github_app_uuid' => $github_app->uuid]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
throw $e;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,16 @@ public function viaLink()
|
||||||
$this->generateInviteLink(sendEmail: false);
|
$this->generateInviteLink(sendEmail: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function invitationUrl(string $routeName, array $parameters): string
|
||||||
|
{
|
||||||
|
$fqdn = instanceSettings()->fqdn;
|
||||||
|
if (filled($fqdn)) {
|
||||||
|
return rtrim($fqdn, '/').route($routeName, $parameters, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return route($routeName, $parameters);
|
||||||
|
}
|
||||||
|
|
||||||
private function generateInviteLink(bool $sendEmail = false)
|
private function generateInviteLink(bool $sendEmail = false)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -61,7 +71,7 @@ private function generateInviteLink(bool $sendEmail = false)
|
||||||
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
|
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
|
||||||
}
|
}
|
||||||
$uuid = new_public_id(32);
|
$uuid = new_public_id(32);
|
||||||
$link = url('/').config('constants.invitation.link.base_url').$uuid;
|
$link = $this->invitationUrl('team.invitation.show', ['uuid' => $uuid]);
|
||||||
$user = User::whereEmail($this->email)->first();
|
$user = User::whereEmail($this->email)->first();
|
||||||
|
|
||||||
if (is_null($user)) {
|
if (is_null($user)) {
|
||||||
|
|
@ -73,7 +83,7 @@ private function generateInviteLink(bool $sendEmail = false)
|
||||||
'force_password_reset' => true,
|
'force_password_reset' => true,
|
||||||
]);
|
]);
|
||||||
$token = Crypt::encryptString("{$user->email}@@@{$uuid}@@@{$password}");
|
$token = Crypt::encryptString("{$user->email}@@@{$uuid}@@@{$password}");
|
||||||
$link = route('auth.link', ['token' => $token]);
|
$link = $this->invitationUrl('auth.link', ['token' => $token]);
|
||||||
}
|
}
|
||||||
$invitation = TeamInvitation::whereEmail($this->email)->first();
|
$invitation = TeamInvitation::whereEmail($this->email)->first();
|
||||||
if (! is_null($invitation)) {
|
if (! is_null($invitation)) {
|
||||||
|
|
|
||||||
|
|
@ -366,7 +366,7 @@ private function get_environment_variables(?string $environment_variable = null)
|
||||||
|
|
||||||
private function set_environment_variables(?string $environment_variable = null): ?string
|
private function set_environment_variables(?string $environment_variable = null): ?string
|
||||||
{
|
{
|
||||||
if (is_null($environment_variable) && $environment_variable === '') {
|
if (is_null($environment_variable)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$environment_variable = trim($environment_variable);
|
$environment_variable = trim($environment_variable);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ class InstanceSettings extends Model
|
||||||
'dev_helper_version',
|
'dev_helper_version',
|
||||||
'is_wire_navigate_enabled',
|
'is_wire_navigate_enabled',
|
||||||
'is_mcp_server_enabled',
|
'is_mcp_server_enabled',
|
||||||
|
'webhook_allowed_internal_hosts',
|
||||||
|
'webhook_allow_localhost',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
|
|
@ -80,10 +82,16 @@ class InstanceSettings extends Model
|
||||||
'sentinel_token' => 'encrypted',
|
'sentinel_token' => 'encrypted',
|
||||||
'is_wire_navigate_enabled' => 'boolean',
|
'is_wire_navigate_enabled' => 'boolean',
|
||||||
'is_mcp_server_enabled' => 'boolean',
|
'is_mcp_server_enabled' => 'boolean',
|
||||||
|
'webhook_allowed_internal_hosts' => 'array',
|
||||||
|
'webhook_allow_localhost' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
|
static::created(function () {
|
||||||
|
Once::flush();
|
||||||
|
});
|
||||||
|
|
||||||
static::updated(function ($settings) {
|
static::updated(function ($settings) {
|
||||||
// Clear once() cache so subsequent calls get fresh data
|
// Clear once() cache so subsequent calls get fresh data
|
||||||
Once::flush();
|
Once::flush();
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,6 @@ public function isReadOnlyVolume(): bool
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
ray($e->getMessage(), 'Error checking read-only volume');
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,6 @@ public function isReadOnlyVolume(): bool
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
ray($e->getMessage(), 'Error checking read-only persistent volume');
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,11 +178,10 @@ public function testConnection(bool $shouldSave = false)
|
||||||
'bucket' => $this['bucket'],
|
'bucket' => $this['bucket'],
|
||||||
'endpoint' => $this['endpoint'],
|
'endpoint' => $this['endpoint'],
|
||||||
'use_path_style_endpoint' => true,
|
'use_path_style_endpoint' => true,
|
||||||
'http' => [
|
'http' => array_merge(SafeWebhookUrl::httpClientOptions($this['endpoint']), [
|
||||||
'connect_timeout' => self::CONNECTION_TIMEOUT_SECONDS,
|
'connect_timeout' => self::CONNECTION_TIMEOUT_SECONDS,
|
||||||
'timeout' => self::REQUEST_TIMEOUT_SECONDS,
|
'timeout' => self::REQUEST_TIMEOUT_SECONDS,
|
||||||
'allow_redirects' => false,
|
]),
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
// Test the connection by listing files with ListObjectsV2 (S3)
|
// Test the connection by listing files with ListObjectsV2 (S3)
|
||||||
$disk->files();
|
$disk->files();
|
||||||
|
|
|
||||||
|
|
@ -1533,7 +1533,6 @@ private function disableSshMux(): void
|
||||||
public function generateCaCertificate()
|
public function generateCaCertificate()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
ray('Generating CA certificate for server', $this->id);
|
|
||||||
SslHelper::generateSslCertificate(
|
SslHelper::generateSslCertificate(
|
||||||
commonName: 'Coolify CA Certificate',
|
commonName: 'Coolify CA Certificate',
|
||||||
serverId: $this->id,
|
serverId: $this->id,
|
||||||
|
|
@ -1541,7 +1540,6 @@ public function generateCaCertificate()
|
||||||
validityDays: 10 * 365
|
validityDays: 10 * 365
|
||||||
);
|
);
|
||||||
$caCertificate = $this->sslCertificates()->where('is_ca_certificate', true)->first();
|
$caCertificate = $this->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||||
ray('CA certificate generated', $caCertificate);
|
|
||||||
if ($caCertificate) {
|
if ($caCertificate) {
|
||||||
$certificateContent = $caCertificate->ssl_certificate;
|
$certificateContent = $caCertificate->ssl_certificate;
|
||||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
|
||||||
|
|
@ -1594,7 +1594,6 @@ public function saveComposeConfigs()
|
||||||
$envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName);
|
$envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
ray($e->getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,23 +15,11 @@ public function send($notifiable, Notification $notification): void
|
||||||
$webhookSettings = $notifiable->webhookNotificationSettings;
|
$webhookSettings = $notifiable->webhookNotificationSettings;
|
||||||
|
|
||||||
if (! $webhookSettings || ! $webhookSettings->isEnabled() || ! $webhookSettings->webhook_url) {
|
if (! $webhookSettings || ! $webhookSettings->isEnabled() || ! $webhookSettings->webhook_url) {
|
||||||
if (isDev()) {
|
|
||||||
ray('Webhook notification skipped - not enabled or no URL configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = $notification->toWebhook();
|
$payload = $notification->toWebhook();
|
||||||
|
|
||||||
if (isDev()) {
|
|
||||||
ray('Dispatching webhook notification', [
|
|
||||||
'notification' => get_class($notification),
|
|
||||||
'url' => $webhookSettings->webhook_url,
|
|
||||||
'payload' => $payload,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
SendWebhookJob::dispatch($payload, $webhookSettings->webhook_url);
|
SendWebhookJob::dispatch($payload, $webhookSettings->webhook_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ public function __construct(public int $hetznerServerId, public int $teamId, pub
|
||||||
|
|
||||||
public function via(object $notifiable): array
|
public function via(object $notifiable): array
|
||||||
{
|
{
|
||||||
ray('hello');
|
|
||||||
ray($notifiable);
|
|
||||||
|
|
||||||
return $notifiable->getEnabledChannels('hetzner_deletion_failed');
|
return $notifiable->getEnabledChannels('hetzner_deletion_failed');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,13 @@
|
||||||
|
|
||||||
namespace App\Rules;
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use PurplePixie\PhpDns\DNSQuery;
|
||||||
|
use PurplePixie\PhpDns\DNSTypes;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class SafeWebhookUrl implements ValidationRule
|
class SafeWebhookUrl implements ValidationRule
|
||||||
{
|
{
|
||||||
|
|
@ -18,8 +22,8 @@ public function __construct(private ?Closure $resolver = null) {}
|
||||||
*
|
*
|
||||||
* Validates that a webhook URL is safe for server-side requests.
|
* Validates that a webhook URL is safe for server-side requests.
|
||||||
* Blocks loopback addresses, cloud metadata endpoints (link-local),
|
* Blocks loopback addresses, cloud metadata endpoints (link-local),
|
||||||
* and dangerous hostnames while allowing private network IPs
|
* private/reserved ranges, and dangerous hostnames unless the
|
||||||
* for self-hosted deployments.
|
* instance operator explicitly allowlists the intranet target.
|
||||||
*/
|
*/
|
||||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
{
|
{
|
||||||
|
|
@ -43,12 +47,17 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (str_ends_with($host, '.')) {
|
||||||
|
$fail('The :attribute host must not end with a trailing dot.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$host = strtolower($host);
|
$host = strtolower($host);
|
||||||
$hostForIpCheck = $this->normalizeHostForIpCheck($host);
|
$hostForIpCheck = $this->normalizeHostForIpCheck($host);
|
||||||
$hostForDns = rtrim($hostForIpCheck, '.');
|
$hostForDns = rtrim($hostForIpCheck, '.');
|
||||||
|
|
||||||
$blockedHosts = ['localhost', '0.0.0.0', '::1'];
|
if ($this->isBlockedHostname($hostForDns) && ! $this->isAllowedHostname($hostForDns)) {
|
||||||
if (in_array($hostForDns, $blockedHosts, true) || str_ends_with($hostForDns, '.internal')) {
|
|
||||||
$this->logBlockedHost($attribute, $host);
|
$this->logBlockedHost($attribute, $host);
|
||||||
$fail('The :attribute must not point to localhost or internal hosts.');
|
$fail('The :attribute must not point to localhost or internal hosts.');
|
||||||
|
|
||||||
|
|
@ -56,9 +65,9 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter_var($hostForIpCheck, FILTER_VALIDATE_IP)) {
|
if (filter_var($hostForIpCheck, FILTER_VALIDATE_IP)) {
|
||||||
if ($this->isBlockedIp($hostForIpCheck)) {
|
if (! $this->isAllowedIp($hostForIpCheck, $hostForDns)) {
|
||||||
$this->logBlockedIp($attribute, $host, $hostForIpCheck);
|
$this->logBlockedIp($attribute, $host, $hostForIpCheck);
|
||||||
$fail('The :attribute must not point to loopback or link-local addresses.');
|
$fail('The :attribute must not point to private, reserved, loopback, or link-local addresses.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -67,16 +76,124 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$resolvedIps = $this->resolveHost($hostForDns);
|
$resolvedIps = $this->resolveHost($hostForDns);
|
||||||
|
if ($resolvedIps === []) {
|
||||||
|
$fail('The :attribute host could not be resolved.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($resolvedIps as $resolvedIp) {
|
foreach ($resolvedIps as $resolvedIp) {
|
||||||
if ($this->isBlockedIp($resolvedIp)) {
|
if (! $this->isAllowedIp($resolvedIp, $hostForDns)) {
|
||||||
$this->logBlockedIp($attribute, $host, $resolvedIp);
|
$this->logBlockedIp($attribute, $host, $resolvedIp);
|
||||||
$fail('The :attribute must not point to loopback or link-local addresses.');
|
$fail('The :attribute must not point to private, reserved, loopback, or link-local addresses.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build HTTP client options that pin the validated host to the resolved IPs.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function httpClientOptions(string $url): array
|
||||||
|
{
|
||||||
|
$options = ['allow_redirects' => false];
|
||||||
|
|
||||||
|
if (! defined('CURLOPT_RESOLVE')) {
|
||||||
|
throw new \RuntimeException('Webhook URL DNS pinning is unavailable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = self::resolveUrlForRequest($url);
|
||||||
|
|
||||||
|
if ($target['ips'] === [] || filter_var($target['host'], FILTER_VALIDATE_IP)) {
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
$options['curl'] = [
|
||||||
|
CURLOPT_RESOLVE => array_map(
|
||||||
|
fn (string $ip): string => sprintf('%s:%d:%s', $target['host'], $target['port'], filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '['.$ip.']' : $ip),
|
||||||
|
$target['ips'],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build mc --resolve mappings that pin the endpoint host for S3 backups.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function minioClientResolveOptions(string $url): array
|
||||||
|
{
|
||||||
|
$target = self::resolveUrlForRequest($url);
|
||||||
|
|
||||||
|
if ($target['ips'] === [] || filter_var($target['host'], FILTER_VALIDATE_IP)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
fn (string $ip): string => sprintf('%s:%d=%s', $target['host'], $target['port'], filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '['.$ip.']' : $ip),
|
||||||
|
$target['ips'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function redactedUrlForLog(string $url): string
|
||||||
|
{
|
||||||
|
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||||
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
$port = parse_url($url, PHP_URL_PORT);
|
||||||
|
|
||||||
|
if (! is_string($scheme) || ! is_string($host)) {
|
||||||
|
return '[invalid-url]';
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower($scheme).'://'.strtolower($host).($port ? ':'.$port : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{host: string, port: int, ips: array<int, string>}
|
||||||
|
*/
|
||||||
|
private static function resolveUrlForRequest(string $url): array
|
||||||
|
{
|
||||||
|
$rule = new self;
|
||||||
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
if (! is_string($host) || $host === '') {
|
||||||
|
throw new \RuntimeException('Webhook URL host could not be resolved.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_ends_with($host, '.')) {
|
||||||
|
throw new \RuntimeException('Webhook URL host must not end with a trailing dot.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = strtolower(parse_url($url, PHP_URL_SCHEME) ?? '');
|
||||||
|
$port = parse_url($url, PHP_URL_PORT) ?: ($scheme === 'https' ? 443 : 80);
|
||||||
|
$hostForDns = rtrim($rule->normalizeHostForIpCheck(strtolower($host)), '.');
|
||||||
|
|
||||||
|
if (filter_var($hostForDns, FILTER_VALIDATE_IP)) {
|
||||||
|
if (! $rule->isAllowedIp($hostForDns, $hostForDns)) {
|
||||||
|
throw new \RuntimeException('Webhook URL resolved to an unsafe IP address.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['host' => $hostForDns, 'port' => $port, 'ips' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedIps = $rule->resolveHost($hostForDns);
|
||||||
|
if ($resolvedIps === []) {
|
||||||
|
throw new \RuntimeException('Webhook URL host could not be resolved.');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($resolvedIps as $resolvedIp) {
|
||||||
|
if (! $rule->isAllowedIp($resolvedIp, $hostForDns)) {
|
||||||
|
throw new \RuntimeException('Webhook URL resolved to an unsafe IP address.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['host' => $hostForDns, 'port' => $port, 'ips' => $resolvedIps];
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeHostForIpCheck(string $host): string
|
private function normalizeHostForIpCheck(string $host): string
|
||||||
{
|
{
|
||||||
return (str_starts_with($host, '[') && str_ends_with($host, ']'))
|
return (str_starts_with($host, '[') && str_ends_with($host, ']'))
|
||||||
|
|
@ -93,6 +210,15 @@ private function resolveHost(string $host): array
|
||||||
return array_values(array_filter(($this->resolver)($host), fn (string $ip): bool => filter_var($ip, FILTER_VALIDATE_IP) !== false));
|
return array_values(array_filter(($this->resolver)($host), fn (string $ip): bool => filter_var($ip, FILTER_VALIDATE_IP) !== false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($host === 'localhost') {
|
||||||
|
return ['127.0.0.1', '::1'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$customDnsServers = $this->customDnsServers();
|
||||||
|
if ($customDnsServers !== []) {
|
||||||
|
return $this->resolveHostWithCustomDnsServers($host, $customDnsServers);
|
||||||
|
}
|
||||||
|
|
||||||
$records = @dns_get_record($host, DNS_A | DNS_AAAA);
|
$records = @dns_get_record($host, DNS_A | DNS_AAAA);
|
||||||
if ($records === false) {
|
if ($records === false) {
|
||||||
$records = [];
|
$records = [];
|
||||||
|
|
@ -119,60 +245,320 @@ private function resolveHost(string $host): array
|
||||||
return array_values(array_unique($ips));
|
return array_values(array_unique($ips));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isBlockedIp(string $ip): bool
|
/**
|
||||||
|
* @param array<int, string> $dnsServers
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function resolveHostWithCustomDnsServers(string $host, array $dnsServers): array
|
||||||
|
{
|
||||||
|
$ips = [];
|
||||||
|
|
||||||
|
foreach ($dnsServers as $dnsServer) {
|
||||||
|
foreach ([DNSTypes::NAME_A, DNSTypes::NAME_AAAA] as $type) {
|
||||||
|
try {
|
||||||
|
$query = new DNSQuery($dnsServer, 53, 5);
|
||||||
|
$records = $query->query($host, $type);
|
||||||
|
|
||||||
|
if ($records === false || $query->hasError()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
if ($record->getType() === $type && filter_var($record->getData(), FILTER_VALIDATE_IP)) {
|
||||||
|
$ips[] = $record->getData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($ips));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function customDnsServers(): array
|
||||||
|
{
|
||||||
|
$servers = $this->instanceSettings()?->custom_dns_servers ?? '';
|
||||||
|
|
||||||
|
if (! is_string($servers)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(
|
||||||
|
fn (string $server): string => trim($server),
|
||||||
|
explode(',', $servers),
|
||||||
|
), fn (string $server): bool => filter_var($server, FILTER_VALIDATE_IP) !== false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAllowedIp(string $ip, string $host): bool
|
||||||
{
|
{
|
||||||
$embeddedIpv4 = $this->extractIpv4FromMappedIpv6($ip);
|
$embeddedIpv4 = $this->extractIpv4FromMappedIpv6($ip);
|
||||||
if ($embeddedIpv4 !== null) {
|
if ($embeddedIpv4 !== null) {
|
||||||
return $this->isBlockedIpv4($embeddedIpv4);
|
$ip = $embeddedIpv4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isPublicIp($ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isLocalhostIp($ip)) {
|
||||||
|
return $this->allowLocalhost()
|
||||||
|
&& ($this->isAllowedHostname($host) || $this->isAllowlistedIp($ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isPrivateIp($ip)) {
|
||||||
|
return $this->isAllowedHostname($host) || $this->isAllowlistedIp($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->isAllowlistedIp($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isPublicIp(string $ip): bool
|
||||||
|
{
|
||||||
|
$embeddedIpv4 = $this->extractIpv4FromMappedIpv6($ip);
|
||||||
|
if ($embeddedIpv4 !== null) {
|
||||||
|
return filter_var($embeddedIpv4, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false
|
||||||
|
&& ! $this->isSpecialUseIpv4($embeddedIpv4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false
|
||||||
|
&& ! $this->isSpecialUseIp($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isLocalhostIp(string $ip): bool
|
||||||
|
{
|
||||||
|
$embeddedIpv4 = $this->extractIpv4FromMappedIpv6($ip);
|
||||||
|
if ($embeddedIpv4 !== null) {
|
||||||
|
return $this->isLocalhostIp($embeddedIpv4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
return $this->isBlockedIpv4($ip);
|
return $this->ipv4InCidr($ip, '127.0.0.0/8');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->isBlockedIpv6($ip);
|
return @inet_pton($ip) === @inet_pton('::1');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isBlockedIpv4(string $ip): bool
|
private function isPrivateIp(string $ip): bool
|
||||||
{
|
{
|
||||||
if ($ip === '0.0.0.0' || str_starts_with($ip, '127.')) {
|
$embeddedIpv4 = $this->extractIpv4FromMappedIpv6($ip);
|
||||||
|
if ($embeddedIpv4 !== null) {
|
||||||
|
$ip = $embeddedIpv4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false
|
||||||
|
&& filter_var($ip, FILTER_VALIDATE_IP) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSpecialUseIp(string $ip): bool
|
||||||
|
{
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return $this->isSpecialUseIpv4($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->isSpecialUseIpv6($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSpecialUseIpv4(string $ip): bool
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
'0.0.0.0/8',
|
||||||
|
'100.64.0.0/10',
|
||||||
|
'127.0.0.0/8',
|
||||||
|
'169.254.0.0/16',
|
||||||
|
'192.0.0.0/24',
|
||||||
|
'192.0.2.0/24',
|
||||||
|
'198.18.0.0/15',
|
||||||
|
'198.51.100.0/24',
|
||||||
|
'203.0.113.0/24',
|
||||||
|
'224.0.0.0/4',
|
||||||
|
'240.0.0.0/4',
|
||||||
|
'255.255.255.255/32',
|
||||||
|
] as $cidr) {
|
||||||
|
if ($this->ipv4InCidr($ip, $cidr)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSpecialUseIpv6(string $ip): bool
|
||||||
|
{
|
||||||
|
$ipBytes = @inet_pton($ip);
|
||||||
|
if ($ipBytes === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'::/128',
|
||||||
|
'::1/128',
|
||||||
|
'::ffff:0:0/96',
|
||||||
|
'64:ff9b::/96',
|
||||||
|
'100::/64',
|
||||||
|
'2001::/23',
|
||||||
|
'2001:2::/48',
|
||||||
|
'2001:db8::/32',
|
||||||
|
'2002::/16',
|
||||||
|
'fc00::/7',
|
||||||
|
'fe80::/10',
|
||||||
|
'ff00::/8',
|
||||||
|
] as $cidr) {
|
||||||
|
[$network, $prefix] = explode('/', $cidr, 2);
|
||||||
|
$networkBytes = @inet_pton($network);
|
||||||
|
if ($networkBytes !== false && $this->binaryInCidr($ipBytes, $networkBytes, (int) $prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isBlockedHostname(string $host): bool
|
||||||
|
{
|
||||||
|
return in_array($host, ['localhost'], true)
|
||||||
|
|| str_ends_with($host, '.local')
|
||||||
|
|| str_ends_with($host, '.internal')
|
||||||
|
|| str_ends_with($host, '.cluster.local');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAllowedHostname(string $host): bool
|
||||||
|
{
|
||||||
|
foreach ($this->allowlistEntries() as $entry) {
|
||||||
|
if (! str_contains($entry, '/') && strtolower($entry) === $host) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAllowlistedIp(string $ip): bool
|
||||||
|
{
|
||||||
|
foreach ($this->allowlistEntries() as $entry) {
|
||||||
|
if (str_contains($entry, '/')) {
|
||||||
|
if ($this->ipInCidr($ip, $entry)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($entry, FILTER_VALIDATE_IP) && @inet_pton($entry) === @inet_pton($ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function allowlistEntries(): array
|
||||||
|
{
|
||||||
|
$entries = $this->instanceSettings()?->webhook_allowed_internal_hosts ?? [];
|
||||||
|
|
||||||
|
if (is_string($entries)) {
|
||||||
|
$entries = explode(',', $entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_array($entries)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(
|
||||||
|
fn (mixed $entry): string => rtrim(strtolower(trim((string) $entry)), '.'),
|
||||||
|
$entries,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function allowLocalhost(): bool
|
||||||
|
{
|
||||||
|
return (bool) ($this->instanceSettings()?->webhook_allow_localhost ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function instanceSettings(): ?InstanceSettings
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return InstanceSettings::query()->find(0);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ipInCidr(string $ip, string $cidr): bool
|
||||||
|
{
|
||||||
|
[$network, $prefix] = array_pad(explode('/', $cidr, 2), 2, null);
|
||||||
|
if ($network === null || $prefix === null || ! is_numeric($prefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return $this->ipv4InCidr($ip, $cidr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) || ! filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = (int) $prefix;
|
||||||
|
if ($prefix < 0 || $prefix > 128) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipBytes = @inet_pton($ip);
|
||||||
|
$networkBytes = @inet_pton($network);
|
||||||
|
if ($ipBytes === false || $networkBytes === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->binaryInCidr($ipBytes, $networkBytes, $prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ipv4InCidr(string $ip, string $cidr): bool
|
||||||
|
{
|
||||||
|
[$network, $prefix] = array_pad(explode('/', $cidr, 2), 2, null);
|
||||||
|
if ($network === null || $prefix === null || ! is_numeric($prefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = (int) $prefix;
|
||||||
|
if ($prefix < 0 || $prefix > 32) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipLong = ip2long($ip);
|
||||||
|
$networkLong = ip2long($network);
|
||||||
|
if ($ipLong === false || $networkLong === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mask = $prefix === 0 ? 0 : (-1 << (32 - $prefix));
|
||||||
|
|
||||||
|
return ($ipLong & $mask) === ($networkLong & $mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function binaryInCidr(string $ipBytes, string $networkBytes, int $prefix): bool
|
||||||
|
{
|
||||||
|
$bytes = intdiv($prefix, 8);
|
||||||
|
$bits = $prefix % 8;
|
||||||
|
|
||||||
|
if ($bytes > 0 && substr($ipBytes, 0, $bytes) !== substr($networkBytes, 0, $bytes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bits === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$long = ip2long($ip);
|
$mask = 0xFF << (8 - $bits) & 0xFF;
|
||||||
if ($long === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$unsigned = sprintf('%u', $long);
|
return (ord($ipBytes[$bytes]) & $mask) === (ord($networkBytes[$bytes]) & $mask);
|
||||||
$linkLocalStart = sprintf('%u', ip2long('169.254.0.0'));
|
|
||||||
$linkLocalEnd = sprintf('%u', ip2long('169.254.255.255'));
|
|
||||||
|
|
||||||
return $unsigned >= $linkLocalStart && $unsigned <= $linkLocalEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isBlockedIpv6(string $ip): bool
|
|
||||||
{
|
|
||||||
$packed = @inet_pton($ip);
|
|
||||||
if ($packed === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($packed === inet_pton('::1') || $packed === inet_pton('::')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$bytes = unpack('C16', $packed);
|
|
||||||
if ($bytes === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$firstByte = $bytes[1];
|
|
||||||
$secondByte = $bytes[2];
|
|
||||||
|
|
||||||
// fe80::/10 link-local and fc00::/7 unique local addresses.
|
|
||||||
return ($firstByte === 0xFE && ($secondByte & 0xC0) === 0x80)
|
|
||||||
|| (($firstByte & 0xFE) === 0xFC);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extractIpv4FromMappedIpv6(string $ip): ?string
|
private function extractIpv4FromMappedIpv6(string $ip): ?string
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Exceptions\RateLimitException;
|
use App\Exceptions\RateLimitException;
|
||||||
|
use Illuminate\Http\Client\RequestException;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
class HetznerService
|
class HetznerService
|
||||||
|
|
@ -24,7 +25,7 @@ private function request(string $method, string $endpoint, array $data = [])
|
||||||
->timeout(30)
|
->timeout(30)
|
||||||
->retry(3, function (int $attempt, \Exception $exception) {
|
->retry(3, function (int $attempt, \Exception $exception) {
|
||||||
// Handle rate limiting (429 Too Many Requests)
|
// Handle rate limiting (429 Too Many Requests)
|
||||||
if ($exception instanceof \Illuminate\Http\Client\RequestException) {
|
if ($exception instanceof RequestException) {
|
||||||
$response = $exception->response;
|
$response = $exception->response;
|
||||||
|
|
||||||
if ($response && $response->status() === 429) {
|
if ($response && $response->status() === 429) {
|
||||||
|
|
@ -129,17 +130,9 @@ public function uploadSshKey(string $name, string $publicKey): array
|
||||||
|
|
||||||
public function createServer(array $params): array
|
public function createServer(array $params): array
|
||||||
{
|
{
|
||||||
ray('Hetzner createServer request', [
|
|
||||||
'endpoint' => '/servers',
|
|
||||||
'params' => $params,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->request('post', '/servers', $params);
|
$response = $this->request('post', '/servers', $params);
|
||||||
|
|
||||||
ray('Hetzner createServer response', [
|
|
||||||
'response' => $response,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $response['server'] ?? [];
|
return $response['server'] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,12 @@ class ValidationPatterns
|
||||||
*/
|
*/
|
||||||
public const ENVIRONMENT_VARIABLE_KEY_PATTERN = '/\A[A-Za-z_][A-Za-z0-9_.]*\z/u';
|
public const ENVIRONMENT_VARIABLE_KEY_PATTERN = '/\A[A-Za-z_][A-Za-z0-9_.]*\z/u';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Characters that are valid in some URL positions but unsafe for values
|
||||||
|
* that are later reused in shell assignment contexts.
|
||||||
|
*/
|
||||||
|
public const APPLICATION_DOMAIN_FORBIDDEN_PATTERN = '/[`$;&|<>()\\\\\r\n]/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pattern for SQL-safe unquoted database identifiers (usernames, database names).
|
* Pattern for SQL-safe unquoted database identifiers (usernames, database names).
|
||||||
* Allows letters, digits, underscore; first char must be letter or underscore.
|
* Allows letters, digits, underscore; first char must be letter or underscore.
|
||||||
|
|
@ -511,6 +517,156 @@ public static function shellSafeCommandRules(int $maxLength = 1000): array
|
||||||
return ['nullable', 'string', 'max:'.$maxLength, 'regex:'.self::SHELL_SAFE_COMMAND_PATTERN];
|
return ['nullable', 'string', 'max:'.$maxLength, 'regex:'.self::SHELL_SAFE_COMMAND_PATTERN];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation rules for comma-separated application URL fields.
|
||||||
|
*/
|
||||||
|
public static function applicationDomainRules(int $maxLength = 2048): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'max:'.$maxLength,
|
||||||
|
function (string $attribute, mixed $value, \Closure $fail): void {
|
||||||
|
foreach (self::validateApplicationDomains($value) as $error) {
|
||||||
|
$fail($error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a comma-separated list of application URLs.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function validateApplicationDomains(mixed $value): array
|
||||||
|
{
|
||||||
|
if (blank($value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($value)) {
|
||||||
|
return ['The domains field must be a string.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
foreach (self::applicationDomainList($value) as $url) {
|
||||||
|
if (preg_match(self::APPLICATION_DOMAIN_FORBIDDEN_PATTERN, $url) === 1) {
|
||||||
|
$errors[] = "Invalid URL: {$url}";
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isValidDomainUrl($url)) {
|
||||||
|
$errors[] = "Invalid URL: {$url}";
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
|
||||||
|
if (! in_array(strtolower($scheme), ['http', 'https'], true)) {
|
||||||
|
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blank(parse_url($url, PHP_URL_HOST))) {
|
||||||
|
$errors[] = "Invalid URL: {$url}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a comma-separated application URL list for storage.
|
||||||
|
*/
|
||||||
|
public static function normalizeApplicationDomains(?string $value): ?string
|
||||||
|
{
|
||||||
|
$urls = self::applicationDomainList($value);
|
||||||
|
|
||||||
|
if ($urls === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($urls)
|
||||||
|
->map(fn (string $url) => self::normalizeApplicationDomainUrl($url))
|
||||||
|
->implode(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize URL components that are case-insensitive while preserving
|
||||||
|
* case-sensitive path, query, and fragment components.
|
||||||
|
*/
|
||||||
|
private static function normalizeApplicationDomainUrl(string $url): string
|
||||||
|
{
|
||||||
|
$components = parse_url($url);
|
||||||
|
|
||||||
|
if ($components === false) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = '';
|
||||||
|
|
||||||
|
if (isset($components['scheme'])) {
|
||||||
|
$normalized .= strtolower($components['scheme']).'://';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($components['user'])) {
|
||||||
|
$normalized .= $components['user'];
|
||||||
|
|
||||||
|
if (isset($components['pass'])) {
|
||||||
|
$normalized .= ':'.$components['pass'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized .= '@';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($components['host'])) {
|
||||||
|
$normalized .= strtolower($components['host']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($components['port'])) {
|
||||||
|
$normalized .= ':'.$components['port'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($components['path'])) {
|
||||||
|
$normalized .= $components['path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('query', $components)) {
|
||||||
|
$normalized .= '?'.$components['query'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('fragment', $components)) {
|
||||||
|
$normalized .= '#'.$components['fragment'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a comma-separated application URL list into trimmed URL strings.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function applicationDomainList(?string $value): array
|
||||||
|
{
|
||||||
|
if (blank($value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return str($value)
|
||||||
|
->replaceStart(',', '')
|
||||||
|
->replaceEnd(',', '')
|
||||||
|
->trim()
|
||||||
|
->explode(',')
|
||||||
|
->map(fn (string $url) => trim($url))
|
||||||
|
->filter(fn (string $url) => filled($url))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get validation rules for Docker volume name fields
|
* Get validation rules for Docker volume name fields
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@
|
||||||
namespace App\Traits;
|
namespace App\Traits;
|
||||||
|
|
||||||
use App\Livewire\GlobalSearch;
|
use App\Livewire\GlobalSearch;
|
||||||
|
use App\Models\Application;
|
||||||
|
use App\Models\Environment;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Service;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
trait ClearsGlobalSearchCache
|
trait ClearsGlobalSearchCache
|
||||||
|
|
@ -20,7 +25,6 @@ protected static function bootClearsGlobalSearchCache()
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Silently fail cache clearing - don't break the save operation
|
// Silently fail cache clearing - don't break the save operation
|
||||||
ray('Failed to clear global search cache on saving: '.$e->getMessage());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -33,7 +37,6 @@ protected static function bootClearsGlobalSearchCache()
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Silently fail cache clearing - don't break the create operation
|
// Silently fail cache clearing - don't break the create operation
|
||||||
ray('Failed to clear global search cache on creation: '.$e->getMessage());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -46,7 +49,6 @@ protected static function bootClearsGlobalSearchCache()
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Silently fail cache clearing - don't break the delete operation
|
// Silently fail cache clearing - don't break the delete operation
|
||||||
ray('Failed to clear global search cache on deletion: '.$e->getMessage());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -58,14 +60,14 @@ private function hasSearchableChanges(): bool
|
||||||
$searchableFields = ['name', 'description'];
|
$searchableFields = ['name', 'description'];
|
||||||
|
|
||||||
// Add model-specific searchable fields
|
// Add model-specific searchable fields
|
||||||
if ($this instanceof \App\Models\Application) {
|
if ($this instanceof Application) {
|
||||||
$searchableFields[] = 'fqdn';
|
$searchableFields[] = 'fqdn';
|
||||||
$searchableFields[] = 'docker_compose_domains';
|
$searchableFields[] = 'docker_compose_domains';
|
||||||
} elseif ($this instanceof \App\Models\Server) {
|
} elseif ($this instanceof Server) {
|
||||||
$searchableFields[] = 'ip';
|
$searchableFields[] = 'ip';
|
||||||
} elseif ($this instanceof \App\Models\Service) {
|
} elseif ($this instanceof Service) {
|
||||||
// Services don't have direct fqdn, but name and description are covered
|
// Services don't have direct fqdn, but name and description are covered
|
||||||
} elseif ($this instanceof \App\Models\Project || $this instanceof \App\Models\Environment) {
|
} elseif ($this instanceof Project || $this instanceof Environment) {
|
||||||
// Projects and environments only have name and description as searchable
|
// Projects and environments only have name and description as searchable
|
||||||
}
|
}
|
||||||
// Database models only have name and description as searchable
|
// Database models only have name and description as searchable
|
||||||
|
|
@ -81,7 +83,6 @@ private function hasSearchableChanges(): bool
|
||||||
return false;
|
return false;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// If checking changes fails, assume changes exist to be safe
|
// If checking changes fails, assume changes exist to be safe
|
||||||
ray('Failed to check searchable changes: '.$e->getMessage());
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -91,18 +92,18 @@ private function getTeamIdForCache()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// For Project models (has direct team_id)
|
// For Project models (has direct team_id)
|
||||||
if ($this instanceof \App\Models\Project) {
|
if ($this instanceof Project) {
|
||||||
return $this->team_id ?? null;
|
return $this->team_id ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Environment models (get team_id through project)
|
// For Environment models (get team_id through project)
|
||||||
if ($this instanceof \App\Models\Environment) {
|
if ($this instanceof Environment) {
|
||||||
return $this->project?->team_id;
|
return $this->project?->team_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For database models, team is accessed through environment.project.team
|
// For database models, team is accessed through environment.project.team
|
||||||
if (method_exists($this, 'team')) {
|
if (method_exists($this, 'team')) {
|
||||||
if ($this instanceof \App\Models\Server) {
|
if ($this instanceof Server) {
|
||||||
$team = $this->team;
|
$team = $this->team;
|
||||||
} else {
|
} else {
|
||||||
$team = $this->team();
|
$team = $this->team();
|
||||||
|
|
@ -120,7 +121,6 @@ private function getTeamIdForCache()
|
||||||
return null;
|
return null;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// If we can't determine team ID, return null
|
// If we can't determine team ID, return null
|
||||||
ray('Failed to get team ID for cache: '.$e->getMessage());
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ protected function executeWithSshRetry(callable $callback, array $context = [],
|
||||||
$lastErrorMessage = '';
|
$lastErrorMessage = '';
|
||||||
// Randomly fail the command with a key exchange error for testing
|
// Randomly fail the command with a key exchange error for testing
|
||||||
// if (random_int(1, 10) === 1) { // 10% chance to fail
|
// if (random_int(1, 10) === 1) { // 10% chance to fail
|
||||||
// ray('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer');
|
|
||||||
// throw new \RuntimeException('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer');
|
// throw new \RuntimeException('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer');
|
||||||
// }
|
// }
|
||||||
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
|
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,9 @@ function sharedDataApplications()
|
||||||
'is_spa' => 'boolean',
|
'is_spa' => 'boolean',
|
||||||
'is_auto_deploy_enabled' => 'boolean',
|
'is_auto_deploy_enabled' => 'boolean',
|
||||||
'is_force_https_enabled' => 'boolean',
|
'is_force_https_enabled' => 'boolean',
|
||||||
|
'is_preview_deployments_enabled' => 'boolean',
|
||||||
'static_image' => Rule::enum(StaticImageTypes::class),
|
'static_image' => Rule::enum(StaticImageTypes::class),
|
||||||
'domains' => 'string|nullable',
|
'domains' => ValidationPatterns::applicationDomainRules(),
|
||||||
'redirect' => Rule::enum(RedirectTypes::class),
|
'redirect' => Rule::enum(RedirectTypes::class),
|
||||||
'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
|
'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
|
||||||
'docker_registry_image_name' => ValidationPatterns::dockerImageNameRules(),
|
'docker_registry_image_name' => ValidationPatterns::dockerImageNameRules(),
|
||||||
|
|
@ -213,10 +214,12 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||||
$request->offsetUnset('is_spa');
|
$request->offsetUnset('is_spa');
|
||||||
$request->offsetUnset('is_auto_deploy_enabled');
|
$request->offsetUnset('is_auto_deploy_enabled');
|
||||||
$request->offsetUnset('is_force_https_enabled');
|
$request->offsetUnset('is_force_https_enabled');
|
||||||
|
$request->offsetUnset('is_preview_deployments_enabled');
|
||||||
$request->offsetUnset('connect_to_docker_network');
|
$request->offsetUnset('connect_to_docker_network');
|
||||||
$request->offsetUnset('force_domain_override');
|
$request->offsetUnset('force_domain_override');
|
||||||
$request->offsetUnset('autogenerate_domain');
|
$request->offsetUnset('autogenerate_domain');
|
||||||
$request->offsetUnset('is_container_label_escape_enabled');
|
$request->offsetUnset('is_container_label_escape_enabled');
|
||||||
$request->offsetUnset('is_preserve_repository_enabled');
|
$request->offsetUnset('is_preserve_repository_enabled');
|
||||||
|
$request->offsetUnset('include_source_commit_in_build');
|
||||||
$request->offsetUnset('docker_compose_raw');
|
$request->offsetUnset('docker_compose_raw');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,36 @@ function getCurrentServiceContainerStatus(Server $server, int $id): Collection
|
||||||
return $containers;
|
return $containers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentDatabaseContainerStatus(Server $server, int $id): Collection
|
||||||
|
{
|
||||||
|
$containers = collect([]);
|
||||||
|
if (! $server->isSwarm()) {
|
||||||
|
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.databaseId={$id}' --format '{{json .}}' "], $server);
|
||||||
|
$containers = format_docker_command_output_to_json($containers);
|
||||||
|
|
||||||
|
return $containers->filter();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $containers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentServiceSubContainerStatus(Server $server, int $id, string $name): Collection
|
||||||
|
{
|
||||||
|
return filterServiceSubContainersByName(getCurrentServiceContainerStatus($server, $id), $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterServiceSubContainersByName(Collection $containers, string $name): Collection
|
||||||
|
{
|
||||||
|
return $containers->filter(function ($container) use ($name) {
|
||||||
|
$labels = data_get($container, 'Labels', []);
|
||||||
|
if (is_string($labels)) {
|
||||||
|
$labels = format_docker_labels_to_json($labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($labels)->get('coolify.name') === $name;
|
||||||
|
})->values();
|
||||||
|
}
|
||||||
|
|
||||||
function format_docker_command_output_to_json($rawOutput): Collection
|
function format_docker_command_output_to_json($rawOutput): Collection
|
||||||
{
|
{
|
||||||
$outputLines = explode(PHP_EOL, $rawOutput);
|
$outputLines = explode(PHP_EOL, $rawOutput);
|
||||||
|
|
@ -1247,18 +1277,38 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainerLogs(Server $server, string $container_id, int $lines = 100): string
|
function normalizeLogLines(mixed $lines, int $default = 100, int $max = 10000): int
|
||||||
{
|
{
|
||||||
if ($server->isSwarm()) {
|
$lines = filter_var($lines, FILTER_VALIDATE_INT);
|
||||||
$output = instant_remote_process([
|
if ($lines === false || $lines <= 0) {
|
||||||
"docker service logs -n {$lines} {$container_id} 2>&1",
|
return $default;
|
||||||
], $server);
|
|
||||||
} else {
|
|
||||||
$output = instant_remote_process([
|
|
||||||
"docker logs -n {$lines} {$container_id} 2>&1",
|
|
||||||
], $server);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return min($lines, $max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLogTimestampFlag(mixed $showTimestamps): bool
|
||||||
|
{
|
||||||
|
return filter_var($showTimestamps, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildContainerLogsCommand(Server $server, string $container_id, int $lines = 100, bool $showTimestamps = false): string
|
||||||
|
{
|
||||||
|
$command = "docker logs -n {$lines}";
|
||||||
|
if ($server->isSwarm()) {
|
||||||
|
$command = "docker service logs -n {$lines}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($showTimestamps) {
|
||||||
|
$command .= ' --timestamps';
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{$command} ".escapeshellarg($container_id).' 2>&1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContainerLogs(Server $server, string $container_id, int $lines = 100, bool $showTimestamps = false): string
|
||||||
|
{
|
||||||
|
$output = instant_remote_process([buildContainerLogsCommand($server, $container_id, $lines, $showTimestamps)], $server);
|
||||||
$output = removeAnsiColors($output);
|
$output = removeAnsiColors($output);
|
||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,54 @@
|
||||||
use App\Models\ServiceApplication;
|
use App\Models\ServiceApplication;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
function isValidDomainUrl(string $url): bool
|
||||||
|
{
|
||||||
|
$components = parse_url($url);
|
||||||
|
|
||||||
|
if ($components === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = $components['scheme'] ?? '';
|
||||||
|
$host = $components['host'] ?? '';
|
||||||
|
|
||||||
|
if (! in_array(strtolower($scheme), ['http', 'https'], true) || $host === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$urlToValidate = $scheme.'://';
|
||||||
|
|
||||||
|
if (isset($components['user'])) {
|
||||||
|
$urlToValidate .= $components['user'];
|
||||||
|
|
||||||
|
if (isset($components['pass'])) {
|
||||||
|
$urlToValidate .= ':'.$components['pass'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$urlToValidate .= '@';
|
||||||
|
}
|
||||||
|
|
||||||
|
$urlToValidate .= str_replace('_', '-', $host);
|
||||||
|
|
||||||
|
if (isset($components['port'])) {
|
||||||
|
$urlToValidate .= ':'.$components['port'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($components['path'])) {
|
||||||
|
$urlToValidate .= $components['path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($components['query'])) {
|
||||||
|
$urlToValidate .= '?'.$components['query'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($components['fragment'])) {
|
||||||
|
$urlToValidate .= '#'.$components['fragment'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter_var($urlToValidate, FILTER_VALIDATE_URL) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
|
function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
|
||||||
{
|
{
|
||||||
$conflicts = [];
|
$conflicts = [];
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use App\Models\GithubApp;
|
use App\Models\GithubApp;
|
||||||
use App\Models\GitlabApp;
|
use App\Models\GitlabApp;
|
||||||
|
use App\Models\PrivateKey;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
@ -13,9 +14,144 @@
|
||||||
use Lcobucci\JWT\Signer\Rsa\Sha256;
|
use Lcobucci\JWT\Signer\Rsa\Sha256;
|
||||||
use Lcobucci\JWT\Token\Builder;
|
use Lcobucci\JWT\Token\Builder;
|
||||||
|
|
||||||
function generateGithubToken(GithubApp $source, string $type)
|
/**
|
||||||
|
* Extract and normalize the hostname from a GitHub URL.
|
||||||
|
*
|
||||||
|
* @param string|null $url The URL to parse
|
||||||
|
* @return string|null The lowercase hostname, or null if the URL is blank or has no parseable host
|
||||||
|
*/
|
||||||
|
function githubUrlHost(?string $url): ?string
|
||||||
{
|
{
|
||||||
$response = Http::get("{$source->api_url}/zen");
|
if (blank($url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
|
||||||
|
if (! is_string($host) || blank($host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the scheme://host[:port] origin for a GitHub URL.
|
||||||
|
*
|
||||||
|
* This helper fails explicitly for blank, scheme-less, or malformed input when
|
||||||
|
* githubUrlHost() cannot parse a host, because returning the original input
|
||||||
|
* would not be a valid origin. Callers should pass already-validated URLs.
|
||||||
|
*
|
||||||
|
* @param string $url The URL to derive the origin from
|
||||||
|
* @return string The normalized origin
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException When the URL does not contain a parseable scheme and host
|
||||||
|
*/
|
||||||
|
function githubUrlOrigin(string $url): string
|
||||||
|
{
|
||||||
|
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||||
|
$host = githubUrlHost($url);
|
||||||
|
$port = parse_url($url, PHP_URL_PORT);
|
||||||
|
|
||||||
|
if (! is_string($scheme) || blank($scheme) || ! $host) {
|
||||||
|
throw new InvalidArgumentException('GitHub URL must include a valid scheme and host.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scheme.'://'.$host.($port ? ":{$port}" : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the URL points at github.com.
|
||||||
|
*
|
||||||
|
* @param string|null $htmlUrl The GitHub HTML URL to check
|
||||||
|
*/
|
||||||
|
function isGithubDotComHost(?string $htmlUrl): bool
|
||||||
|
{
|
||||||
|
return githubUrlHost($htmlUrl) === 'github.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the URL points at a *.ghe.com GitHub Enterprise Cloud host.
|
||||||
|
*
|
||||||
|
* @param string|null $htmlUrl The GitHub HTML URL to check
|
||||||
|
*/
|
||||||
|
function isGheDotComHost(?string $htmlUrl): bool
|
||||||
|
{
|
||||||
|
$host = githubUrlHost($htmlUrl);
|
||||||
|
|
||||||
|
return is_string($host)
|
||||||
|
&& Str::endsWith($host, '.ghe.com')
|
||||||
|
&& ! Str::startsWith($host, 'api.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the URL belongs to GitHub's cloud family (github.com or *.ghe.com).
|
||||||
|
*
|
||||||
|
* @param string|null $htmlUrl The GitHub HTML URL to check
|
||||||
|
*/
|
||||||
|
function isGithubCloudFamilyHost(?string $htmlUrl): bool
|
||||||
|
{
|
||||||
|
return isGithubDotComHost($htmlUrl) || isGheDotComHost($htmlUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the URL belongs to a self-hosted GitHub Enterprise Server.
|
||||||
|
*
|
||||||
|
* @param string|null $htmlUrl The GitHub HTML URL to check
|
||||||
|
*/
|
||||||
|
function isGithubEnterpriseServerHost(?string $htmlUrl): bool
|
||||||
|
{
|
||||||
|
return filled($htmlUrl) && ! isGithubCloudFamilyHost($htmlUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the GitHub REST API base URL from a GitHub HTML URL.
|
||||||
|
*
|
||||||
|
* @param string $htmlUrl The GitHub HTML URL
|
||||||
|
* @return string The API base URL (api.github.com, api.<host> for *.ghe.com, or <origin>/api/v3 for GHES)
|
||||||
|
*/
|
||||||
|
function githubApiUrlFromHtmlUrl(string $htmlUrl): string
|
||||||
|
{
|
||||||
|
if (isGithubDotComHost($htmlUrl)) {
|
||||||
|
return 'https://api.github.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGheDotComHost($htmlUrl)) {
|
||||||
|
return 'https://api.'.githubUrlHost($htmlUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return githubUrlOrigin($htmlUrl).'/api/v3';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a GitHub organization slug by trimming surrounding slashes and whitespace.
|
||||||
|
*
|
||||||
|
* @param string|null $organization The raw organization value
|
||||||
|
* @return string|null The trimmed organization, or null when blank
|
||||||
|
*/
|
||||||
|
function normalizeGithubOrganization(?string $organization): ?string
|
||||||
|
{
|
||||||
|
if (blank($organization)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string) $organization, "/ \t\n\r\0\x0B");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL-encode a single GitHub path segment.
|
||||||
|
*
|
||||||
|
* @param string $segment The raw path segment
|
||||||
|
* @return string The raw-URL-encoded segment
|
||||||
|
*/
|
||||||
|
function encodeGithubPathSegment(string $segment): string
|
||||||
|
{
|
||||||
|
return rawurlencode($segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertGithubClockInSync(string $apiUrl): void
|
||||||
|
{
|
||||||
|
$response = Http::get("{$apiUrl}/zen");
|
||||||
$serverTime = CarbonImmutable::now()->setTimezone('UTC');
|
$serverTime = CarbonImmutable::now()->setTimezone('UTC');
|
||||||
$githubTime = Carbon::parse($response->header('date'));
|
$githubTime = Carbon::parse($response->header('date'));
|
||||||
$timeDiff = abs($serverTime->diffInSeconds($githubTime));
|
$timeDiff = abs($serverTime->diffInSeconds($githubTime));
|
||||||
|
|
@ -29,6 +165,11 @@ function generateGithubToken(GithubApp $source, string $type)
|
||||||
'Please synchronize your system clock.'
|
'Please synchronize your system clock.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateGithubToken(GithubApp $source, string $type)
|
||||||
|
{
|
||||||
|
assertGithubClockInSync($source->api_url);
|
||||||
|
|
||||||
$signingKey = InMemory::plainText($source->privateKey->private_key);
|
$signingKey = InMemory::plainText($source->privateKey->private_key);
|
||||||
$algorithm = new Sha256;
|
$algorithm = new Sha256;
|
||||||
|
|
@ -117,11 +258,86 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateGithubAppJwt(string $privateKey, string|int $appId): string
|
||||||
|
{
|
||||||
|
$algorithm = new Sha256;
|
||||||
|
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
|
||||||
|
$now = CarbonImmutable::now()->setTimezone('UTC');
|
||||||
|
$now = $now->setTime($now->format('H'), $now->format('i'), $now->format('s'));
|
||||||
|
|
||||||
|
return $tokenBuilder
|
||||||
|
->issuedBy((string) $appId)
|
||||||
|
->issuedAt($now->modify('-1 minute'))
|
||||||
|
->expiresAt($now->modify('+8 minutes'))
|
||||||
|
->getToken($algorithm, InMemory::plainText($privateKey))
|
||||||
|
->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncGithubAppName(GithubApp $source, bool $throw = false): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (blank($source->app_id) || blank($source->private_key_id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$privateKey = $source->privateKey ?: PrivateKey::find($source->private_key_id);
|
||||||
|
|
||||||
|
if (! $privateKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertGithubClockInSync($source->api_url);
|
||||||
|
|
||||||
|
$jwt = generateGithubAppJwt($privateKey->private_key, $source->app_id);
|
||||||
|
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Accept' => 'application/vnd.github+json',
|
||||||
|
'X-GitHub-Api-Version' => '2022-11-28',
|
||||||
|
'Authorization' => "Bearer {$jwt}",
|
||||||
|
])->get("{$source->api_url}/app");
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
throw new RuntimeException(data_get($response->json(), 'message', 'Failed to fetch GitHub App information.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$appSlug = data_get($response->json(), 'slug');
|
||||||
|
|
||||||
|
if (blank($appSlug)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source->name = $appSlug;
|
||||||
|
|
||||||
|
if ($source->exists) {
|
||||||
|
$source->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$privateKey->name = "github-app-{$appSlug}";
|
||||||
|
$privateKey->save();
|
||||||
|
|
||||||
|
return $appSlug;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($throw) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getInstallationPath(GithubApp $source): string
|
function getInstallationPath(GithubApp $source): string
|
||||||
{
|
{
|
||||||
$name = str(Str::kebab($source->name));
|
$name = encodeGithubPathSegment(Str::kebab($source->name));
|
||||||
$installation_path = $source->html_url === 'https://github.com' ? 'apps' : 'github-apps';
|
|
||||||
$state = Str::random(64);
|
$state = Str::random(64);
|
||||||
|
$organization = normalizeGithubOrganization($source->organization);
|
||||||
|
|
||||||
|
if (isGithubEnterpriseServerHost($source->html_url)) {
|
||||||
|
$path = "github-apps/{$name}";
|
||||||
|
} elseif (isGheDotComHost($source->html_url) && filled($organization)) {
|
||||||
|
$path = 'apps/'.encodeGithubPathSegment($organization)."/{$name}";
|
||||||
|
} else {
|
||||||
|
$path = "apps/{$name}";
|
||||||
|
}
|
||||||
|
|
||||||
Cache::put('github-app-setup-state:'.hash('sha256', $state), [
|
Cache::put('github-app-setup-state:'.hash('sha256', $state), [
|
||||||
'action' => 'install',
|
'action' => 'install',
|
||||||
|
|
@ -129,15 +345,19 @@ function getInstallationPath(GithubApp $source): string
|
||||||
'team_id' => $source->team_id,
|
'team_id' => $source->team_id,
|
||||||
], now()->addMinutes(60));
|
], now()->addMinutes(60));
|
||||||
|
|
||||||
return "$source->html_url/$installation_path/$name/installations/new?".http_build_query(['state' => $state]);
|
return rtrim($source->html_url, '/')."/{$path}/installations/new?".http_build_query(['state' => $state]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPermissionsPath(GithubApp $source)
|
function getPermissionsPath(GithubApp $source)
|
||||||
{
|
{
|
||||||
$github = GithubApp::where('uuid', $source->uuid)->first();
|
$name = encodeGithubPathSegment(Str::kebab($source->name));
|
||||||
$name = str(Str::kebab($github->name));
|
$organization = normalizeGithubOrganization($source->organization);
|
||||||
|
|
||||||
return "$github->html_url/settings/apps/$name/permissions";
|
if (filled($organization)) {
|
||||||
|
return rtrim($source->html_url, '/').'/organizations/'.encodeGithubPathSegment($organization)."/settings/apps/{$name}/permissions";
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim($source->html_url, '/')."/settings/apps/{$name}/permissions";
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadRepositoryByPage(GithubApp $source, string $token, int $page)
|
function loadRepositoryByPage(GithubApp $source, string $token, int $page)
|
||||||
|
|
@ -189,7 +409,6 @@ function getGithubCommitRangeFiles(?GithubApp $source, string $owner, string $re
|
||||||
|
|
||||||
return $files->pluck('filename')->filter()->values()->toArray();
|
return $files->pluck('filename')->filter()->values()->toArray();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
ray('Error fetching GitHub commit range files: '.$e->getMessage());
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +434,6 @@ function getGithubPullRequestFiles(?GithubApp $source, string $owner, string $re
|
||||||
|
|
||||||
return $files->pluck('filename')->filter()->values()->toArray();
|
return $files->pluck('filename')->filter()->values()->toArray();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
ray('Error fetching GitHub PR files: '.$e->getMessage());
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@ function send_internal_notification(string $message): void
|
||||||
try {
|
try {
|
||||||
$team = Team::find(0);
|
$team = Team::find(0);
|
||||||
$team?->notify(new GeneralNotification($message));
|
$team?->notify(new GeneralNotification($message));
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable) {
|
||||||
ray($e->getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -370,8 +370,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
$pullRequestId = $pull_request_id;
|
$pullRequestId = $pull_request_id;
|
||||||
$isPullRequest = $pullRequestId == 0 ? false : true;
|
$isPullRequest = $pullRequestId == 0 ? false : true;
|
||||||
$server = data_get($resource, 'destination.server');
|
$server = data_get($resource, 'destination.server');
|
||||||
$fileStorages = $resource->fileStorages();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$yaml = Yaml::parse($compose);
|
$yaml = Yaml::parse($compose);
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
|
|
@ -503,6 +501,40 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
'is_preview' => false,
|
'is_preview' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also populate docker_compose_domains for dockercompose apps from direct SERVICE_* declarations.
|
||||||
|
if ($resource->build_pack === 'dockercompose' && ($key->startsWith('SERVICE_FQDN_') || $key->startsWith('SERVICE_URL_'))) {
|
||||||
|
$parsed = parseServiceEnvironmentVariable($key->value());
|
||||||
|
$normalizedServiceName = str($parsed['service_name'])->replace('-', '_')->replace('.', '_')->value();
|
||||||
|
$serviceExists = false;
|
||||||
|
foreach (array_keys($services) as $serviceNameKey) {
|
||||||
|
if (str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value() === $normalizedServiceName) {
|
||||||
|
$serviceExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($serviceExists) {
|
||||||
|
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains') ?: '[]'));
|
||||||
|
$domainExists = data_get($domains->get($normalizedServiceName), 'domain');
|
||||||
|
if (is_null($domainExists)) {
|
||||||
|
$serviceNameForDomain = str($parsed['service_name'])->replace('_', '-')->value();
|
||||||
|
$domainValue = generateUrl(server: $server, random: "$serviceNameForDomain-$uuid");
|
||||||
|
if ($value && get_class($value) === Illuminate\Support\Stringable::class && $value->startsWith('/')) {
|
||||||
|
$path = $value->value();
|
||||||
|
if ($path !== '/') {
|
||||||
|
$domainValue = "$domainValue$path";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($parsed['port'] && is_numeric($parsed['port'])) {
|
||||||
|
$domainValue = "$domainValue:{$parsed['port']}";
|
||||||
|
}
|
||||||
|
$domains->put($normalizedServiceName, ['domain' => $domainValue]);
|
||||||
|
$resource->docker_compose_domains = $domains->toJson();
|
||||||
|
$resource->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -610,7 +642,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
|
|
||||||
// Only add domain if the service exists
|
// Only add domain if the service exists
|
||||||
if ($serviceExists) {
|
if ($serviceExists) {
|
||||||
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
|
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains') ?: '[]'));
|
||||||
$domainExists = data_get($domains->get($serviceName), 'domain');
|
$domainExists = data_get($domains->get($serviceName), 'domain');
|
||||||
|
|
||||||
// Update domain using URL with port if applicable
|
// Update domain using URL with port if applicable
|
||||||
|
|
@ -703,14 +735,11 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
$source = $parsed['source'];
|
$source = $parsed['source'];
|
||||||
$target = $parsed['target'];
|
$target = $parsed['target'];
|
||||||
// Mode is available in $parsed['mode'] if needed
|
// Mode is available in $parsed['mode'] if needed
|
||||||
$foundConfig = $fileStorages->whereMountPath($target)->first();
|
$foundConfig = $originalResource->fileStorages()->whereMountPath($target)->first();
|
||||||
if (sourceIsLocal($source)) {
|
if (sourceIsLocal($source)) {
|
||||||
$type = str('bind');
|
$type = str('bind');
|
||||||
if ($foundConfig) {
|
if ($foundConfig) {
|
||||||
$contentNotNull_temp = data_get($foundConfig, 'content');
|
$content = data_get($foundConfig, 'content');
|
||||||
if ($contentNotNull_temp) {
|
|
||||||
$content = $contentNotNull_temp;
|
|
||||||
}
|
|
||||||
$isDirectory = data_get($foundConfig, 'is_directory');
|
$isDirectory = data_get($foundConfig, 'is_directory');
|
||||||
} else {
|
} else {
|
||||||
// By default, we cannot determine if the bind is a directory or not, so we set it to directory
|
// By default, we cannot determine if the bind is a directory or not, so we set it to directory
|
||||||
|
|
@ -756,12 +785,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$foundConfig = $fileStorages->whereMountPath($target)->first();
|
$foundConfig = $originalResource->fileStorages()->whereMountPath($target)->first();
|
||||||
if ($foundConfig) {
|
if ($foundConfig) {
|
||||||
$contentNotNull_temp = data_get($foundConfig, 'content');
|
$content = data_get($foundConfig, 'content');
|
||||||
if ($contentNotNull_temp) {
|
|
||||||
$content = $contentNotNull_temp;
|
|
||||||
}
|
|
||||||
$isDirectory = data_get($foundConfig, 'is_directory');
|
$isDirectory = data_get($foundConfig, 'is_directory');
|
||||||
} else {
|
} else {
|
||||||
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory
|
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory
|
||||||
|
|
@ -1488,9 +1514,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
|
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
|
||||||
} catch (Exception $e) {
|
} catch (Exception) {
|
||||||
// If parsing fails, keep the original docker_compose_raw unchanged
|
// If parsing fails, keep the original docker_compose_raw unchanged
|
||||||
ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data_forget($resource, 'environment_variables');
|
data_forget($resource, 'environment_variables');
|
||||||
|
|
@ -2070,7 +2095,6 @@ function serviceParser(Service $resource): Collection
|
||||||
'service_id' => $resource->id,
|
'service_id' => $resource->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
$fileStorages = $savedService->fileStorages();
|
|
||||||
if ($savedService->image !== $image) {
|
if ($savedService->image !== $image) {
|
||||||
$savedService->image = $image;
|
$savedService->image = $image;
|
||||||
$savedService->save();
|
$savedService->save();
|
||||||
|
|
@ -2090,14 +2114,11 @@ function serviceParser(Service $resource): Collection
|
||||||
$source = $parsed['source'];
|
$source = $parsed['source'];
|
||||||
$target = $parsed['target'];
|
$target = $parsed['target'];
|
||||||
// Mode is available in $parsed['mode'] if needed
|
// Mode is available in $parsed['mode'] if needed
|
||||||
$foundConfig = $fileStorages->whereMountPath($target)->first();
|
$foundConfig = $originalResource->fileStorages()->whereMountPath($target)->first();
|
||||||
if (sourceIsLocal($source)) {
|
if (sourceIsLocal($source)) {
|
||||||
$type = str('bind');
|
$type = str('bind');
|
||||||
if ($foundConfig) {
|
if ($foundConfig) {
|
||||||
$contentNotNull_temp = data_get($foundConfig, 'content');
|
$content = data_get($foundConfig, 'content');
|
||||||
if ($contentNotNull_temp) {
|
|
||||||
$content = $contentNotNull_temp;
|
|
||||||
}
|
|
||||||
$isDirectory = data_get($foundConfig, 'is_directory');
|
$isDirectory = data_get($foundConfig, 'is_directory');
|
||||||
} else {
|
} else {
|
||||||
// By default, we cannot determine if the bind is a directory or not, so we set it to directory
|
// By default, we cannot determine if the bind is a directory or not, so we set it to directory
|
||||||
|
|
@ -2143,12 +2164,9 @@ function serviceParser(Service $resource): Collection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$foundConfig = $fileStorages->whereMountPath($target)->first();
|
$foundConfig = $originalResource->fileStorages()->whereMountPath($target)->first();
|
||||||
if ($foundConfig) {
|
if ($foundConfig) {
|
||||||
$contentNotNull_temp = data_get($foundConfig, 'content');
|
$content = data_get($foundConfig, 'content');
|
||||||
if ($contentNotNull_temp) {
|
|
||||||
$content = $contentNotNull_temp;
|
|
||||||
}
|
|
||||||
$isDirectory = data_get($foundConfig, 'is_directory');
|
$isDirectory = data_get($foundConfig, 'is_directory');
|
||||||
} else {
|
} else {
|
||||||
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory
|
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory
|
||||||
|
|
@ -2747,7 +2765,6 @@ function serviceParser(Service $resource): Collection
|
||||||
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
|
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// If parsing fails, keep the original docker_compose_raw unchanged
|
// If parsing fails, keep the original docker_compose_raw unchanged
|
||||||
ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data_forget($resource, 'environment_variables');
|
data_forget($resource, 'environment_variables');
|
||||||
|
|
|
||||||
|
|
@ -1764,7 +1764,6 @@ function validateDNSEntry(string $fqdn, Server $server)
|
||||||
$query = new DNSQuery($dns_server);
|
$query = new DNSQuery($dns_server);
|
||||||
$results = $query->query($host, $type);
|
$results = $query->query($host, $type);
|
||||||
if ($results === false || $query->hasError()) {
|
if ($results === false || $query->hasError()) {
|
||||||
ray('Error: '.$query->getLasterror());
|
|
||||||
} else {
|
} else {
|
||||||
foreach ($results as $result) {
|
foreach ($results as $result) {
|
||||||
if ($result->getType() == $type) {
|
if ($result->getType() == $type) {
|
||||||
|
|
@ -3749,6 +3748,27 @@ function redirectRoute(Component $component, string $name, array $parameters = [
|
||||||
return $component->redirectRoute($name, $parameters, navigate: $navigate);
|
return $component->redirectRoute($name, $parameters, navigate: $navigate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coolifyRegistryUrl(): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return instanceSettings()->docker_registry_url ?: 'docker.io';
|
||||||
|
} catch (Throwable) {
|
||||||
|
return config('constants.coolify.registry_url', 'docker.io');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function coolifyHelperImage(): string
|
||||||
|
{
|
||||||
|
$configuredHelperImage = config('constants.coolify.helper_image');
|
||||||
|
$configuredDefaultHelperImage = config('constants.coolify.registry_url', 'docker.io').'/coollabsio/coolify-helper';
|
||||||
|
|
||||||
|
if ($configuredHelperImage !== $configuredDefaultHelperImage) {
|
||||||
|
return $configuredHelperImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return coolifyRegistryUrl().'/coollabsio/coolify-helper';
|
||||||
|
}
|
||||||
|
|
||||||
function getHelperVersion(): string
|
function getHelperVersion(): string
|
||||||
{
|
{
|
||||||
$settings = instanceSettings();
|
$settings = instanceSettings();
|
||||||
|
|
@ -3766,9 +3786,6 @@ function loggy($message = null, array $context = [])
|
||||||
if (! isDev()) {
|
if (! isDev()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (function_exists('ray') && config('app.debug')) {
|
|
||||||
ray($message, $context);
|
|
||||||
}
|
|
||||||
if (is_null($message)) {
|
if (is_null($message)) {
|
||||||
return app('log');
|
return app('log');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@
|
||||||
"spatie/laravel-activitylog": "^4.11.0",
|
"spatie/laravel-activitylog": "^4.11.0",
|
||||||
"spatie/laravel-data": "^4.19.1",
|
"spatie/laravel-data": "^4.19.1",
|
||||||
"spatie/laravel-markdown": "^2.7.1",
|
"spatie/laravel-markdown": "^2.7.1",
|
||||||
"spatie/laravel-ray": "^1.43.5",
|
|
||||||
"spatie/laravel-schemaless-attributes": "^2.5.1",
|
"spatie/laravel-schemaless-attributes": "^2.5.1",
|
||||||
"spatie/url": "^2.4",
|
"spatie/url": "^2.4",
|
||||||
"stevebauman/purify": "^6.3.1",
|
"stevebauman/purify": "^6.3.1",
|
||||||
|
|
|
||||||
792
composer.lock
generated
792
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "64b77285a7140ce68e83db2659e9a21d",
|
"content-hash": "dff5cce0f3fe7ba422d9d121cce7c082",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
|
|
@ -4753,134 +4753,6 @@
|
||||||
},
|
},
|
||||||
"time": "2020-10-15T08:29:30+00:00"
|
"time": "2020-10-15T08:29:30+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "php-di/invoker",
|
|
||||||
"version": "2.3.7",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHP-DI/Invoker.git",
|
|
||||||
"reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1",
|
|
||||||
"reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=7.3",
|
|
||||||
"psr/container": "^1.0|^2.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"athletic/athletic": "~0.1.8",
|
|
||||||
"mnapoli/hard-mode": "~0.3.0",
|
|
||||||
"phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Invoker\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "Generic and extensible callable invoker",
|
|
||||||
"homepage": "https://github.com/PHP-DI/Invoker",
|
|
||||||
"keywords": [
|
|
||||||
"callable",
|
|
||||||
"dependency",
|
|
||||||
"dependency-injection",
|
|
||||||
"injection",
|
|
||||||
"invoke",
|
|
||||||
"invoker"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/PHP-DI/Invoker/issues",
|
|
||||||
"source": "https://github.com/PHP-DI/Invoker/tree/2.3.7"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/mnapoli",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-08-30T10:22:22+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "php-di/php-di",
|
|
||||||
"version": "7.1.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHP-DI/PHP-DI.git",
|
|
||||||
"reference": "f88054cc052e40dbe7b383c8817c19442d480352"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352",
|
|
||||||
"reference": "f88054cc052e40dbe7b383c8817c19442d480352",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"laravel/serializable-closure": "^1.0 || ^2.0",
|
|
||||||
"php": ">=8.0",
|
|
||||||
"php-di/invoker": "^2.0",
|
|
||||||
"psr/container": "^1.1 || ^2.0"
|
|
||||||
},
|
|
||||||
"provide": {
|
|
||||||
"psr/container-implementation": "^1.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"friendsofphp/php-cs-fixer": "^3",
|
|
||||||
"friendsofphp/proxy-manager-lts": "^1",
|
|
||||||
"mnapoli/phpunit-easymock": "^1.3",
|
|
||||||
"phpunit/phpunit": "^9.6 || ^10 || ^11",
|
|
||||||
"vimeo/psalm": "^5|^6"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"src/functions.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"DI\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "The dependency injection container for humans",
|
|
||||||
"homepage": "https://php-di.org/",
|
|
||||||
"keywords": [
|
|
||||||
"PSR-11",
|
|
||||||
"container",
|
|
||||||
"container-interop",
|
|
||||||
"dependency injection",
|
|
||||||
"di",
|
|
||||||
"ioc",
|
|
||||||
"psr11"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/PHP-DI/PHP-DI/issues",
|
|
||||||
"source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/mnapoli",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/php-di/php-di",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-08-16T11:10:48+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "phpdocumentor/reflection-common",
|
"name": "phpdocumentor/reflection-common",
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
|
|
@ -7026,70 +6898,6 @@
|
||||||
},
|
},
|
||||||
"time": "2024-11-07T21:57:40+00:00"
|
"time": "2024-11-07T21:57:40+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "spatie/backtrace",
|
|
||||||
"version": "1.8.2",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/spatie/backtrace.git",
|
|
||||||
"reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/spatie/backtrace/zipball/8ffe78be5ed355b5009e3dd989d183433e9a5adc",
|
|
||||||
"reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.3 || ^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"ext-json": "*",
|
|
||||||
"laravel/serializable-closure": "^1.3 || ^2.0",
|
|
||||||
"phpunit/phpunit": "^9.3 || ^11.4.3",
|
|
||||||
"spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6",
|
|
||||||
"symfony/var-dumper": "^5.1|^6.0|^7.0|^8.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Spatie\\Backtrace\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Freek Van de Herten",
|
|
||||||
"email": "freek@spatie.be",
|
|
||||||
"homepage": "https://spatie.be",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A better backtrace",
|
|
||||||
"homepage": "https://github.com/spatie/backtrace",
|
|
||||||
"keywords": [
|
|
||||||
"Backtrace",
|
|
||||||
"spatie"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/spatie/backtrace/issues",
|
|
||||||
"source": "https://github.com/spatie/backtrace/tree/1.8.2"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/sponsors/spatie",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://spatie.be/open-source/support-us",
|
|
||||||
"type": "other"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-03-11T13:48:28+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "spatie/commonmark-shiki-highlighter",
|
"name": "spatie/commonmark-shiki-highlighter",
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
|
|
@ -7462,95 +7270,6 @@
|
||||||
],
|
],
|
||||||
"time": "2026-05-19T14:06:37+00:00"
|
"time": "2026-05-19T14:06:37+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "spatie/laravel-ray",
|
|
||||||
"version": "1.43.9",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/spatie/laravel-ray.git",
|
|
||||||
"reference": "85137a6ea1d3ecd5ad3adcb43512fff9a5529e72"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/spatie/laravel-ray/zipball/85137a6ea1d3ecd5ad3adcb43512fff9a5529e72",
|
|
||||||
"reference": "85137a6ea1d3ecd5ad3adcb43512fff9a5529e72",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"composer-runtime-api": "^2.2",
|
|
||||||
"ext-json": "*",
|
|
||||||
"illuminate/contracts": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"illuminate/database": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"illuminate/queue": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"illuminate/support": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"php": "^7.4|^8.0",
|
|
||||||
"spatie/backtrace": "^1.7.1",
|
|
||||||
"spatie/ray": "^1.45.0",
|
|
||||||
"symfony/stopwatch": "4.2|^5.1|^6.0|^7.0|^8.0",
|
|
||||||
"zbateson/mail-mime-parser": "^1.3.1|^2.0|^3.0|^4.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"guzzlehttp/guzzle": "^7.3",
|
|
||||||
"laravel/framework": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"laravel/pint": "^1.29",
|
|
||||||
"orchestra/testbench-core": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
|
|
||||||
"pestphp/pest": "^1.22|^2.0|^3.0|^4.0",
|
|
||||||
"phpstan/phpstan": "^1.10.57|^2.0.2",
|
|
||||||
"phpunit/phpunit": "^9.3|^10.1|^11.0.10|^12.4",
|
|
||||||
"rector/rector": "^0.19.2|^1.0.1|^2.0.0",
|
|
||||||
"spatie/pest-plugin-snapshots": "^1.1|^2.0",
|
|
||||||
"symfony/var-dumper": "^4.2|^5.1|^6.0|^7.0.3|^8.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"laravel": {
|
|
||||||
"providers": [
|
|
||||||
"Spatie\\LaravelRay\\RayServiceProvider"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "1.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Spatie\\LaravelRay\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Freek Van der Herten",
|
|
||||||
"email": "freek@spatie.be",
|
|
||||||
"homepage": "https://spatie.be",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Easily debug Laravel apps",
|
|
||||||
"homepage": "https://github.com/spatie/laravel-ray",
|
|
||||||
"keywords": [
|
|
||||||
"laravel-ray",
|
|
||||||
"spatie"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/spatie/laravel-ray/issues",
|
|
||||||
"source": "https://github.com/spatie/laravel-ray/tree/1.43.9"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/sponsors/spatie",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://spatie.be/open-source/support-us",
|
|
||||||
"type": "other"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-04-28T06:07:04+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-schemaless-attributes",
|
"name": "spatie/laravel-schemaless-attributes",
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
|
|
@ -7757,91 +7476,6 @@
|
||||||
],
|
],
|
||||||
"time": "2026-04-28T06:26:02+00:00"
|
"time": "2026-04-28T06:26:02+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "spatie/ray",
|
|
||||||
"version": "1.48.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/spatie/ray.git",
|
|
||||||
"reference": "974ac9c6e315033ab8ace883d60e094522f88ede"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/spatie/ray/zipball/974ac9c6e315033ab8ace883d60e094522f88ede",
|
|
||||||
"reference": "974ac9c6e315033ab8ace883d60e094522f88ede",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-curl": "*",
|
|
||||||
"ext-json": "*",
|
|
||||||
"php": "^7.4|^8.0",
|
|
||||||
"ramsey/uuid": "^3.0|^4.1",
|
|
||||||
"spatie/backtrace": "^1.7.1",
|
|
||||||
"spatie/macroable": "^1.0|^2.0",
|
|
||||||
"symfony/stopwatch": "^4.2|^5.1|^6.0|^7.0|^8.0",
|
|
||||||
"symfony/var-dumper": "^4.2|^5.1|^6.0|^7.0.3|^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"illuminate/support": "^7.20|^8.18|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"nesbot/carbon": "^2.63|^3.8.4",
|
|
||||||
"pestphp/pest": "^1.22",
|
|
||||||
"phpstan/phpstan": "^1.10.57|^2.0.3",
|
|
||||||
"phpunit/phpunit": "^9.5",
|
|
||||||
"rector/rector": "^0.19.2|^1.0.1|^2.0.0",
|
|
||||||
"spatie/phpunit-snapshot-assertions": "^4.2",
|
|
||||||
"spatie/test-time": "^1.2"
|
|
||||||
},
|
|
||||||
"bin": [
|
|
||||||
"bin/remove-ray.sh"
|
|
||||||
],
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "1.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"src/helpers.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"Spatie\\Ray\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Freek Van der Herten",
|
|
||||||
"email": "freek@spatie.be",
|
|
||||||
"homepage": "https://spatie.be",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Debug with Ray to fix problems faster",
|
|
||||||
"homepage": "https://github.com/spatie/ray",
|
|
||||||
"keywords": [
|
|
||||||
"ray",
|
|
||||||
"spatie"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/spatie/ray/issues",
|
|
||||||
"source": "https://github.com/spatie/ray/tree/1.48.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/sponsors/spatie",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://spatie.be/open-source/support-us",
|
|
||||||
"type": "other"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-03-31T12:44:31+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "spatie/shiki-php",
|
"name": "spatie/shiki-php",
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
|
|
@ -9503,90 +9137,6 @@
|
||||||
],
|
],
|
||||||
"time": "2026-04-10T16:19:22+00:00"
|
"time": "2026-04-10T16:19:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "symfony/polyfill-iconv",
|
|
||||||
"version": "v1.37.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/polyfill-iconv.git",
|
|
||||||
"reference": "2c5729fd241b4b22f6e4b436bc3354a4f262df57"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/2c5729fd241b4b22f6e4b436bc3354a4f262df57",
|
|
||||||
"reference": "2c5729fd241b4b22f6e4b436bc3354a4f262df57",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=7.2"
|
|
||||||
},
|
|
||||||
"provide": {
|
|
||||||
"ext-iconv": "*"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-iconv": "For best performance"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"thanks": {
|
|
||||||
"url": "https://github.com/symfony/polyfill",
|
|
||||||
"name": "symfony/polyfill"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"bootstrap.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Polyfill\\Iconv\\": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Nicolas Grekas",
|
|
||||||
"email": "p@tchwork.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Symfony polyfill for the Iconv extension",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"keywords": [
|
|
||||||
"compatibility",
|
|
||||||
"iconv",
|
|
||||||
"polyfill",
|
|
||||||
"portable",
|
|
||||||
"shim"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/symfony/polyfill-iconv/tree/v1.37.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://symfony.com/sponsor",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/fabpot",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/nicolas-grekas",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-04-10T16:19:22+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-grapheme",
|
"name": "symfony/polyfill-intl-grapheme",
|
||||||
"version": "v1.38.1",
|
"version": "v1.38.1",
|
||||||
|
|
@ -10922,72 +10472,6 @@
|
||||||
],
|
],
|
||||||
"time": "2026-03-28T09:44:51+00:00"
|
"time": "2026-03-28T09:44:51+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "symfony/stopwatch",
|
|
||||||
"version": "v8.0.8",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/stopwatch.git",
|
|
||||||
"reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/85954ed72d5440ea4dc9a10b7e49e01df766ffa3",
|
|
||||||
"reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=8.4",
|
|
||||||
"symfony/service-contracts": "^2.5|^3"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Component\\Stopwatch\\": ""
|
|
||||||
},
|
|
||||||
"exclude-from-classmap": [
|
|
||||||
"/Tests/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Fabien Potencier",
|
|
||||||
"email": "fabien@symfony.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Provides a way to profile code",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/symfony/stopwatch/tree/v8.0.8"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://symfony.com/sponsor",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/fabpot",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/nicolas-grekas",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-03-30T15:14:47+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "symfony/string",
|
"name": "symfony/string",
|
||||||
"version": "v8.0.13",
|
"version": "v8.0.13",
|
||||||
|
|
@ -12192,214 +11676,6 @@
|
||||||
},
|
},
|
||||||
"time": "2018-08-08T15:08:14+00:00"
|
"time": "2018-08-08T15:08:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "zbateson/mail-mime-parser",
|
|
||||||
"version": "4.0.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/zbateson/mail-mime-parser.git",
|
|
||||||
"reference": "3db681988a48fdffdba551dcc6b2f4c2da574540"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/3db681988a48fdffdba551dcc6b2f4c2da574540",
|
|
||||||
"reference": "3db681988a48fdffdba551dcc6b2f4c2da574540",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"guzzlehttp/psr7": "^2.5",
|
|
||||||
"php": ">=8.1",
|
|
||||||
"php-di/php-di": "^6.0|^7.0",
|
|
||||||
"psr/log": "^1|^2|^3",
|
|
||||||
"zbateson/mb-wrapper": "^2.0 || ^3.0",
|
|
||||||
"zbateson/stream-decorators": "^2.1 || ^3.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"friendsofphp/php-cs-fixer": "^3.0",
|
|
||||||
"monolog/monolog": "^2|^3",
|
|
||||||
"phpstan/phpstan": "^2.0",
|
|
||||||
"phpunit/phpunit": "^10.5"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-iconv": "For best support/performance",
|
|
||||||
"ext-mbstring": "For best support/performance"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"ZBateson\\MailMimeParser\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"BSD-2-Clause"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Zaahid Bateson"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Contributors",
|
|
||||||
"homepage": "https://github.com/zbateson/mail-mime-parser/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "MIME email message parser",
|
|
||||||
"homepage": "https://mail-mime-parser.org",
|
|
||||||
"keywords": [
|
|
||||||
"MimeMailParser",
|
|
||||||
"email",
|
|
||||||
"mail",
|
|
||||||
"mailparse",
|
|
||||||
"mime",
|
|
||||||
"mimeparse",
|
|
||||||
"parser",
|
|
||||||
"php-imap"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"docs": "https://mail-mime-parser.org/#usage-guide",
|
|
||||||
"issues": "https://github.com/zbateson/mail-mime-parser/issues",
|
|
||||||
"source": "https://github.com/zbateson/mail-mime-parser"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/zbateson",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-03-11T18:03:41+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "zbateson/mb-wrapper",
|
|
||||||
"version": "3.0.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/zbateson/mb-wrapper.git",
|
|
||||||
"reference": "f0ee6af2712e92e52ee2552588cd69d21ab3363f"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/f0ee6af2712e92e52ee2552588cd69d21ab3363f",
|
|
||||||
"reference": "f0ee6af2712e92e52ee2552588cd69d21ab3363f",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=8.1",
|
|
||||||
"symfony/polyfill-iconv": "^1.9",
|
|
||||||
"symfony/polyfill-mbstring": "^1.9"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"friendsofphp/php-cs-fixer": "*",
|
|
||||||
"phpstan/phpstan": "*",
|
|
||||||
"phpunit/phpunit": "^10.0|^11.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-iconv": "For best support/performance",
|
|
||||||
"ext-mbstring": "For best support/performance"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"ZBateson\\MbWrapper\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"BSD-2-Clause"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Zaahid Bateson"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Wrapper for mbstring with fallback to iconv for encoding conversion and string manipulation",
|
|
||||||
"keywords": [
|
|
||||||
"charset",
|
|
||||||
"encoding",
|
|
||||||
"http",
|
|
||||||
"iconv",
|
|
||||||
"mail",
|
|
||||||
"mb",
|
|
||||||
"mb_convert_encoding",
|
|
||||||
"mbstring",
|
|
||||||
"mime",
|
|
||||||
"multibyte",
|
|
||||||
"string"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/zbateson/mb-wrapper/issues",
|
|
||||||
"source": "https://github.com/zbateson/mb-wrapper/tree/3.0.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/zbateson",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-02-13T19:33:26+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "zbateson/stream-decorators",
|
|
||||||
"version": "3.0.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/zbateson/stream-decorators.git",
|
|
||||||
"reference": "0c0e79a8c960055c0e2710357098eedc07e6697a"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/0c0e79a8c960055c0e2710357098eedc07e6697a",
|
|
||||||
"reference": "0c0e79a8c960055c0e2710357098eedc07e6697a",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"guzzlehttp/psr7": "^2.5",
|
|
||||||
"php": ">=8.1",
|
|
||||||
"zbateson/mb-wrapper": "^2.0 || ^3.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"friendsofphp/php-cs-fixer": "*",
|
|
||||||
"phpstan/phpstan": "*",
|
|
||||||
"phpunit/phpunit": "^10.0 || ^11.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"ZBateson\\StreamDecorators\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"BSD-2-Clause"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Zaahid Bateson"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "PHP psr7 stream decorators for mime message part streams",
|
|
||||||
"keywords": [
|
|
||||||
"base64",
|
|
||||||
"charset",
|
|
||||||
"decorators",
|
|
||||||
"mail",
|
|
||||||
"mime",
|
|
||||||
"psr7",
|
|
||||||
"quoted-printable",
|
|
||||||
"stream",
|
|
||||||
"uuencode"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/zbateson/stream-decorators/issues",
|
|
||||||
"source": "https://github.com/zbateson/stream-decorators/tree/3.0.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/zbateson",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-02-13T19:45:34+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "zircote/swagger-php",
|
"name": "zircote/swagger-php",
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
|
|
@ -17401,6 +16677,70 @@
|
||||||
],
|
],
|
||||||
"time": "2026-04-16T21:33:58+00:00"
|
"time": "2026-04-16T21:33:58+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/backtrace",
|
||||||
|
"version": "1.8.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/backtrace.git",
|
||||||
|
"reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/backtrace/zipball/8ffe78be5ed355b5009e3dd989d183433e9a5adc",
|
||||||
|
"reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.3 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"laravel/serializable-closure": "^1.3 || ^2.0",
|
||||||
|
"phpunit/phpunit": "^9.3 || ^11.4.3",
|
||||||
|
"spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6",
|
||||||
|
"symfony/var-dumper": "^5.1|^6.0|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\Backtrace\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van de Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A better backtrace",
|
||||||
|
"homepage": "https://github.com/spatie/backtrace",
|
||||||
|
"keywords": [
|
||||||
|
"Backtrace",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/backtrace/issues",
|
||||||
|
"source": "https://github.com/spatie/backtrace/tree/1.8.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/sponsors/spatie",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://spatie.be/open-source/support-us",
|
||||||
|
"type": "other"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-11T13:48:28+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/error-solutions",
|
"name": "spatie/error-solutions",
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
|
|
@ -18076,5 +17416,5 @@
|
||||||
"php": "^8.4"
|
"php": "^8.4"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.9.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@
|
||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
'autoupdate' => env('AUTOUPDATE'),
|
'autoupdate' => env('AUTOUPDATE'),
|
||||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||||
'registry_url' => env('REGISTRY_URL', 'ghcr.io'),
|
'registry_url' => env('REGISTRY_URL', 'docker.io'),
|
||||||
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
|
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'docker.io').'/coollabsio/coolify-helper'),
|
||||||
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'),
|
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'docker.io').'/coollabsio/coolify-realtime'),
|
||||||
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
||||||
'cdn_url' => env('CDN_URL', 'https://cdn.coollabs.io'),
|
'cdn_url' => env('CDN_URL', 'https://cdn.coollabs.io'),
|
||||||
'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/versions.json'),
|
'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/versions.json'),
|
||||||
|
|
@ -86,7 +86,6 @@
|
||||||
|
|
||||||
'invitation' => [
|
'invitation' => [
|
||||||
'link' => [
|
'link' => [
|
||||||
'base_url' => '/invitations/',
|
|
||||||
'expiration_days' => 3,
|
'expiration_days' => 3,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
|
||||||
108
config/ray.php
108
config/ray.php
|
|
@ -1,108 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
/*
|
|
||||||
* This setting controls whether data should be sent to Ray.
|
|
||||||
*
|
|
||||||
* By default, `ray()` will only transmit data in non-production environments.
|
|
||||||
*/
|
|
||||||
'enable' => env('RAY_ENABLED', true),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, all cache events will automatically be sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_cache_to_ray' => env('SEND_CACHE_TO_RAY', false),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, all things passed to `dump` or `dd`
|
|
||||||
* will be sent to Ray as well.
|
|
||||||
*/
|
|
||||||
'send_dumps_to_ray' => env('SEND_DUMPS_TO_RAY', true),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled all job events will automatically be sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_jobs_to_ray' => env('SEND_JOBS_TO_RAY', false),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, all things logged to the application log
|
|
||||||
* will be sent to Ray as well.
|
|
||||||
*/
|
|
||||||
'send_log_calls_to_ray' => env('SEND_LOG_CALLS_TO_RAY', true),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, all queries will automatically be sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_queries_to_ray' => env('SEND_QUERIES_TO_RAY', false),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When enabled, all duplicate queries will automatically be sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_duplicate_queries_to_ray' => env('SEND_DUPLICATE_QUERIES_TO_RAY', false),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, slow queries will automatically be sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_slow_queries_to_ray' => env('SEND_SLOW_QUERIES_TO_RAY', false),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries that are longer than this number of milliseconds will be regarded as slow.
|
|
||||||
*/
|
|
||||||
'slow_query_threshold_in_ms' => env('RAY_SLOW_QUERY_THRESHOLD_IN_MS', 500),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, all requests made to this app will automatically be sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_requests_to_ray' => env('SEND_REQUESTS_TO_RAY', false),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When enabled, all Http Client requests made by this app will be automatically sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_http_client_requests_to_ray' => env('SEND_HTTP_CLIENT_REQUESTS_TO_RAY', false),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, all views that are rendered automatically be sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_views_to_ray' => env('SEND_VIEWS_TO_RAY', false),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, all exceptions will be automatically sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_exceptions_to_ray' => env('SEND_EXCEPTIONS_TO_RAY', true),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When enabled, all deprecation notices will be automatically sent to Ray.
|
|
||||||
*/
|
|
||||||
'send_deprecated_notices_to_ray' => env('SEND_DEPRECATED_NOTICES_TO_RAY', false),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The host used to communicate with the Ray app.
|
|
||||||
* When using Docker on Mac or Windows, you can replace localhost with 'host.docker.internal'
|
|
||||||
* When using Docker on Linux, you can replace localhost with '172.17.0.1'
|
|
||||||
* When using Homestead with the VirtualBox provider, you can replace localhost with '10.0.2.2'
|
|
||||||
* When using Homestead with the Parallels provider, you can replace localhost with '10.211.55.2'
|
|
||||||
*/
|
|
||||||
'host' => env('RAY_HOST', 'host.docker.internal'),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The port number used to communicate with the Ray app.
|
|
||||||
*/
|
|
||||||
'port' => env('RAY_PORT', 23517),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Absolute base path for your sites or projects in Homestead,
|
|
||||||
* Vagrant, Docker, or another remote development server.
|
|
||||||
*/
|
|
||||||
'remote_path' => env('RAY_REMOTE_PATH', null),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Absolute base path for your sites or projects on your local
|
|
||||||
* computer where your IDE or code editor is running on.
|
|
||||||
*/
|
|
||||||
'local_path' => env('RAY_LOCAL_PATH', null),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When this setting is enabled, the package will not try to format values sent to Ray.
|
|
||||||
*/
|
|
||||||
'always_send_raw_values' => false,
|
|
||||||
];
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('instance_settings', function (Blueprint $table) {
|
||||||
|
$table->string('docker_registry_url')->default('docker.io')->after('is_auto_update_enabled');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('instance_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('docker_registry_url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('instance_settings', function (Blueprint $table) {
|
||||||
|
$table->json('webhook_allowed_internal_hosts')->nullable();
|
||||||
|
$table->boolean('webhook_allow_localhost')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('instance_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['webhook_allowed_internal_hosts', 'webhook_allow_localhost']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -20,14 +20,33 @@ class DevelopmentRailpackExamplesSeeder extends Seeder
|
||||||
{
|
{
|
||||||
public const PROJECT_UUID = 'railpack-examples';
|
public const PROJECT_UUID = 'railpack-examples';
|
||||||
|
|
||||||
public const ENVIRONMENT_UUID = 'railpack-examples-production';
|
|
||||||
|
|
||||||
public const GIT_REPOSITORY = 'coollabsio/coolify-examples';
|
public const GIT_REPOSITORY = 'coollabsio/coolify-examples';
|
||||||
|
|
||||||
public const GIT_BRANCH = 'next';
|
public const GIT_BRANCH = 'next';
|
||||||
|
|
||||||
public const REPOSITORY_PROJECT_ID = 603035348;
|
public const REPOSITORY_PROJECT_ID = 603035348;
|
||||||
|
|
||||||
|
public const LIMA_SERVERS = [
|
||||||
|
[
|
||||||
|
'server_uuid' => 'lima-ubuntu-2404',
|
||||||
|
'server_name' => 'lima-ubuntu-2404',
|
||||||
|
'port' => 2222,
|
||||||
|
'environment_name' => 'ubuntu24',
|
||||||
|
'environment_uuid' => 'railpack-examples-ubuntu24',
|
||||||
|
'uuid_prefix' => 'ubuntu24-',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'server_uuid' => 'lima-ubuntu-2604',
|
||||||
|
'server_name' => 'lima-ubuntu-2604',
|
||||||
|
'port' => 2223,
|
||||||
|
'environment_name' => 'ubuntu26',
|
||||||
|
'environment_uuid' => 'railpack-examples-ubuntu26',
|
||||||
|
'uuid_prefix' => 'ubuntu26-',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
private const LIMA_SENTINEL_URL = 'http://host.lima.internal:8000';
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
if (! $this->isDevelopmentEnvironment()) {
|
if (! $this->isDevelopmentEnvironment()) {
|
||||||
|
|
@ -37,16 +56,22 @@ public function run(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->ensureDevelopmentPrerequisitesExist();
|
$this->ensureDevelopmentPrerequisitesExist();
|
||||||
$destination = StandaloneDocker::query()->find(0);
|
|
||||||
|
|
||||||
if (! $destination) {
|
if (! StandaloneDocker::query()->find(0)) {
|
||||||
throw new RuntimeException('StandaloneDocker with id=0 is required before running DevelopmentRailpackExamplesSeeder.');
|
throw new RuntimeException('StandaloneDocker with id=0 is required before running DevelopmentRailpackExamplesSeeder.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$environment = $this->prepareEnvironment();
|
$this->cleanupLegacyLimaProjects();
|
||||||
|
$this->cleanupLegacyProductionExamples();
|
||||||
|
|
||||||
foreach (self::examples() as $example) {
|
foreach (self::LIMA_SERVERS as $limaServer) {
|
||||||
$this->upsertApplication($environment, $destination, $example);
|
$this->seedEnvironment(
|
||||||
|
environmentUuid: $limaServer['environment_uuid'],
|
||||||
|
environmentName: $limaServer['environment_name'],
|
||||||
|
destination: $this->limaDestination($limaServer['server_uuid']),
|
||||||
|
uuidPrefix: $limaServer['uuid_prefix'],
|
||||||
|
nameSuffix: " ({$limaServer['environment_name']})",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,6 +465,37 @@ private function ensureDevelopmentPrerequisitesExist(): void
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
foreach (self::LIMA_SERVERS as $limaServer) {
|
||||||
|
$server = Server::query()->firstOrCreate(
|
||||||
|
['uuid' => $limaServer['server_uuid']],
|
||||||
|
[
|
||||||
|
'name' => $limaServer['server_name'],
|
||||||
|
'description' => 'This is a Lima VM for local development testing',
|
||||||
|
'ip' => 'host.docker.internal',
|
||||||
|
'port' => $limaServer['port'],
|
||||||
|
'team_id' => 0,
|
||||||
|
'private_key_id' => 1,
|
||||||
|
'proxy' => [
|
||||||
|
'type' => ProxyTypes::TRAEFIK->value,
|
||||||
|
'status' => ProxyStatus::EXITED->value,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$server->settings->forceFill([
|
||||||
|
'sentinel_custom_url' => self::LIMA_SENTINEL_URL,
|
||||||
|
])->saveQuietly();
|
||||||
|
|
||||||
|
StandaloneDocker::query()->firstOrCreate(
|
||||||
|
['server_id' => $server->id],
|
||||||
|
[
|
||||||
|
'uuid' => "{$limaServer['server_uuid']}-docker",
|
||||||
|
'name' => "{$limaServer['server_name']} Docker",
|
||||||
|
'network' => 'coolify',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
StandaloneDocker::query()->firstOrCreate(
|
StandaloneDocker::query()->firstOrCreate(
|
||||||
['id' => 0],
|
['id' => 0],
|
||||||
[
|
[
|
||||||
|
|
@ -489,7 +545,71 @@ private function isDevelopmentEnvironment(): bool
|
||||||
return in_array(config('app.env'), ['local', 'development', 'dev'], true);
|
return in_array(config('app.env'), ['local', 'development', 'dev'], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function prepareEnvironment(): Environment
|
private function limaDestination(string $serverUuid): StandaloneDocker
|
||||||
|
{
|
||||||
|
$limaDestination = Server::query()
|
||||||
|
->where('uuid', $serverUuid)
|
||||||
|
->first()
|
||||||
|
?->standaloneDockers()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $limaDestination) {
|
||||||
|
throw new RuntimeException("Lima StandaloneDocker destination is required for {$serverUuid} before running DevelopmentRailpackExamplesSeeder.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $limaDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanupLegacyLimaProjects(): void
|
||||||
|
{
|
||||||
|
Project::query()
|
||||||
|
->whereIn('uuid', [
|
||||||
|
'railpack-examples-lima-ubuntu-2404',
|
||||||
|
'railpack-examples-lima-ubuntu-2604',
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->each(function (Project $project): void {
|
||||||
|
Application::withTrashed()
|
||||||
|
->whereIn('environment_id', $project->environments()->pluck('id'))
|
||||||
|
->get()
|
||||||
|
->each
|
||||||
|
->forceDelete();
|
||||||
|
|
||||||
|
$project->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanupLegacyProductionExamples(): void
|
||||||
|
{
|
||||||
|
$project = Project::query()->where('uuid', self::PROJECT_UUID)->first();
|
||||||
|
|
||||||
|
if (! $project) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Application::withTrashed()
|
||||||
|
->whereIn('environment_id', $project->environments()->pluck('id'))
|
||||||
|
->whereIn('uuid', collect(self::examples())->pluck('uuid'))
|
||||||
|
->get()
|
||||||
|
->each
|
||||||
|
->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedEnvironment(
|
||||||
|
string $environmentUuid,
|
||||||
|
string $environmentName,
|
||||||
|
StandaloneDocker $destination,
|
||||||
|
string $uuidPrefix = '',
|
||||||
|
string $nameSuffix = '',
|
||||||
|
): void {
|
||||||
|
$environment = $this->prepareEnvironment($environmentUuid, $environmentName);
|
||||||
|
|
||||||
|
foreach (self::examples() as $example) {
|
||||||
|
$this->upsertApplication($environment, $destination, $example, $uuidPrefix, $nameSuffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepareEnvironment(string $environmentUuid, string $environmentName): Environment
|
||||||
{
|
{
|
||||||
$project = Project::query()->firstOrNew(['uuid' => self::PROJECT_UUID]);
|
$project = Project::query()->firstOrNew(['uuid' => self::PROJECT_UUID]);
|
||||||
$project->fill([
|
$project->fill([
|
||||||
|
|
@ -499,17 +619,29 @@ private function prepareEnvironment(): Environment
|
||||||
]);
|
]);
|
||||||
$project->save();
|
$project->save();
|
||||||
|
|
||||||
$environment = $project->environments()->first();
|
$environment = $project->environments()
|
||||||
|
->where(function ($query) use ($environmentName, $environmentUuid): void {
|
||||||
|
$query
|
||||||
|
->where('name', $environmentName)
|
||||||
|
->orWhere('uuid', $environmentUuid);
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$existingEnvironment = $project->environments()->first();
|
||||||
|
|
||||||
|
if (! $environment && $project->environments()->count() === 1 && $existingEnvironment?->name === 'production') {
|
||||||
|
$environment = $existingEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
if (! $environment) {
|
if (! $environment) {
|
||||||
$environment = $project->environments()->create([
|
$environment = $project->environments()->create([
|
||||||
'name' => 'production',
|
'name' => $environmentName,
|
||||||
'uuid' => self::ENVIRONMENT_UUID,
|
'uuid' => $environmentUuid,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$environment->update([
|
$environment->update([
|
||||||
'name' => 'production',
|
'name' => $environmentName,
|
||||||
'uuid' => self::ENVIRONMENT_UUID,
|
'uuid' => $environmentUuid,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -519,13 +651,15 @@ private function prepareEnvironment(): Environment
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $example
|
* @param array<string, mixed> $example
|
||||||
*/
|
*/
|
||||||
private function upsertApplication(Environment $environment, StandaloneDocker $destination, array $example): void
|
private function upsertApplication(Environment $environment, StandaloneDocker $destination, array $example, string $uuidPrefix = '', string $nameSuffix = ''): void
|
||||||
{
|
{
|
||||||
$application = Application::withTrashed()->firstOrNew(['uuid' => $example['uuid']]);
|
$uuid = $uuidPrefix.$example['uuid'];
|
||||||
|
$name = $example['name'].$nameSuffix;
|
||||||
|
$application = Application::withTrashed()->firstOrNew(['uuid' => $uuid]);
|
||||||
$application->fill([
|
$application->fill([
|
||||||
'name' => $example['name'],
|
'name' => $name,
|
||||||
'description' => $example['name'],
|
'description' => $name,
|
||||||
'fqdn' => "http://{$example['uuid']}.127.0.0.1.sslip.io",
|
'fqdn' => "http://{$uuid}.127.0.0.1.sslip.io",
|
||||||
'repository_project_id' => $example['repository_project_id'] ?? self::REPOSITORY_PROJECT_ID,
|
'repository_project_id' => $example['repository_project_id'] ?? self::REPOSITORY_PROJECT_ID,
|
||||||
'git_repository' => $example['git_repository'] ?? self::GIT_REPOSITORY,
|
'git_repository' => $example['git_repository'] ?? self::GIT_REPOSITORY,
|
||||||
'git_branch' => $example['git_branch'] ?? self::GIT_BRANCH,
|
'git_branch' => $example['git_branch'] ?? self::GIT_BRANCH,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,11 @@
|
||||||
|
|
||||||
class ProjectSeeder extends Seeder
|
class ProjectSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
private const LIMA_ENVIRONMENTS = [
|
||||||
|
['name' => 'ubuntu24', 'uuid' => 'ubuntu24'],
|
||||||
|
['name' => 'ubuntu26', 'uuid' => 'ubuntu26'],
|
||||||
|
];
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$project = Project::create([
|
$project = Project::create([
|
||||||
|
|
@ -16,7 +21,14 @@ public function run(): void
|
||||||
'team_id' => 0,
|
'team_id' => 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Update the auto-created environment with a deterministic UUID
|
foreach (self::LIMA_ENVIRONMENTS as $index => $environment) {
|
||||||
$project->environments()->first()->update(['uuid' => 'production']);
|
if ($index === 0) {
|
||||||
|
$project->environments()->first()->update($environment);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$project->environments()->create($environment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@
|
||||||
|
|
||||||
class ServerSeeder extends Seeder
|
class ServerSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
private const LIMA_SENTINEL_URL = 'http://host.lima.internal:8000';
|
||||||
|
|
||||||
|
private const LIMA_SERVERS = [
|
||||||
|
['uuid' => 'lima-ubuntu-2404', 'name' => 'lima-ubuntu-2404', 'port' => 2222],
|
||||||
|
['uuid' => 'lima-ubuntu-2604', 'name' => 'lima-ubuntu-2604', 'port' => 2223],
|
||||||
|
];
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
Server::create([
|
Server::create([
|
||||||
|
|
@ -24,5 +31,25 @@ public function run(): void
|
||||||
'status' => ProxyStatus::EXITED->value,
|
'status' => ProxyStatus::EXITED->value,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
foreach (self::LIMA_SERVERS as $limaServer) {
|
||||||
|
$server = Server::create([
|
||||||
|
'uuid' => $limaServer['uuid'],
|
||||||
|
'name' => $limaServer['name'],
|
||||||
|
'description' => 'This is a Lima VM for local development testing',
|
||||||
|
'ip' => 'host.docker.internal',
|
||||||
|
'port' => $limaServer['port'],
|
||||||
|
'team_id' => 0,
|
||||||
|
'private_key_id' => 1,
|
||||||
|
'proxy' => [
|
||||||
|
'type' => ProxyTypes::TRAEFIK->value,
|
||||||
|
'status' => ProxyStatus::EXITED->value,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$server->settings->forceFill([
|
||||||
|
'sentinel_custom_url' => self::LIMA_SENTINEL_URL,
|
||||||
|
])->saveQuietly();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ services:
|
||||||
- GROUP_ID=${GROUPID:-1000}
|
- GROUP_ID=${GROUPID:-1000}
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8000}:8080"
|
- "${APP_PORT:-8000}:8080"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
environment:
|
environment:
|
||||||
AUTORUN_ENABLED: false
|
AUTORUN_ENABLED: false
|
||||||
PUSHER_HOST: "${PUSHER_HOST}"
|
PUSHER_HOST: "${PUSHER_HOST}"
|
||||||
|
|
@ -70,6 +72,8 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
||||||
- "6002:6002"
|
- "6002:6002"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
- ./storage:/var/www/html/storage
|
- ./storage:/var/www/html/storage
|
||||||
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
|
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ services:
|
||||||
- GROUP_ID=${GROUPID:-1000}
|
- GROUP_ID=${GROUPID:-1000}
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8000}:8080"
|
- "${APP_PORT:-8000}:8080"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
environment:
|
environment:
|
||||||
AUTORUN_ENABLED: false
|
AUTORUN_ENABLED: false
|
||||||
PUSHER_HOST: "${PUSHER_HOST}"
|
PUSHER_HOST: "${PUSHER_HOST}"
|
||||||
|
|
@ -70,6 +72,8 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
||||||
- "6002:6002"
|
- "6002:6002"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
- ./storage:/var/www/html/storage
|
- ./storage:/var/www/html/storage
|
||||||
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
|
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
coolify:
|
coolify:
|
||||||
image: "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}"
|
image: "${REGISTRY_URL:-docker.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}"
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: /data/coolify/source/.env
|
source: /data/coolify/source/.env
|
||||||
|
|
@ -60,7 +60,7 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
soketi:
|
soketi:
|
||||||
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.16'
|
image: '${REGISTRY_URL:-docker.io}/coollabsio/coolify-realtime:1.0.16'
|
||||||
ports:
|
ports:
|
||||||
- "${SOKETI_PORT:-6001}:6001"
|
- "${SOKETI_PORT:-6001}:6001"
|
||||||
- "6002:6002"
|
- "6002:6002"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
services:
|
services:
|
||||||
coolify-testing-host:
|
coolify-testing-host:
|
||||||
init: true
|
init: true
|
||||||
image: "ghcr.io/coollabsio/coolify-testing-host:latest"
|
image: "docker.io/coollabsio/coolify-testing-host:latest"
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: coolify-testing-host
|
container_name: coolify-testing-host
|
||||||
volumes:
|
volumes:
|
||||||
- //var/run/docker.sock://var/run/docker.sock
|
- //var/run/docker.sock://var/run/docker.sock
|
||||||
- ./:/data/coolify
|
- ./:/data/coolify
|
||||||
coolify:
|
coolify:
|
||||||
image: "ghcr.io/coollabsio/coolify:latest"
|
image: "docker.io/coollabsio/coolify:latest"
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: coolify
|
container_name: coolify
|
||||||
restart: always
|
restart: always
|
||||||
|
|
|
||||||
35
docker/lima/ubuntu-2404.yaml
Normal file
35
docker/lima/ubuntu-2404.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
images:
|
||||||
|
- location: https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img
|
||||||
|
arch: x86_64
|
||||||
|
- location: https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img
|
||||||
|
arch: aarch64
|
||||||
|
|
||||||
|
cpus: 2
|
||||||
|
memory: 2GiB
|
||||||
|
disk: 20GiB
|
||||||
|
|
||||||
|
containerd:
|
||||||
|
system: false
|
||||||
|
user: false
|
||||||
|
|
||||||
|
mounts: []
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
localPort: 2222
|
||||||
|
loadDotSSHPubKeys: false
|
||||||
|
|
||||||
|
provision:
|
||||||
|
- mode: system
|
||||||
|
script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
install -d -m 700 /root/.ssh
|
||||||
|
cat >/root/.ssh/authorized_keys <<'EOF'
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 sail@76ff66d2e2dd
|
||||||
|
EOF
|
||||||
|
chmod 600 /root/.ssh/authorized_keys
|
||||||
|
|
||||||
|
sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
|
||||||
|
sed -i 's/^#\?PubkeyAuthentication .*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
|
||||||
|
systemctl restart ssh
|
||||||
35
docker/lima/ubuntu-2604.yaml
Normal file
35
docker/lima/ubuntu-2604.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
images:
|
||||||
|
- location: https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-amd64.img
|
||||||
|
arch: x86_64
|
||||||
|
- location: https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-arm64.img
|
||||||
|
arch: aarch64
|
||||||
|
|
||||||
|
cpus: 2
|
||||||
|
memory: 2GiB
|
||||||
|
disk: 20GiB
|
||||||
|
|
||||||
|
containerd:
|
||||||
|
system: false
|
||||||
|
user: false
|
||||||
|
|
||||||
|
mounts: []
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
localPort: 2223
|
||||||
|
loadDotSSHPubKeys: false
|
||||||
|
|
||||||
|
provision:
|
||||||
|
- mode: system
|
||||||
|
script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
install -d -m 700 /root/.ssh
|
||||||
|
cat >/root/.ssh/authorized_keys <<'EOF'
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 sail@76ff66d2e2dd
|
||||||
|
EOF
|
||||||
|
chmod 600 /root/.ssh/authorized_keys
|
||||||
|
|
||||||
|
sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
|
||||||
|
sed -i 's/^#\?PubkeyAuthentication .*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
|
||||||
|
systemctl restart ssh
|
||||||
197
openapi.json
197
openapi.json
|
|
@ -165,6 +165,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
||||||
},
|
},
|
||||||
|
"is_preview_deployments_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable preview deployments for pull requests."
|
||||||
|
},
|
||||||
"static_image": {
|
"static_image": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -615,6 +619,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
||||||
},
|
},
|
||||||
|
"is_preview_deployments_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable preview deployments for pull requests."
|
||||||
|
},
|
||||||
"static_image": {
|
"static_image": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -1065,6 +1073,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
||||||
},
|
},
|
||||||
|
"is_preview_deployments_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable preview deployments for pull requests."
|
||||||
|
},
|
||||||
"static_image": {
|
"static_image": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -1626,6 +1638,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
||||||
},
|
},
|
||||||
|
"is_preview_deployments_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable preview deployments for pull requests."
|
||||||
|
},
|
||||||
"use_build_server": {
|
"use_build_server": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
|
|
@ -1961,6 +1977,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
||||||
},
|
},
|
||||||
|
"is_preview_deployments_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable preview deployments for pull requests."
|
||||||
|
},
|
||||||
"use_build_server": {
|
"use_build_server": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
|
|
@ -2333,6 +2353,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
||||||
},
|
},
|
||||||
|
"is_preview_deployments_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable preview deployments for pull requests."
|
||||||
|
},
|
||||||
"install_command": {
|
"install_command": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The install command."
|
"description": "The install command."
|
||||||
|
|
@ -2553,6 +2577,10 @@
|
||||||
"is_preserve_repository_enabled": {
|
"is_preserve_repository_enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false."
|
"description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false."
|
||||||
|
},
|
||||||
|
"include_source_commit_in_build": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Include source commit information in the build. Default is false."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -2675,6 +2703,16 @@
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"default": 100
|
"default": 100
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "show_timestamps",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Show timestamps in the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|
@ -5952,6 +5990,80 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"\/databases\/{uuid}\/logs": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Databases"
|
||||||
|
],
|
||||||
|
"summary": "Get database logs.",
|
||||||
|
"description": "Get database logs by UUID.",
|
||||||
|
"operationId": "get-database-logs-by-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the database.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lines",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Number of lines to show from the end of the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"default": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "show_timestamps",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Show timestamps in the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Get database logs by UUID.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"logs": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}\/executions\/{execution_uuid}": {
|
"\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}\/executions\/{execution_uuid}": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -7439,7 +7551,6 @@
|
||||||
"schema": {
|
"schema": {
|
||||||
"required": [
|
"required": [
|
||||||
"name",
|
"name",
|
||||||
"api_url",
|
|
||||||
"html_url",
|
"html_url",
|
||||||
"app_id",
|
"app_id",
|
||||||
"installation_id",
|
"installation_id",
|
||||||
|
|
@ -11293,6 +11404,90 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"\/services\/{uuid}\/logs": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Services"
|
||||||
|
],
|
||||||
|
"summary": "Get service logs.",
|
||||||
|
"description": "Get logs for a specific service sub-resource by service UUID. The `sub_service_name` query parameter must match the `name` field of one of the service applications or databases returned by `GET \/services\/{uuid}`.",
|
||||||
|
"operationId": "get-service-logs-by-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the service.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sub_service_name",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Sub-service name from `GET \/services\/{uuid}` under `applications[].name` or `databases[].name`. Do not use `human_name` or the Docker container name with the service UUID suffix.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "appwrite-console"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lines",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Number of lines to show from the end of the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"default": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "show_timestamps",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Show timestamps in the logs.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Get service logs by UUID.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"logs": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"\/services\/{uuid}\/envs": {
|
"\/services\/{uuid}\/envs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
140
openapi.yaml
140
openapi.yaml
|
|
@ -118,6 +118,9 @@ paths:
|
||||||
is_force_https_enabled:
|
is_force_https_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
||||||
|
is_preview_deployments_enabled:
|
||||||
|
type: boolean
|
||||||
|
description: 'Enable preview deployments for pull requests.'
|
||||||
static_image:
|
static_image:
|
||||||
type: string
|
type: string
|
||||||
enum: ['nginx:alpine']
|
enum: ['nginx:alpine']
|
||||||
|
|
@ -405,6 +408,9 @@ paths:
|
||||||
is_force_https_enabled:
|
is_force_https_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
||||||
|
is_preview_deployments_enabled:
|
||||||
|
type: boolean
|
||||||
|
description: 'Enable preview deployments for pull requests.'
|
||||||
static_image:
|
static_image:
|
||||||
type: string
|
type: string
|
||||||
enum: ['nginx:alpine']
|
enum: ['nginx:alpine']
|
||||||
|
|
@ -692,6 +698,9 @@ paths:
|
||||||
is_force_https_enabled:
|
is_force_https_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
||||||
|
is_preview_deployments_enabled:
|
||||||
|
type: boolean
|
||||||
|
description: 'Enable preview deployments for pull requests.'
|
||||||
static_image:
|
static_image:
|
||||||
type: string
|
type: string
|
||||||
enum: ['nginx:alpine']
|
enum: ['nginx:alpine']
|
||||||
|
|
@ -1063,6 +1072,9 @@ paths:
|
||||||
is_force_https_enabled:
|
is_force_https_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
||||||
|
is_preview_deployments_enabled:
|
||||||
|
type: boolean
|
||||||
|
description: 'Enable preview deployments for pull requests.'
|
||||||
use_build_server:
|
use_build_server:
|
||||||
type: boolean
|
type: boolean
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
@ -1277,6 +1289,9 @@ paths:
|
||||||
is_force_https_enabled:
|
is_force_https_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
||||||
|
is_preview_deployments_enabled:
|
||||||
|
type: boolean
|
||||||
|
description: 'Enable preview deployments for pull requests.'
|
||||||
use_build_server:
|
use_build_server:
|
||||||
type: boolean
|
type: boolean
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
@ -1507,6 +1522,9 @@ paths:
|
||||||
is_force_https_enabled:
|
is_force_https_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
||||||
|
is_preview_deployments_enabled:
|
||||||
|
type: boolean
|
||||||
|
description: 'Enable preview deployments for pull requests.'
|
||||||
install_command:
|
install_command:
|
||||||
type: string
|
type: string
|
||||||
description: 'The install command.'
|
description: 'The install command.'
|
||||||
|
|
@ -1663,6 +1681,9 @@ paths:
|
||||||
is_preserve_repository_enabled:
|
is_preserve_repository_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'
|
description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'
|
||||||
|
include_source_commit_in_build:
|
||||||
|
type: boolean
|
||||||
|
description: 'Include source commit information in the build. Default is false.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -1716,6 +1737,14 @@ paths:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
default: 100
|
default: 100
|
||||||
|
-
|
||||||
|
name: show_timestamps
|
||||||
|
in: query
|
||||||
|
description: 'Show timestamps in the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: 'Get application logs by UUID.'
|
description: 'Get application logs by UUID.'
|
||||||
|
|
@ -3908,6 +3937,57 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
'/databases/{uuid}/logs':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Databases
|
||||||
|
summary: 'Get database logs.'
|
||||||
|
description: 'Get database logs by UUID.'
|
||||||
|
operationId: get-database-logs-by-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the database.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
-
|
||||||
|
name: lines
|
||||||
|
in: query
|
||||||
|
description: 'Number of lines to show from the end of the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
default: 100
|
||||||
|
-
|
||||||
|
name: show_timestamps
|
||||||
|
in: query
|
||||||
|
description: 'Show timestamps in the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Get database logs by UUID.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
logs: { type: string }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
'/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}':
|
'/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}':
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -4806,7 +4886,6 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- api_url
|
|
||||||
- html_url
|
- html_url
|
||||||
- app_id
|
- app_id
|
||||||
- installation_id
|
- installation_id
|
||||||
|
|
@ -7150,6 +7229,65 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
'/services/{uuid}/logs':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Services
|
||||||
|
summary: 'Get service logs.'
|
||||||
|
description: 'Get logs for a specific service sub-resource by service UUID. The `sub_service_name` query parameter must match the `name` field of one of the service applications or databases returned by `GET /services/{uuid}`.'
|
||||||
|
operationId: get-service-logs-by-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the service.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
-
|
||||||
|
name: sub_service_name
|
||||||
|
in: query
|
||||||
|
description: 'Sub-service name from `GET /services/{uuid}` under `applications[].name` or `databases[].name`. Do not use `human_name` or the Docker container name with the service UUID suffix.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: appwrite-console
|
||||||
|
-
|
||||||
|
name: lines
|
||||||
|
in: query
|
||||||
|
description: 'Number of lines to show from the end of the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
default: 100
|
||||||
|
-
|
||||||
|
name: show_timestamps
|
||||||
|
in: query
|
||||||
|
description: 'Show timestamps in the logs.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Get service logs by UUID.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
logs: { type: string }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
'/services/{uuid}/envs':
|
'/services/{uuid}/envs':
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,4 @@ ROOT_USERNAME=
|
||||||
ROOT_USER_EMAIL=
|
ROOT_USER_EMAIL=
|
||||||
ROOT_USER_PASSWORD=
|
ROOT_USER_PASSWORD=
|
||||||
|
|
||||||
REGISTRY_URL=ghcr.io
|
REGISTRY_URL=docker.io
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
coolify:
|
coolify:
|
||||||
image: "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}"
|
image: "${REGISTRY_URL:-docker.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}"
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: /data/coolify/source/.env
|
source: /data/coolify/source/.env
|
||||||
|
|
@ -60,7 +60,7 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
soketi:
|
soketi:
|
||||||
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.16'
|
image: '${REGISTRY_URL:-docker.io}/coollabsio/coolify-realtime:1.0.16'
|
||||||
ports:
|
ports:
|
||||||
- "${SOKETI_PORT:-6001}:6001"
|
- "${SOKETI_PORT:-6001}:6001"
|
||||||
- "6002:6002"
|
- "6002:6002"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
services:
|
services:
|
||||||
coolify-testing-host:
|
coolify-testing-host:
|
||||||
init: true
|
init: true
|
||||||
image: "ghcr.io/coollabsio/coolify-testing-host:latest"
|
image: "docker.io/coollabsio/coolify-testing-host:latest"
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: coolify-testing-host
|
container_name: coolify-testing-host
|
||||||
volumes:
|
volumes:
|
||||||
- //var/run/docker.sock://var/run/docker.sock
|
- //var/run/docker.sock://var/run/docker.sock
|
||||||
- ./:/data/coolify
|
- ./:/data/coolify
|
||||||
coolify:
|
coolify:
|
||||||
image: "ghcr.io/coollabsio/coolify:latest"
|
image: "docker.io/coollabsio/coolify:latest"
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: coolify
|
container_name: coolify
|
||||||
restart: always
|
restart: always
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24)
|
## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24)
|
||||||
## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false)
|
## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false)
|
||||||
## AUTOUPDATE - Set to "false" to disable auto-updates
|
## AUTOUPDATE - Set to "false" to disable auto-updates
|
||||||
## REGISTRY_URL - Custom registry URL for Docker images (default: ghcr.io)
|
## REGISTRY_URL - Custom registry URL for Docker images (default: docker.io)
|
||||||
|
|
||||||
set -e # Exit immediately if a command exits with a non-zero status
|
set -e # Exit immediately if a command exits with a non-zero status
|
||||||
## $1 could be empty, so we need to disable this check
|
## $1 could be empty, so we need to disable this check
|
||||||
|
|
@ -50,7 +50,7 @@ else
|
||||||
REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2)
|
REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2)
|
||||||
echo "Using registry URL from .env: $REGISTRY_URL"
|
echo "Using registry URL from .env: $REGISTRY_URL"
|
||||||
else
|
else
|
||||||
REGISTRY_URL="ghcr.io"
|
REGISTRY_URL="docker.io"
|
||||||
echo "Using default registry URL: $REGISTRY_URL"
|
echo "Using default registry URL: $REGISTRY_URL"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -920,9 +920,9 @@ echo -e " - Please wait."
|
||||||
getAJoke
|
getAJoke
|
||||||
|
|
||||||
if [[ $- == *x* ]]; then
|
if [[ $- == *x* ]]; then
|
||||||
bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true"
|
||||||
else
|
else
|
||||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true"
|
||||||
fi
|
fi
|
||||||
echo " - Coolify installed successfully."
|
echo " - Coolify installed successfully."
|
||||||
echo " - Waiting for Coolify to be ready..."
|
echo " - Waiting for Coolify to be ready..."
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,15 @@
|
||||||
CDN="https://cdn.coollabs.io/coolify-nightly"
|
CDN="https://cdn.coollabs.io/coolify-nightly"
|
||||||
LATEST_IMAGE=${1:-latest}
|
LATEST_IMAGE=${1:-latest}
|
||||||
LATEST_HELPER_VERSION=${2:-latest}
|
LATEST_HELPER_VERSION=${2:-latest}
|
||||||
REGISTRY_URL=${3:-ghcr.io}
|
|
||||||
SKIP_BACKUP=${4:-false}
|
|
||||||
ENV_FILE="/data/coolify/source/.env"
|
ENV_FILE="/data/coolify/source/.env"
|
||||||
|
if [ -n "${3+x}" ]; then
|
||||||
|
REGISTRY_URL="$3"
|
||||||
|
elif [ -f "$ENV_FILE" ] && grep -q "^REGISTRY_URL=" "$ENV_FILE"; then
|
||||||
|
REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2- | head -n1)
|
||||||
|
else
|
||||||
|
REGISTRY_URL="docker.io"
|
||||||
|
fi
|
||||||
|
SKIP_BACKUP=${4:-false}
|
||||||
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
||||||
|
|
||||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||||
|
|
@ -80,7 +86,7 @@ fi
|
||||||
|
|
||||||
# Get all unique images from docker compose config
|
# Get all unique images from docker compose config
|
||||||
# LATEST_IMAGE env var is needed for image substitution in compose files
|
# LATEST_IMAGE env var is needed for image substitution in compose files
|
||||||
IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u)
|
IMAGES=$(REGISTRY_URL=${REGISTRY_URL} LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u)
|
||||||
|
|
||||||
if [ -z "$IMAGES" ]; then
|
if [ -z "$IMAGES" ]; then
|
||||||
log "ERROR: Failed to extract images from docker-compose files"
|
log "ERROR: Failed to extract images from docker-compose files"
|
||||||
|
|
@ -127,7 +133,21 @@ update_env_var() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_env_var() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
if grep -q "^${key}=" "$ENV_FILE"; then
|
||||||
|
sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE"
|
||||||
|
log "Updated ${key}"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||||
|
log "Added ${key}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
log "Checking environment variables..."
|
log "Checking environment variables..."
|
||||||
|
set_env_var "REGISTRY_URL" "$REGISTRY_URL"
|
||||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||||
|
|
@ -164,7 +184,7 @@ echo "3/6 Pulling Docker images..."
|
||||||
echo " This may take a few minutes depending on your connection."
|
echo " This may take a few minutes depending on your connection."
|
||||||
|
|
||||||
# Also pull the helper image (not in compose files but needed for upgrade)
|
# Also pull the helper image (not in compose files but needed for upgrade)
|
||||||
HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
HELPER_IMAGE="${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
||||||
echo " - Pulling $HELPER_IMAGE..."
|
echo " - Pulling $HELPER_IMAGE..."
|
||||||
log "Pulling image: $HELPER_IMAGE"
|
log "Pulling image: $HELPER_IMAGE"
|
||||||
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
|
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
|
||||||
|
|
@ -256,7 +276,7 @@ nohup bash -c "
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log 'Running docker compose up...'
|
log 'Running docker compose up...'
|
||||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||||
log 'Docker compose up completed'
|
log 'Docker compose up completed'
|
||||||
|
|
||||||
# Final log entry
|
# Final log entry
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
|
|
||||||
<div class="grid justify-start grid-cols-1 gap-4 text-left xl:grid-cols-3">
|
<div class="grid justify-start grid-cols-1 gap-4 text-left xl:grid-cols-3">
|
||||||
<template x-for="service in filteredServices" :key="service.name">
|
<template x-for="service in filteredServices" :key="service.name">
|
||||||
<div class="relative" x-on:click="setType('one-click-service-' + service.name)"
|
<div class="relative" x-on:click="setType('one-click-service-' + service.id)"
|
||||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }">
|
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }">
|
||||||
<x-resource-view>
|
<x-resource-view>
|
||||||
<x-slot:title>
|
<x-slot:title>
|
||||||
|
|
@ -214,7 +214,7 @@ class="px-2 py-0.5 text-xs rounded-full bg-amber-100 text-amber-800 dark:bg-ambe
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="shouldShowDocIcon(service)">
|
<template x-if="shouldShowDocIcon(service)">
|
||||||
<a :href="getDocLink(service) || coolifyDocsUrl(service.name)" target="_blank"
|
<a :href="getDocLink(service) || coolifyDocsUrl(service)" target="_blank"
|
||||||
@click.stop @mouseenter="resolveDocLink(service)"
|
@click.stop @mouseenter="resolveDocLink(service)"
|
||||||
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
||||||
:class="{ 'opacity-50': docCheckInProgress[service.name] }"
|
:class="{ 'opacity-50': docCheckInProgress[service.name] }"
|
||||||
|
|
@ -287,8 +287,8 @@ function searchResources() {
|
||||||
// Remove flavor suffixes: -with-*, -without-*
|
// Remove flavor suffixes: -with-*, -without-*
|
||||||
return normalized.replace(/-(with|without)-.+$/, '');
|
return normalized.replace(/-(with|without)-.+$/, '');
|
||||||
},
|
},
|
||||||
coolifyDocsUrl(serviceName) {
|
coolifyDocsUrl(service) {
|
||||||
const baseName = this.extractBaseServiceName(serviceName);
|
const baseName = service.docsSlug || this.extractBaseServiceName(service.name);
|
||||||
return 'https://coolify.io/docs/services/' + baseName;
|
return 'https://coolify.io/docs/services/' + baseName;
|
||||||
},
|
},
|
||||||
officialDocsUrl(service) {
|
officialDocsUrl(service) {
|
||||||
|
|
@ -322,7 +322,7 @@ function searchResources() {
|
||||||
this.docCheckInProgress[serviceName] = true;
|
this.docCheckInProgress[serviceName] = true;
|
||||||
|
|
||||||
// 1. Try Coolify docs first
|
// 1. Try Coolify docs first
|
||||||
const coolifyUrl = this.coolifyDocsUrl(serviceName);
|
const coolifyUrl = this.coolifyDocsUrl(service);
|
||||||
const coolifyExists = await this.checkUrlExists(coolifyUrl);
|
const coolifyExists = await this.checkUrlExists(coolifyUrl);
|
||||||
|
|
||||||
if (coolifyExists) {
|
if (coolifyExists) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<h1>Server</h1>
|
<h1>Server</h1>
|
||||||
@if ($server->proxySet() || $server->isSentinelEnabled())
|
@php
|
||||||
|
$showSentinelStatus = $server->isFunctional() && $server->isSentinelEnabled();
|
||||||
|
@endphp
|
||||||
|
@if ($server->proxySet() || $showSentinelStatus)
|
||||||
<div data-testid="server-status-summary" class="flex flex-wrap items-center gap-2">
|
<div data-testid="server-status-summary" class="flex flex-wrap items-center gap-2">
|
||||||
@if ($server->proxySet())
|
@if ($server->proxySet())
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
|
@ -38,7 +41,7 @@
|
||||||
type="warning" />
|
type="warning" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if ($server->isSentinelEnabled())
|
@if ($showSentinelStatus)
|
||||||
@if ($server->isSentinelLive())
|
@if ($server->isSentinelLive())
|
||||||
<x-status-badge label="Sentinel" status="In sync" type="success" />
|
<x-status-badge label="Sentinel" status="In sync" type="success" />
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,19 @@ class="flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded bg-warn
|
||||||
@else
|
@else
|
||||||
You can't use this server until it is validated.
|
You can't use this server until it is validated.
|
||||||
@endif
|
@endif
|
||||||
|
@if ($this->limaStartCommand)
|
||||||
|
<div
|
||||||
|
class="mt-4 rounded border border-coolgray-200 bg-coolgray-100 p-3 text-sm dark:border-coolgray-300 dark:bg-coolgray-100">
|
||||||
|
<div class="font-semibold">Start this Lima VM locally</div>
|
||||||
|
<div class="mt-1 dark:text-neutral-400">
|
||||||
|
Run this from the Coolify repository root before validating this server:
|
||||||
|
</div>
|
||||||
|
<code
|
||||||
|
class="mt-2 block overflow-x-auto rounded bg-black px-3 py-2 font-mono text-xs text-white">
|
||||||
|
{{ $this->limaStartCommand }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@if ($isValidating)
|
@if ($isValidating)
|
||||||
<div x-data="{ slideOverOpen: true }">
|
<div x-data="{ slideOverOpen: true }">
|
||||||
<x-slide-over closeWithX fullScreen>
|
<x-slide-over closeWithX fullScreen>
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,15 @@ class="flex flex-col h-full gap-8 sm:flex-row">
|
||||||
environments!
|
environments!
|
||||||
</x-callout>
|
</x-callout>
|
||||||
@endif
|
@endif
|
||||||
|
<h4 class="pt-4">Webhook/S3 Endpoint Controls</h4>
|
||||||
|
<x-forms.textarea id="webhook_allowed_internal_hosts" rows="4"
|
||||||
|
label="Allowed Internal Webhook/S3 Targets"
|
||||||
|
helper="Optional instance-level allowlist for webhook and S3 endpoint destinations. Supports exact hostnames, IPs, and CIDR ranges separated by commas or new lines."
|
||||||
|
placeholder="hooks.company.local, 10.50.0.0/16, 192.168.10.20" />
|
||||||
|
<div class="md:w-96">
|
||||||
|
<x-forms.checkbox id="webhook_allow_localhost" label="Allow Localhost Webhook/S3 Targets"
|
||||||
|
helper="Allows localhost/loopback targets only when they are also listed above. Use only for local development or trusted single-instance setups." />
|
||||||
|
</div>
|
||||||
<h4 class="pt-4">MCP Server</h4>
|
<h4 class="pt-4">MCP Server</h4>
|
||||||
<div class="md:w-96">
|
<div class="md:w-96">
|
||||||
<x-forms.checkbox instantSave id="is_mcp_server_enabled" label="Enable MCP Server Instance-wide"
|
<x-forms.checkbox instantSave id="is_mcp_server_enabled" label="Enable MCP Server Instance-wide"
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,6 @@ class="px-4 py-2 text-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-co
|
||||||
placeholder="2001:db8::1" autocomplete="new-password" />
|
placeholder="2001:db8::1" autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($buildActivityId)
|
|
||||||
<div class="w-full mt-4">
|
|
||||||
<livewire:activity-monitor header="Building Helper Image" :activityId="$buildActivityId"
|
|
||||||
:fullHeight="false" />
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if(isDev())
|
@if(isDev())
|
||||||
<x-forms.input canGate="update" :canResource="$settings" id="dev_helper_version" label="Dev Helper Version (Development Only)"
|
<x-forms.input canGate="update" :canResource="$settings" id="dev_helper_version" label="Dev Helper Version (Development Only)"
|
||||||
helper="Override the default coolify-helper image version. Leave empty to use the default version from config ({{ config('constants.coolify.helper_version') }}). Examples: 1.0.11, latest, dev"
|
helper="Override the default coolify-helper image version. Leave empty to use the default version from config ({{ config('constants.coolify.helper_version') }}). Examples: 1.0.11, latest, dev"
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,15 @@
|
||||||
<x-forms.input required label="Frequency (cron expression)" disabled placeholder="disabled"
|
<x-forms.input required label="Frequency (cron expression)" disabled placeholder="disabled"
|
||||||
helper="Frequency (cron expression) (automatically update coolify).<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every day at 00:00" />
|
helper="Frequency (cron expression) (automatically update coolify).<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every day at 00:00" />
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
<h4 class="pt-4">Docker Registry</h4>
|
||||||
|
<div class="md:w-96">
|
||||||
|
<x-forms.select id="docker_registry_url" label="Docker Registry"
|
||||||
|
helper="The Docker registry used to pull Coolify images during updates.<br>Switch to Docker Hub if you experience rate limiting with GitHub Container Registry.">
|
||||||
|
<option value="docker.io">Docker Hub (docker.io)</option>
|
||||||
|
<option value="ghcr.io">GitHub Container Registry (ghcr.io)</option>
|
||||||
|
</x-forms.select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -373,7 +373,8 @@ function createGithubApp(webhook_endpoint, use_custom_webhook_endpoint, custom_w
|
||||||
baseUrl = devWebhook;
|
baseUrl = devWebhook;
|
||||||
}
|
}
|
||||||
const webhookBaseUrl = `${baseUrl}/webhooks`;
|
const webhookBaseUrl = `${baseUrl}/webhooks`;
|
||||||
const path = organization ? `organizations/${organization}/settings/apps/new` : 'settings/apps/new';
|
const organizationPath = organization ? encodeURIComponent(organization.replace(/^\/+|\/+$/g, '')) : '';
|
||||||
|
const path = organizationPath ? `organizations/${organizationPath}/settings/apps/new` : 'settings/apps/new';
|
||||||
const default_permissions = {
|
const default_permissions = {
|
||||||
contents: 'read',
|
contents: 'read',
|
||||||
metadata: 'read',
|
metadata: 'read',
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
@can('manageInvitations', currentTeam())
|
<div>
|
||||||
<form wire:submit='viaLink' class="flex gap-2 flex-col lg:flex-row items-end">
|
@can('manageInvitations', currentTeam())
|
||||||
<div class="flex flex-1 lg:w-fit w-full gap-2">
|
<form wire:submit='viaLink' class="flex gap-2 flex-col lg:flex-row items-end">
|
||||||
<x-forms.input id="email" type="email" label="Email" name="email" placeholder="Email" required />
|
<div class="flex flex-1 lg:w-fit w-full gap-2">
|
||||||
<x-forms.select id="role" name="role" label="Role">
|
<x-forms.input id="email" type="email" label="Email" name="email" placeholder="Email" required />
|
||||||
@if (auth()->user()->role() === 'owner')
|
<x-forms.select id="role" name="role" label="Role">
|
||||||
<option value="owner">Owner</option>
|
@if (auth()->user()->role() === 'owner')
|
||||||
|
<option value="owner">Owner</option>
|
||||||
|
@endif
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="member">Member</option>
|
||||||
|
</x-forms.select>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 lg:w-fit w-full">
|
||||||
|
<x-forms.button type="submit">Generate Invitation Link</x-forms.button>
|
||||||
|
@if (is_transactional_emails_enabled())
|
||||||
|
<x-forms.button wire:click.prevent='viaEmail'>Send Invitation via Email</x-forms.button>
|
||||||
@endif
|
@endif
|
||||||
<option value="admin">Admin</option>
|
</div>
|
||||||
<option value="member">Member</option>
|
</form>
|
||||||
</x-forms.select>
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 lg:w-fit w-full">
|
|
||||||
<x-forms.button type="submit">Generate Invitation Link</x-forms.button>
|
|
||||||
@if (is_transactional_emails_enabled())
|
|
||||||
<x-forms.button wire:click.prevent='viaEmail'>Send Invitation via Email</x-forms.button>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
@endcan
|
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@
|
||||||
Route::post('/databases/{uuid}/backups', [DatabasesController::class, 'create_backup'])->middleware(['api.ability:write']);
|
Route::post('/databases/{uuid}/backups', [DatabasesController::class, 'create_backup'])->middleware(['api.ability:write']);
|
||||||
Route::patch('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'update_backup'])->middleware(['api.ability:write']);
|
Route::patch('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'update_backup'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
|
||||||
|
Route::get('/databases/{uuid}/logs', [DatabasesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
||||||
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']);
|
||||||
|
|
||||||
|
|
@ -195,6 +196,7 @@
|
||||||
Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
|
Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
|
||||||
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
|
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
||||||
|
Route::get('/services/{uuid}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24)
|
## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24)
|
||||||
## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false)
|
## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false)
|
||||||
## AUTOUPDATE - Set to "false" to disable auto-updates
|
## AUTOUPDATE - Set to "false" to disable auto-updates
|
||||||
## REGISTRY_URL - Custom registry URL for Docker images (default: ghcr.io)
|
## REGISTRY_URL - Custom registry URL for Docker images (default: docker.io)
|
||||||
|
|
||||||
set -e # Exit immediately if a command exits with a non-zero status
|
set -e # Exit immediately if a command exits with a non-zero status
|
||||||
## $1 could be empty, so we need to disable this check
|
## $1 could be empty, so we need to disable this check
|
||||||
|
|
@ -50,7 +50,7 @@ else
|
||||||
REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2)
|
REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2)
|
||||||
echo "Using registry URL from .env: $REGISTRY_URL"
|
echo "Using registry URL from .env: $REGISTRY_URL"
|
||||||
else
|
else
|
||||||
REGISTRY_URL="ghcr.io"
|
REGISTRY_URL="docker.io"
|
||||||
echo "Using default registry URL: $REGISTRY_URL"
|
echo "Using default registry URL: $REGISTRY_URL"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -920,9 +920,9 @@ echo -e " - Please wait."
|
||||||
getAJoke
|
getAJoke
|
||||||
|
|
||||||
if [[ $- == *x* ]]; then
|
if [[ $- == *x* ]]; then
|
||||||
bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true"
|
||||||
else
|
else
|
||||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true"
|
||||||
fi
|
fi
|
||||||
echo " - Coolify installed successfully."
|
echo " - Coolify installed successfully."
|
||||||
echo " - Waiting for Coolify to be ready..."
|
echo " - Waiting for Coolify to be ready..."
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,15 @@
|
||||||
CDN="https://cdn.coollabs.io/coolify"
|
CDN="https://cdn.coollabs.io/coolify"
|
||||||
LATEST_IMAGE=${1:-latest}
|
LATEST_IMAGE=${1:-latest}
|
||||||
LATEST_HELPER_VERSION=${2:-latest}
|
LATEST_HELPER_VERSION=${2:-latest}
|
||||||
REGISTRY_URL=${3:-ghcr.io}
|
|
||||||
SKIP_BACKUP=${4:-false}
|
|
||||||
ENV_FILE="/data/coolify/source/.env"
|
ENV_FILE="/data/coolify/source/.env"
|
||||||
|
if [ -n "${3+x}" ]; then
|
||||||
|
REGISTRY_URL="$3"
|
||||||
|
elif [ -f "$ENV_FILE" ] && grep -q "^REGISTRY_URL=" "$ENV_FILE"; then
|
||||||
|
REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2- | head -n1)
|
||||||
|
else
|
||||||
|
REGISTRY_URL="docker.io"
|
||||||
|
fi
|
||||||
|
SKIP_BACKUP=${4:-false}
|
||||||
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
||||||
|
|
||||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||||
|
|
@ -80,7 +86,7 @@ fi
|
||||||
|
|
||||||
# Get all unique images from docker compose config
|
# Get all unique images from docker compose config
|
||||||
# LATEST_IMAGE env var is needed for image substitution in compose files
|
# LATEST_IMAGE env var is needed for image substitution in compose files
|
||||||
IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u)
|
IMAGES=$(REGISTRY_URL=${REGISTRY_URL} LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u)
|
||||||
|
|
||||||
if [ -z "$IMAGES" ]; then
|
if [ -z "$IMAGES" ]; then
|
||||||
log "ERROR: Failed to extract images from docker-compose files"
|
log "ERROR: Failed to extract images from docker-compose files"
|
||||||
|
|
@ -127,7 +133,21 @@ update_env_var() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_env_var() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
if grep -q "^${key}=" "$ENV_FILE"; then
|
||||||
|
sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE"
|
||||||
|
log "Updated ${key}"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||||
|
log "Added ${key}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
log "Checking environment variables..."
|
log "Checking environment variables..."
|
||||||
|
set_env_var "REGISTRY_URL" "$REGISTRY_URL"
|
||||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||||
|
|
@ -173,7 +193,7 @@ echo "3/6 Pulling Docker images..."
|
||||||
echo " This may take a few minutes depending on your connection."
|
echo " This may take a few minutes depending on your connection."
|
||||||
|
|
||||||
# Also pull the helper image (not in compose files but needed for upgrade)
|
# Also pull the helper image (not in compose files but needed for upgrade)
|
||||||
HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
HELPER_IMAGE="${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
||||||
echo " - Pulling $HELPER_IMAGE..."
|
echo " - Pulling $HELPER_IMAGE..."
|
||||||
log "Pulling image: $HELPER_IMAGE"
|
log "Pulling image: $HELPER_IMAGE"
|
||||||
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
|
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
|
||||||
|
|
@ -265,7 +285,7 @@ nohup bash -c "
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log 'Running docker compose up...'
|
log 'Running docker compose up...'
|
||||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||||
log 'Docker compose up completed'
|
log 'Docker compose up completed'
|
||||||
|
|
||||||
# Final log entry
|
# Final log entry
|
||||||
|
|
|
||||||
81
tests/Feature/Api/ApplicationSourceCommitSettingApiTest.php
Normal file
81
tests/Feature/Api/ApplicationSourceCommitSettingApiTest.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Application;
|
||||||
|
use App\Models\Environment;
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\StandaloneDocker;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
||||||
|
|
||||||
|
$this->team = Team::factory()->create();
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
$plainTextToken = Str::random(40);
|
||||||
|
$token = $this->user->tokens()->create([
|
||||||
|
'name' => 'source-commit-api-test-'.Str::random(6),
|
||||||
|
'token' => hash('sha256', $plainTextToken),
|
||||||
|
'abilities' => ['*'],
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
]);
|
||||||
|
$this->bearerToken = $token->getKey().'|'.$plainTextToken;
|
||||||
|
|
||||||
|
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
|
||||||
|
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||||
|
$this->application = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function sourceCommitSettingApiHeaders(string $bearerToken): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Authorization' => 'Bearer '.$bearerToken,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PATCH /api/v1/applications/{uuid} include_source_commit_in_build', function () {
|
||||||
|
test('updates the application setting through the API', function () {
|
||||||
|
expect((bool) $this->application->settings->include_source_commit_in_build)->toBeFalse();
|
||||||
|
|
||||||
|
$this->withHeaders(sourceCommitSettingApiHeaders($this->bearerToken))
|
||||||
|
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||||
|
'include_source_commit_in_build' => true,
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
expect((bool) $this->application->fresh()->settings->include_source_commit_in_build)->toBeTrue();
|
||||||
|
|
||||||
|
$this->withHeaders(sourceCommitSettingApiHeaders($this->bearerToken))
|
||||||
|
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||||
|
'include_source_commit_in_build' => false,
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
expect((bool) $this->application->fresh()->settings->include_source_commit_in_build)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non boolean values', function () {
|
||||||
|
$this->withHeaders(sourceCommitSettingApiHeaders($this->bearerToken))
|
||||||
|
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||||
|
'include_source_commit_in_build' => 'not-a-boolean',
|
||||||
|
])
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors('include_source_commit_in_build');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
|
@ -13,10 +14,7 @@
|
||||||
config()->set('app.maintenance.driver', 'file');
|
config()->set('app.maintenance.driver', 'file');
|
||||||
config()->set('cache.default', 'array');
|
config()->set('cache.default', 'array');
|
||||||
|
|
||||||
InstanceSettings::query()->delete();
|
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
|
||||||
$settings = new InstanceSettings;
|
|
||||||
$settings->id = 0;
|
|
||||||
$settings->save();
|
|
||||||
|
|
||||||
// Create a team with owner
|
// Create a team with owner
|
||||||
$this->team = Team::factory()->create();
|
$this->team = Team::factory()->create();
|
||||||
|
|
@ -29,7 +27,9 @@
|
||||||
$this->bearerToken = $this->token->plainTextToken;
|
$this->bearerToken = $this->token->plainTextToken;
|
||||||
|
|
||||||
// Create a private key for the team
|
// Create a private key for the team
|
||||||
$this->privateKey = PrivateKey::factory()->create([
|
$this->privateKey = PrivateKey::create([
|
||||||
|
'name' => 'Test Key',
|
||||||
|
'private_key' => validGithubAppsApiPrivateKey(),
|
||||||
'team_id' => $this->team->id,
|
'team_id' => $this->team->id,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -41,6 +41,26 @@ function createGithubAppsApiToken($context, array $abilities): string
|
||||||
return $context->user->createToken('github-apps-test-token', $abilities)->plainTextToken;
|
return $context->user->createToken('github-apps-test-token', $abilities)->plainTextToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a temporary 2048-bit RSA private key for GitHub Apps API tests.
|
||||||
|
*
|
||||||
|
* Generated in-process so tests do not depend on external files or secrets;
|
||||||
|
* the key is used only as a signing fixture and is never persisted.
|
||||||
|
*
|
||||||
|
* @return string PEM-encoded RSA private key
|
||||||
|
*/
|
||||||
|
function validGithubAppsApiPrivateKey(): string
|
||||||
|
{
|
||||||
|
$key = openssl_pkey_new([
|
||||||
|
'private_key_bits' => 2048,
|
||||||
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||||
|
]);
|
||||||
|
|
||||||
|
openssl_pkey_export($key, $privateKey);
|
||||||
|
|
||||||
|
return $privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
describe('GET /api/v1/github-apps', function () {
|
describe('GET /api/v1/github-apps', function () {
|
||||||
test('returns 401 when not authenticated', function () {
|
test('returns 401 when not authenticated', function () {
|
||||||
$response = $this->getJson('/api/v1/github-apps');
|
$response = $this->getJson('/api/v1/github-apps');
|
||||||
|
|
@ -195,6 +215,11 @@ function createGithubAppsApiToken($context, array $abilities): string
|
||||||
|
|
||||||
// Create another team with a GitHub app
|
// Create another team with a GitHub app
|
||||||
$otherTeam = Team::factory()->create();
|
$otherTeam = Team::factory()->create();
|
||||||
|
$otherPrivateKey = PrivateKey::create([
|
||||||
|
'name' => 'Other Key',
|
||||||
|
'private_key' => validGithubAppsApiPrivateKey(),
|
||||||
|
'team_id' => $otherTeam->id,
|
||||||
|
]);
|
||||||
GithubApp::create([
|
GithubApp::create([
|
||||||
'name' => 'Team 2 App',
|
'name' => 'Team 2 App',
|
||||||
'api_url' => 'https://api.github.com',
|
'api_url' => 'https://api.github.com',
|
||||||
|
|
@ -204,7 +229,7 @@ function createGithubAppsApiToken($context, array $abilities): string
|
||||||
'client_id' => 'team2-client-id',
|
'client_id' => 'team2-client-id',
|
||||||
'client_secret' => 'team2-secret',
|
'client_secret' => 'team2-secret',
|
||||||
'webhook_secret' => 'team2-webhook',
|
'webhook_secret' => 'team2-webhook',
|
||||||
'private_key_id' => $this->privateKey->id,
|
'private_key_id' => $otherPrivateKey->id,
|
||||||
'team_id' => $otherTeam->id,
|
'team_id' => $otherTeam->id,
|
||||||
'is_system_wide' => false,
|
'is_system_wide' => false,
|
||||||
]);
|
]);
|
||||||
|
|
@ -260,3 +285,179 @@ function createGithubAppsApiToken($context, array $abilities): string
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GitHub app API url normalization', function () {
|
||||||
|
test('normalizes ghe dot com api url when creating github apps', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||||
|
])->postJson('/api/v1/github-apps', [
|
||||||
|
'name' => 'GHE App',
|
||||||
|
'organization' => '/octocorp/',
|
||||||
|
'html_url' => 'https://github.ghe.com',
|
||||||
|
'app_id' => 12345,
|
||||||
|
'installation_id' => 67890,
|
||||||
|
'client_id' => 'test-client-id',
|
||||||
|
'client_secret' => 'test-client-secret',
|
||||||
|
'webhook_secret' => 'test-webhook-secret',
|
||||||
|
'private_key_uuid' => $this->privateKey->uuid,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated()
|
||||||
|
->assertJsonFragment([
|
||||||
|
'organization' => 'octocorp',
|
||||||
|
'api_url' => 'https://api.github.ghe.com',
|
||||||
|
'html_url' => 'https://github.ghe.com',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves provided api url when creating github apps', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||||
|
])->postJson('/api/v1/github-apps', [
|
||||||
|
'name' => 'GHE App',
|
||||||
|
'organization' => '/octocorp/',
|
||||||
|
'api_url' => 'https://github.com/api/v3',
|
||||||
|
'html_url' => 'https://github.com',
|
||||||
|
'app_id' => 12345,
|
||||||
|
'installation_id' => 67890,
|
||||||
|
'client_id' => 'test-client-id',
|
||||||
|
'client_secret' => 'test-client-secret',
|
||||||
|
'webhook_secret' => 'test-webhook-secret',
|
||||||
|
'private_key_uuid' => $this->privateKey->uuid,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated()
|
||||||
|
->assertJsonFragment([
|
||||||
|
'organization' => 'octocorp',
|
||||||
|
'api_url' => 'https://github.com/api/v3',
|
||||||
|
'html_url' => 'https://github.com',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves provided api url when updating github apps', function () {
|
||||||
|
$githubApp = GithubApp::create([
|
||||||
|
'name' => 'GHE App',
|
||||||
|
'api_url' => 'https://github.company.internal/api/v3',
|
||||||
|
'html_url' => 'https://github.company.internal',
|
||||||
|
'app_id' => 12345,
|
||||||
|
'installation_id' => 67890,
|
||||||
|
'client_id' => 'test-client-id',
|
||||||
|
'client_secret' => 'test-client-secret',
|
||||||
|
'webhook_secret' => 'test-webhook-secret',
|
||||||
|
'private_key_id' => $this->privateKey->id,
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
'is_system_wide' => false,
|
||||||
|
'is_public' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||||
|
])->patchJson("/api/v1/github-apps/{$githubApp->id}", [
|
||||||
|
'html_url' => 'https://github.com',
|
||||||
|
'api_url' => 'https://github.com/api/v3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful()
|
||||||
|
->assertJsonPath('data.api_url', 'https://github.com/api/v3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves provided api url when updating api url only', function () {
|
||||||
|
$githubApp = GithubApp::create([
|
||||||
|
'name' => 'GHE App',
|
||||||
|
'api_url' => 'https://api.github.com',
|
||||||
|
'html_url' => 'https://github.com',
|
||||||
|
'app_id' => 12345,
|
||||||
|
'installation_id' => 67890,
|
||||||
|
'client_id' => 'test-client-id',
|
||||||
|
'client_secret' => 'test-client-secret',
|
||||||
|
'webhook_secret' => 'test-webhook-secret',
|
||||||
|
'private_key_id' => $this->privateKey->id,
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
'is_system_wide' => false,
|
||||||
|
'is_public' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||||
|
])->patchJson("/api/v1/github-apps/{$githubApp->id}", [
|
||||||
|
'api_url' => 'https://github.com/api/v3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful()
|
||||||
|
->assertJsonPath('data.api_url', 'https://github.com/api/v3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects invalid organization when creating github apps', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||||
|
])->postJson('/api/v1/github-apps', [
|
||||||
|
'name' => 'GHE App',
|
||||||
|
'organization' => 'octo/corp',
|
||||||
|
'api_url' => 'https://api.octocorp.ghe.com',
|
||||||
|
'html_url' => 'https://octocorp.ghe.com',
|
||||||
|
'app_id' => 12345,
|
||||||
|
'installation_id' => 67890,
|
||||||
|
'client_id' => 'test-client-id',
|
||||||
|
'client_secret' => 'test-client-secret',
|
||||||
|
'webhook_secret' => 'test-webhook-secret',
|
||||||
|
'private_key_uuid' => $this->privateKey->uuid,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors(['organization']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loads repositories and branches through normalized ghe dot com api url', function () {
|
||||||
|
$this->privateKey->update([
|
||||||
|
'private_key' => validGithubAppsApiPrivateKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$githubApp = GithubApp::create([
|
||||||
|
'name' => 'GHE App',
|
||||||
|
'api_url' => 'https://api.octocorp.ghe.com',
|
||||||
|
'html_url' => 'https://octocorp.ghe.com',
|
||||||
|
'app_id' => 12345,
|
||||||
|
'installation_id' => 67890,
|
||||||
|
'client_id' => 'test-client-id',
|
||||||
|
'client_secret' => 'test-client-secret',
|
||||||
|
'webhook_secret' => 'test-webhook-secret',
|
||||||
|
'private_key_id' => $this->privateKey->id,
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
'is_system_wide' => false,
|
||||||
|
'is_public' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
Http::fake([
|
||||||
|
'https://api.octocorp.ghe.com/zen' => Http::response('Keep it logically awesome.', 200, [
|
||||||
|
'Date' => now()->toRfc7231String(),
|
||||||
|
]),
|
||||||
|
'https://api.octocorp.ghe.com/app/installations/67890/access_tokens' => Http::response([
|
||||||
|
'token' => 'installation-token',
|
||||||
|
]),
|
||||||
|
'https://api.octocorp.ghe.com/installation/repositories*' => Http::response([
|
||||||
|
'repositories' => [
|
||||||
|
['name' => 'repo', 'full_name' => 'octocorp/repo'],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'https://api.octocorp.ghe.com/repos/octocorp/repo/branches' => Http::response([
|
||||||
|
['name' => 'main'],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||||
|
])->getJson("/api/v1/github-apps/{$githubApp->id}/repositories")
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJsonPath('repositories.0.name', 'repo');
|
||||||
|
|
||||||
|
$this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||||
|
])->getJson("/api/v1/github-apps/{$githubApp->id}/repositories/octocorp/repo/branches")
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJsonPath('branches.0.name', 'main');
|
||||||
|
|
||||||
|
Http::assertSent(fn ($request) => $request->url() === 'https://api.octocorp.ghe.com/installation/repositories?per_page=100&page=1');
|
||||||
|
Http::assertSent(fn ($request) => $request->url() === 'https://api.octocorp.ghe.com/repos/octocorp/repo/branches');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue