Compare commits

...

15 Commits

Author SHA1 Message Date
Armando Ruocco
bba751a17c test: cover the empty WAL archive decision in both hooks
Add unit coverage for the WAL archive and restore-job decision helpers.
The tables pin the version-skew contract: when the operator sets the
decision it overrides both the Cluster annotation and, for archiving, the
on-disk marker file, while a nil decision (an operator that predates the
field) reproduces the previous annotation-based behavior.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
2026-07-21 17:15:16 +02:00
Armando Ruocco
6fddc14398 refactor: extract the empty WAL archive decision into a helper
The WAL archive hook and the restore job hook each derived, inline,
whether to verify the archive destination before proceeding. Pull each
decision into a small function so the operator-decision-versus-fallback
logic can be exercised on its own. Behavior is unchanged: archiving still
fails on a marker-file stat error, and restore still falls back to the
Cluster annotation alone.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
2026-07-21 17:07:00 +02:00
Marco Nenciarini
7efb2cdf56 feat: honor the operator's check_empty_wal_archive decision
Archive() and the restore job hook each re-derived, on their own,
whether to verify the WAL archive destination is empty, by reading a
Cluster annotation and, for Archive, an on-disk marker file. That
decision belongs to the operator, which already tracks both the
annotation and the marker file's lifecycle.

