mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
Fix/secret manager request timeout (#12270)
This commit is contained in:
@@ -1,144 +0,0 @@
|
||||
name: Build Coolify Release Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["release/v*"]
|
||||
paths-ignore:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
- docker/testing-host/Dockerfile
|
||||
- templates/**
|
||||
- CHANGELOG.md
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: coolify-release-branch-build-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: coollabsio/coolify
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
dev_version: ${{ steps.version.outputs.dev_version }}
|
||||
short_sha: ${{ steps.version.outputs.short_sha }}
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Resolve version from branch name
|
||||
id: version
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
if [[ ! "${BRANCH}" =~ ^release/v([0-9]+)\.([0-9]+)$ ]]; then
|
||||
echo "Invalid release branch: ${BRANCH}. Expected release/vX.Y, such as release/v4.5."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEV_VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}-dev"
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "dev_version=${DEV_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${DEV_VERSION}.${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push release branch image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
build-args: |
|
||||
COOLIFY_VERSION=${{ needs.prepare.outputs.version }}
|
||||
tags: |
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.version }}-${{ matrix.arch }}
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.version }}-${{ matrix.arch }}
|
||||
|
||||
publish:
|
||||
needs: [prepare, build]
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
registry: [ghcr.io, docker.io]
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
if: matrix.registry == 'ghcr.io'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
if: matrix.registry == 'docker.io'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Publish release branch manifest on ${{ matrix.registry }}
|
||||
env:
|
||||
REGISTRY: ${{ matrix.registry }}
|
||||
DEV_VERSION: ${{ needs.prepare.outputs.dev_version }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:${VERSION}-amd64" \
|
||||
"${IMAGE}:${VERSION}-aarch64" \
|
||||
--tag "${IMAGE}:${VERSION}" \
|
||||
--tag "${IMAGE}:${DEV_VERSION}"
|
||||
|
||||
notify:
|
||||
needs: [prepare, build, publish]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
status: ${{ contains(needs.*.result, 'failure') && 'failure' || (contains(needs.*.result, 'cancelled') && 'cancelled' || 'success') }}
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Sync main to next
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/10 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: sync-main-to-next
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Merge main into next
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Merge main into next
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git fetch origin main next
|
||||
git checkout --quiet -B next origin/next
|
||||
|
||||
if git merge --no-edit origin/main; then
|
||||
git push origin HEAD:refs/heads/next
|
||||
exit 0
|
||||
fi
|
||||
|
||||
conflicts=$(git diff --name-only --diff-filter=U)
|
||||
git merge --abort
|
||||
|
||||
if [ -z "$conflicts" ]; then
|
||||
echo '::error title=main to next sync failed::The merge failed without conflicts, so no pull request was created.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sync_branch='automation/sync-main-to-next'
|
||||
existing_pr=$(gh pr list --base next --head "$sync_branch" --state open --json url --jq '.[0].url')
|
||||
if [ -n "$existing_pr" ]; then
|
||||
# The conflict was already reported when this pull request was opened.
|
||||
# Stay green until it is merged instead of failing every scheduled run.
|
||||
echo "::notice title=main to next sync blocked::main still conflicts with next; resolve the open pull request: $existing_pr"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git push --force origin origin/main:"refs/heads/$sync_branch"
|
||||
gh pr create \
|
||||
--base next \
|
||||
--head "$sync_branch" \
|
||||
--title 'chore: merge main into next' \
|
||||
--body 'This pull request was created automatically because main could not be merged into next without conflicts. Resolve conflicts on this temporary branch; never update main with next.'
|
||||
|
||||
echo '::error title=main to next sync blocked::main could not be merged into next without conflicts; a pull request was opened to resolve them.'
|
||||
exit 1
|
||||
@@ -1,81 +0,0 @@
|
||||
name: Sync main to release branches
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/10 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: sync-main-to-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Merge main into release branches
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Merge main into every release/v* branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git fetch origin main '+refs/heads/release/v*:refs/remotes/origin/release/v*'
|
||||
|
||||
branches=$(git for-each-ref --format='%(refname:lstrip=3)' 'refs/remotes/origin/release/v*')
|
||||
if [ -z "$branches" ]; then
|
||||
echo 'No release/v* branch exists, so there is nothing to sync.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
failed=0
|
||||
for branch in $branches; do
|
||||
echo "::group::Sync main into $branch"
|
||||
git checkout --quiet -B "$branch" "origin/$branch"
|
||||
|
||||
if git merge --no-edit origin/main; then
|
||||
git push origin "HEAD:refs/heads/$branch"
|
||||
echo '::endgroup::'
|
||||
continue
|
||||
fi
|
||||
|
||||
conflicts=$(git diff --name-only --diff-filter=U)
|
||||
git merge --abort
|
||||
echo '::endgroup::'
|
||||
|
||||
if [ -z "$conflicts" ]; then
|
||||
echo "::error title=main to $branch sync failed::The merge failed without conflicts, so no pull request was created."
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
|
||||
sync_branch="automation/sync-main-to-${branch//\//-}"
|
||||
existing_pr=$(gh pr list --base "$branch" --head "$sync_branch" --state open --json url --jq '.[0].url')
|
||||
if [ -n "$existing_pr" ]; then
|
||||
# The conflict was already reported when this pull request was opened.
|
||||
# Stay green until it is merged instead of failing every scheduled run.
|
||||
echo "::notice title=main to $branch sync blocked::main still conflicts with $branch; resolve the open pull request: $existing_pr"
|
||||
continue
|
||||
fi
|
||||
|
||||
git push --force origin origin/main:"refs/heads/$sync_branch"
|
||||
gh pr create \
|
||||
--base "$branch" \
|
||||
--head "$sync_branch" \
|
||||
--title "chore: merge main into $branch" \
|
||||
--body "This pull request was created automatically because main could not be merged into $branch without conflicts. Resolve conflicts on this temporary branch; never update main with $branch."
|
||||
|
||||
echo "::error title=main to $branch sync blocked::main could not be merged into $branch without conflicts; a pull request was opened to resolve them."
|
||||
failed=1
|
||||
done
|
||||
|
||||
exit "$failed"
|
||||
@@ -267,8 +267,8 @@ The persistent-storage implementation is the reference pattern: `Project\Service
|
||||
## Git Workflow
|
||||
|
||||
- Production branch: `main`
|
||||
- Development branch: `release/v4.x`, where `x` is the next minor release (for example, `release/v4.5` while production is `4.4.x`). Do not use `next`.
|
||||
- Fix PRs should target the current production branch; feature PRs should target the current `release/v4.x` branch
|
||||
- Development branch: `next`. Do not create `release/v*` branches.
|
||||
- Fix PRs should target the current production branch; feature PRs should target `next`
|
||||
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
+3
-4
@@ -193,12 +193,11 @@ Choose the branch based on the type of change:
|
||||
| --- | --- | --- |
|
||||
| Fixes and small improvements | `main` | `main` |
|
||||
| Security fixes | `main` | `main` |
|
||||
| New features and larger changes | `release/v4.x` | `release/v4.x` |
|
||||
| New features and larger changes | `next` | `next` |
|
||||
|
||||
- For a fix, branch from `main` and target `main`.
|
||||
- For a feature, branch from `release/v4.x` and target `release/v4.x`.
|
||||
- `x` is the number of the next minor release. For example, when the production version is `4.4.x`, use `release/v4.5`. There is no `next` branch.
|
||||
- If a fix is discovered while developing a feature, submit it separately to `main`. Maintainers will merge `main` into `release/v4.x` so the fix is included there too.
|
||||
- For a feature, branch from `next` and target `next`.
|
||||
- If a fix is discovered while developing a feature, submit it separately to `main`. `main` is merged into `next` automatically, so the fix is included there too.
|
||||
- Pull requests targeting the wrong branch may be closed or asked to retarget.
|
||||
|
||||
|
||||
|
||||
+19
-20
@@ -5,54 +5,54 @@
|
||||
| Branch | Purpose |
|
||||
| --- | --- |
|
||||
| `main` | Latest production source |
|
||||
| `release/v4.x` | Feature integration and RC releases for the next minor release, such as `release/v4.5` |
|
||||
| `feature/*` | New features based on and merged into the current `release/v4.x` branch |
|
||||
| `next` | Feature integration and RC releases for the next version |
|
||||
| `feature/*` | New features based on and merged into `next` |
|
||||
| `hotfix/X.Y.Z` | Production fixes based on `main` |
|
||||
|
||||
`x` is the number of the next minor release. When `4.4.x` is the production version, the release branch is `release/v4.5`. Create a new `release/v4.x` branch from `main` after each stable minor release. Do not use a long-lived `next` branch.
|
||||
`next` is the only long-lived development branch. Do not create `release/v*` branches.
|
||||
|
||||
Release workflows never edit or commit versions. Stable versions come from `config/constants.php`; RC versions come from `coolify.nightly.version` in `versions.json` and `other/nightly/versions.json`.
|
||||
|
||||
## Where changes go
|
||||
|
||||
- Fixes, security updates, and small improvements target `main`.
|
||||
- New features and larger changes target the current `release/v4.x` branch.
|
||||
- **Sync main to release branches** merges `main` into every `release/v*` branch every 10 minutes, so every production fix is included in the next release. If a merge conflicts, it opens a `chore: merge main into release/vX.Y` pull request to resolve.
|
||||
- Do not merge `release/v4.x` into `main` until an RC is approved for a stable release.
|
||||
- New features and larger changes target `next`.
|
||||
- **Sync main to next** merges `main` into `next` every 10 minutes, so every production fix is included in the next release. If the merge conflicts, it opens a `chore: merge main into next` pull request to resolve.
|
||||
- Do not merge `next` into `main` until an RC is approved for a stable release.
|
||||
|
||||
## Feature and RC flow
|
||||
|
||||
```text
|
||||
feature/* → release/v4.x → RC
|
||||
feature/* → next → RC
|
||||
```
|
||||
|
||||
1. Merge feature branches into `release/v4.x`.
|
||||
1. Merge feature branches into `next`.
|
||||
2. Set `coolify.nightly.version` in both version files to the intended RC, such as `4.5-rc.1`.
|
||||
3. Every push to `release/v4.x` runs **Build Coolify Release Branch**. It calculates the version from the branch name and publishes `4.5-dev.<short-sha>` and the moving `4.5-dev` tag. It never publishes an RC or stable tag.
|
||||
3. Every push to `next` runs **Build Coolify Next**. It publishes `sha-<short-sha>`, `4.5-rc.1.<short-sha>`, and the moving `next` tag. It never publishes the exact `4.5-rc.1` tag.
|
||||
4. Create a reviewed draft GitHub Release named `v4.5-rc.1` and mark it as a prerelease.
|
||||
5. Run **Release Coolify RC** manually from `release/v4.x` and enter `v4.5-rc.1`.
|
||||
5. Run **Release Coolify RC** manually from `next` and enter `v4.5-rc.1`.
|
||||
6. The workflow validates the draft and configured nightly version, builds the exact RC, publishes `4.5-rc.1`, updates the `next` image tag, and publishes the draft prerelease.
|
||||
7. Advance `coolify.nightly.version` to the next intended RC version.
|
||||
|
||||
## Stable release flow
|
||||
|
||||
```text
|
||||
release/v4.x → main → stable release
|
||||
next → main → stable release
|
||||
```
|
||||
|
||||
1. Temporarily stop merging features into `release/v4.x`.
|
||||
2. Change the version on `release/v4.x` from the approved RC to the stable version, such as `4.5.0`.
|
||||
3. Merge `release/v4.x` into `main`.
|
||||
1. Temporarily stop merging features into `next`.
|
||||
2. Change the version on `next` from the approved RC to the stable version, such as `4.5.0`.
|
||||
3. Merge `next` into `main`.
|
||||
4. Create a reviewed draft GitHub Release named `v4.5.0`.
|
||||
5. Run the stable release workflow from `main`.
|
||||
6. The workflow rebuilds the exact stable version, publishes `4.5.0` and `latest`, then publishes the draft.
|
||||
7. Update the CDN only after the release is approved.
|
||||
8. Create the next release branch from `main`, such as `release/v4.6`, and set its development version. Delete the old release branch.
|
||||
8. Advance `next` to the next development version, such as `4.6-rc.1`.
|
||||
|
||||
## Hotfix flow
|
||||
|
||||
```text
|
||||
main → hotfix/X.Y.Z → main → release/v4.x
|
||||
main → hotfix/X.Y.Z → main → next
|
||||
```
|
||||
|
||||
1. Create `hotfix/X.Y.Z` from `main` when a patch needs an integration branch. A single fix may use a normal branch from `main` instead.
|
||||
@@ -61,7 +61,7 @@ main → hotfix/X.Y.Z → main → release/v4.x
|
||||
4. Merge the fix into `main`.
|
||||
5. Create a reviewed draft GitHub Release named `vX.Y.Z`.
|
||||
6. Run the stable release workflow from `main`.
|
||||
7. Merge `main` into `release/v4.x`, resolve the version in favor of the next intended RC, and delete the hotfix branch if one was used.
|
||||
7. **Sync main to next** merges `main` into `next`. If the version conflicts, resolve it in favor of the next intended RC in the sync pull request. Delete the hotfix branch if one was used.
|
||||
8. Update the CDN only after the release is approved.
|
||||
|
||||
## Image tags
|
||||
@@ -69,9 +69,8 @@ main → hotfix/X.Y.Z → main → release/v4.x
|
||||
| Tag | Meaning |
|
||||
| --- | --- |
|
||||
| `latest` | Latest stable release |
|
||||
| `next` | Latest successful `next` build (legacy branch) |
|
||||
| `X.Y-dev` | Latest successful `release/vX.Y` build |
|
||||
| `X.Y-dev.<short-sha>` | Exact `release/vX.Y` commit build |
|
||||
| `next` | Latest successful `next` build or RC release |
|
||||
| `X.Y-rc.N.<short-sha>` | Exact `next` commit build |
|
||||
| `X.Y.Z` | Exact stable release |
|
||||
| `X.Y-rc.N` | Exact RC release |
|
||||
| `sha-<commit>` | Exact commit build |
|
||||
|
||||
@@ -64,9 +64,11 @@ class IntegrationTokensController extends Controller
|
||||
} elseif (($body['provider'] ?? null) === 'infisical') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.client_id'] = ['required', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
} elseif (($body['provider'] ?? null) === 'vault') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.namespace'] = ['nullable', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
$validator = customApiValidator($body, $rules);
|
||||
|
||||
@@ -76,11 +76,13 @@ class IntegrationTokenEditor extends Component
|
||||
if ($this->integrationToken->provider === 'infisical') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.client_id'] = ['required', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
if ($this->integrationToken->provider === 'vault') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.namespace'] = ['nullable', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
|
||||
@@ -78,6 +78,7 @@ class IntegrationTokenForm extends Component
|
||||
if ($this->provider === 'infisical') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.client_id'] = ['required', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
if ($this->provider === 'doppler') {
|
||||
@@ -87,6 +88,7 @@ class IntegrationTokenForm extends Component
|
||||
if ($this->provider === 'vault') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.namespace'] = ['nullable', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
|
||||
@@ -30,7 +30,10 @@ class Show extends Component
|
||||
|
||||
private function loadPrivateKeys(): void
|
||||
{
|
||||
$this->privateKeys = PrivateKey::where('team_id', $this->server->team_id)->get()->where('is_git_related', false);
|
||||
$this->privateKeys = PrivateKey::where('team_id', $this->server->team_id)->get()
|
||||
->where('is_git_related', false)
|
||||
->sortBy(fn (PrivateKey $privateKey) => $privateKey->id !== $this->server->private_key_id)
|
||||
->values();
|
||||
}
|
||||
|
||||
public function setPrivateKey($privateKeyId)
|
||||
@@ -51,6 +54,7 @@ class Show extends Component
|
||||
throw new \Exception($error);
|
||||
}
|
||||
});
|
||||
$this->loadPrivateKeys();
|
||||
$this->dispatch('success', 'Private key updated successfully.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -121,6 +121,16 @@ class IntegrationToken extends BaseModel
|
||||
return $this->provider === 'cloudflare' && data_get($this->metadata, 'proxied_dns') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional request timeout in seconds for self-hosted secret managers.
|
||||
*
|
||||
* @param array<string, mixed> $metadata
|
||||
*/
|
||||
public static function requestTimeout(array $metadata): ?int
|
||||
{
|
||||
return filled($metadata['timeout'] ?? null) ? (int) $metadata['timeout'] : null;
|
||||
}
|
||||
|
||||
public function dopplerTokenType(): ?string
|
||||
{
|
||||
if ($this->provider !== 'doppler') {
|
||||
|
||||
@@ -63,6 +63,7 @@ class SecretManagerLink extends BaseModel
|
||||
data_get($metadata, 'base_url', 'https://app.infisical.com'),
|
||||
(string) data_get($metadata, 'client_id'),
|
||||
$token->token,
|
||||
IntegrationToken::requestTimeout($metadata),
|
||||
))->fetchSecrets(
|
||||
(string) data_get($settings, 'project_id'),
|
||||
(string) data_get($settings, 'environment'),
|
||||
@@ -72,6 +73,7 @@ class SecretManagerLink extends BaseModel
|
||||
(string) data_get($metadata, 'base_url'),
|
||||
$token->token,
|
||||
data_get($metadata, 'namespace'),
|
||||
IntegrationToken::requestTimeout($metadata),
|
||||
))->fetchSecrets(
|
||||
(string) data_get($settings, 'mount', 'secret'),
|
||||
(string) data_get($settings, 'path'),
|
||||
|
||||
@@ -14,7 +14,7 @@ class InfisicalService
|
||||
/** @var array<string, mixed> */
|
||||
private array $httpClientOptions;
|
||||
|
||||
public function __construct(string $baseUrl, private string $clientId, private string $clientSecret)
|
||||
public function __construct(string $baseUrl, private string $clientId, private string $clientSecret, private ?int $timeout = null)
|
||||
{
|
||||
$this->baseUrl = rtrim($baseUrl, '/');
|
||||
Validator::make(['base_url' => $this->baseUrl], ['base_url' => new SafeExternalUrl])->validate();
|
||||
@@ -87,7 +87,7 @@ class InfisicalService
|
||||
{
|
||||
return Http::acceptJson()
|
||||
->withOptions($this->httpClientOptions)
|
||||
->connectTimeout(5)
|
||||
->timeout(10);
|
||||
->connectTimeout($this->timeout ?? 5)
|
||||
->timeout($this->timeout ?? 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\IntegrationToken;
|
||||
|
||||
/**
|
||||
* Validates an integration token against its provider API before it is saved.
|
||||
*/
|
||||
@@ -16,11 +18,13 @@ class IntegrationTokenValidator
|
||||
(string) data_get($metadata, 'base_url', 'https://app.infisical.com'),
|
||||
(string) data_get($metadata, 'client_id'),
|
||||
$token,
|
||||
IntegrationToken::requestTimeout($metadata),
|
||||
))->validate(),
|
||||
'vault' => (new VaultService(
|
||||
(string) data_get($metadata, 'base_url'),
|
||||
$token,
|
||||
data_get($metadata, 'namespace'),
|
||||
IntegrationToken::requestTimeout($metadata),
|
||||
))->validate(),
|
||||
default => false,
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ class VaultService
|
||||
/** @var array<string, mixed> */
|
||||
private array $httpClientOptions;
|
||||
|
||||
public function __construct(string $baseUrl, private string $token, private ?string $namespace = null)
|
||||
public function __construct(string $baseUrl, private string $token, private ?string $namespace = null, private ?int $timeout = null)
|
||||
{
|
||||
$this->baseUrl = rtrim($baseUrl, '/');
|
||||
Validator::make(['base_url' => $this->baseUrl], ['base_url' => new SafeExternalUrl])->validate();
|
||||
@@ -56,8 +56,8 @@ class VaultService
|
||||
$client = Http::withHeaders(['X-Vault-Token' => $this->token])
|
||||
->acceptJson()
|
||||
->withOptions($this->httpClientOptions)
|
||||
->connectTimeout(5)
|
||||
->timeout(10);
|
||||
->connectTimeout($this->timeout ?? 5)
|
||||
->timeout($this->timeout ?? 10);
|
||||
|
||||
if (filled($this->namespace)) {
|
||||
$client = $client->withHeaders(['X-Vault-Namespace' => $this->namespace]);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.5.0"
|
||||
"version": "4.4.7"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.6-rc.1"
|
||||
"version": "4.5-rc.1"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.18"
|
||||
"version": "1.0.17"
|
||||
},
|
||||
"sentinel": {
|
||||
"version": "1.1.2"
|
||||
},
|
||||
"sentinel-host": {
|
||||
"version": "1.1.1",
|
||||
"digest": "sha256:b8b686616eb09fc014c616eeb6ff6c2c96fd62429d2fafa06706695fdbe72f1b"
|
||||
"version": "1.1.2",
|
||||
"digest": "sha256:149d2097943716d30af9659eeeb11259601335442242ecb0b0331982a82a3bd2"
|
||||
}
|
||||
},
|
||||
"traefik": {
|
||||
|
||||
@@ -20,12 +20,16 @@
|
||||
<x-forms.input required id="metadata.base_url" label="Base URL"
|
||||
placeholder="https://app.infisical.com" />
|
||||
<x-forms.input required id="metadata.client_id" label="Client ID" />
|
||||
<x-forms.input type="number" min="1" max="300" id="metadata.timeout" label="Request timeout (optional)"
|
||||
placeholder="10" helper="Seconds to wait for each request. Increase it if your instance responds slowly." />
|
||||
</div>
|
||||
@elseif ($integrationToken->provider === 'vault')
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input required id="metadata.base_url" label="Base URL"
|
||||
placeholder="https://vault.example.com:8200" />
|
||||
<x-forms.input id="metadata.namespace" label="Namespace (optional)" />
|
||||
<x-forms.input type="number" min="1" max="300" id="metadata.timeout" label="Request timeout (optional)"
|
||||
placeholder="10" helper="Seconds to wait for each request. Increase it if your instance responds slowly." />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<x-forms.input required id="metadata.base_url" label="Base URL"
|
||||
placeholder="https://app.infisical.com"
|
||||
helper="Use the URL of your self-hosted Infisical instance, or the Infisical cloud URL." />
|
||||
<x-forms.input type="number" min="1" max="300" id="metadata.timeout" label="Request timeout (optional)"
|
||||
placeholder="10" helper="Seconds to wait for each request. Increase it if your instance responds slowly." />
|
||||
</div>
|
||||
@else
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
@@ -34,6 +36,8 @@
|
||||
placeholder="https://vault.example.com:8200" />
|
||||
<x-forms.input id="metadata.namespace" label="Namespace (optional)"
|
||||
placeholder="admin/team-a" helper="Only for Vault Enterprise / HCP Vault." />
|
||||
<x-forms.input type="number" min="1" max="300" id="metadata.timeout" label="Request timeout (optional)"
|
||||
placeholder="10" helper="Seconds to wait for each request. Increase it if your instance responds slowly." />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
# Backup file pg_dump custom-format backup (.dmp), optionally gzipped (.gz)
|
||||
# APP_KEY APP_KEY of the instance that created the backup (base64:...), not shown
|
||||
# Version Coolify version or image tag, e.g. 4.3.23 or sha-99c8409 (empty: latest stable)
|
||||
# Cloud y for a Coolify Cloud backup: starts Coolify with SELF_HOSTED=false, as Cloud runs.
|
||||
# Self-hosted mode on a Cloud backup adds the localhost server and makes app:init
|
||||
# try SSH to every server, so Reverb does not start and the container stays unhealthy.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Base image (once per version, until destroy): a separate builder VM with internet access installs
|
||||
@@ -52,7 +55,7 @@ die() {
|
||||
}
|
||||
|
||||
usage() {
|
||||
sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'
|
||||
sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -204,6 +207,7 @@ BACKUP_FILE=${BACKUP_FILE/#\~/$HOME}
|
||||
read -r -s -p "APP_KEY of the backed-up instance (input is hidden): " APP_KEY
|
||||
echo
|
||||
read -r -p "Coolify version or image tag (empty for latest stable): " VERSION
|
||||
read -r -p "Is this a Coolify Cloud backup? [y/N]: " CLOUD_BACKUP
|
||||
|
||||
[ -s "$BACKUP_FILE" ] || die "Backup file '$BACKUP_FILE' does not exist or is empty."
|
||||
[[ "$APP_KEY" == base64:* ]] || die "APP_KEY must start with 'base64:'."
|
||||
@@ -216,6 +220,7 @@ if [ -z "$VERSION" ]; then
|
||||
VERSION=$(curl -fsSL https://cdn.coollabs.io/coolify/versions.json | jq -r '.coolify.v4.version')
|
||||
echo "Latest stable version: $VERSION"
|
||||
fi
|
||||
[[ "${CLOUD_BACKUP,,}" =~ ^(y|yes)$ ]] && SELF_HOSTED=false || SELF_HOSTED=true
|
||||
[[ "$VERSION" =~ ^[A-Za-z0-9._-]+$ ]] || die "Version '$VERSION' contains characters that are not allowed."
|
||||
BASE_IMAGE="$IMAGE_DIR/coolify-base-$VERSION.qcow2"
|
||||
|
||||
@@ -272,6 +277,7 @@ log "Restoring the backup and starting Coolify (migrations run on start)"
|
||||
# Send APP_KEY through stdin, so it does not show in the process list of this host or the VM.
|
||||
{
|
||||
printf 'APP_KEY=%q\n' "$APP_KEY"
|
||||
printf 'SELF_HOSTED=%q\n' "$SELF_HOSTED"
|
||||
cat <<'REMOTE'
|
||||
set -euo pipefail
|
||||
cd /data/coolify/source
|
||||
@@ -295,6 +301,8 @@ for flag in HORIZON_ENABLED SCHEDULER_ENABLED; do
|
||||
sed -i "/^${flag}=/d" .env
|
||||
echo "${flag}=false" >>.env
|
||||
done
|
||||
sed -i '/^SELF_HOSTED=/d' .env
|
||||
echo "SELF_HOSTED=${SELF_HOSTED}" >>.env
|
||||
|
||||
# Recreate the database: tables from a newer fresh install must not remain.
|
||||
docker exec coolify-db psql -q -U coolify -d postgres -v ON_ERROR_STOP=1 \
|
||||
|
||||
@@ -187,6 +187,25 @@ test('a vault link uses the base url and namespace from the token metadata', fun
|
||||
Http::assertSent(fn ($request) => $request->hasHeader('X-Vault-Namespace', 'team-a'));
|
||||
});
|
||||
|
||||
test('an infisical link uses the request timeout from the token metadata', function () {
|
||||
$timeouts = [];
|
||||
Http::fake(function ($request, array $options) use (&$timeouts) {
|
||||
$timeouts[] = [$options['connect_timeout'], $options['timeout']];
|
||||
|
||||
return str_contains($request->url(), '/auth/universal-auth/login')
|
||||
? Http::response(['accessToken' => 'short-lived-token'])
|
||||
: Http::response(['secrets' => [['secretKey' => 'KEY', 'secretValue' => 'value']]]);
|
||||
});
|
||||
|
||||
$link = createSecretManagerLink('infisical',
|
||||
['project_id' => 'project-1', 'environment' => 'prod'],
|
||||
['base_url' => 'https://example.com', 'client_id' => 'client-id', 'timeout' => '30'],
|
||||
);
|
||||
|
||||
expect($link->fetchSecrets())->toBe(['KEY' => 'value'])
|
||||
->and($timeouts)->toBe([[30, 30], [30, 30]]);
|
||||
});
|
||||
|
||||
test('services resolve environment variables from their configured secret manager', function () {
|
||||
Http::fake([
|
||||
'https://api.doppler.com/v3/configs/config/secrets/download*' => Http::response([
|
||||
|
||||
@@ -125,6 +125,21 @@ describe('InfisicalService', function () {
|
||||
&& $request['expandSecretReferences'] === 'true');
|
||||
});
|
||||
|
||||
test('uses a 5 second connect timeout and a 10 second request timeout by default', function (?int $timeout, array $expected) {
|
||||
$timeouts = [];
|
||||
Http::fake(function ($request, array $options) use (&$timeouts) {
|
||||
$timeouts[] = [$options['connect_timeout'], $options['timeout']];
|
||||
|
||||
return Http::response(['accessToken' => 'short-lived-token']);
|
||||
});
|
||||
|
||||
expect((new InfisicalService('https://example.com', 'client-id', 'client-secret', $timeout))->validate())->toBeTrue()
|
||||
->and($timeouts)->toBe([$expected]);
|
||||
})->with([
|
||||
'default' => [null, [5, 10]],
|
||||
'custom' => [60, [60, 60]],
|
||||
]);
|
||||
|
||||
test('throws when the login fails', function () {
|
||||
Http::fake([
|
||||
'https://example.com/infisical/api/v1/auth/universal-auth/login' => Http::response([
|
||||
|
||||
@@ -470,6 +470,39 @@ test('saving a vault token with unchanged connection settings and a blank token
|
||||
->and($savedToken->metadata)->toBe(['base_url' => 'https://example.com:8200', 'namespace' => 'team-a']);
|
||||
});
|
||||
|
||||
test('changing the vault request timeout keeps the stored token and validates with the new timeout', function () {
|
||||
$timeouts = [];
|
||||
Http::fake(function ($request, array $options) use (&$timeouts) {
|
||||
$timeouts[] = $options['timeout'];
|
||||
|
||||
return Http::response(['data' => []]);
|
||||
});
|
||||
$savedToken = vaultIntegrationToken($this->team);
|
||||
|
||||
Livewire::test(IntegrationTokenEditor::class, ['integration_token_uuid' => $savedToken->uuid])
|
||||
->set('metadata.timeout', '45')
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('success');
|
||||
|
||||
$savedToken->refresh();
|
||||
expect($timeouts)->toBe([45])
|
||||
->and($savedToken->token)->toBe('hvs.stored-secret')
|
||||
->and(IntegrationToken::requestTimeout($savedToken->metadata))->toBe(45);
|
||||
});
|
||||
|
||||
test('a request timeout outside 1 to 300 seconds is rejected', function (string $timeout) {
|
||||
Http::fake();
|
||||
$savedToken = vaultIntegrationToken($this->team);
|
||||
|
||||
Livewire::test(IntegrationTokenEditor::class, ['integration_token_uuid' => $savedToken->uuid])
|
||||
->set('metadata.timeout', $timeout)
|
||||
->call('save')
|
||||
->assertHasErrors(['metadata.timeout']);
|
||||
|
||||
Http::assertNothingSent();
|
||||
})->with(['0', '301', 'abc']);
|
||||
|
||||
test('the token list shows its empty state after the editor deletes the last token', function () {
|
||||
$token = IntegrationToken::query()->create([
|
||||
'team_id' => $this->team->id,
|
||||
|
||||
@@ -135,4 +135,13 @@ describe('after the user switches the session team', function () {
|
||||
|
||||
expect($this->server->fresh()->private_key_id)->toBe($this->sameTeamKey->id);
|
||||
});
|
||||
|
||||
test('lists the active key first', function () {
|
||||
expect($this->component->get('privateKeys')->first()->id)->toBe($this->currentPrivateKey->id);
|
||||
|
||||
$keys = $this->component->call('setPrivateKey', $this->sameTeamKey->id)->get('privateKeys');
|
||||
|
||||
expect($keys->first()->id)->toBe($this->sameTeamKey->id)
|
||||
->and($keys->pluck('id'))->toContain($this->currentPrivateKey->id);
|
||||
});
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.5.0"
|
||||
"version": "4.4.7"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.6-rc.1"
|
||||
"version": "4.5-rc.1"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.18"
|
||||
"version": "1.0.17"
|
||||
},
|
||||
"sentinel": {
|
||||
"version": "1.1.2"
|
||||
},
|
||||
"sentinel-host": {
|
||||
"version": "1.1.1",
|
||||
"digest": "sha256:b8b686616eb09fc014c616eeb6ff6c2c96fd62429d2fafa06706695fdbe72f1b"
|
||||
"version": "1.1.2",
|
||||
"digest": "sha256:149d2097943716d30af9659eeeb11259601335442242ecb0b0331982a82a3bd2"
|
||||
}
|
||||
},
|
||||
"traefik": {
|
||||
|
||||
Reference in New Issue
Block a user