Fix/secret manager request timeout (#12270)

This commit is contained in:
Andras Bacsai
2026-10-21 22:58:00 +02:00
committed by GitHub
24 changed files with 225 additions and 269 deletions
@@ -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') }}
+65
View File
@@ -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"
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+5 -1
View File
@@ -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) {
+10
View File
@@ -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') {
+2
View File
@@ -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'),
+3 -3
View File
@@ -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,
};
+3 -3
View File
@@ -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]);
+5 -5
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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": {