Honor cnpg-i's new WALArchiveRequest/RestoreRequest field
CheckEmptyWalArchive when the operator sets it: obey it directly,
without re-inspecting the marker file. Only fall back to the previous
annotation-and-marker-file logic when talking to an operator that
predates this field.

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
2026-07-21 17:07:00 +02:00
Marco Nenciarini
1d777435d0
ci: run image publish in a separate parallel job (#1026)
Neither task depends on the other's output: task ci runs lint, tests,
e2e, and docs, while task publish builds and pushes the multi-arch
images. Running them as sequential steps in one job only adds their
durations. Splitting publish into its own job lets it run alongside ci
instead, and lets each job carry only the permissions it actually needs:
ci never writes to the registry, and neither task touches repository
contents, so only packages: write on the publish job remains.

QEMU is only needed for publish, since it's the only job that builds
non-native (arm64) platforms; ci's e2e-ephemeral image build is
amd64-only. The disk cleanup steps aren't needed for publish either: it
only builds two lean multi-arch images, well within the free disk space
available even on the default ubuntu-latest runner.

Note that publishing testing images no longer waits on ci passing, since
gating it away would remove the parallelism this is meant to gain.

Note: main's required status checks are currently ["DCO", "ci"]. A
failure in the new publish job will no longer block merging a PR, since
it isn't in that list (today it does, since publishing is a step inside
the required ci job). Flagging for a decision on whether publish should
be added as a required check.

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
Signed-off-by: Niccolò Fei <niccolo.fei@enterprisedb.com>
Co-authored-by: Niccolò Fei <niccolo.fei@enterprisedb.com>
2026-07-21 17:03:39 +02:00
Armando Ruocco
fb3f0f9dab
fix(walrestore): serve pg_rewind without prefetching and flag machinery (#1007)
When a demoted primary rejoins the cluster after a failover, the
instance manager runs `pg_rewind --restore-target-wal`, and the
resulting `restore_command` invocations are served by this sidecar
exactly as if an instance in recovery were asking. pg_rewind walks the
timeline backwards, fetches every WAL file it needs exactly once, and
treats any restore failure as fatal, so two optimizations meant for
recovery break it: prefetching the following segments is wasted work
that, past the end of the timeline, is guaranteed to end in an archive
miss, and the end-of-wal-stream flag recorded by that miss makes a later
invocation fail without contacting the object store, on a segment the
archive actually has, aborting the whole rewind.

The restore request now carries the context it is made in
(cloudnative-pg/cnpg-i#351). When it says `MODE_REWIND`, the sidecar
restores exactly the requested file: no prefetching, no
end-of-wal-stream flag check, no flag recording. Requests from operators
predating the field keep the current behavior.

This is the plugin-side counterpart of
cloudnative-pg/cloudnative-pg#11204. The cnpg-i dependency points to a
pseudo-version of the protocol pull request and will move to the next
tagged release once it is available.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
Co-authored-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
2026-07-21 15:01:51 +02:00
renovate[bot]
fe82a311ec
chore(deps): update amazon/aws-cli docker tag to v2.36.3 (#1027)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-21 09:53:34 +02:00
renovate[bot]
5e62b7ef88
chore(deps): update all cloudnative-pg daggerverse dependencies to f7187c3 (#1028)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-21 09:22:11 +02:00
Gabriele Bartolini
806f47b483
chore: expand account names in CODEOWNERS (#1016)
Add @NiccoloFei as contributor

Closes #1015

Signed-off-by: Gabriele Bartolini <gabriele.bartolini@enterprisedb.com>
2026-07-21 08:21:32 +10:00
renovate[bot]
7dab33a923
chore(deps): update actions/checkout action to v7.0.1 (#1024)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) |
action | patch | `v7.0.0` → `v7.0.1` |

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 18:17:40 +02:00
Marco Nenciarini
f386c2d85a
ci: make runner labels configurable via CI_RUNNERS variable (#1023)
Switch all workflow jobs from a hardcoded ubuntu-latest to ${{
vars.CI_RUNNERS || 'ubuntu-latest' }}, so runner sizing can be changed
via the CI_RUNNERS organization/repository variable without editing
workflow files.

The cloudnative-pg organization CI_RUNNERS variable is already set to
ubuntu-latest-16-cores.

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
2026-07-20 18:17:21 +02:00
renovate[bot]
98537c4d0e
fix(deps): update github.com/cloudnative-pg/barman-cloud digest to 950b0f5 (#1022)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[github.com/cloudnative-pg/barman-cloud](https://redirect.github.com/cloudnative-pg/barman-cloud)
| require | digest | `43158c2` → `950b0f5` |

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 17:47:21 +02:00
renovate[bot]
5aa67b0951
chore(deps): update fsouza/fake-gcs-server docker tag to v1.55.1 (#1018)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
|
[fsouza/fake-gcs-server](https://redirect.github.com/fsouza/fake-gcs-server)
| patch | `1.55.0` → `1.55.1` |

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 17:46:48 +02:00
renovate[bot]
ccc950dd9f
chore(deps): update amazon/aws-cli docker tag to v2.36.2 (#1014)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| amazon/aws-cli | patch | `2.36.1` → `2.36.2` |

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Leonardo Cecchi <leonardo.cecchi@enterprisedb.com>
2026-07-20 17:10:20 +02:00
renovate[bot]
13450a370d
fix(deps): update module github.com/cloudnative-pg/cnpg-i to v0.6.0 (#1021)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[github.com/cloudnative-pg/cnpg-i](https://redirect.github.com/cloudnative-pg/cnpg-i)
| `v0.5.0` → `v0.6.0` |
![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fcloudnative-pg%2fcnpg-i/v0.6.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fcloudnative-pg%2fcnpg-i/v0.5.0/v0.6.0?slim=true)
|

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 17:08:30 +02:00
renovate[bot]
0d75cb9ca2
chore(deps): lock file maintenance (#1019)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 4am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cloudnative-pg/plugin-barman-cloud).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjUuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJhdXRvbWF0ZWQiLCJuby1pc3N1ZSJdfQ==-->

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Leonardo Cecchi <leonardo.cecchi@enterprisedb.com>
2026-07-20 17:00:18 +02:00
16 changed files with 567 additions and 189 deletions

View File

@ -6,14 +6,18 @@ on:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ci:
runs-on: ubuntu-latest
runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }}
permissions:
packages: write
contents: write
contents: read
steps:
- name: Cleanup Disk
if: vars.CI_RUNNERS == '' || vars.CI_RUNNERS == 'ubuntu-latest'
uses: jlumbroso/free-disk-space@v1.3.1
with:
android: true
@ -23,6 +27,7 @@ jobs:
large-packages: false
swap-storage: false
- name: Cleanup docker cache
if: vars.CI_RUNNERS == '' || vars.CI_RUNNERS == 'ubuntu-latest'
run: |
echo "-------------Disk info before cleanup----------------"
df -h
@ -32,13 +37,11 @@ jobs:
df -h
echo "-----------------------------------------------------"
- name: Checkout
uses: actions/checkout@v7.0.0
uses: actions/checkout@v7.0.1
# We need the full history for the commitlint task
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Install QEMU static binaries
uses: docker/setup-qemu-action@v4
- name: Install Task
uses: arduino/setup-task@v3.0.0
- name: Install Dagger
@ -50,6 +53,27 @@ jobs:
- name: Run CI task
run: |
task ci
publish:
runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install QEMU static binaries
uses: docker/setup-qemu-action@v4
- name: Install Task
uses: arduino/setup-task@v3.0.0
- name: Install Dagger
env:
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
DAGGER_VERSION: 0.21.7
run: |
curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh
- name: Write manifest
run: |
task manifest

View File

@ -20,12 +20,12 @@ permissions: read-all
jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }}
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
uses: actions/checkout@v7.0.1
- name: Setup Node
uses: actions/setup-node@v7
@ -60,7 +60,7 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }}
steps:
- name: Deploy to GitHub Pages
id: deployment

View File

@ -9,7 +9,7 @@ permissions: read-all
jobs:
release-please:
runs-on: ubuntu-latest
runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }}
permissions:
contents: write
pull-requests: write
@ -19,7 +19,7 @@ jobs:
# We'll use the cli until there's a fix for
# https://github.com/googleapis/release-please/issues/2280.
- name: Checkout
uses: actions/checkout@v7.0.0
uses: actions/checkout@v7.0.1
- name: Install node
uses: actions/setup-node@v7
with:

View File

@ -7,13 +7,13 @@ permissions: read-all
jobs:
release-publish-artifacts:
runs-on: ubuntu-latest
runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }}
permissions:
packages: write
contents: write
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
uses: actions/checkout@v7.0.1
- name: Install QEMU static binaries
uses: docker/setup-qemu-action@v4
- name: Install Task

View File

@ -2,4 +2,4 @@
# responsible for code in a repository. For details, please refer to
# https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-code-owners
* @cloudnative-pg/maintainers
* @leonardoce @mnencia @gbartolini @fcanovai @armru @NiccoloFei

View File

@ -46,7 +46,7 @@ tasks:
- wordlist-ordered
env:
# renovate: datasource=git-refs depName=spellcheck lookupName=https://github.com/cloudnative-pg/daggerverse currentValue=main
DAGGER_SPELLCHECK_SHA: 7431c24907de3c6b1516114c8990d4f8d23d92cd
DAGGER_SPELLCHECK_SHA: f7187c307f2ba79290efbfff5ce436b956162c53
cmds:
- >
GITHUB_REF= dagger -s call -m github.com/cloudnative-pg/daggerverse/spellcheck@${DAGGER_SPELLCHECK_SHA}
@ -60,7 +60,7 @@ tasks:
desc: Check for conventional commits
env:
# renovate: datasource=git-refs depName=commitlint lookupName=https://github.com/cloudnative-pg/daggerverse currentValue=main
DAGGER_COMMITLINT_SHA: 7431c24907de3c6b1516114c8990d4f8d23d92cd
DAGGER_COMMITLINT_SHA: f7187c307f2ba79290efbfff5ce436b956162c53
cmds:
- >
GITHUB_REF= dagger -s call -m github.com/cloudnative-pg/daggerverse/commitlint@${DAGGER_COMMITLINT_SHA}
@ -74,7 +74,7 @@ tasks:
- wordlist-ordered
env:
# renovate: datasource=git-refs depName=uncommitted lookupName=https://github.com/cloudnative-pg/daggerverse currentValue=main
DAGGER_UNCOMMITTED_SHA: 7431c24907de3c6b1516114c8990d4f8d23d92cd
DAGGER_UNCOMMITTED_SHA: f7187c307f2ba79290efbfff5ce436b956162c53
cmds:
- GITHUB_REF= dagger -s call -m github.com/cloudnative-pg/daggerverse/uncommitted@${DAGGER_UNCOMMITTED_SHA} check-uncommitted --source . stdout
sources:
@ -86,7 +86,7 @@ tasks:
- controller-gen
env:
# renovate: datasource=git-refs depName=crd-gen-refs lookupName=https://github.com/cloudnative-pg/daggerverse currentValue=main
DAGGER_CRDGENREF_SHA: 7431c24907de3c6b1516114c8990d4f8d23d92cd
DAGGER_CRDGENREF_SHA: f7187c307f2ba79290efbfff5ce436b956162c53
# renovate: datasource=go depName=github.com/elastic/crd-ref-docs
CRDREFDOCS_VERSION: v0.3.0
cmds:
@ -383,7 +383,7 @@ tasks:
run: once
env:
# renovate: datasource=git-refs depName=controller-gen lookupName=https://github.com/cloudnative-pg/daggerverse currentValue=main
DAGGER_CONTROLLER_GEN_SHA: 7431c24907de3c6b1516114c8990d4f8d23d92cd
DAGGER_CONTROLLER_GEN_SHA: f7187c307f2ba79290efbfff5ce436b956162c53
cmds:
- >
GITHUB_REF= dagger -s call -m github.com/cloudnative-pg/daggerverse/controller-gen@${DAGGER_CONTROLLER_GEN_SHA}

4
go.mod
View File

@ -5,9 +5,9 @@ go 1.26.4
require (
github.com/cert-manager/cert-manager v1.21.0
github.com/cloudnative-pg/api v1.30.0
github.com/cloudnative-pg/barman-cloud v0.5.2-0.20260709152604-43158c204df1
github.com/cloudnative-pg/barman-cloud v0.5.2-0.20260720143032-950b0f57e122
github.com/cloudnative-pg/cloudnative-pg v1.30.0
github.com/cloudnative-pg/cnpg-i v0.5.0
github.com/cloudnative-pg/cnpg-i v0.6.0
github.com/cloudnative-pg/cnpg-i-machinery v0.4.2
github.com/cloudnative-pg/machinery v0.5.0
github.com/onsi/ginkgo/v2 v2.32.0

8
go.sum
View File

@ -20,12 +20,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudnative-pg/api v1.30.0 h1:L8hnvV/tPEQA1xYEi41FUBFA7FUNVGju8+SlgFlDDjI=
github.com/cloudnative-pg/api v1.30.0/go.mod h1:XrKBbOWObL33si0FNuwX4uHNf5JShiZyOUqd6LxbJQo=
github.com/cloudnative-pg/barman-cloud v0.5.2-0.20260709152604-43158c204df1 h1:+AV67yFPqnwmHQ5PIYkAaJUDsOrrwJCvH+J1JzHXyQc=
github.com/cloudnative-pg/barman-cloud v0.5.2-0.20260709152604-43158c204df1/go.mod h1:ZQLkdpk44FW5/BGWzABTOEcV9qPbwC+rbdscg2I8mBI=
github.com/cloudnative-pg/barman-cloud v0.5.2-0.20260720143032-950b0f57e122 h1:NuOztBdp+bUr/xYtaAw8o/x880hvWDRhJGl+R1YsZNw=
github.com/cloudnative-pg/barman-cloud v0.5.2-0.20260720143032-950b0f57e122/go.mod h1:ZQLkdpk44FW5/BGWzABTOEcV9qPbwC+rbdscg2I8mBI=
github.com/cloudnative-pg/cloudnative-pg v1.30.0 h1:fnhVq44xXx97MNiuvJsPrX1vSjYbgdyBK5MSGfdHdp0=
github.com/cloudnative-pg/cloudnative-pg v1.30.0/go.mod h1:QkolwBOWZ+GvAiJt6KpDSymwkpf0K19/p4Q6MQlTM8U=
github.com/cloudnative-pg/cnpg-i v0.5.0 h1:/TOzpNT6cwNgrpftTtrnLKdoHgMwd+88vZgXjlVgXeE=
github.com/cloudnative-pg/cnpg-i v0.5.0/go.mod h1:7Gh4+UzhBpGhr4DreB1GN9wGYfvxwXCXZUyVt3zE/3I=
github.com/cloudnative-pg/cnpg-i v0.6.0 h1:LA//DLkFOLIjU0ASOpFkydZhGir9IAIDfgSsTTX9IpU=
github.com/cloudnative-pg/cnpg-i v0.6.0/go.mod h1:4kcpLAj+feMTnh0TVadXRASdVnEhBytNSm/BvktLgmI=
github.com/cloudnative-pg/cnpg-i-machinery v0.4.2 h1:0reS9MtyLYINHXQ/MfxJ9jp39hhBf8e3Qdj+T5Nsq6I=
github.com/cloudnative-pg/cnpg-i-machinery v0.4.2/go.mod h1:gvrKabgxXq0zGthXGucemDdsxakLEQDMxn43M4HLW30=
github.com/cloudnative-pg/machinery v0.5.0 h1:hhTnkzn+AiN3NmbjCQ6RXj5rfqV3K6arzq6kdXAzcnQ=

View File

@ -27,6 +27,7 @@ import (
"path"
"time"
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
"github.com/cloudnative-pg/barman-cloud/pkg/archiver"
barmanCommand "github.com/cloudnative-pg/barman-cloud/pkg/command"
barmanCredentials "github.com/cloudnative-pg/barman-cloud/pkg/credentials"
@ -155,13 +156,17 @@ func (w WALServiceImplementation) Archive(
return nil, err
}
// Step 2: Check if the archive location is safe to perform archiving
checkFileExisting, err := fileutils.FileExists(emptyWalArchiveFile)
// Step 2: Check if the archive location is safe to perform archiving.
checkEmptyWalArchive, err := resolveArchiveEmptyWalArchiveCheck(
request.CheckEmptyWalArchive,
configuration.Cluster,
emptyWalArchiveFile,
)
if err != nil {
return nil, fmt.Errorf("while checking for empty wal archive check file %q: %w", emptyWalArchiveFile, err)
return nil, err
}
if utils.IsEmptyWalArchiveCheckEnabled(&configuration.Cluster.ObjectMeta) && checkFileExisting {
if checkEmptyWalArchive {
if err := CheckBackupDestination(
ctx,
&objectStore.Spec.Configuration,
@ -223,6 +228,31 @@ func (w WALServiceImplementation) Archive(
return &wal.WALArchiveResult{}, nil
}
// resolveArchiveEmptyWalArchiveCheck reports whether the WAL archive
// destination must be verified before archiving this segment.
//
// The operator owns the marker file's lifecycle, so when it sets the decision
// (non-nil) that value already accounts for the marker and is obeyed as-is. A
// nil value comes from an operator that predates this field, so we fall back to
// the previous logic: the Cluster annotation combined with the on-disk marker
// file.
func resolveArchiveEmptyWalArchiveCheck(
operatorDecision *bool,
cluster *cnpgv1.Cluster,
markerFilePath string,
) (bool, error) {
if operatorDecision != nil {
return *operatorDecision, nil
}
markerFilePresent, err := fileutils.FileExists(markerFilePath)
if err != nil {
return false, fmt.Errorf("while checking for empty wal archive check file %q: %w", markerFilePath, err)
}
return utils.IsEmptyWalArchiveCheckEnabled(&cluster.ObjectMeta) && markerFilePresent, nil
}
// Restore implements the WALService interface
func (w WALServiceImplementation) Restore(
ctx context.Context,
@ -249,9 +279,11 @@ func (w WALServiceImplementation) Restore(
"Restoring WAL file",
"objectStore", objectStore.Name,
"serverName", serverName,
"walName", walName)
"walName", walName,
"mode", request.GetMode())
return &wal.WALRestoreResult{}, w.restoreFromBarmanObjectStore(
ctx, configuration.Cluster, &objectStore, serverName, walName, destinationPath)
ctx, configuration.Cluster, &objectStore, serverName, walName, destinationPath,
request.GetMode() == wal.WALRestoreRequest_MODE_REWIND)
}
// resolveRestoreObjectStore selects the object store and server name to use when
@ -288,6 +320,7 @@ func (w WALServiceImplementation) restoreFromBarmanObjectStore(
serverName string,
walName string,
destinationPath string,
rewindMode bool,
) error {
contextLogger := log.FromContext(ctx)
startTime := time.Now()
@ -319,6 +352,20 @@ func (w WALServiceImplementation) restoreFromBarmanObjectStore(
return fmt.Errorf("while creating the restorer: %w", err)
}
// A flag left over from a normal-recovery invocation that ran before this pod
// was demoted must not survive into a pg_rewind restore: the flag machinery
// does not apply while restoring on behalf of pg_rewind (see
// shouldUseEndOfWALStreamFlag below), so it is never checked here, but left
// untouched it would resurface and wrongly abort the first normal-recovery
// invocation that runs once the rewind is done. This runs unconditionally,
// before the spool short-circuit in Step 1, so a request for a WAL file that
// happens to already be staged in the spool cannot skip the clear.
if rewindMode {
if err := clearEndOfWALStreamFlag(walRestorer); err != nil {
return err
}
}
// Step 1: check if this WAL file is not already in the spool
var wasInSpool bool
if wasInSpool, err = walRestorer.RestoreFromSpool(walName, destinationPath); err != nil {
@ -331,8 +378,10 @@ func (w WALServiceImplementation) restoreFromBarmanObjectStore(
return nil
}
// We skip this step if streaming connection is not available
if isStreamingAvailable(cluster, w.InstanceName) {
// Step 2: return error if the end-of-wal-stream flag is set.
// We skip this step if the flag machinery does not apply to this invocation
useEndOfWALStreamFlag := shouldUseEndOfWALStreamFlag(cluster, w.InstanceName, rewindMode)
if useEndOfWALStreamFlag {
if err := checkEndOfWALStreamFlag(walRestorer); err != nil {
return err
}
@ -340,10 +389,7 @@ func (w WALServiceImplementation) restoreFromBarmanObjectStore(
// Step 3: gather the WAL files names to restore. If the required file isn't a regular WAL, we download it directly.
var walFilesList []string
maxParallel := 1
if barmanConfiguration.Wal != nil && barmanConfiguration.Wal.MaxParallel > 1 {
maxParallel = barmanConfiguration.Wal.MaxParallel
}
maxParallel := maxWALFilesPerInvocation(barmanConfiguration, rewindMode)
if IsWALFile(walName) {
// If this is a regular WAL file, we try to prefetch
if walFilesList, err = gatherWALFilesToRestore(walName, maxParallel); err != nil {
@ -365,9 +411,9 @@ func (w WALServiceImplementation) restoreFromBarmanObjectStore(
return classifyWALRestoreError(walStatus[0].WalName, walStatus[0].Err)
}
// We skip this step if streaming connection is not available
// We skip this step if the flag machinery does not apply to this invocation
endOfWALStream := isEndOfWALStream(walStatus)
if isStreamingAvailable(cluster, w.InstanceName) && endOfWALStream {
if useEndOfWALStreamFlag && endOfWALStream {
contextLogger.Info(
"Set end-of-wal-stream flag as one of the WAL files to be prefetched was not found")
@ -415,6 +461,38 @@ func (w WALServiceImplementation) SetFirstRequired(
panic("implement me")
}
// maxWALFilesPerInvocation returns how many WAL files a single restore
// invocation is allowed to fetch, the requested one included. Prefetching is
// disabled when restoring on behalf of pg_rewind, which walks the WAL
// backward: the following segments would never be requested, and past the end
// of the timeline they do not even exist
func maxWALFilesPerInvocation(barmanConfiguration *barmanapi.BarmanObjectStoreConfiguration, rewindMode bool) int {
if rewindMode {
return 1
}
if barmanConfiguration.Wal != nil && barmanConfiguration.Wal.MaxParallel > 1 {
return barmanConfiguration.Wal.MaxParallel
}
return 1
}
// shouldUseEndOfWALStreamFlag returns true when the end-of-wal-stream flag
// machinery applies to the current invocation. The flag makes the following
// invocation fail, so that PostgreSQL stops polling the WAL archive and
// switches to streaming replication. It does not apply when no streaming
// connection is available, nor when restoring on behalf of pg_rewind:
// pg_rewind cannot fall back to streaming replication, and a stale flag would
// make it abort on a segment that is available in the archive
func shouldUseEndOfWALStreamFlag(cluster *cnpgv1.Cluster, podName string, rewindMode bool) bool {
if rewindMode {
return false
}
return isStreamingAvailable(cluster, podName)
}
// isStreamingAvailable checks if this pod can replicate via streaming connection.
func isStreamingAvailable(cluster *cnpgv1.Cluster, podName string) bool {
if cluster == nil {
@ -491,6 +569,22 @@ func checkEndOfWALStreamFlag(walRestorer *barmanRestorer.WALRestorer) error {
return nil
}
// clearEndOfWALStreamFlag removes the end-of-wal-stream flag, if present, without
// treating it as an error. It is used instead of checkEndOfWALStreamFlag when
// restoring on behalf of pg_rewind, which must not abort on a flag it did not
// set itself.
func clearEndOfWALStreamFlag(walRestorer *barmanRestorer.WALRestorer) error {
contain, err := walRestorer.IsEndOfWALStream()
if err != nil {
return err
}
if contain {
return walRestorer.ResetEndOfWalStream()
}
return nil
}
// isEndOfWALStream returns true if one of the downloads has returned
// a file-not-found error.
func isEndOfWALStream(results []barmanRestorer.Result) bool {

View File

@ -20,12 +20,21 @@ SPDX-License-Identifier: Apache-2.0
package common
import (
"context"
"os"
"path/filepath"
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
barmanRestorer "github.com/cloudnative-pg/barman-cloud/pkg/restorer"
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config"
)
var _ = Describe("resolveRestoreObjectStore", func() {
@ -96,3 +105,142 @@ var _ = Describe("resolveRestoreObjectStore", func() {
"cluster-server", "cluster-store"),
)
})
var _ = Describe("maxWALFilesPerInvocation", func() {
configWithMaxParallel := func(maxParallel int) *barmanapi.BarmanObjectStoreConfiguration {
return &barmanapi.BarmanObjectStoreConfiguration{
Wal: &barmanapi.WalBackupConfiguration{MaxParallel: maxParallel},
}
}
DescribeTable(
"computes how many WAL files a single invocation may fetch",
func(cfg *barmanapi.BarmanObjectStoreConfiguration, rewindMode bool, want int) {
Expect(maxWALFilesPerInvocation(cfg, rewindMode)).To(Equal(want))
},
Entry("no WAL configuration", &barmanapi.BarmanObjectStoreConfiguration{}, false, 1),
Entry("parallel restore configured", configWithMaxParallel(8), false, 8),
// pg_rewind walks the timeline backwards: prefetching must stay off no
// matter what the object store configuration asks for
Entry("rewind mode overrides the configured parallelism", configWithMaxParallel(8), true, 1),
)
})
var _ = Describe("shouldUseEndOfWALStreamFlag", func() {
clusterWithPrimary := func(currentPrimary string) *cnpgv1.Cluster {
return &cnpgv1.Cluster{
Status: cnpgv1.ClusterStatus{CurrentPrimary: currentPrimary},
}
}
DescribeTable(
"decides whether the end-of-wal-stream flag machinery applies",
func(cluster *cnpgv1.Cluster, podName string, rewindMode bool, want bool) {
Expect(shouldUseEndOfWALStreamFlag(cluster, podName, rewindMode)).To(Equal(want))
},
Entry("replica with streaming available", clusterWithPrimary("cluster-1"), "cluster-2", false, true),
Entry("primary cannot stream from anyone", clusterWithPrimary("cluster-1"), "cluster-1", false, false),
// pg_rewind cannot fall back to streaming replication: the flag machinery
// must stay off even where a standby would use it
Entry("rewind mode wins over streaming availability", clusterWithPrimary("cluster-1"), "cluster-2", true, false),
)
})
var _ = Describe("clearEndOfWALStreamFlag", func() {
newRestorer := func() *barmanRestorer.WALRestorer {
restorer, err := barmanRestorer.New(context.Background(), nil, GinkgoT().TempDir())
Expect(err).ToNot(HaveOccurred())
return restorer
}
It("is a no-op when the flag is not set", func() {
restorer := newRestorer()
Expect(clearEndOfWALStreamFlag(restorer)).To(Succeed())
isEOS, err := restorer.IsEndOfWALStream()
Expect(err).ToNot(HaveOccurred())
Expect(isEOS).To(BeFalse())
})
// Regression guard: a flag left over from a normal-recovery invocation that
// ran before this pod was demoted must not survive a pg_rewind restore, or
// it would resurface and wrongly abort the first normal-recovery invocation
// that runs once the rewind is done.
It("removes a pre-existing flag without returning an error", func() {
restorer := newRestorer()
Expect(restorer.SetEndOfWALStream()).To(Succeed())
Expect(clearEndOfWALStreamFlag(restorer)).To(Succeed())
isEOS, err := restorer.IsEndOfWALStream()
Expect(err).ToNot(HaveOccurred())
Expect(isEOS).To(BeFalse())
})
})
var _ = Describe("resolveArchiveEmptyWalArchiveCheck", func() {
// skipAnnotation mirrors the unexported constant in cloudnative-pg's
// pkg/utils; hard-coding the literal makes a divergence surface as a
// failing test rather than silently disabling the check.
const skipAnnotation = "cnpg.io/skipEmptyWalArchiveCheck"
clusterWith := func(annotationValue *string) *cnpgv1.Cluster {
cluster := &cnpgv1.Cluster{}
if annotationValue != nil {
cluster.Annotations = map[string]string{skipAnnotation: *annotationValue}
}
return cluster
}
// markerPath returns the marker file path inside a fresh temp dir,
// creating the file there when present is true.
markerPath := func(present bool) string {
filePath := filepath.Join(GinkgoT().TempDir(), metadata.CheckEmptyWalArchiveFile)
if present {
Expect(os.WriteFile(filePath, []byte{}, 0o600)).To(Succeed())
}
return filePath
}
When("the operator sets the decision", func() {
It("obeys true, ignoring the annotation and the marker file", func() {
// annotation would skip the check and the marker is absent, yet the
// operator's explicit true must still win.
got, err := resolveArchiveEmptyWalArchiveCheck(
ptr.To(true), clusterWith(ptr.To("enabled")), markerPath(false))
Expect(err).NotTo(HaveOccurred())
Expect(got).To(BeTrue())
})
It("obeys false, ignoring the annotation and the marker file", func() {
// annotation would keep the check on and the marker is present, yet the
// operator's explicit false must still win.
got, err := resolveArchiveEmptyWalArchiveCheck(
ptr.To(false), clusterWith(nil), markerPath(true))
Expect(err).NotTo(HaveOccurred())
Expect(got).To(BeFalse())
})
})
When("the operator predates the field (nil decision)", func() {
DescribeTable(
"falls back to the annotation combined with the marker file",
func(annotationValue *string, markerPresent bool, expected bool) {
got, err := resolveArchiveEmptyWalArchiveCheck(
nil, clusterWith(annotationValue), markerPath(markerPresent))
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(expected))
},
Entry("no annotation and marker present: check runs", nil, true, true),
Entry("no annotation and marker absent: check skipped", nil, false, false),
Entry("opt-out annotation and marker present: check skipped", ptr.To("enabled"), true, false),
Entry("unrelated annotation value and marker present: check runs", ptr.To("something-else"), true, true),
Entry("empty annotation value and marker present: check runs", ptr.To(""), true, true),
)
})
})

View File

@ -113,6 +113,7 @@ func (impl JobHookImpl) Restore(
configuration.Cluster,
&targetObjectStore.Spec.Configuration,
targetObjectStore.Name,
req.CheckEmptyWalArchive,
); err != nil {
return nil, err
}
@ -250,6 +251,7 @@ func (impl *JobHookImpl) checkBackupDestination(
cluster *cnpgv1.Cluster,
barmanConfiguration *cnpgv1.BarmanObjectStoreConfiguration,
objectStoreName string,
operatorCheckEmptyWalArchive *bool,
) error {
// Get environment from cache
env, err := barmanCredentials.EnvSetCloudCredentialsAndCertificates(ctx,
@ -288,14 +290,26 @@ func (impl *JobHookImpl) checkBackupDestination(
}
}
// Check if we're ok to archive in the desired destination
if utils.IsEmptyWalArchiveCheckEnabled(&cluster.ObjectMeta) {
if resolveRestoreEmptyWalArchiveCheck(operatorCheckEmptyWalArchive, cluster) {
return common.CheckBackupDestination(ctx, barmanConfiguration, walArchiver, serverName)
}
return nil
}
// resolveRestoreEmptyWalArchiveCheck reports whether the destination must be
// verified before restoring. When the operator sets the decision (non-nil) it
// is obeyed as-is; a nil value comes from an operator that predates this field,
// so we fall back to the Cluster annotation. Unlike archiving, restore is a
// one-shot operation that has never been gated on the first-archive marker
// file, so the annotation is the only fallback needed.
func resolveRestoreEmptyWalArchiveCheck(operatorDecision *bool, cluster *cnpgv1.Cluster) bool {
if operatorDecision != nil {
return *operatorDecision
}
return utils.IsEmptyWalArchiveCheckEnabled(&cluster.ObjectMeta)
}
// restoreCustomWalDir moves the current pg_wal data to the specified custom wal dir and applies the symlink
// returns indicating if any changes were made and any error encountered in the process
func (impl JobHookImpl) restoreCustomWalDir(ctx context.Context) (bool, error) {

View File

@ -0,0 +1,66 @@
/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SPDX-License-Identifier: Apache-2.0
*/
package restore
import (
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
"k8s.io/utils/ptr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("resolveRestoreEmptyWalArchiveCheck", func() {
// skipAnnotation mirrors the unexported constant in cloudnative-pg's
// pkg/utils; hard-coding the literal makes a divergence surface as a
// failing test rather than silently disabling the check.
const skipAnnotation = "cnpg.io/skipEmptyWalArchiveCheck"
clusterWith := func(annotationValue *string) *cnpgv1.Cluster {
cluster := &cnpgv1.Cluster{}
if annotationValue != nil {
cluster.Annotations = map[string]string{skipAnnotation: *annotationValue}
}
return cluster
}
When("the operator sets the decision", func() {
It("obeys true even when the annotation would skip the check", func() {
Expect(resolveRestoreEmptyWalArchiveCheck(ptr.To(true), clusterWith(ptr.To("enabled")))).To(BeTrue())
})
It("obeys false even when the annotation would keep the check on", func() {
Expect(resolveRestoreEmptyWalArchiveCheck(ptr.To(false), clusterWith(nil))).To(BeFalse())
})
})
When("the operator predates the field (nil decision)", func() {
DescribeTable(
"falls back to the Cluster annotation, never to a marker file",
func(annotationValue *string, expected bool) {
Expect(resolveRestoreEmptyWalArchiveCheck(nil, clusterWith(annotationValue))).To(Equal(expected))
},
Entry("no annotation: check runs", nil, true),
Entry("opt-out annotation: check skipped", ptr.To("enabled"), false),
Entry("unrelated annotation value: check runs", ptr.To("something-else"), true),
Entry("empty annotation value: check runs", ptr.To(""), true),
)
})
})

View File

@ -0,0 +1,32 @@
/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SPDX-License-Identifier: Apache-2.0
*/
package restore
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestRestore(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Restore job hook test suite")
}

View File

@ -72,8 +72,8 @@ func newGCSDeployment(namespace, name string) *appsv1.Deployment {
{
Name: name,
// renovate: datasource=docker depName=fsouza/fake-gcs-server versioning=docker
// Version: 1.55.0
Image: "fsouza/fake-gcs-server@sha256:2845d1504bb4a5a4fe54b16dd70e398814eb139af49b01b07839e016120ff123",
// Version: 1.55.1
Image: "fsouza/fake-gcs-server@sha256:91afded49de804aa61b5f3eb6c7cd65205acf9e5c5e047cf0ba7d9507af806c8",
Ports: []corev1.ContainerPort{
{
ContainerPort: 4443,

View File

@ -121,8 +121,8 @@ func newS3ClientDeployment(namespace string) *appsv1.Deployment {
{
Name: s3ClientName,
// renovate: datasource=docker depName=amazon/aws-cli versioning=docker
// Version: 2.36.1
Image: "docker.io/amazon/aws-cli@sha256:406ca32d31e640a56e8d52921b40528cc64bfa59ec9cb4ee1456db6746cb7292",
// Version: 2.36.3
Image: "docker.io/amazon/aws-cli@sha256:bdd02067a00c354684086071b475955c54caa7bd88b851aac99a51326fe19652",
Command: []string{"sleep", "infinity"},
Env: []corev1.EnvVar{
{

View File

@ -12,15 +12,15 @@
section-matter "^1.0.0"
strip-bom-string "^1.0.0"
"@algolia/abtesting@1.21.2":
version "1.21.2"
resolved "https://registry.yarnpkg.com/@algolia/abtesting/-/abtesting-1.21.2.tgz#798e51b0226b6ed303a7d891235606b674a87857"
integrity sha512-uXj0rgk30EpsKvOpuS+R+1XFDrnm56hED1Lz56e8uBkZdKCxw99LS2U8eXBqAHYU8kpkbsnV1GC8velBG070Hg==
"@algolia/abtesting@1.22.0":
version "1.22.0"
resolved "https://registry.yarnpkg.com/@algolia/abtesting/-/abtesting-1.22.0.tgz#7537637f52d2fe00b3714fe2d5ce8cb856be35ff"
integrity sha512-BFR6zNowNKcY7Ou7TaJc9QWexES4YKPbmf/OTFofpdsdhz4x6q0lbxp3duO0EHnyrN7rE4ba/TSXuY+BDGu4+g==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/autocomplete-core@1.19.2":
version "1.19.2"
@ -62,126 +62,126 @@
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.9.tgz#c5b05e23c71027e4e45a301f286593dffdcdfbdf"
integrity sha512-YosP9Uoek6y/Ur1r1qeogk4biMe/hzkyNcgMCciw0//3XpCM7VlYLSHnyt/vOnEOGhCCc0+3v+unEiH6zz+Z1A==
"@algolia/client-abtesting@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.55.2.tgz#5c7d1632051b05ef155a6ea7e4a9454d5c6d5a08"
integrity sha512-y7Epol8HcjlBxKXHhyhfFPFhm78B3P6x9cCbCyGTdxjsdVCptXCy5hpkZWxjGpnaLHvWsHS4QRF0TiBOLst2xg==
"@algolia/client-abtesting@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.56.0.tgz#25be0db6f97ae91dfdbaffbe9eae99e963310891"
integrity sha512-7r4Z3NC7yU1oAQVWJNA2HX7tX481F3pJvCGyLIXiTdBcthz4Q/o21jwcMYDFkuI92UWTNBQQmHYgwHo1zS5dzg==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/client-analytics@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.55.2.tgz#770694ceef05213a2dc1c64bf3cb9c0b33a16b71"
integrity sha512-8Pxj2VVmpM2d+UZufnlTq7T1QIcYPVugLV5XC50PnHsV5uRM9CSoYkg2Y+CwqwRk2La0xK5QsfZ0obIU+9XftQ==
"@algolia/client-analytics@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.56.0.tgz#bcace36358cda26ca285f3a100544db0ce29aabe"
integrity sha512-avmjXQSq+jadFO8Xl2em05/uQdQnEmHsJyOAdVbZkmVgpMfxL12aJwVVfGNwYr9nulcpuJN1X0lTaQ5wxuNGcA==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/client-common@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.55.2.tgz#17347bdd5a118b7966231a73c59cac44d256071c"
integrity sha512-9L4IpIYUqA63a7sw1trnHQGUvwiAjKz67nsgDnal98JGAc7wyposRb0Iag+eiMuyzFFaSHLe2/rGyIo+PafRBA==
"@algolia/client-common@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.56.0.tgz#6190221a7091dfa1a0e0a3b5964e92b1b59968ba"
integrity sha512-v2TPStUhY//ripPjIVclZ8AWc7DEGooXULZGFlFu37zNatgHjw34oZZ+OSbbc/YHO+xZwPl62I1k8xH1m4S2eg==
"@algolia/client-insights@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.55.2.tgz#3e575307d01d2e6ccd7394b7428edd22ec68057c"
integrity sha512-ZBm2ytY5EHFcj+kjNsXxMNO/TGlOHe2fBFXGKHJOM1bk1rAy4o2YI+d9oV/w/jrqx44pvJMJlc8X6vKnCuDgUQ==
"@algolia/client-insights@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.56.0.tgz#2ff179c8924ff188d4604e32aef645c709cc76be"
integrity sha512-P0ehROpM4Sem3Sqo5x2cKPgj67D3G3jy0rh1Amwkcvsfr6tkvIcdCmerieanqTF7NxUMPNFLkpIFeMO8Rpa50w==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/client-personalization@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.55.2.tgz#1c5458095fdfdd2dd0ed8dcd5b94962db2852400"
integrity sha512-3FGVW/jDk7sdYwqa2NKnF/qXWcttc4bvGrwNbvqz3VoWSRv42CNvRk+3Y9QJFIUf1vY50hAuVWUoFKdyc8vaXA==
"@algolia/client-personalization@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.56.0.tgz#64f8197c46c60306a115bc4024f6ebce2d8140d8"
integrity sha512-SXK3Vn3WVxyzbm31oePZBJkp1wpOyuWdd4B/Pv7n0aXDxmeSWhC1R1FC1517mMrFAIaPH4Rt0x6RUe7ZNjz8FA==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/client-query-suggestions@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.55.2.tgz#7bf49bf6f7832943646dcaa1ffc2873e54bb783e"
integrity sha512-JsG8LovDAYul5t8e533tZ3O1uZILxso5zsTtB7ONc5RJ8ACdTxAAC/jaOnsBNYb+x+STP7fzx/Iro55v5DNgoQ==
"@algolia/client-query-suggestions@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.56.0.tgz#e4a28bc249e9c69f0b5ef280b72a12e32e8c53c8"
integrity sha512-5+ZdX8garFnmycnZgKhtXHePEaLj5zqDxI/0lkhhluzCcvTn0/PvvTirTg8hHYetQHvn7GDyeAiqTAieMvMW4A==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/client-search@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.55.2.tgz#8cd3d994598210e5428ca9a64dd3f17620bfbf08"
integrity sha512-5wDnoIfC75zJ2MSHv5SSzTlRL2z7jQMbqQ5jrzottuq2p3oBObv8pD/JpXWu8pRaimaxNr3/Bs/KZIGVXxJ7hg==
"@algolia/client-search@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.56.0.tgz#66b19d5382bf47a16105817b78e372f5e9039508"
integrity sha512-+mKUdYvqOi0BcvpAEyCEw49vSBptufIcfibtHz2bdr1pI789M46Yt0uQEk/sxtK3teh71OQvVFHaTDzShUWewQ==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/events@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950"
integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==
"@algolia/ingestion@1.55.2":
version "1.55.2"
resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.55.2.tgz#e247f70c5055569af70e3c676853bad4c422ab6c"
integrity sha512-da+SC6ikpza98W7C5ChsKEQDvZc8PQLQ0sxmQ5yMRsHpdD3iPKnclJA6ViB5Nr5T9qOX+IDswC6AyqY4V3rtug==
"@algolia/ingestion@1.56.0":
version "1.56.0"
resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.56.0.tgz#1b3c41b8f1c5a2d309610683e3d1b99f962d92df"
integrity sha512-9g/zj+AZx5moFcdFIrYQoVrueXivjUcc3MQHtCYT8WhIuk1lUh1AyEhvJCS0XBZld09cLvd1AZ3BvDBpVpX2UA==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/monitoring@1.55.2":
version "1.55.2"
resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.55.2.tgz#56c8b9e5b8e64d5f8fd41a37854eb5559cb798d4"
integrity sha512-Y8kEcPqCiIEeaGv83l9RRA09mfYECqAJHNnOyEtZc9UirI6XBMUyFVss/sSeYUiV/Lf30hkbWcl00V1uXsf86Q==
"@algolia/monitoring@1.56.0":
version "1.56.0"
resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.56.0.tgz#11cc9198e778466eafde5a12d02fd02cfcbd1710"
integrity sha512-Qf3Sr6f9A9uxCZUf3MXS0d2b877uYzEB5yxqpVGXAhcJnBCQjrRRon0KvefpGkxy+BshrIJs96OUoMtGqXTFDA==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/recommend@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.55.2.tgz#9549d2fe261dca9ac94c144131ea980c56a6442b"
integrity sha512-5zmobuCQqFZkx+84Nt+suL7vo6jTh2CfAs2ndDSeTS2QHvnzP8YEEGWtWftjyACI0cK/FuH8urWwCHP+d2j8TA==
"@algolia/recommend@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.56.0.tgz#6c96193cb91cf5c9f3c884fdf064e15dfe6cb742"
integrity sha512-GXWG1rWc5wu8hY4N33Y3b6ernY6sAdAvmKWN/zHAiACOx40WnpG0TVX5YazCAr/9gOYGInSiM2A0y2jy2xbiDA==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
"@algolia/requester-browser-xhr@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.55.2.tgz#087322fcdfb35fdf8226d2ca966628c49f711d74"
integrity sha512-qnGUUuWG66dRMnr33owLsrYIh9fHVxtU4R2rd3SpneAHuoAUcGbDOWNrj05glVU6M8yOqo9gQ22K8zpz0I8Xpg==
"@algolia/requester-browser-xhr@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.56.0.tgz#b1f4705c53f1602ec14339999bfbf0e3e9a7bbc8"
integrity sha512-7t24cBxaInS3mZb7ddEaZT/tp6q+/aR4YttsQVyP1/i+LmwPR34atO35KjaLFCcRVrlP7sYOAqkCfg6lIRB+ew==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-fetch@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.55.2.tgz#a01695738861a80181f231ce7e8fca6f7a6e4072"
integrity sha512-lKZ5uhafMvR7dWCJEyuaeyZitid1I3ICx+k0vGf5x/ktdIQvc7bndCiOPpmIDqUmN26FE3jTehkAzSqee95G2Q==
"@algolia/requester-fetch@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.56.0.tgz#03f0aeea08efc991ab9f0663f1ea63df720fd9ba"
integrity sha512-R7ePHgVYmDFjZpvrsVAfbDz/d4RxKAYZ5/vgLfIsCVRZRryjWl/3INOxpOICzitehQ5FjNtNjcLQTrmHPTcHBQ==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/client-common" "5.56.0"
"@algolia/requester-node-http@5.55.2":
version "5.55.2"
resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.55.2.tgz#5ceae3de5e223366fc5b3e643fcc0542909b87e8"
integrity sha512-Zc90xvKWUvxcNicvvTO9Pr/hT2TAnkixOIzJm/KMj5Ptm2pKjk71ngTsdkbRtJQvhZ2Kr9N1YdIjLrNHB5P2xw==
"@algolia/requester-node-http@5.56.0":
version "5.56.0"
resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.56.0.tgz#238bfa98ed145e261c1db7e133bee6390ba58c94"
integrity sha512-PIOUXlSnrqM0S+WOgDRb4RzotydJH7ZoT6tOyL7tAO7qJOfvX5wsEW8Pe+PMKMwvuI4/gIyK9cg2H7lJXqnc4Q==
dependencies:
"@algolia/client-common" "5.55.2"
"@algolia/client-common" "5.56.0"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.29.7":
version "7.29.7"
@ -2616,9 +2616,9 @@
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
"@sinclair/typebox@^0.27.8":
version "0.27.10"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.10.tgz#beefe675f1853f73676aecc915b2bd2ac98c4fc6"
integrity sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==
version "0.27.12"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.12.tgz#0cacd3cff047a32936b1ace47ea7c86eaab60a7f"
integrity sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==
"@sindresorhus/is@^4.6.0":
version "4.6.0"
@ -3400,24 +3400,24 @@ algoliasearch-helper@^3.26.0:
"@algolia/events" "^4.0.1"
algoliasearch@^5.37.0:
version "5.55.2"
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.55.2.tgz#5aa9426faedfa63c6433e6ba78b1843c8208e012"
integrity sha512-OyacJsaeuLUvGWOynNqYc6sx88XvyoG39wMT8SYqL3l9wwaorDW/LPRbUPfhzw0bWsUWzNCZTnFYOrWFBKsUaw==
version "5.56.0"
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.56.0.tgz#042f1a39dfa951356206f43dece1c8d1ea1721bd"
integrity sha512-PrqppUmhT4ENdas2pH9caE7efUcxy6EcSFhWzosiVuQBzu2tQ5yLTI6jwomT/1cuBnivzGfxiJCqDNN9FRRh+Q==
dependencies:
"@algolia/abtesting" "1.21.2"
"@algolia/client-abtesting" "5.55.2"
"@algolia/client-analytics" "5.55.2"
"@algolia/client-common" "5.55.2"
"@algolia/client-insights" "5.55.2"
"@algolia/client-personalization" "5.55.2"
"@algolia/client-query-suggestions" "5.55.2"
"@algolia/client-search" "5.55.2"
"@algolia/ingestion" "1.55.2"
"@algolia/monitoring" "1.55.2"
"@algolia/recommend" "5.55.2"
"@algolia/requester-browser-xhr" "5.55.2"
"@algolia/requester-fetch" "5.55.2"
"@algolia/requester-node-http" "5.55.2"
"@algolia/abtesting" "1.22.0"
"@algolia/client-abtesting" "5.56.0"
"@algolia/client-analytics" "5.56.0"
"@algolia/client-common" "5.56.0"
"@algolia/client-insights" "5.56.0"
"@algolia/client-personalization" "5.56.0"
"@algolia/client-query-suggestions" "5.56.0"
"@algolia/client-search" "5.56.0"
"@algolia/ingestion" "1.56.0"
"@algolia/monitoring" "1.56.0"
"@algolia/recommend" "5.56.0"
"@algolia/requester-browser-xhr" "5.56.0"
"@algolia/requester-fetch" "5.56.0"
"@algolia/requester-node-http" "5.56.0"
ansi-align@^3.0.1:
version "3.0.1"
@ -3501,12 +3501,12 @@ astring@^1.8.0:
integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==
autoprefixer@^10.4.19, autoprefixer@^10.4.23:
version "10.5.3"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.5.3.tgz#cd6f4c611cef98055c998b5de8e6b1759f024447"
integrity sha512-bJRzflk8GgE4JX+iZNEwz9f9p460NCHnU7bd+CZ9vIjIlZuTkt6F3WSl2oNO8StZBFx17nLEsiQ6H2wcZiY7nA==
version "10.5.4"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.5.4.tgz#3b7dd848b4155514a95ad1f6ce598844bea09697"
integrity sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==
dependencies:
browserslist "^4.28.6"
caniuse-lite "^1.0.30001805"
caniuse-lite "^1.0.30001806"
fraction.js "^5.3.4"
picocolors "^1.1.1"
postcss-value-parser "^4.2.0"
@ -3777,10 +3777,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001803, caniuse-lite@^1.0.30001805:
version "1.0.30001805"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz#78d5d5968a69b7ff81af87a96d7ddc7ea6670b1e"
integrity sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001803, caniuse-lite@^1.0.30001806:
version "1.0.30001806"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz#1bc8e502b723fa393455dfbedd5ccec0c29bb74e"
integrity sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==
ccount@^2.0.0:
version "2.0.1"
@ -4573,9 +4573,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.389:
version "1.5.392"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz#31e9f9af0d8df3d1489468a4242b173605617482"
integrity sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==
version "1.5.393"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz#aec971df2a6f4f7e51fbacb200d66cebfc7d3c17"
integrity sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==
emoji-regex@^8.0.0:
version "8.0.0"
@ -4922,9 +4922,9 @@ fast-json-stable-stringify@^2.0.0:
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fast-uri@^3.0.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.3.tgz#f695a40f006aba505631573a0021ddb21194ad11"
integrity sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==
version "3.1.4"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.4.tgz#3b3daf9ce68f41f956df0b505132c0cfce9ec7af"
integrity sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==
fastq@^1.6.0:
version "1.20.1"
@ -6893,7 +6893,7 @@ multicast-dns@^7.2.5:
dns-packet "^5.2.2"
thunky "^1.0.2"
nanoid@^3.3.12:
nanoid@^3.3.16:
version "3.3.16"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c"
integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==
@ -7818,11 +7818,11 @@ postcss-zindex@^6.0.2:
integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==
postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.33, postcss@^8.5.4:
version "8.5.19"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.19.tgz#45ad5cfde499408e20147348237551381a922037"
integrity sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==
version "8.5.20"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.20.tgz#316a139da79484ce55969fa7bb6f2f1abcc14060"
integrity sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==
dependencies:
nanoid "^3.3.12"
nanoid "^3.3.16"
picocolors "^1.1.1"
source-map-js "^1.2.1"
@ -9463,9 +9463,9 @@ write-file-atomic@^3.0.3:
typedarray-to-buffer "^3.1.5"
ws@^7.3.1:
version "7.5.12"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.12.tgz#4ca2c04966db4dcd2f9bc4d1419d23cd1e4a18db"
integrity sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg==
version "7.5.13"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.13.tgz#12aa507eaca76c295c278b1aebf4698ab2c1845f"
integrity sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==
ws@^8.18.0:
version "8.21.1"