Compare commits

...
85 Commits
Author SHA1 Message Date
NotAlexNoyle c645ed1adf Exclude CODEOWNERS from language statistics 2026-11-01 09:18:32 -05:00
NotAlexNoyle 87234d9f76 Add CODEOWNERS (one staff approval) 2026-11-01 09:18:32 -05:00
Alexey Palazhchenko 0d5eb3343a Prepare v2.7.0 release (#5538) 2025-11-10 21:18:24 +04:00
Alexey Palazhchenko 52ad11f0af Bump deps (#5537) 2025-11-10 20:45:21 +04:00
Alexey Palazhchenko 71b6eb3a08 Bump DocumentDB (#5536) 2025-11-09 22:46:54 +04:00
5cb62a1155 Add MCP tools (#5452)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-11-09 20:36:10 +04:00
Alexey Palazhchenko 867537dd01 Use Go 1.25.4 (#5534) 2025-11-08 09:35:59 +04:00
487880fd19 Add basic MCP server (#5279)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-11-06 09:08:44 +04:00
Chi FujiiandAlexey Palazhchenko 3b29cece0c Refactor DataAPI with graceful shutdown and add OpenAPI specification endpoint (#5357)
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-10-27 18:55:12 +04:00
Alexey Palazhchenko c1bb0b6db6 Update DocumentDB (#5528) 2025-10-22 18:46:58 +04:00
Alexey Palazhchenko a04b20a58f Use forked lazyerrors (#5527) 2025-10-22 16:13:02 +04:00
Josh Soref 2b2ce550d4 Fix CODEOWNERS grammar (#5495)
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-10-21 12:51:38 +04:00
Alexey Palazhchenko 87293d3434 Skip flaky test for now (#5526) 2025-10-21 12:50:19 +04:00
Alexey Palazhchenko 7b0d200262 Use Go 1.25.3 (#5525) 2025-10-21 11:43:35 +04:00
Patryk KwiatekandAlexey Palazhchenko d1d037a030 Add a test for *documentdb.Conn finalizing (#5484)
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-10-21 11:28:03 +04:00
Alexey Palazhchenko c37a62b0a2 Bump dependencies (#5519) 2025-10-09 17:57:31 +04:00
Alexey Palazhchenko cff1577d19 Improve proxy mode observability (#5440) 2025-10-03 19:53:25 +04:00
Alexey Palazhchenko efa50d4877 Add tracing of SQL queries (#5514) 2025-10-02 21:02:46 +04:00
Alexey Palazhchenko 067eb2f143 Rename OpenTelemetry spans (#5512) 2025-09-29 14:58:13 +04:00
Alexey Palazhchenko 4e8e51751d Minor observability cleanup (#5511) 2025-09-28 22:32:36 +03:00
Alexey Palazhchenko ae0b94de12 Bump deps (#5509) 2025-09-28 20:56:00 +03:00
Alexey Palazhchenko 2c24483c06 Use Go 1.25.1 (#5508) 2025-09-28 15:48:00 +03:00
Josh Soref 221d5cbd08 Fix spelling in .go files (#5492)
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-09-08 10:33:12 +09:00
Alexey Palazhchenko 62ce1019b1 Simplify licensing 2025-09-07 12:08:29 +04:00
Josh Soref ef9934e3ed Replace malicious container registry references with ghcr (#5486) 2025-09-03 20:05:21 +04:00
Alexey Palazhchenko e78eaffb58 Finalize PostgreSQL metrics (#5485) 2025-09-03 15:25:44 +04:00
Chi FujiiandAlexey Palazhchenko 370b1586c5 Add PostgreSQL metrics (#5427)
Closes #5402.

Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-09-02 14:50:16 +04:00
Alexander Tobi FashakinandCopilot 1f8d160296 Update cloud installation docs (#5466)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-01 19:52:16 +04:00
3656ebd1c4 Use generated structs in Data API (#5382)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-09-01 13:24:21 +04:00
Patryk KwiatekandAlexey Palazhchenko e3a7406b9e Update wire dependency (#5465)
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-09-01 12:48:10 +04:00
Alexey Palazhchenko 4847e59e25 Update dependencies (#5479) 2025-09-01 11:55:26 +04:00
Alexey Palazhchenko b6fa806add Move to documentdb/documentdb (#5468) 2025-08-27 12:35:57 +04:00
Chi FujiiandAlexey Palazhchenko 5394f00550 Avoid hijacking connections from the pool and immediately closing them (#5443)
Closes #5402.
Closes FerretDB/FerretDB-DocumentDB#270.

Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-08-27 11:37:26 +04:00
Alexey Palazhchenko 9a48b7132c Add helpers for lazy logging (#5467) 2025-08-27 10:26:35 +04:00
Alexey Palazhchenko 4cea93524d Fix FerretDB Cloud link 🤦 2025-08-26 19:38:39 +04:00
Alexey Palazhchenko 3cd4554d87 Add Cease and Desist Notice (#5462) 2025-08-25 18:50:08 +04:00
Alexey Palazhchenko 2e6ca25078 Add FerretDB Cloud blog post (#5461) 2025-08-25 17:04:31 +04:00
Alexey Palazhchenko 901a8f6030 Add MongoDB's complaint (#5460) 2025-08-25 12:45:03 +04:00
Alexey Palazhchenko b1feb6377d Add documentation page stub for MCP server (#5451) 2025-08-22 07:49:36 +00:00
Alexey Palazhchenko 3fd8c982a5 Bump deps (#5455) 2025-08-22 11:32:52 +04:00
Alexander Tobi FashakinandCopilot c05de8bdea Document cloud instance deployment and setup (#5424)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-21 20:15:06 +04:00
Alexey Palazhchenko d97f2612b8 Add TODO comment about Debian 13 Trixie (#5450)
Refs FerretDB/FerretDB#5449.
2025-08-21 13:42:59 +04:00
Alexey Palazhchenko 6d8e86b774 Remove flaky test (#5444) 2025-08-20 12:17:35 +04:00
Alexander Tobi Fashakin 7a117e768d Update compatible apps (#5442) 2025-08-20 05:51:21 +00:00
Alexander Tobi FashakinandCopilot bfd1650ee2 Update content guide (#5441)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-19 19:09:52 +04:00
Alexey Palazhchenko d9a14c883e Add comments to supervisord config (#5430) 2025-08-19 14:07:17 +04:00
Alexey Palazhchenko 2b652dae20 Use middleware (#5377) 2025-08-19 13:57:06 +04:00
Alexey Palazhchenko 6631594402 Add middleware (#5437) 2025-08-19 07:38:31 +04:00
c5f69fe962 Add blog post on Formio (#5395)
Co-authored-by: Chi Fujii <chi.fujii@ferretdb.io>
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-08-18 22:39:46 +04:00
Alexey Palazhchenko a65042d381 Update expected PostgreSQL version (#5438) 2025-08-18 20:12:33 +04:00
Alexey Palazhchenko 24981ad991 Tweak conninfo (#5435) 2025-08-18 12:23:23 +04:00
Alexey Palazhchenko 2078f859f3 Make Run wait for all Handle calls to finish (#5434) 2025-08-18 11:52:42 +04:00
Chi FujiiandAlexey Palazhchenko 16670ff34f Upload YugabyteDB log files (#5401)
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-08-18 09:27:56 +04:00
Alexey Palazhchenko b994d18792 Update DocumentDB (#5431) 2025-08-14 19:21:04 +04:00
Alexey Palazhchenko 4ea90be4aa Use QEMU on arm64 for Yugabyte for now (#5429) 2025-08-14 12:32:49 +04:00
3770c1e4eb Add release blog post for FerretDB v2.5.0 (#5412)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-08-14 12:06:53 +04:00
Chi Fujii 5fd75c160a Bump YugabyteDB to stable version (#5428) 2025-08-14 10:55:16 +04:00
Alexey Palazhchenko c60131a69c Update documentation to point to the next release (#5423) 2025-08-14 10:54:45 +04:00
Alexey Palazhchenko b4eda04f37 Prepare v2.5.0 release (#5414) 2025-08-12 15:06:25 +04:00
Alexey Palazhchenko 6e2d3affd9 Update DocumentDB (#5411) 2025-08-12 10:25:34 +04:00
Alexey Palazhchenko 2cbf8cac53 Update Prometheus metrics (#5403) 2025-08-11 19:51:18 +04:00
Patryk Kwiatek b933b1f365 Test flaky tests detection (#5393) 2025-08-06 14:12:10 +04:00
Alexey Palazhchenko 96d34a4615 Enable MongoDB test commands (#5400) 2025-08-04 11:17:26 +04:00
Alexey Palazhchenko 1a5a6ec3c3 Update DocumentDB (#5398) 2025-08-01 18:29:12 +04:00
Chi Fujii 997c02f379 Run all YugabyteDB integration tests (#5283) 2025-08-01 14:31:33 +04:00
Alexey Palazhchenko 2412bf3595 Make a resource.Untrack thread-safe (#5396) 2025-08-01 12:10:01 +04:00
Alexey Palazhchenko 20697b6b74 Small tweaks (#5394) 2025-07-31 09:53:28 +04:00
Alexander Tobi FashakinandAlexey Palazhchenko cb18c69dcc Add WeKan blog post (#5381)
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-07-30 19:32:21 +04:00
Alexey Palazhchenko e99a07db05 Bump DocumentDB version (#5392) 2025-07-30 19:13:25 +04:00
Chi FujiiandAlexey Palazhchenko b1189f2594 Add YugabyteDB integration tests (#5274)
Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
2025-07-30 16:53:31 +04:00
Alexey Palazhchenko 783d39086c Encapsulate PostgreSQL pool in the Handler (#5389) 2025-07-30 11:28:47 +04:00
Alexey Palazhchenko c87f9e3c18 Small cleanup (#5388) 2025-07-30 11:02:43 +04:00
Patryk Kwiatek e5615b35ca Use Codecov for flaky integration tests detection (#5361) 2025-07-30 05:51:39 +00:00
Alexey PalazhchenkoandCopilot 327a8fb759 Extract wiring into a separate package (#5383)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-30 09:23:06 +04:00
Alexander Tobi Fashakin 319128c43f Enable full visibility for images on zoom (#5346) 2025-07-28 20:21:27 +04:00
Alexey Palazhchenko bccd6276dd Update DocumentDB (#5380) 2025-07-28 12:17:48 +04:00
Alexey Palazhchenko 1b5913f2bf Simplify flags handling (#5378) 2025-07-28 09:16:48 +04:00
Chi Fujii 66f9bc149d Make accessing YugabyteDB logs easier (#5362)
Closes #85.
2025-07-25 08:12:33 +00:00
Chi Fujii f06bf851a9 Skip YugabyteDB setup for arm64 (#5372)
Closes #85.
2025-07-25 07:06:06 +00:00
Alexey Palazhchenko c891abbdf6 Refactor normal and error responses (#5326) 2025-07-24 19:24:29 +04:00
Alexey PalazhchenkoandChi Fujii f65c7b643a Tweak Ollama and MCPHost setup (#5375)
Co-authored-by: Chi Fujii <chi.fujii@ferretdb.io>
2025-07-24 09:06:14 +04:00
Alexander Tobi Fashakin bc0e9128e3 Add Vault blog post (#5368)
Closes #5152.
2025-07-23 19:44:59 +04:00
Patryk Kwiatek e1ea96acf4 Add components in codecov (#5364) 2025-07-23 11:06:56 +04:00
Alexey Palazhchenko f64490cf94 Update DocumentDB (#5353) 2025-07-17 12:14:08 +04:00
Alexander Tobi Fashakin 5a05644fed Add release blogpost for 2.4.0 (#5354) 2025-07-16 19:56:04 +04:00
498 changed files with 11597 additions and 4262 deletions

No files matched your search

+3
View File
@@ -4,4 +4,7 @@
*.webp filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.go diff=golang
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
+1 -1
View File
@@ -1,5 +1,5 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Order is important; the last matching pattern takes the most precedence.
# Order is important; the last matching pattern has the highest precedence.
* @AlekSi @chilagrow
+3 -3
View File
@@ -9,7 +9,7 @@
- [local image](../build/deps/postgres-documentdb.Dockerfile);
- [bw image](../build/ferretdb-bw/Dockerfile).
3. Update constants in [`documentdb.go`](../build/version/documentdb.go).
4. Check DocumentDB images tags in documentation (where we specify full tags).
4. Check DocumentDB images/tags in documentation (where we specify full tags).
5. Re-run tests, linters.
6. Check issues and pull requests, update milestones and labels.
7. Check if there are breaking changes or anything that should be reflected in the changelog and/or user documentation.
@@ -64,8 +64,8 @@
1. Publish and announce blog post.
2. Add link to blog post to `CHANGELOG.md` and release page.
3. Bump the latest version on https://beacon.ferretdb.com and https://beacon.ferretdb.io.
4. Update DocumentDB image tags to point back to development version.
5. Update DocumentDB images tags in the current documentation (where we specify full tags)
4. Update DocumentDB images/tags in Dockerfiles to point back to development version.
5. Update DocumentDB images/tags in the current documentation (where we specify full tags)
to point to the next (non-existing) version.
6. Update URLs with `/refs/tags/` in them in the current documentation
to point to the next (non-existing) version.
+26 -1
View File
@@ -30,7 +30,32 @@ flag_management:
# https://docs.codecov.com/docs/pull-request-comments
comment:
layout: reach,diff,files,flags
layout: reach,diff,files,flags,components
# https://docs.codecov.com/docs/components
component_management:
default_rules:
statuses:
- type: project
target: auto
threshold: 1%
individual_components:
- component_id: clientconn
name: "clientconn"
paths:
- internal/clientconn/**
- component_id: dataapi
name: "dataapi"
paths:
- internal/dataapi/**
- component_id: documentdb
name: "documentdb"
paths:
- internal/documentdb/**
- component_id: handler
name: "handler"
paths:
- internal/handler/**
# https://docs.codecov.com/docs/github-checks
# Annotations may be hidden by pressing the "a" key or by unselecting "Show annotations" in the top right of the file.
+32 -7
View File
@@ -20,6 +20,10 @@ on:
required: false
type: boolean
default: false
skip_flaky_check:
required: false
type: boolean
default: false
env:
GOPATH: /home/runner/go
@@ -39,7 +43,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
@@ -73,6 +77,14 @@ jobs:
- name: Run init
run: bin/task init
- name: Setup Ollama and MCPHost
run: |
mkdir -p tmp/logs
curl -fsSL https://ollama.com/install.sh | sh
sudo systemctl stop ollama
bin/task ollama > tmp/logs/ollama.log 2>&1 &
bin/task mcphost > tmp/logs/mcphost.log 2>&1 &
- name: Wait for and setup environment
run: bin/task env-setup
@@ -91,17 +103,16 @@ jobs:
- name: Collect logs
if: failure()
run: |
mkdir -p /tmp/logs
bin/task env-logs-collect > /tmp/logs/compose-logs.txt
cp -v integration/*.json.tmp integration/*.txt.tmp /tmp/logs/
bin/task env-logs-collect
cp -v integration/*.json.tmp integration/*.txt.tmp tmp/logs/
- name: Compress logs before upload
if: failure()
run: zip -r -q -9 logs.zip /tmp/logs
run: zip -r -q -9 logs.zip tmp/logs
- name: Upload logs
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: logs-${{ inputs.task }}-${{ inputs.shard_index }}-${{ inputs.shard_total }}
path: logs.zip
@@ -112,7 +123,7 @@ jobs:
uses: codecov/codecov-action@v5
with:
files: ./integration/integration-${{ inputs.task }}.txt
flags: integration,${{ inputs.task }}-${{ inputs.shard_index }}
flags: integration,${{ inputs.task }},${{ inputs.task }}-${{ inputs.shard_index }}
disable_file_fixes: true
fail_ci_if_error: true
verbose: true
@@ -125,6 +136,20 @@ jobs:
flag-name: integration-${{ inputs.task }}-${{ inputs.shard_index }}
parallel: true
- name: Convert test results to JUnit XML format
if: always() && !inputs.skip_flaky_check
run: >
./bin/go-junit-report -parser=gojson
-in ./integration/*.json.tmp
-out ./integration/${{ inputs.task }}.xml
- name: Upload test results to Codecov
if: always()
uses: codecov/test-results-action@v1
with:
flags: integration,${{ inputs.task }},${{ inputs.task }}-${{ inputs.shard_index }}
file: ./integration/${{ inputs.task }}.xml
# we don't want them on CI
- name: Clean test and fuzz caches
if: always()
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
+2 -2
View File
@@ -43,7 +43,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: true
@@ -90,7 +90,7 @@ jobs:
# it has to be exactly "docs.tar" for actions/deploy-pages below
- name: Upload archive artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: docs
path: tmp/docs.tar
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
+35 -5
View File
@@ -42,7 +42,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
@@ -102,7 +102,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
@@ -123,6 +123,16 @@ jobs:
run: go generate -x
working-directory: tools
# install.sh starts ollama service, stop it since we want to run with `bin/task ollama` with environment variables
- name: Setup Ollama
run: |
curl -fsSL https://ollama.com/install.sh | sudo -E sh
sudo systemctl stop ollama
- name: Start Ollama in the background
run: |
bin/task ollama &
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -133,6 +143,10 @@ jobs:
- name: Start environment
run: bin/task env-up-detach
- name: Pull and load Ollama model
run: |
bin/task ollama-model
- name: Run init
run: bin/task init
@@ -172,6 +186,20 @@ jobs:
flag-name: unit
parallel: true
- name: Convert test results to JUnit XML format
if: always()
run: >
./bin/go-junit-report
-in ./unit.txt
-out ./unit.xml
- name: Upload test results to Codecov
if: always()
uses: codecov/test-results-action@v1
with:
flags: unit
file: ./unit.xml
# we don't want them on CI
- name: Clean test and fuzz caches
if: always()
@@ -199,17 +227,19 @@ jobs:
strategy:
fail-fast: false
matrix:
task: [postgresql]
task: [postgresql, yugabytedb]
shard_index: [1, 2, 3, 4, 5]
shard_total: [5]
include:
- { task: "mongodb", shard_index: 1, shard_total: 1 }
- { task: "yugabytedb", skip_flaky_check: true } # Too many failures for now
uses: ./.github/workflows/_integration.yml
with:
task: ${{ matrix.task }}
shard_index: ${{ matrix.shard_index }}
shard_total: ${{ matrix.shard_total }}
skip_flaky_check: ${{ matrix.skip_flaky_check || false }}
integration-no-xfail:
name: postgresql no-xfail
@@ -265,7 +295,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
@@ -325,7 +355,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `golangci-lint run --new` to work
lfs: false # LFS is used only by website
+6 -6
View File
@@ -64,7 +64,7 @@ jobs:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
if: github.event_name != 'pull_request_target'
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
@@ -72,7 +72,7 @@ jobs:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout pull request code
if: github.event_name == 'pull_request_target'
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
@@ -156,7 +156,7 @@ jobs:
run: bin/task build-development
- name: Upload development binaries
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: bin-dev
path: tmp/bin-dev/
@@ -167,7 +167,7 @@ jobs:
run: bin/task build-production
- name: Upload production binaries
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: bin
path: tmp/bin/
@@ -178,7 +178,7 @@ jobs:
run: bin/task packages
- name: Upload .deb packages
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: debs
path: tmp/debs/
@@ -186,7 +186,7 @@ jobs:
if-no-files-found: error
- name: Upload .rpm packages
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: rpms
path: tmp/rpms/
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
+2 -2
View File
@@ -45,7 +45,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for `git describe` to work
lfs: false # LFS is used only by website
@@ -106,7 +106,7 @@ jobs:
- name: Upload FerretDB log
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ferretdb-log
path: ferretdb-log.zip
+2 -2
View File
@@ -45,7 +45,7 @@ jobs:
steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for wrangler to work
lfs: true
@@ -116,7 +116,7 @@ jobs:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0 # for wrangler to work
lfs: true
+1
View File
@@ -13,6 +13,7 @@ telemetry.json
*.tmp
*.txt
!website/static/*.txt
*.xml
.task/
bin/
+2 -1
View File
@@ -53,6 +53,7 @@ linters-settings:
- "!**/internal/documentdb/**.go"
- "!**/internal/driver/*.go"
- "!**/internal/handler/**.go"
- "!**/internal/mcp/*.go"
- "!**/internal/mongoerrors/*.go"
- "!**/internal/util/logging/*.go"
- "!**/internal/util/scram/*.go"
@@ -179,7 +180,7 @@ linters-settings:
alias: otelsdkresource
- pkg: go.opentelemetry.io/otel/sdk/trace
alias: otelsdktrace
- pkg: go.opentelemetry.io/otel/semconv/v1.21.0
- pkg: go.opentelemetry.io/otel/semconv/v1.34.0
alias: otelsemconv
# ineffassign
lll:
+146
View File
@@ -2,6 +2,152 @@
<!-- markdownlint-disable MD024 MD034 -->
## [v2.7.0](https://github.com/FerretDB/FerretDB/releases/tag/v2.7.0) (2025-11-10)
(We skipped v2.6.0 to align DocumentDB and FerretDB version numbers.)
This version works best with
[DocumentDB v0.107.0-ferretdb-2.7.0](https://github.com/FerretDB/documentdb/releases/tag/v0.107.0-ferretdb-2.7.0).
### Breaking changes
There are some changes in our Prometheus metrics and OpenTelemetry traces.
Please note that they are
[not stable yet](https://docs.ferretdb.io/v2.7/configuration/observability/).
### What's Changed
#### Basic MCP server
This release adds basic MCP server support.
Future release will bring additional functionality.
#### Connectivity improvements
PostgreSQL connections are now reused, often yielding modest performance gains.
### New Features 🎉
- Add basic MCP server by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5279
- Implement DataAPI graceful shutdown and add OpenAPI spec endpoint by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5357
- Improve proxy mode observability by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5440
- Add tracing of SQL queries by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5514
### Enhancements 🛠
- Add `PostgreSQL` metrics by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5427
- Rename OpenTelemetry spans by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5512
### Documentation 📄
- Add blog post on Formio by @Fashander in https://github.com/FerretDB/FerretDB/pull/5395
- Document cloud instance deployment and setup by @Fashander in https://github.com/FerretDB/FerretDB/pull/5424
- Update content guide by @Fashander in https://github.com/FerretDB/FerretDB/pull/5441
- Update compatible apps by @Fashander in https://github.com/FerretDB/FerretDB/pull/5442
- Add documentation page stub for MCP server by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5451
- Add FerretDB Cloud blog post by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5461
- Update cloud installation docs by @Fashander in https://github.com/FerretDB/FerretDB/pull/5466
### Other Changes 🤖
- Use middleware by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5377
- Use generated structs in Data API by @noisersup in https://github.com/FerretDB/FerretDB/pull/5382
- Upload YugabyteDB log files by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5401
- Update documentation to point to the next release by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5423
- Bump YugabyteDB to stable version by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5428
- Use QEMU on arm64 for Yugabyte for now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5429
- Add comments to supervisord config by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5430
- Update DocumentDB by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5431
- Make `Run` wait for all `Handle` calls to finish by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5434
- Tweak `conninfo` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5435
- Add middleware by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5437
- Update expected PostgreSQL version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5438
- Avoid hijacking connections from the pool and immediately closing them by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5443
- Remove flaky test by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5444
- Add TODO comment about Debian 13 Trixie by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5450
- Add MCP tools by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5452
- Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5455
- Update `wire` dependency by @noisersup in https://github.com/FerretDB/FerretDB/pull/5465
- Add helpers for lazy logging by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5467
- Move to `documentdb/documentdb` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5468
- Update dependencies by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5479
- Add a test for `*documentdb.Conn` finalizing by @noisersup in https://github.com/FerretDB/FerretDB/pull/5484
- Finalize PostgreSQL metrics by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5485
- Fix spelling in `.go` files by @jsoref in https://github.com/FerretDB/FerretDB/pull/5492
- Fix `CODEOWNERS` grammar by @jsoref in https://github.com/FerretDB/FerretDB/pull/5495
- Use Go 1.25.1 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5508
- Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5509
- Minor observability cleanup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5511
- Bump dependencies by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5519
- Use Go 1.25.3 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5525
- Skip flaky test for now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5526
- Use forked lazyerrors by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5527
- Update DocumentDB by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5528
- Use Go 1.25.4 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5534
- Bump DocumentDB by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5536
- Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5537
### New Contributors
- @jsoref made their first contribution in https://github.com/FerretDB/FerretDB/pull/5486
[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/80?closed=1).
[All commits](https://github.com/FerretDB/FerretDB/compare/v2.5.0...v2.7.0).
## [v2.5.0](https://github.com/FerretDB/FerretDB/releases/tag/v2.5.0) (2025-08-12)
This version works best with
[DocumentDB v0.106.0-ferretdb-2.5.0](https://github.com/FerretDB/documentdb/releases/tag/v0.106.0-ferretdb-2.5.0).
### Breaking changes
#### Metric names
We changed Prometheus metric names and `HELP` texts to highlight the fact that they are
[not stable yet](https://docs.ferretdb.io/configuration/observability/#metrics).
We plan to promote most of them to stable in the next release.
### What's Changed
#### RPM packages for DocumentDB
DocumentDB `.rpm` packages for Red Hat Enterprise Linux are
[now available](https://docs.ferretdb.io/installation/documentdb/rpm/)!
### Documentation 📄
- Enable full visibility for images on zoom by @Fashander in https://github.com/FerretDB/FerretDB/pull/5346
- Add Vault blog post by @Fashander in https://github.com/FerretDB/FerretDB/pull/5368
- Add WeKan blog post by @Fashander in https://github.com/FerretDB/FerretDB/pull/5381
### Other Changes 🤖
- Add YugabyteDB integration tests by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5274
- Run all YugabyteDB integration tests by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5283
- Refactor normal and error responses by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5326
- Update DocumentDB by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5353
- Use Codecov for flaky integration tests detection by @noisersup in https://github.com/FerretDB/FerretDB/pull/5361
- Make accessing YugabyteDB logs easier by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5362
- Add components in codecov by @noisersup in https://github.com/FerretDB/FerretDB/pull/5364
- Skip YugabyteDB setup for `arm64` by @chilagrow in https://github.com/FerretDB/FerretDB/pull/5372
- Tweak Ollama and MCPHost setup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5375
- Simplify flags handling by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5378
- Update DocumentDB by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5380
- Extract wiring into a separate package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5383
- Small cleanup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5388
- Encapsulate PostgreSQL pool in the Handler by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5389
- Bump DocumentDB version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5392
- Test flaky tests detection by @noisersup in https://github.com/FerretDB/FerretDB/pull/5393
- Small tweaks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5394
- Make a resource.Untrack thread-safe by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5396
- Update DocumentDB by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5398
- Enable MongoDB test commands by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5400
- Update Prometheus metrics by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5403
- Update DocumentDB by @AlekSi in https://github.com/FerretDB/FerretDB/pull/5411
[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/79?closed=1).
[All commits](https://github.com/FerretDB/FerretDB/compare/v2.4.0...v2.5.0).
## [v2.4.0](https://github.com/FerretDB/FerretDB/releases/tag/v2.4.0) (2025-07-16)
This version works best with
+1 -1
View File
@@ -30,7 +30,7 @@ On macOS and Windows, [Docker Desktop](https://www.docker.com/products/docker-de
On Windows, it should be [configured to use WSL 2](https://docs.docker.com/desktop/windows/wsl/) without any distro;
all commands should be run on the host.
You will need Go 1.24 or later on the host.
You will need Go 1.25 or later on the host.
If your package manager doesn't provide it yet,
please install it from [go.dev](https://go.dev/dl/).
-2
View File
@@ -1,2 +0,0 @@
FerretDB database system (https://www.ferretdb.com/).
Copyright 2021 FerretDB Inc.
+1 -3
View File
@@ -19,7 +19,7 @@
FerretDB is an open-source alternative to MongoDB.
It is a proxy that converts MongoDB 5.0+ wire protocol queries to SQL
and uses PostgreSQL with [DocumentDB extension](https://github.com/microsoft/documentdb) as a database engine.
and uses PostgreSQL with [DocumentDB extension](https://github.com/documentdb/documentdb) as a database engine.
```mermaid
flowchart LR
@@ -116,7 +116,5 @@ See [there](https://pkg.go.dev/github.com/FerretDB/FerretDB/v2/build/version) fo
- [Slack chat](https://slack.ferretdb.io/) for quick questions.
- [GitHub Discussions](https://github.com/FerretDB/FerretDB/discussions) for longer topics.
- [GitHub Issues](https://github.com/FerretDB/FerretDB/issues) for bugs and missing features.
- [Open Office Hours meeting](https://calendar.google.com/calendar/event?action=TEMPLATE&tmeid=NGhrZTA5dXZ0MzQzN2gyaGVtZmx2aWxmN2pfMjAyNDA0MDhUMTcwMDAwWiBjX24zN3RxdW9yZWlsOWIwMm0wNzQwMDA3MjQ0QGc&tmsrc=c_n37tquoreil9b02m0740007244%40group.calendar.google.com&scp=ALL)
every Monday at 17:00 UTC at [Google Meet](https://meet.google.com/mcb-arhw-qbq).
If you want to contact FerretDB Inc., please use [this form](https://www.ferretdb.com/contact/).
+73 -6
View File
@@ -17,6 +17,8 @@ vars:
BUILD_TAGS: ferretdb_dev
MODE: diff-normal
SERVICES: jaeger mongodb mongodb-secure postgres yugabytedb
LLM: qwen3:0.6b # sync with code
LLM_CONTEXT: 40960 # https://ollama.com/library/qwen3:0.6b
tasks:
# invoked when `task` is run without arguments
@@ -113,9 +115,13 @@ tasks:
- docker compose logs --follow
env-logs-collect:
desc: "Collect logs from Docker and save them to tmp/logs"
cmds:
- docker compose ps --all
- docker compose logs
- bin/envtool{{exeExt}} shell mkdir tmp/logs
- docker compose ps --all > tmp/logs/compose-ps.txt
- docker compose logs > tmp/logs/compose-logs.txt
- docker compose exec yugabytedb sh -c "bin/yugabyted collect_logs --stdout | gunzip | tar -xf - --one-top-level=/tmp/logs"
- docker compose cp yugabytedb:/tmp/logs tmp/logs/yugabytedb
env-up:
desc: "Start development environment"
@@ -194,7 +200,7 @@ tasks:
desc: "Run all unit tests"
cmds:
- go test -c -o /dev/null ./...
- go test -count=1 -timeout={{.TEST_TIMEOUT}} {{.RACE_FLAG}} -tags={{.BUILD_TAGS}} -shuffle=on -coverprofile=cover.txt ./...
- go test -v -count=1 -timeout={{.TEST_TIMEOUT}} {{.RACE_FLAG}} -tags={{.BUILD_TAGS}} -shuffle=on -coverprofile=cover.txt ./... | tee unit.txt
- go test -count=1 -timeout={{.TEST_TIMEOUT}} {{.RACE_FLAG}} -tags={{.BUILD_TAGS}} -shuffle=on -bench=. -benchtime=1x ./...
- bin/task{{exeExt}} -d tools tools-test
@@ -258,6 +264,47 @@ tasks:
-target-backend=mongodb
-no-xfail={{.NO_XFAIL}}
test-integration-yugabytedb:
desc: "Run integration tests for FerretDB with YugabyteDB sequentially"
vars:
# DPanic on InternalError stops execution of remaining tests,
# ensure to complete all tests by not setting development build tag and without race detector.
# TODO https://github.com/FerretDB/FerretDB/issues/5397
BUILD_TAGS: ""
RACE_FLAG: -race=false
deps:
- task: gen-version
vars:
BUILD_TAGS:
ref: .BUILD_TAGS
RACE_FLAG:
ref: .RACE_FLAG
- task: test-integration-build
dir: integration
cmds:
- >
../bin/envtool{{exeExt}} tests run
--shard-index={{.SHARD_INDEX}}
--shard-total={{.SHARD_TOTAL}}
--run='{{.TEST_RUN}}'
--skip='{{.TEST_SKIP}}'
--raw-prefix=yugabytedb-{{.SHARD_INDEX}}-{{.SHARD_TOTAL}}
--
-count=1
-parallel=1
-p=1
-timeout={{.TEST_TIMEOUT}}
{{.RACE_FLAG}}
-tags={{.BUILD_TAGS}}
-shuffle=on
-coverpkg=../...
-coverprofile=integration-yugabytedb.txt
./...
-target-backend=ferretdb-yugabytedb
-postgresql-url='postgres://pg-user:pg-pass@127.0.0.1:5433/yugabyte'
-compat-url='mongodb://username:password@127.0.0.1:47017/?replicaSet=rs0'
-no-xfail={{.NO_XFAIL}}
bench-unit:
desc: "Run unit benchmarks"
cmds:
@@ -275,6 +322,7 @@ tasks:
--no-auth
--listen-addr=:27017
--listen-data-api-addr=127.0.0.1:8899
--listen-mcp-addr=127.0.0.1:8081
--proxy-addr=127.0.0.1:37017
--postgresql-url='postgres://username:password@127.0.0.1:5432/postgres'
--otel-traces-url=http://127.0.0.1:4318/v1/traces
@@ -289,6 +337,7 @@ tasks:
--mode={{.MODE}}
--listen-addr=:27017
--listen-data-api-addr=127.0.0.1:8899
--listen-mcp-addr=127.0.0.1:8081
--proxy-addr=127.0.0.1:47017
--postgresql-url='postgres://username:password@127.0.0.1:5432/postgres'
--otel-traces-url=http://127.0.0.1:4318/v1/traces
@@ -305,6 +354,7 @@ tasks:
--no-auth
--listen-addr=:27017
--listen-data-api-addr=127.0.0.1:8899
--listen-mcp-addr=127.0.0.1:8081
--proxy-addr=127.0.0.1:37017
--postgresql-url='postgres://username:password@127.0.0.1:5433/yugabyte'
--otel-traces-url=http://127.0.0.1:4318/v1/traces
@@ -319,6 +369,7 @@ tasks:
--mode={{.MODE}}
--listen-addr=:27017
--listen-data-api-addr=127.0.0.1:8899
--listen-mcp-addr=127.0.0.1:8081
--proxy-addr=127.0.0.1:47017
--postgresql-url='postgres://username:password@127.0.0.1:5433/yugabyte'
--otel-traces-url=http://127.0.0.1:4318/v1/traces
@@ -362,18 +413,34 @@ tasks:
- bin/pkgsite{{exeExt}} -http=127.0.0.1:6060 -open
ollama:
desc: "Run Ollama server"
desc: "Run local Ollama server"
cmds:
- ollama serve
env:
# for https://ollama.com/library/phi4-mini
OLLAMA_CONTEXT_LENGTH: 131072
OLLAMA_CONTEXT_LENGTH: "{{.LLM_CONTEXT}}"
OLLAMA_DEBUG: 2
ollama-model:
desc: "Pull and load Ollama model"
cmds:
- ollama pull {{.LLM}}
- ollama run --think=false {{.LLM}} 'Hi!'
mcphost:
desc: "Run MCPHost for local Ollama server"
cmds:
- bin/mcphost --config=build/mcphost.json --model=ollama:{{.LLM}}
psql:
desc: "Run psql"
cmds:
- docker compose exec postgres psql -p 5432 -U username -d postgres
ysqlsh:
desc: "Run YugabyteDB SQL shell"
cmds:
- docker compose exec yugabytedb ./bin/ysqlsh postgres://username:password@host.docker.internal:5433/yugabyte
mongosh:
desc: "Run MongoDB shell (`mongosh`)"
cmds:
+4
View File
@@ -1,3 +1,7 @@
# https://github.com/cloudwego/eino#dependencies
# TODO https://github.com/cloudwego/eino/issues/291
CVE-2025-30153
# https://nvd.nist.gov/vuln/detail/CVE-2025-51471
# https://github.com/ollama/ollama/pull/10750
CVE-2025-51471
+1 -1
View File
@@ -1 +1 @@
FROM ghcr.io/ferretdb/ferretdb-prettier:3.5.3-2
FROM ghcr.io/ferretdb/ferretdb-prettier:3.6.2-1
+1 -1
View File
@@ -1 +1 @@
FROM ghcr.io/ferretdb/ferretdb-textlint:14.8.4-1
FROM ghcr.io/ferretdb/ferretdb-textlint:15.2.0-1
+1 -1
View File
@@ -1 +1 @@
FROM ghcr.io/ferretdb/ferretdb-wrangler:4.20.0-1
FROM ghcr.io/ferretdb/ferretdb-wrangler:4.24.0-1
+1 -1
View File
@@ -1 +1 @@
FROM jaegertracing/all-in-one:1.70.0
FROM jaegertracing/all-in-one:1.74.0
-1
View File
@@ -1 +0,0 @@
FROM ghcr.io/danny-avila/librechat:v0.7.8
+1 -1
View File
@@ -1,5 +1,5 @@
# Use development image and full tag close to the release.
FROM ghcr.io/ferretdb/postgres-documentdb-dev:17-0.105.0-ferretdb-2.4.0
FROM ghcr.io/ferretdb/postgres-documentdb-dev:17-0.107.0-ferretdb-2.7.0
# Use moving development image during development.
# FROM ghcr.io/ferretdb/postgres-documentdb-dev:17-ferretdb
+1 -1
View File
@@ -1,2 +1,2 @@
FROM aquasec/trivy:0.63.0
FROM aquasec/trivy:0.67.2
WORKDIR /workdir
+1 -1
View File
@@ -1 +1 @@
FROM yugabytedb/yugabyte:2.25.2.0-b359
FROM yugabytedb/yugabyte:2025.1.1.0-b197
+4 -4
View File
@@ -1,5 +1,5 @@
# Use production image and full tag close to the release.
FROM --platform=linux/amd64 ghcr.io/ferretdb/postgres-documentdb:17-0.105.0-ferretdb-2.4.0
FROM --platform=linux/amd64 ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0
# Use moving development image during development.
# FROM --platform=linux/amd64 ghcr.io/ferretdb/postgres-documentdb-dev:17-ferretdb
@@ -15,9 +15,9 @@ apt update
apt install -y git-lfs mongodb-mongosh tmux nano
cd /tmp
curl -OJL https://go.dev/dl/go1.24.5.linux-amd64.tar.gz
tar -xf go1.24.5.linux-amd64.tar.gz
rm go1.24.5.linux-amd64.tar.gz
curl -OJL https://go.dev/dl/go1.25.4.linux-amd64.tar.gz
tar -xf go1.25.4.linux-amd64.tar.gz
rm go1.25.4.linux-amd64.tar.gz
mv go /usr/local/
ln -s /usr/local/go/bin/* /usr/local/bin/
go version
+6 -3
View File
@@ -12,7 +12,8 @@ ARG LABEL_COMMIT
# prepare stage
FROM --platform=$BUILDPLATFORM golang:1.24.5 AS development-prepare
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM --platform=$BUILDPLATFORM golang:1.25.4-bookworm AS development-prepare
# use a single directory for all Go caches to simplify RUN --mount commands below
ENV GOPATH=/cache/gopath
@@ -36,7 +37,8 @@ EOF
# build stage
FROM golang:1.24.5 AS development-build
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM golang:1.25.4-bookworm AS development-build
ARG TARGETARCH
@@ -103,7 +105,8 @@ COPY --from=development-build /src/bin/ferretdb /ferretdb
# final stage
FROM golang:1.24.5 AS development
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM golang:1.25.4-bookworm AS development
ENV GOCOVERDIR=/tmp/cover
ENV GORACE=halt_on_error=1,history_size=2
+5 -3
View File
@@ -12,7 +12,8 @@ ARG LABEL_COMMIT
# prepare stage
FROM --platform=$BUILDPLATFORM golang:1.24.5 AS eval-dev-prepare
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM --platform=$BUILDPLATFORM golang:1.25.4-bookworm AS eval-dev-prepare
# use a single directory for all Go caches to simplify RUN --mount commands below
ENV GOPATH=/cache/gopath
@@ -36,7 +37,8 @@ EOF
# build stage
FROM golang:1.24.5 AS eval-dev-build
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM golang:1.25.4-bookworm AS eval-dev-build
ARG TARGETARCH
@@ -97,7 +99,7 @@ EOF
# final stage
# Use development image and full tag close to the release.
FROM ghcr.io/ferretdb/postgres-documentdb-dev:17-0.105.0-ferretdb-2.4.0 AS eval-dev
FROM ghcr.io/ferretdb/postgres-documentdb-dev:17-0.107.0-ferretdb-2.7.0 AS eval-dev
# Use moving development image during development.
# FROM ghcr.io/ferretdb/postgres-documentdb-dev:17-ferretdb AS eval-dev
+5 -3
View File
@@ -12,7 +12,8 @@ ARG LABEL_COMMIT
# prepare stage
FROM --platform=$BUILDPLATFORM golang:1.24.5 AS eval-prepare
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM --platform=$BUILDPLATFORM golang:1.25.4-bookworm AS eval-prepare
# use a single directory for all Go caches to simplify RUN --mount commands below
ENV GOPATH=/cache/gopath
@@ -36,7 +37,8 @@ EOF
# build stage
FROM golang:1.24.5 AS eval-build
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM golang:1.25.4-bookworm AS eval-build
ARG TARGETARCH
@@ -89,7 +91,7 @@ EOF
# final stage
# Use production image and full tag close to the release.
FROM ghcr.io/ferretdb/postgres-documentdb:17-0.105.0-ferretdb-2.4.0 AS eval
FROM ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0 AS eval
# Use moving development image during development.
# FROM ghcr.io/ferretdb/postgres-documentdb-dev:17-ferretdb AS eval
+4 -3
View File
@@ -1,11 +1,11 @@
; https://supervisord.org/configuration.html
; https://supervisord.org/configuration.html#supervisord-section-values
[supervisord]
nodaemon=true
user=root
loglevel=info
logfile=/dev/stderr
logfile_maxbytes=0
; https://supervisord.org/configuration.html#program-x-section-values
[program:postgres]
command=/usr/local/bin/docker-entrypoint.sh postgres
; lower priority postgres starts before ferretdb (default priority 999)
@@ -16,6 +16,7 @@ stdout_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true
; https://supervisord.org/configuration.html#program-x-section-values
[program:ferretdb]
command=/usr/local/bin/ferretdb
stdout_logfile=/dev/stderr
+4 -2
View File
@@ -12,7 +12,8 @@ ARG LABEL_COMMIT
# prepare stage
FROM --platform=$BUILDPLATFORM golang:1.24.5 AS production-prepare
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM --platform=$BUILDPLATFORM golang:1.25.4-bookworm AS production-prepare
# use a single directory for all Go caches to simplify RUN --mount commands below
ENV GOPATH=/cache/gopath
@@ -36,7 +37,8 @@ EOF
# build stage
FROM golang:1.24.5 AS production-build
# TODO https://github.com/FerretDB/FerretDB/issues/5449
FROM golang:1.25.4-bookworm AS production-build
ARG TARGETARCH
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"FerretDB": {
"type": "remote",
"url": "http://127.0.0.1:8081/mcp"
}
}
}
+3
View File
@@ -1,3 +1,6 @@
setParameter:
enableTestCommands: 1 # for sleep
net:
port: 47017 # matches host's port for replica set to work
bindIpAll: true
+3
View File
@@ -1,3 +1,6 @@
setParameter:
enableTestCommands: 1 # for sleep
net:
port: 37017 # matches host's port for replica set to work
bindIpAll: true
+10 -8
View File
@@ -18,18 +18,20 @@ import "runtime"
const (
// DocumentDB is a version of DocumentDB this version of FerretDB is compatible with.
DocumentDB = "0.105.0 gitref: HEAD sha:8453d93b buildId:0"
DocumentDB = "0.107.0 gitref: HEAD sha:e63835403d buildId:0"
// DocumentDBURL points to the release page of the DocumentDB version above.
DocumentDBURL = "https://github.com/FerretDB/documentdb/releases/tag/v0.105.0-ferretdb-2.4.0"
DocumentDBURL = "https://github.com/FerretDB/documentdb/releases/tag/v0.107.0-ferretdb-2.7.0"
)
// DocumentDBSafeToUpdate represents versions of DocumentDB that FerretDB can update.
var DocumentDBSafeToUpdate = []string{
"0.102.0 gitref: HEAD sha:80462f5 buildId:0", // v2.1.0
"0.103.0 gitref: HEAD sha:7514232 buildId:0", // v2.2.0
"0.104.0 gitref: HEAD sha:2045d0e buildId:0", // v2.3.0, v2.3.1
"0.105.0 gitref: HEAD sha:8453d93b buildId:0", // v2.4.0
"0.102.0 gitref: HEAD sha:80462f5 buildId:0", // v2.1.0
"0.103.0 gitref: HEAD sha:7514232 buildId:0", // v2.2.0
"0.104.0 gitref: HEAD sha:2045d0e buildId:0", // v2.3.0, v2.3.1
"0.105.0 gitref: HEAD sha:8453d93b buildId:0", // v2.4.0
"0.106.0 gitref: HEAD sha:beb9d25d98 buildId:0", // v2.5.0
// FerretDB v2.6.0 wasn't released
}
// PostgreSQLTest is a version of PostgreSQL used by tests.
@@ -41,6 +43,6 @@ func init() {
arch = "aarch64-unknown-linux-gnu"
}
PostgreSQLTest = "PostgreSQL 17.5 (Debian 17.5-1.pgdg120+1) on " + arch + ", " +
"compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit"
PostgreSQLTest = "PostgreSQL 17.6 (Debian 17.6-2.pgdg12+1) on " + arch + ", " +
"compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit"
}
+1 -1
View File
@@ -93,7 +93,7 @@ const ferretdbModule = "github.com/FerretDB/FerretDB/v2"
// semVerTag is a https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string,
// but with a leading `v`.
//
//nolint:lll // for readibility
//nolint:lll // for readability
var semVerTag = regexp.MustCompile(`^v(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
// Get returns current build's info.
+1 -1
View File
@@ -261,7 +261,7 @@ func main() {
switch cmd {
case "setup":
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
err = setup(ctx, logger)
+1 -1
View File
@@ -28,7 +28,7 @@ import (
"slices"
"strings"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/AlekSi/lazyerrors"
)
// copyFile copies a file from src to dst, overwriting dst if it exists.
+2 -2
View File
@@ -23,6 +23,7 @@ import (
"net/url"
"time"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"github.com/jackc/pgx/v5"
"github.com/prometheus/client_golang/prometheus"
@@ -30,7 +31,6 @@ import (
"github.com/FerretDB/FerretDB/v2/internal/documentdb"
"github.com/FerretDB/FerretDB/v2/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/v2/internal/util/debug"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/state"
)
@@ -161,7 +161,7 @@ func setup(ctx context.Context, logger *slog.Logger) error {
return lazyerrors.Error(err)
}
go h.Serve(ctx)
go h.Run(ctx)
if err = setupMongoDB(ctx, logger, "mongodb://username:password@127.0.0.1:47017/", "mongodb-secure"); err != nil {
return lazyerrors.Error(err)
+6 -7
View File
@@ -31,12 +31,12 @@ import (
"strings"
"time"
"github.com/AlekSi/lazyerrors"
"go.opentelemetry.io/otel"
otelattribute "go.opentelemetry.io/otel/attribute"
otelcodes "go.opentelemetry.io/otel/codes"
oteltrace "go.opentelemetry.io/otel/trace"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
"github.com/FerretDB/FerretDB/v2/internal/util/observability"
)
@@ -72,7 +72,7 @@ type testResult struct {
// listTestFuncs returns a sorted slice of all top-level test functions (tests, benchmarks, examples, fuzz functions)
// matching given regular expression in the specified directory and subdirectories.
func listTestFuncs(ctx context.Context, dir, re string, logger *slog.Logger) ([]string, error) {
ctx, span := otel.Tracer("").Start(ctx, "listTestFuncs")
ctx, span := otel.Tracer("").Start(ctx, "envtool.listTestFuncs")
defer span.End()
var buf bytes.Buffer
@@ -158,7 +158,7 @@ func shardTestFuncs(index, total uint, testFuncs []string) ([]string, error) {
// testArgs handles `envtool tests run` arguments and returns a slice of `go test` arguments.
func testArgs(ctx context.Context, dir string, index, total uint, run, skip string, logger *slog.Logger) ([]string, uint, error) {
ctx, span := otel.Tracer("").Start(ctx, "testArgs")
ctx, span := otel.Tracer("").Start(ctx, "envtool.testArgs")
defer span.End()
listRE := "."
@@ -309,7 +309,7 @@ func runGoTest(runCtx context.Context, opts *runGoTestOpts) (resErr error) {
totalTests = strconv.Itoa(int(opts.total))
}
runCtx, runSpan := otel.Tracer("").Start(runCtx, "run")
runCtx, runSpan := otel.Tracer("").Start(runCtx, "envtool.runGoTest")
runSpan.SetAttributes(otelattribute.String("db.ferretdb.envtool.total_tests", totalTests))
defer runSpan.End()
@@ -377,7 +377,6 @@ func runGoTest(runCtx context.Context, opts *runGoTestOpts) (resErr error) {
key = resultKey(event.Package, parentTest(event.Test))
parent := results[key]
// TODO https://github.com/FerretDB/FerretDB/issues/4465
if parent == nil {
panic(fmt.Sprintf(
"no parent test found: package=%q, test=%q, key=%q", event.Package, event.Test, key,
@@ -585,14 +584,14 @@ func testsRun(ctx context.Context, params *TestsRunParams, logger *slog.Logger)
ot, err := observability.NewOTelTraceExporter(&observability.OTelTraceExporterOpts{
Logger: logger,
Service: "envtool-tests",
URL: "http://127.0.0.1:4318/v1/traces",
Service: "envtool-tests",
})
if err != nil {
return lazyerrors.Error(err)
}
ctx, span := otel.Tracer("").Start(ctx, "testsRun")
ctx, span := otel.Tracer("").Start(ctx, "envtool.testsRun")
ctx, cancel := context.WithCancel(ctx)
done := make(chan struct{})
+58 -105
View File
@@ -12,17 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main is the entry point for FerretDB server.
package main
import (
"cmp"
"context"
"encoding/json"
"expvar"
"fmt"
"log"
"log/slog"
"math"
"os"
"runtime"
runtimedebug "runtime/debug"
@@ -34,15 +33,10 @@ import (
"github.com/alecthomas/kong"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/expfmt"
"go.uber.org/automaxprocs/maxprocs"
_ "golang.org/x/crypto/x509roots/fallback" // register root TLS certificates for production Docker image
"github.com/FerretDB/FerretDB/v2/build/version"
"github.com/FerretDB/FerretDB/v2/internal/clientconn"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/v2/internal/dataapi"
"github.com/FerretDB/FerretDB/v2/internal/documentdb"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/handler/middleware"
"github.com/FerretDB/FerretDB/v2/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/v2/internal/util/debug"
@@ -51,6 +45,7 @@ import (
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
"github.com/FerretDB/FerretDB/v2/internal/util/observability"
"github.com/FerretDB/FerretDB/v2/internal/util/setup"
"github.com/FerretDB/FerretDB/v2/internal/util/state"
"github.com/FerretDB/FerretDB/v2/internal/util/telemetry"
)
@@ -79,6 +74,7 @@ var cli struct {
TLSKeyFile string `default:"" help:"TLS key file path."`
TLSCaFile string `default:"" help:"TLS CA file path."`
DataAPIAddr string `default:"" help:"Listen TCP address for HTTP Data API."`
MCPAddr string `default:"" help:"Listen TCP address for HTTP MCP server."`
} `embed:"" prefix:"listen-" group:"Interfaces"`
Proxy struct {
@@ -103,7 +99,8 @@ var cli struct {
MetricsUUID bool `default:"false" help:"Add instance UUID to all metrics." group:"Miscellaneous" negatable:""`
OTel struct {
Traces struct {
ServiceName string `default:"ferretdb" help:"OpenTelemetry service name."`
Traces struct {
URL string `default:"" help:"OpenTelemetry OTLP/HTTP traces endpoint URL (e.g. 'http://host:4318/v1/traces')."`
} `embed:"" prefix:"traces-"`
} `embed:"" prefix:"otel-" group:"Miscellaneous"`
@@ -249,7 +246,23 @@ func setupDefaultLogger(format string, uuid string) *slog.Logger {
}
// checkFlags checks that CLI flags are not self-contradictory and produces warnings if needed.
// It also replaces "-" with "" for some flags to make it easier to configure FerretDB with environment variables.
func checkFlags(logger *slog.Logger) {
// keep in sync with documentation
for _, p := range []*string{
&cli.Listen.Addr,
&cli.Listen.Unix,
&cli.Listen.TLS,
&cli.Listen.DataAPIAddr,
&cli.Listen.MCPAddr,
&cli.DebugAddr,
&cli.OTel.Traces.URL,
} {
if *p == "-" {
*p = ""
}
}
ctx := context.Background()
if devbuild.Enabled {
@@ -263,6 +276,13 @@ func checkFlags(logger *slog.Logger) {
if !cli.Auth {
logger.WarnContext(ctx, "Authentication is disabled; the server will accept any connection")
}
if cli.Auth && cli.Listen.MCPAddr != "" {
logger.Log(
ctx, logging.LevelFatal,
"MCP server does not support authentication; please disable authentication or MCP server",
)
}
}
// dumpMetrics dumps all Prometheus metrics to stderr.
@@ -353,19 +373,6 @@ func run() {
checkFlags(logger)
maxprocsOpts := []maxprocs.Option{
maxprocs.Min(2),
maxprocs.RoundQuotaFunc(func(v float64) int {
return int(math.Ceil(v))
}),
maxprocs.Logger(func(format string, a ...any) {
logger.Info(fmt.Sprintf(format, a...))
}),
}
if _, err = maxprocs.Set(maxprocsOpts...); err != nil {
logger.Warn("Failed to set GOMAXPROCS", logging.Error(err))
}
ctx, stop := ctxutil.SigTerm(context.Background())
go func() {
@@ -381,7 +388,7 @@ func run() {
var wg sync.WaitGroup
if cmp.Or(cli.DebugAddr, "-") != "-" {
if cli.DebugAddr != "" {
wg.Add(1)
go func() {
@@ -410,11 +417,11 @@ func run() {
l.LogAttrs(ctx, logging.LevelFatal, "Failed to create debug handler", logging.Error(e))
}
h.Serve(ctx)
h.Run(ctx)
}()
}
if cmp.Or(cli.OTel.Traces.URL, "-") != "-" {
if cli.OTel.Traces.URL != "" {
wg.Add(1)
go func() {
@@ -424,9 +431,8 @@ func run() {
ot, e := observability.NewOTelTraceExporter(&observability.OTelTraceExporterOpts{
Logger: l,
Service: "ferretdb",
Version: version.Get().Version,
URL: cli.OTel.Traces.URL,
Service: cli.OTel.ServiceName,
})
if e != nil {
l.LogAttrs(ctx, logging.LevelFatal, "Failed to create Otel tracer", logging.Error(e))
@@ -436,7 +442,7 @@ func run() {
}()
}
lm := connmetrics.NewListenerMetrics()
mm := middleware.NewMetrics()
{
wg.Add(1)
@@ -453,7 +459,7 @@ func run() {
DNT: os.Getenv("DO_NOT_TRACK"),
ExecName: os.Args[0],
P: stateProvider,
ConnMetrics: lm.ConnMetrics,
Metrics: mm,
L: l,
UndecidedDelay: cli.Dev.Telemetry.UndecidedDelay,
ReportInterval: cli.Dev.Telemetry.ReportInterval,
@@ -466,97 +472,44 @@ func run() {
}()
}
p, err := documentdb.NewPool(cli.PostgreSQLURL, logging.WithName(logger, "pool"), stateProvider)
if err != nil {
logger.LogAttrs(ctx, logging.LevelFatal, "Failed to construct pool", logging.Error(err))
}
tcpAddr := cli.Listen.Addr
if cmp.Or(tcpAddr, "-") == "-" {
tcpAddr = ""
}
unixAddr := cli.Listen.Unix
if cmp.Or(unixAddr, "-") == "-" {
unixAddr = ""
}
tlsAddr := cli.Listen.TLS
if cmp.Or(tlsAddr, "-") == "-" {
tlsAddr = ""
}
handlerOpts := &handler.NewOpts{
Pool: p,
Auth: cli.Auth,
TCPHost: tcpAddr,
ReplSetName: cli.Dev.ReplSetName,
L: logging.WithName(logger, "handler"),
ConnMetrics: lm.ConnMetrics,
//exhaustruct:enforce
res := setup.Setup(ctx, &setup.SetupOpts{
Logger: logger,
StateProvider: stateProvider,
}
Metrics: mm,
h, err := handler.New(handlerOpts)
if err != nil {
p.Close()
handlerOpts.L.LogAttrs(ctx, logging.LevelFatal, "Failed to construct handler", logging.Error(err))
}
PostgreSQLURL: cli.PostgreSQLURL,
Auth: cli.Auth,
ReplSetName: cli.Dev.ReplSetName,
SessionCleanupInterval: 0,
lis, err := clientconn.Listen(&clientconn.ListenerOpts{
Handler: h,
Metrics: lm,
Logger: logger,
TCP: tcpAddr,
Unix: unixAddr,
TLS: tlsAddr,
TLSCertFile: cli.Listen.TLSCertFile,
TLSKeyFile: cli.Listen.TLSKeyFile,
TLSCAFile: cli.Listen.TLSCaFile,
Mode: middleware.Mode(cli.Mode),
ProxyAddr: cli.Proxy.Addr,
ProxyTLSCertFile: cli.Proxy.TLSCertFile,
ProxyTLSKeyFile: cli.Proxy.TLSKeyFile,
ProxyTLSCAFile: cli.Proxy.TLSCaFile,
TCPAddr: cli.Listen.Addr,
UnixAddr: cli.Listen.Unix,
TLSAddr: cli.Listen.TLS,
TLSCertFile: cli.Listen.TLSCertFile,
TLSKeyFile: cli.Listen.TLSKeyFile,
TLSCAFile: cli.Listen.TLSCaFile,
Mode: middleware.Mode(cli.Mode),
TestRecordsDir: cli.Dev.RecordsDir,
DataAPIAddr: cli.Listen.DataAPIAddr,
MCPAddr: cli.Listen.MCPAddr,
})
if err != nil {
p.Close()
logger.LogAttrs(ctx, logging.LevelFatal, "Failed to construct listener", logging.Error(err))
if res == nil {
os.Exit(1)
}
if cmp.Or(cli.Listen.DataAPIAddr, "-") != "-" {
wg.Add(1)
listener.Store(res.WireListener)
go func() {
defer wg.Done()
metricsRegisterer.MustRegister(res)
l := logging.WithName(logger, "dataapi")
lis, e := dataapi.Listen(&dataapi.ListenOpts{
TCPAddr: cli.Listen.DataAPIAddr,
L: l,
Handler: h,
})
if e != nil {
p.Close()
l.LogAttrs(ctx, logging.LevelFatal, "Failed to construct DataAPI listener", logging.Error(e))
}
lis.Run(ctx)
}()
}
listener.Store(lis)
metricsRegisterer.MustRegister(lis)
lis.Run(ctx)
res.Run(ctx)
wg.Wait()
+10 -5
View File
@@ -15,7 +15,6 @@
package main
import (
"cmp"
"context"
"fmt"
"log/slog"
@@ -39,7 +38,7 @@ type ReadyZ struct {
func (r *ReadyZ) Probe(ctx context.Context) bool {
var urls []string
if cmp.Or(cli.Listen.Addr, "-") != "-" {
if cli.Listen.Addr != "" {
host, port, err := net.SplitHostPort(cli.Listen.Addr)
if err != nil {
r.l.ErrorContext(ctx, "Getting host and port failed", logging.Error(err))
@@ -59,12 +58,12 @@ func (r *ReadyZ) Probe(ctx context.Context) bool {
urls = append(urls, u.String())
}
if cmp.Or(cli.Listen.TLS, "-") != "-" {
if cli.Listen.TLS != "" {
// TODO https://github.com/FerretDB/FerretDB/issues/4427
r.l.WarnContext(ctx, "TLS ping is not implemented yet")
}
if cmp.Or(cli.Listen.Unix, "-") != "-" {
if cli.Listen.Unix != "" {
urls = append(urls, "mongodb://"+url.PathEscape(cli.Listen.Unix))
}
@@ -76,7 +75,13 @@ func (r *ReadyZ) Probe(ctx context.Context) bool {
for _, u := range urls {
r.l.DebugContext(ctx, fmt.Sprintf("Pinging %s", u))
conn, err := wireclient.Connect(ctx, u, r.l)
cleanUri, _, _, _, err := wireclient.Credentials(u)
if err != nil {
r.l.ErrorContext(ctx, "Getting credentials failed", logging.Error(err))
return false
}
conn, err := wireclient.Connect(ctx, cleanUri, r.l)
if err != nil {
r.l.ErrorContext(ctx, "Connection failed", logging.Error(err))
return false
+1 -6
View File
@@ -31,6 +31,7 @@ services:
context: ./build/deps
dockerfile: yugabytedb.Dockerfile
container_name: ferretdb_yugabytedb
platform: linux/amd64 # TODO https://github.com/FerretDB/FerretDB/issues/5369
# postgresql.conf is populated by "ysql_pg_conf_csv" flag
# https://docs.yugabyte.com/preview/reference/configuration/yugabyted/#pass-additional-flags-to-yb-master-and-yb-tserver
command: >
@@ -123,12 +124,6 @@ services:
- 6831:6831/udp # Compact Thrift from BuildKit
- 16686:16686 # UI on http://127.0.0.1:16686/
librechat:
build:
context: ./build/deps
dockerfile: librechat.Dockerfile
container_name: librechat_jaeger
trivy:
build:
context: ./build/deps
+35 -42
View File
@@ -35,12 +35,9 @@ import (
"time"
"github.com/FerretDB/FerretDB/v2/build/version"
"github.com/FerretDB/FerretDB/v2/internal/clientconn"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/v2/internal/documentdb"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/handler/middleware"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/setup"
"github.com/FerretDB/FerretDB/v2/internal/util/state"
"github.com/FerretDB/FerretDB/v2/internal/util/telemetry"
)
@@ -74,8 +71,8 @@ type Config struct {
// FerretDB represents an instance of embedded FerretDB implementation.
type FerretDB struct {
tl *telemetry.Reporter
lis *clientconn.Listener
tr *telemetry.Reporter
res *setup.SetupResult
}
// New creates a new instance of embedded FerretDB implementation.
@@ -107,7 +104,7 @@ func New(config *Config) (*FerretDB, error) {
}
logger := logging.WithName(logging.Logger(logOutput, lOpts, ""), "ferretdb")
lm := connmetrics.NewListenerMetrics()
mm := middleware.NewMetrics()
tr, err := telemetry.NewReporter(&telemetry.NewReporterOpts{
URL: "https://beacon.ferretdb.com/",
@@ -116,7 +113,7 @@ func New(config *Config) (*FerretDB, error) {
DNT: os.Getenv("DO_NOT_TRACK"),
ExecName: os.Args[0],
P: stateProvider,
ConnMetrics: lm.ConnMetrics,
Metrics: mm,
L: logging.WithName(logger, "telemetry"),
UndecidedDelay: time.Hour,
ReportInterval: 24 * time.Hour,
@@ -125,46 +122,42 @@ func New(config *Config) (*FerretDB, error) {
return nil, fmt.Errorf("failed to create telemetry reporter: %w", err)
}
p, err := documentdb.NewPool(config.PostgreSQLURL, logging.WithName(logger, "pool"), stateProvider)
if err != nil {
return nil, fmt.Errorf("failed to construct pool: %w", err)
}
handlerOpts := &handler.NewOpts{
Pool: p,
Auth: false,
TCPHost: "",
ReplSetName: "",
L: logging.WithName(logger, "handler"),
ConnMetrics: lm.ConnMetrics,
//exhaustruct:enforce
res := setup.Setup(context.TODO(), &setup.SetupOpts{
Logger: logger,
StateProvider: stateProvider,
}
Metrics: mm,
h, err := handler.New(handlerOpts)
if err != nil {
p.Close()
return nil, fmt.Errorf("failed to construct handler: %w", err)
}
PostgreSQLURL: config.PostgreSQLURL,
Auth: false,
ReplSetName: "",
SessionCleanupInterval: 0,
lis, err := clientconn.Listen(&clientconn.ListenerOpts{
Handler: h,
Metrics: lm,
Logger: logger,
ProxyAddr: "",
ProxyTLSCertFile: "",
ProxyTLSKeyFile: "",
ProxyTLSCAFile: "",
TCP: config.ListenAddr,
TCPAddr: config.ListenAddr,
UnixAddr: "",
TLSAddr: "",
TLSCertFile: "",
TLSKeyFile: "",
TLSCAFile: "",
Mode: middleware.NormalMode,
TestRecordsDir: "",
Mode: middleware.NormalMode,
DataAPIAddr: "",
MCPAddr: "",
})
if err != nil {
p.Close()
return nil, fmt.Errorf("failed to construct listener: %w", err)
if res == nil {
return nil, fmt.Errorf("failed to create FerretDB")
}
return &FerretDB{
tl: tr,
lis: lis,
tr: tr,
res: res,
}, nil
}
@@ -181,14 +174,14 @@ func (f *FerretDB) Run(ctx context.Context) {
go func() {
defer wg.Done()
f.tl.Run(ctx)
f.tr.Run(ctx)
}()
wg.Add(1)
go func() {
defer wg.Done()
f.lis.Run(ctx)
f.res.Run(ctx)
}()
wg.Wait()
@@ -198,7 +191,7 @@ func (f *FerretDB) Run(ctx context.Context) {
func (f *FerretDB) MongoDBURI() string {
u := &url.URL{
Scheme: "mongodb",
Host: f.lis.TCPAddr().String(),
Host: f.res.WireListener.TCPAddr().String(),
Path: "/",
}
+1 -1
View File
@@ -89,7 +89,7 @@ func TestFerretDB(t *testing.T) {
ListenAddr: "127.0.0.1:0",
StateDir: t.TempDir(),
LogLevel: slog.LevelDebug,
LogOutput: testutil.NewLogWriter(t),
LogOutput: t.Output(),
})
require.NoError(t, err)
+38 -35
View File
@@ -1,70 +1,73 @@
module github.com/FerretDB/FerretDB/v2
go 1.24
go 1.25
toolchain go1.24.5
toolchain go1.25.4
// replace github.com/FerretDB/wire => ../wire
require (
github.com/AlekSi/lazyerrors v0.0.0-20251102120325-67e66cc79778
github.com/AlekSi/pointer v1.2.0
github.com/FerretDB/wire v0.1.7-0.20250708055527-67b1f7c98628
github.com/alecthomas/kong v1.12.0
github.com/arl/statsviz v0.6.0
github.com/FerretDB/wire v0.1.7
github.com/alecthomas/kong v1.12.1
github.com/arl/statsviz v0.7.2
github.com/google/uuid v1.6.0
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgx/v5 v5.7.5
github.com/oapi-codegen/runtime v1.1.1
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6
github.com/jackc/pgx/v5 v5.7.6
github.com/modelcontextprotocol/go-sdk v0.2.0
github.com/oapi-codegen/runtime v1.1.2
github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.65.0
github.com/stretchr/testify v1.10.0
github.com/prometheus/common v0.67.2
github.com/stretchr/testify v1.11.1
github.com/xdg-go/scram v1.1.2
go.mongodb.org/mongo-driver/v2 v2.2.2
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
go.uber.org/automaxprocs v1.6.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20250605174209-3bf9d2afd4f0
golang.org/x/net v0.41.0
golang.org/x/sys v0.33.0
golang.org/x/term v0.32.0
go.mongodb.org/mongo-driver/v2 v2.4.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20251028130051-c0531f9c3451
golang.org/x/net v0.46.0
golang.org/x/sys v0.38.0
golang.org/x/term v0.36.0
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+80 -74
View File
@@ -1,23 +1,25 @@
github.com/AlekSi/lazyerrors v0.0.0-20251102120325-67e66cc79778 h1:Hv1Z0ZnUWI1NYHXKUE16SWUcv+VyFARJHQ6dGkd2TIM=
github.com/AlekSi/lazyerrors v0.0.0-20251102120325-67e66cc79778/go.mod h1:S8uLHjY/2pJvG9qkWwDX1AVYYknPI92r2qh/f0uysr0=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/FerretDB/wire v0.1.7-0.20250708055527-67b1f7c98628 h1:QqqnK0XPruJ4Z2XyjaYZn+Up396LFCBFb/mE4u+B7dk=
github.com/FerretDB/wire v0.1.7-0.20250708055527-67b1f7c98628/go.mod h1:bXVDsxwA8ugBIfpD/eLYjHjdC7U95ygaWVX7RW2UysY=
github.com/FerretDB/wire v0.1.7 h1:yh6tL0rU8ub+KuKqiqTlFJGpdfL4V7kg/8PBPDX0sBE=
github.com/FerretDB/wire v0.1.7/go.mod h1:KjV8URsECCZWwJBBzlTvyl9LEXvnE362tL4tg52VHXI=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.12.0 h1:oKd/0fHSdajj5PfGDd3ScvEvpVJf9mT2mb5r9xYadYM=
github.com/alecthomas/kong v1.12.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/kong v1.12.1 h1:iq6aMJDcFYP9uFrLdsiZQ2ZMmcshduyGv4Pek0MQPW0=
github.com/alecthomas/kong v1.12.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE=
github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s=
github.com/arl/statsviz v0.7.2 h1:xnuIfRiXE4kvxEcfGL+IE3mKH1BXNHuE+eJELIh7oOA=
github.com/arl/statsviz v0.7.2/go.mod h1:XlrbiT7xYT03xaW9JMMfD8KFUhBOESJwfyNJu83PbB0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -36,20 +38,20 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
@@ -61,112 +63,116 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/modelcontextprotocol/go-sdk v0.2.0 h1:PESNYOmyM1c369tRkzXLY5hHrazj8x9CY1Xu0fLCryM=
github.com/modelcontextprotocol/go-sdk v0.2.0/go.mod h1:0sL9zUKKs2FTTkeCCVnKqbLJTw5TScefPAzojjU459E=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.2.2 h1:9cYuS3fl1Xhqwpfazso10V7BHQD58kCgtzhfAmJYz9c=
go.mongodb.org/mongo-driver/v2 v2.2.2/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps=
go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo=
go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250605174209-3bf9d2afd4f0 h1:LAtxhSighcSwwB7zC5nvOl3BlSEKP2AtwGyJNnt1O0w=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250605174209-3bf9d2afd4f0/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251028130051-c0531f9c3451 h1:ntT/VrHMoSyusO9qDIYKxtkMfluoOkLrr6AJfVRROsE=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251028130051-c0531f9c3451/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+1 -1
View File
@@ -107,7 +107,7 @@ linters-settings:
alias: otelsdkresource
- pkg: go.opentelemetry.io/otel/sdk/trace
alias: otelsdktrace
- pkg: go.opentelemetry.io/otel/semconv/v1.21.0
- pkg: go.opentelemetry.io/otel/semconv/v1.34.0
alias: otelsemconv
# ineffassign
lll:
+2 -2
View File
@@ -148,10 +148,10 @@ func TestCreateUserCommand(t *testing.T) {
},
"MissingPwdOrExternal": {
payload: bson.D{
{"createUser", "mising_pwd_or_external"},
{"createUser", "missing_pwd_or_external"},
{"roles", bson.A{}},
},
user: "mising_pwd_or_external",
user: "missing_pwd_or_external",
err: &mongo.CommandError{
Code: 2,
Name: "BadValue",
+4 -1
View File
@@ -1266,7 +1266,10 @@ func TestSASLContinueOpQueryErrors(t *testing.T) {
q.FullCollectionName = "admin.$cmd"
q.NumberToReturn = -1
conn, err := wireclient.Connect(ctx, s.MongoDBURI, testutil.Logger(t))
cleanUri, _, _, _, err := wireclient.Credentials(s.MongoDBURI)
require.NoError(t, err)
conn, err := wireclient.Connect(ctx, cleanUri, testutil.Logger(t))
require.NoError(t, err)
_, resBody, err := conn.Request(ctx, q)
+2 -2
View File
@@ -108,7 +108,7 @@ func TestInsertFind(t *testing.T) {
func TestFindOtelComment(t *testing.T) {
ctx, collection := setup.Setup(t, shareddata.Scalars)
ctx, span := otel.Tracer("").Start(ctx, "TestOtelComment")
ctx, span := otel.Tracer("").Start(ctx, "TestFindOtelComment")
defer span.End()
comment, err := observability.CommentFromSpanContext(span.SpanContext())
@@ -580,7 +580,7 @@ func TestDebugCommandErrors(t *testing.T) {
}
AssertMatchesCommandError(t, expected, res.Err())
msg := `(InternalError) [msg_ferretdebugerror.go:59 handler.(*Handler).msgFerretDebugError] lazy error`
msg := `(InternalError) msg_ferretdebugerror.go:59 (handler.(*Handler).msgFerretDebugError): lazy error`
assert.EqualError(t, res.Err(), msg)
var doc bson.D
+14 -14
View File
@@ -269,48 +269,48 @@ func TestHostInfoCommand(t *testing.T) {
for _, elem := range a {
switch elem.Key {
case "system":
var elemForComparsion bson.D
var elemForComparison bson.D
for _, subElem := range elem.Value.(bson.D) {
switch subElem.Key {
case "currentTime":
assert.IsType(t, primitive.DateTime(0), subElem.Value)
elemForComparsion = append(elemForComparsion, bson.E{"currentTime", primitive.DateTime(0)})
elemForComparison = append(elemForComparison, bson.E{"currentTime", primitive.DateTime(0)})
case "hostname", "cpuArch":
assert.IsType(t, "", subElem.Value)
elemForComparsion = append(elemForComparsion, bson.E{subElem.Key, ""})
elemForComparison = append(elemForComparison, bson.E{subElem.Key, ""})
case "cpuAddrSize", "numCores":
assert.IsType(t, int32(0), subElem.Value)
elemForComparsion = append(elemForComparsion, bson.E{subElem.Key, int32(0)})
elemForComparison = append(elemForComparison, bson.E{subElem.Key, int32(0)})
case "numPhysicalCores", "numCpuSockets", "numNumaNodes", "numaEnabled", "memSizeMB", "memLimitMB":
// not implemented in FerretDB, do nothing
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/587
default:
elemForComparsion = append(elemForComparsion, subElem)
elemForComparison = append(elemForComparison, subElem)
}
}
actualComparable = append(actualComparable, bson.E{"system", elemForComparsion})
actualComparable = append(actualComparable, bson.E{"system", elemForComparison})
case "os":
var elemForComparsion bson.D
var elemForComparison bson.D
for _, subElem := range elem.Value.(bson.D) {
switch subElem.Key {
case "type", "name", "version":
assert.IsType(t, "", subElem.Value)
elemForComparsion = append(elemForComparsion, bson.E{subElem.Key, ""})
elemForComparison = append(elemForComparison, bson.E{subElem.Key, ""})
default:
elemForComparsion = append(elemForComparsion, subElem)
elemForComparison = append(elemForComparison, subElem)
}
}
actualComparable = append(actualComparable, bson.E{"os", elemForComparsion})
actualComparable = append(actualComparable, bson.E{"os", elemForComparison})
case "extra":
assert.IsType(t, bson.D{}, elem.Value)
@@ -349,7 +349,7 @@ func TestListCommandsCommand(t *testing.T) {
err := collection.Database().RunCommand(ctx, bson.D{{"listCommands", 42}}).Decode(&res)
require.NoError(t, err)
var actualForComparsion bson.D
var actualForComparison bson.D
for _, v := range res {
switch v.Key {
@@ -383,10 +383,10 @@ func TestListCommandsCommand(t *testing.T) {
}
}
actualForComparsion = append(actualForComparsion, bson.E{"commands", commandsComparable})
actualForComparison = append(actualForComparison, bson.E{"commands", commandsComparable})
default:
actualForComparsion = append(actualForComparsion, v)
actualForComparison = append(actualForComparison, v)
}
}
@@ -395,7 +395,7 @@ func TestListCommandsCommand(t *testing.T) {
{"ok", float64(1)},
}
AssertEqualDocuments(t, expected, actualForComparsion)
AssertEqualDocuments(t, expected, actualForComparison)
}
func TestValidateCommand(t *testing.T) {
@@ -17,8 +17,9 @@ const _CompatTestCaseResultType_name = "NonEmptyResultEmptyResult"
var _CompatTestCaseResultType_index = [...]uint8{0, 14, 25}
func (i CompatTestCaseResultType) String() string {
if i < 0 || i >= CompatTestCaseResultType(len(_CompatTestCaseResultType_index)-1) {
idx := int(i) - 0
if i < 0 || idx >= len(_CompatTestCaseResultType_index)-1 {
return "CompatTestCaseResultType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CompatTestCaseResultType_name[_CompatTestCaseResultType_index[i]:_CompatTestCaseResultType_index[i+1]]
return _CompatTestCaseResultType_name[_CompatTestCaseResultType_index[idx]:_CompatTestCaseResultType_index[idx+1]]
}
+3
View File
@@ -170,6 +170,9 @@ func TestCurrentOpGetMore(tt *testing.T) {
}
func TestCurrentOpExplain(tt *testing.T) {
// TODO https://github.com/FerretDB/FerretDB/issues/4871
tt.Skip("https://github.com/FerretDB/FerretDB/issues/4871")
tt.Parallel()
s := setup.SetupWithOpts(tt, new(setup.SetupOpts))
+3 -4
View File
@@ -249,15 +249,14 @@ func TestFindCommandSingleBatch(t *testing.T) {
func TestFindCommandExhausted(tt *testing.T) {
tt.Parallel()
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/270")
s := setup.SetupWithOpts(tt, &setup.SetupOpts{PoolSize: 1})
collection, ctx := s.Collection, s.Ctx
arr := integration.GenerateDocuments(1, 3)
_, err := collection.InsertMany(ctx, arr)
require.NoError(t, err)
require.NoError(tt, err)
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB/issues/5445")
var res bson.D
err = collection.Database().RunCommand(ctx, bson.D{
+22 -14
View File
@@ -28,6 +28,8 @@ import (
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/FerretDB/FerretDB/v2/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/v2/integration"
"github.com/FerretDB/FerretDB/v2/integration/setup"
"github.com/FerretDB/FerretDB/v2/integration/shareddata"
@@ -310,8 +312,8 @@ func TestGetMoreCommand(t *testing.T) {
// Do not run subtests in t.Parallel() to eliminate the occurrence
// of session error.
// Supporting session would help us understand fix it
// https://github.com/FerretDB/FerretDB/issues/153.
// Supporting session would help us understand fix it.
// TODO https://github.com/FerretDB/FerretDB/issues/153
//
// > Location50738
// > Cannot run getMore on cursor 2053655655200551971,
@@ -467,6 +469,11 @@ func TestGetMoreCommandBatchSize(t *testing.T) {
require.Equal(t, i-1, cursor.RemainingBatchLength())
}
if setup.IsYugabyteDB(t) {
// TODO https://github.com/yugabyte/yugabyte-db/issues/27989
t.Skip("https://github.com/yugabyte/yugabyte-db/issues/27989")
}
// get rest of documents from the cursor to ensure cursor is exhausted
var res bson.D
err = cursor.All(ctx, &res)
@@ -563,8 +570,8 @@ func TestGetMoreCommandConnection(t *testing.T) {
t.Run("SameClient", func(t *testing.T) {
// Do not run subtests in t.Parallel() to eliminate the occurrence
// of session error.
// Supporting session would help us understand fix it
// https://github.com/FerretDB/FerretDB/issues/153.
// Supporting session would help us understand fix it.
// TODO https://github.com/FerretDB/FerretDB/issues/153
//
// > Location50738
// > Cannot run getMore on cursor 2053655655200551971,
@@ -826,16 +833,15 @@ func TestGetMoreCommandMaxTimeMSErrors(t *testing.T) {
func TestGetMoreCommandExhausted(tt *testing.T) {
s := setup.SetupWithOpts(tt, nil)
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/270")
collection := s.Collection
db, ctx := collection.Database(), s.Ctx
arr := integration.GenerateDocuments(0, 10)
_, err := collection.InsertMany(ctx, arr)
require.NoError(t, err)
require.NoError(tt, err)
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB/issues/5445")
var res bson.D
err = db.RunCommand(ctx, bson.D{
@@ -1130,10 +1136,11 @@ func TestFindCommandFirstBatchMaxTimeMS(t *testing.T) {
}
t.Run("GetMore", func(tt *testing.T) {
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/270")
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB/issues/5445")
for i := 0; i < 2; i++ {
time.Sleep(100 * time.Millisecond)
ctxutil.Sleep(ctx, 100*time.Millisecond)
var res bson.D
err = collection.Database().RunCommand(ctx, getMoreCmd).Decode(&res)
require.NoError(t, err)
@@ -1153,9 +1160,10 @@ func TestFindCommandFirstBatchMaxTimeMS(t *testing.T) {
})
t.Run("GetMoreEmpty", func(tt *testing.T) {
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/270")
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB/issues/5445")
ctxutil.Sleep(ctx, 100*time.Millisecond)
time.Sleep(100 * time.Millisecond)
var res bson.D
err = collection.Database().RunCommand(ctx, getMoreCmd).Decode(&res)
require.NoError(t, err)
@@ -1225,7 +1233,7 @@ func TestGetMoreCommandAfterInsertion(t *testing.T) {
}
t.Run("GetMore", func(tt *testing.T) {
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/270")
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB/issues/5445")
for i := 0; i < 2; i++ {
var res bson.D
@@ -1247,7 +1255,7 @@ func TestGetMoreCommandAfterInsertion(t *testing.T) {
})
t.Run("GetMoreEmpty", func(tt *testing.T) {
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB-DocumentDB/issues/270")
t := setup.FailsForFerretDB(tt, "https://github.com/FerretDB/FerretDB/issues/5445")
var res bson.D
err = collection.Database().RunCommand(ctx, getMoreCmd).Decode(&res)
+39 -33
View File
@@ -1,70 +1,76 @@
module github.com/FerretDB/FerretDB/v2/integration
go 1.24
go 1.25
toolchain go1.24.5
toolchain go1.25.4
replace github.com/FerretDB/FerretDB/v2 => ../
// replace github.com/FerretDB/wire => ../../wire
require (
github.com/AlekSi/lazyerrors v0.0.0-20251102120325-67e66cc79778
github.com/AlekSi/pointer v1.2.0
github.com/FerretDB/FerretDB/v2 v2.0.0-00010101000000-000000000000
github.com/FerretDB/wire v0.1.7-0.20250708055527-67b1f7c98628
github.com/FerretDB/wire v0.1.7
github.com/FerretDB/xfail v0.1.0
github.com/google/uuid v1.6.0
github.com/prometheus/client_golang v1.22.0
github.com/stretchr/testify v1.10.0
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
github.com/xdg-go/scram v1.1.2
go.mongodb.org/mongo-driver v1.17.4
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.62.0
go.opentelemetry.io/otel v1.37.0
go.mongodb.org/mongo-driver v1.17.6
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.63.0
go.opentelemetry.io/otel v1.38.0
)
require (
github.com/arl/statsviz v0.6.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/arl/statsviz v0.7.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/modelcontextprotocol/go-sdk v0.2.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/prometheus/common v0.67.2 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver/v2 v2.2.2 // indirect
go.mongodb.org/mongo-driver/v2 v2.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+86 -68
View File
@@ -1,15 +1,21 @@
github.com/AlekSi/lazyerrors v0.0.0-20251102120325-67e66cc79778 h1:Hv1Z0ZnUWI1NYHXKUE16SWUcv+VyFARJHQ6dGkd2TIM=
github.com/AlekSi/lazyerrors v0.0.0-20251102120325-67e66cc79778/go.mod h1:S8uLHjY/2pJvG9qkWwDX1AVYYknPI92r2qh/f0uysr0=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/FerretDB/wire v0.1.7-0.20250708055527-67b1f7c98628 h1:QqqnK0XPruJ4Z2XyjaYZn+Up396LFCBFb/mE4u+B7dk=
github.com/FerretDB/wire v0.1.7-0.20250708055527-67b1f7c98628/go.mod h1:bXVDsxwA8ugBIfpD/eLYjHjdC7U95ygaWVX7RW2UysY=
github.com/FerretDB/wire v0.1.7 h1:yh6tL0rU8ub+KuKqiqTlFJGpdfL4V7kg/8PBPDX0sBE=
github.com/FerretDB/wire v0.1.7/go.mod h1:KjV8URsECCZWwJBBzlTvyl9LEXvnE362tL4tg52VHXI=
github.com/FerretDB/xfail v0.1.0 h1:NmwdtwCmQ12X4VutLL+XilG/vbLRbG6RUVQabnI1JiQ=
github.com/FerretDB/xfail v0.1.0/go.mod h1:u34yzRVZdvuc/fjegLYi+FztscWts9pP9ZUJ7wY9OyE=
github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE=
github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/arl/statsviz v0.7.2 h1:xnuIfRiXE4kvxEcfGL+IE3mKH1BXNHuE+eJELIh7oOA=
github.com/arl/statsviz v0.7.2/go.mod h1:XlrbiT7xYT03xaW9JMMfD8KFUhBOESJwfyNJu83PbB0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -28,20 +34,21 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -50,107 +57,118 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/modelcontextprotocol/go-sdk v0.2.0 h1:PESNYOmyM1c369tRkzXLY5hHrazj8x9CY1Xu0fLCryM=
github.com/modelcontextprotocol/go-sdk v0.2.0/go.mod h1:0sL9zUKKs2FTTkeCCVnKqbLJTw5TScefPAzojjU459E=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.2.2 h1:9cYuS3fl1Xhqwpfazso10V7BHQD58kCgtzhfAmJYz9c=
go.mongodb.org/mongo-driver/v2 v2.2.2/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo=
go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.62.0 h1:IDI0wUpSFq/RUr1rRTHT7nF/Mr3V4kENTn05P39fH7k=
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.62.0/go.mod h1:PxUlDgXfAHM+OrUrqs3pbc2OR59ZLDSe9r5NiS0B/4E=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.63.0 h1:6IOE2J+3fFJKJ/8riwf6XrazdEr261L8TEY6T0uSjEM=
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.63.0/go.mod h1:kbPDiVJGSE06bBx6sJlDMXFQ15/gnY4MA1ppkso9LYE=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+2 -2
View File
@@ -475,7 +475,7 @@ func TestCreateIndexesCompatUnique(t *testing.T) {
insertDoc: bson.D{{"v", "value"}},
new: true,
// This test case passes only because of our hack for
// TODO https://github.com/microsoft/documentdb/issues/25
// TODO https://github.com/documentdb/documentdb/issues/25
// failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/296",
},
"NotExistingFieldIndex": {
@@ -515,7 +515,7 @@ func TestCreateIndexesCompatUnique(t *testing.T) {
},
insertDoc: bson.D{{"v", int32(42)}},
// This test case passes only because of our hack for
// TODO https://github.com/microsoft/documentdb/issues/25
// TODO https://github.com/documentdb/documentdb/issues/25
// failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/296",
},
} {
+2 -2
View File
@@ -116,10 +116,10 @@ func TestQueryArrayCompatDotNotation(t *testing.T) {
"FieldArrayIndex": {
filter: bson.D{{"v.foo[0]", int32(42)}},
},
"FieldArrayAsterix": {
"FieldArrayAsterisk": {
filter: bson.D{{"v.foo[*]", int32(42)}},
},
"FieldAsterix": {
"FieldAsterisk": {
filter: bson.D{{"v.*", int32(42)}},
},
"FieldAt": {
+2 -2
View File
@@ -125,14 +125,14 @@ func testQueryCompatWithProviders(t *testing.T, providers shareddata.Providers,
t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
}
targetIdx, tagetErr := targetCollection.Indexes().CreateOne(ctx, mongo.IndexModel{
targetIdx, targetErr := targetCollection.Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{"v", 1}},
})
compatIdx, compatErr := compatCollection.Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{"v", 1}},
})
require.NoError(t, tagetErr)
require.NoError(t, targetErr)
require.NoError(t, compatErr)
require.Equal(t, compatIdx, targetIdx)
+5
View File
@@ -680,6 +680,11 @@ func TestQueryShowRecordID(t *testing.T) {
cursor, err := tc.collection.Find(ctx, bson.D{}, opts)
require.NoError(t, err)
if setup.IsYugabyteDB(t) {
// TODO https://github.com/yugabyte/yugabyte-db/issues/27989
t.Skip("https://github.com/yugabyte/yugabyte-db/issues/27989")
}
var res []bson.D
err = cursor.All(ctx, &res)
require.NoError(t, cursor.Close(ctx))
@@ -17,6 +17,7 @@ package session
import (
"context"
"fmt"
"net/url"
"testing"
"github.com/FerretDB/wire"
@@ -890,14 +891,17 @@ func createKillSessionUser(t *testing.T, ctx context.Context, db *mongo.Database
}).Err()
require.NoError(t, err)
conn, err := wireclient.Connect(ctx, mongoDBURI, testutil.Logger(t))
clearUri, _, _, authMechanism, err := wireclient.Credentials(mongoDBURI)
require.NoError(t, err)
conn, err := wireclient.Connect(ctx, clearUri, testutil.Logger(t))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, conn.Close())
})
err = conn.Login(ctx, username, password, db.Name())
err = conn.Login(ctx, url.UserPassword(username, password), db.Name(), authMechanism)
require.NoError(t, err)
return conn
+18 -6
View File
@@ -19,6 +19,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"testing"
"github.com/FerretDB/wire"
@@ -43,14 +44,17 @@ func TestSessionConnection(t *testing.T) {
ctx, collection, db, conn1 := s.Ctx, s.Collection, s.Collection.Database(), s.WireConn
cName, dbName := collection.Name(), db.Name()
conn2, err := wireclient.Connect(ctx, s.MongoDBURI, testutil.Logger(t))
clearUri, _, _, authMechanism, err := wireclient.Credentials(s.MongoDBURI)
require.NoError(t, err)
conn2, err := wireclient.Connect(ctx, clearUri, testutil.Logger(t))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, conn2.Close())
})
err = conn2.Login(ctx, "username", "password", "admin")
err = conn2.Login(ctx, url.UserPassword("username", "password"), "admin", authMechanism)
require.NoError(t, err)
_, err = collection.InsertMany(ctx, bson.A{
@@ -122,10 +126,15 @@ func TestSessionConnection(t *testing.T) {
t.Run("CloseConnection", func(t *testing.T) {
var anotherConn *wireclient.Conn
anotherConn, err = wireclient.Connect(ctx, s.MongoDBURI, testutil.Logger(t))
var creds *url.Userinfo
clearUri, creds, _, authMechanism, err = wireclient.Credentials(s.MongoDBURI)
require.NoError(t, err)
err = anotherConn.Login(ctx, "username", "password", "admin")
anotherConn, err = wireclient.Connect(ctx, clearUri, testutil.Logger(t))
require.NoError(t, err)
err = anotherConn.Login(ctx, creds, "admin", authMechanism)
require.NoError(t, err)
sessionID := startSession(t, ctx, anotherConn)
@@ -380,14 +389,17 @@ func TestSessionConnectionDifferentUser(t *testing.T) {
}).Err()
require.NoError(t, err)
userConn, err := wireclient.Connect(ctx, s.MongoDBURI, testutil.Logger(t))
clearUri, _, _, authMechanism, err := wireclient.Credentials(s.MongoDBURI)
require.NoError(t, err)
userConn, err := wireclient.Connect(ctx, clearUri, testutil.Logger(t))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, userConn.Close())
})
err = userConn.Login(ctx, user, pass, dbName)
err = userConn.Login(ctx, url.UserPassword(user, pass), "admin", authMechanism)
require.NoError(t, err)
t.Cleanup(func() {
+3 -3
View File
@@ -22,6 +22,7 @@ import (
"strings"
"testing"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wireclient"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/mongo"
@@ -29,7 +30,6 @@ import (
"go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo"
"go.opentelemetry.io/otel"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/testutil"
)
@@ -95,7 +95,7 @@ func makeClient(ctx context.Context, uri string, disableOtel bool) (*mongo.Clien
func setupClient(tb testing.TB, ctx context.Context, uri string, disableOtel bool) *mongo.Client {
tb.Helper()
ctx, span := otel.Tracer("").Start(ctx, "setupClient")
ctx, span := otel.Tracer("").Start(ctx, "setup.setupClient")
defer span.End()
u, err := url.Parse(uri)
@@ -137,7 +137,7 @@ func setupClient(tb testing.TB, ctx context.Context, uri string, disableOtel boo
func setupWireConn(tb testing.TB, ctx context.Context, uri string, l *slog.Logger) *wireclient.Conn {
tb.Helper()
ctx, span := otel.Tracer("").Start(ctx, "setupWireConn")
ctx, span := otel.Tracer("").Start(ctx, "setup.setupWireConn")
defer span.End()
conn, err := wireclient.Connect(ctx, uri, l)
+9
View File
@@ -36,6 +36,15 @@ func IsMongoDB(tb testing.TB) bool {
return *targetBackendF == "mongodb"
}
// IsYugabyteDB returns true if the current test is running for `ferretdb-yugabytedb` backend.
//
// This function should not be used lightly.
func IsYugabyteDB(tb testing.TB) bool {
tb.Helper()
return *targetBackendF == "ferretdb-yugabytedb"
}
// ensureIssueURL panics if URL is not a valid FerretDB issue URL.
func ensureIssueURL(url string) {
ferretDB := strings.HasPrefix(url, "https://github.com/FerretDB/FerretDB/issues/")
+34 -38
View File
@@ -25,11 +25,8 @@ import (
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"github.com/FerretDB/FerretDB/v2/internal/clientconn"
"github.com/FerretDB/FerretDB/v2/internal/documentdb"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/handler/middleware"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/setup"
"github.com/FerretDB/FerretDB/v2/internal/util/state"
"github.com/FerretDB/FerretDB/v2/internal/util/testutil"
)
@@ -86,13 +83,13 @@ func listenerMongoDBURI(tb testing.TB, hostPort, unixSocketPath string) string {
func setupListener(tb testing.TB, ctx context.Context, opts *ListenerOpts, logger *slog.Logger) string {
tb.Helper()
ctx, span := otel.Tracer("").Start(ctx, "setupListener")
ctx, span := otel.Tracer("").Start(ctx, "setup.setupListener")
defer span.End()
require.Empty(tb, *targetURLF, "-target-url must be empty for in-process FerretDB")
switch *targetBackendF {
case "ferretdb":
case "ferretdb", "ferretdb-yugabytedb":
require.NotEmpty(tb, *postgreSQLURLF, "-postgresql-url must be set for %q", *targetBackendF)
case "mongodb":
@@ -110,59 +107,58 @@ func setupListener(tb testing.TB, ctx context.Context, opts *ListenerOpts, logge
opts = new(ListenerOpts)
}
p, err := documentdb.NewPool(*postgreSQLURLF, logging.WithName(logger, "pool"), sp)
require.NoError(tb, err)
handlerOpts := &handler.NewOpts{
Pool: p,
Auth: true,
// TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/566
TCPHost: "",
ReplSetName: "",
L: logging.WithName(logger, "handler"),
ConnMetrics: listenerMetrics.ConnMetrics,
//exhaustruct:enforce
wireOpts := &setup.SetupOpts{
Logger: logger,
StateProvider: sp,
Metrics: middleware.NewMetrics(),
PostgreSQLURL: *postgreSQLURLF,
Auth: true,
ReplSetName: "", // TODO https://github.com/FerretDB/FerretDB-DocumentDB/issues/566
SessionCleanupInterval: opts.SessionCleanupInterval,
}
h, err := handler.New(handlerOpts)
require.NoError(tb, err)
ProxyAddr: "",
ProxyTLSCertFile: "",
ProxyTLSKeyFile: "",
ProxyTLSCAFile: "",
listenerOpts := clientconn.ListenerOpts{
Handler: h,
Metrics: listenerMetrics,
Logger: logger,
TCPAddr: "",
UnixAddr: "",
TLSAddr: "",
TLSCertFile: "",
TLSKeyFile: "",
TLSCAFile: "",
Mode: middleware.NormalMode,
ProxyAddr: *targetProxyAddrF,
TestRecordsDir: testutil.TmpRecordsDir,
}
if *targetProxyAddrF != "" {
listenerOpts.Mode = middleware.DiffNormalMode
DataAPIAddr: "",
}
switch {
case *targetUnixSocketF:
listenerOpts.Unix = unixSocketPath(tb)
wireOpts.UnixAddr = unixSocketPath(tb)
default:
listenerOpts.TCP = "127.0.0.1:0"
wireOpts.TCPAddr = "127.0.0.1:0"
}
l, err := clientconn.Listen(&listenerOpts)
require.NoError(tb, err)
if *targetProxyAddrF != "" {
wireOpts.Mode = middleware.DiffNormalMode
wireOpts.ProxyAddr = *targetProxyAddrF
}
res := setup.Setup(ctx, wireOpts)
require.NotNil(tb, res)
runDone := make(chan struct{})
go func() {
defer close(runDone)
runCtx, runSpan := otel.Tracer("").Start(ctx, "setupListener.Run")
runCtx, runSpan := otel.Tracer("").Start(ctx, "setup.setupListener.Run")
defer runSpan.End()
l.Run(runCtx)
res.Run(runCtx)
}()
// ensure that all listener's and handler's logs are written before test ends
@@ -174,9 +170,9 @@ func setupListener(tb testing.TB, ctx context.Context, opts *ListenerOpts, logge
switch {
case *targetUnixSocketF:
unixSocketPath = l.UnixAddr().String()
unixSocketPath = res.WireListener.UnixAddr().String()
default:
hostPort = l.TCPAddr().String()
hostPort = res.WireListener.TCPAddr().String()
}
uri := listenerMongoDBURI(tb, hostPort, unixSocketPath)
+60 -16
View File
@@ -62,7 +62,7 @@ var (
// Other globals.
var (
allBackends = []string{"ferretdb", "mongodb"}
allBackends = []string{"ferretdb", "ferretdb-yugabytedb", "mongodb"}
)
// WireConn defines if and how wire client connection is established.
@@ -147,7 +147,7 @@ func SetupWithOpts(tb testing.TB, opts *SetupOpts) *SetupResult {
ctx, cancel := context.WithCancel(testutil.Ctx(tb))
setupCtx, span := otel.Tracer("").Start(ctx, "SetupWithOpts")
setupCtx, span := otel.Tracer("").Start(ctx, "setup.SetupWithOpts")
defer span.End()
if opts == nil {
@@ -204,7 +204,10 @@ func SetupWithOpts(tb testing.TB, opts *SetupOpts) *SetupResult {
var conn *wireclient.Conn
if opts.WireConn != WireConnNoConn {
conn = setupWireConn(tb, setupCtx, uri, testutil.Logger(tb))
clearUri, creds, _, authMechanism, err := wireclient.Credentials(uri)
require.NoError(tb, err)
conn = setupWireConn(tb, setupCtx, clearUri, testutil.Logger(tb))
if opts.WireConn == WireConnAuth {
u, err := url.Parse(uri)
@@ -216,7 +219,7 @@ func SetupWithOpts(tb testing.TB, opts *SetupOpts) *SetupResult {
pass, _ := u.User.Password()
require.NotEmpty(tb, pass)
require.NoError(tb, conn.Login(ctx, user, pass, "admin"))
require.NoError(tb, conn.Login(ctx, creds, "admin", authMechanism))
}
}
@@ -245,7 +248,7 @@ func Setup(tb testing.TB, providers ...shareddata.Provider) (context.Context, *m
func setupCollection(tb testing.TB, ctx context.Context, client *mongo.Client, opts *SetupOpts) *mongo.Collection {
tb.Helper()
ctx, span := otel.Tracer("").Start(ctx, "setupCollection")
ctx, span := otel.Tracer("").Start(ctx, "setup.setupCollection")
defer span.End()
var ownDatabase bool
@@ -261,10 +264,10 @@ func setupCollection(tb testing.TB, ctx context.Context, client *mongo.Client, o
collection := database.Collection(collectionName)
// drop remnants of the previous failed run
_ = collection.Drop(ctx)
if ownDatabase {
_ = database.RunCommand(ctx, bson.D{{"dropAllUsersFromDatabase", 1}})
_ = database.Drop(ctx)
cleanupDatabase(tb, ctx, database)
} else {
cleanupCollection(tb, ctx, collection)
}
// drop collection and (possibly) database unless test failed
@@ -274,16 +277,13 @@ func setupCollection(tb testing.TB, ctx context.Context, client *mongo.Client, o
return
}
err := collection.Drop(ctx)
require.NoError(tb, err)
if ownDatabase {
err = database.RunCommand(ctx, bson.D{{"dropAllUsersFromDatabase", 1}}).Err()
require.NoError(tb, err)
cleanupDatabase(tb, ctx, database)
err = database.Drop(ctx)
require.NoError(tb, err)
return
}
cleanupCollection(tb, ctx, collection)
})
var inserted bool
@@ -309,7 +309,7 @@ func setupCollection(tb testing.TB, ctx context.Context, client *mongo.Client, o
func InsertProviders(tb testing.TB, ctx context.Context, collection *mongo.Collection, providers ...shareddata.Provider) (inserted bool) {
tb.Helper()
ctx, span := otel.Tracer("").Start(ctx, "InsertProviders")
ctx, span := otel.Tracer("").Start(ctx, "setup.InsertProviders")
defer span.End()
for _, provider := range providers {
@@ -342,3 +342,47 @@ func insertBenchmarkProvider(tb testing.TB, ctx context.Context, collection *mon
return
}
// cleanupCollection deletes all documents in the collection for ferretdb-yugabytedb backend
// or drops the collection for other backends.
func cleanupCollection(tb testing.TB, ctx context.Context, collection *mongo.Collection) {
tb.Helper()
if IsYugabyteDB(tb) {
// dropping collection or database fails for ferretdb-yugabytedb
// TODO https://github.com/yugabyte/yugabyte-db/issues/27698
_, err := collection.DeleteMany(ctx, bson.D{})
require.NoError(tb, err)
return
}
err := collection.Drop(ctx)
require.NoError(tb, err)
}
// cleanupDatabase drops all users, then deletes all collections in the database
// for ferretdb-yugabytedb backend or drops the database for other backends.
func cleanupDatabase(tb testing.TB, ctx context.Context, database *mongo.Database) {
tb.Helper()
err := database.RunCommand(ctx, bson.D{{"dropAllUsersFromDatabase", 1}}).Err()
require.NoError(tb, err)
if IsYugabyteDB(tb) {
// dropping collection or database fails for ferretdb-yugabytedb
// TODO https://github.com/yugabyte/yugabyte-db/issues/27698
var collections []string
collections, err = database.ListCollectionNames(ctx, bson.D{})
require.NoError(tb, err)
for _, collection := range collections {
cleanupCollection(tb, ctx, database.Collection(collection))
}
return
}
err = database.Drop(ctx)
require.NoError(tb, err)
}
+4 -10
View File
@@ -21,7 +21,6 @@ import (
"testing"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.opentelemetry.io/otel"
@@ -66,7 +65,7 @@ func SetupCompatWithOpts(tb testing.TB, opts *SetupCompatOpts) *SetupCompatResul
ctx, cancel := context.WithCancel(testutil.Ctx(tb))
setupCtx, span := otel.Tracer("").Start(ctx, "SetupCompatWithOpts")
setupCtx, span := otel.Tracer("").Start(ctx, "setup.SetupCompatWithOpts")
defer span.End()
if opts == nil {
@@ -130,14 +129,13 @@ func SetupCompat(tb testing.TB) (context.Context, []*mongo.Collection, []*mongo.
func setupCompatCollections(tb testing.TB, ctx context.Context, client *mongo.Client, opts *SetupCompatOpts, backend string) []*mongo.Collection {
tb.Helper()
ctx, span := otel.Tracer("").Start(ctx, "setupCompatCollections")
ctx, span := otel.Tracer("").Start(ctx, "setup.setupCompatCollections")
defer span.End()
database := client.Database(opts.databaseName)
// drop remnants of the previous failed run
_ = database.RunCommand(ctx, bson.D{{"dropAllUsersFromDatabase", 1}})
_ = database.Drop(ctx)
cleanupDatabase(tb, ctx, database)
// drop database unless test failed
tb.Cleanup(func() {
@@ -145,11 +143,7 @@ func setupCompatCollections(tb testing.TB, ctx context.Context, client *mongo.Cl
return
}
err := database.RunCommand(ctx, bson.D{{"dropAllUsersFromDatabase", 1}}).Err()
require.NoError(tb, err)
err = database.Drop(ctx)
require.NoError(tb, err)
cleanupDatabase(tb, ctx, database)
})
providers := slices.Clone(opts.Providers)
+2 -8
View File
@@ -27,7 +27,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
"go.mongodb.org/mongo-driver/mongo"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/v2/internal/util/debug"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/observability"
@@ -35,9 +34,6 @@ import (
"github.com/FerretDB/FerretDB/v2/integration/shareddata"
)
// listenerMetrics are shared between tests.
var listenerMetrics = connmetrics.NewListenerMetrics()
// shutdown cancels context passed to startup components.
var shutdown context.CancelFunc
@@ -63,8 +59,6 @@ func Startup() {
*debugSetupF = true
}
prometheus.DefaultRegisterer.MustRegister(listenerMetrics)
ctx, shutdown = context.WithCancel(ctx)
startupWG.Add(1)
@@ -82,7 +76,7 @@ func Startup() {
l.LogAttrs(ctx, logging.LevelFatal, "Failed to create debug handler", logging.Error(err))
}
h.Serve(ctx)
h.Run(ctx)
}()
if *otelTracesURLF != "" {
@@ -93,8 +87,8 @@ func Startup() {
ot, err := observability.NewOTelTraceExporter(&observability.OTelTraceExporterOpts{
Logger: logging.WithName(l, "otel"),
Service: "integration-tests",
URL: *otelTracesURLF,
Service: "integration-tests",
})
if err != nil {
l.LogAttrs(ctx, logging.LevelFatal, "Failed to create Otel tracer", logging.Error(err))
+1 -1
View File
@@ -1446,7 +1446,7 @@ func TestUpdateFieldCompatSet(t *testing.T) {
doublesProvider := []shareddata.Provider{shareddata.Doubles}
testCases := map[string]updateCompatTestCase{
"SetNullInExisingField": {
"SetNullInExistingField": {
update: bson.D{{"$set", bson.D{{"v", nil}}}},
},
"DuplicateKeys": {
+35 -481
View File
@@ -29,89 +29,24 @@ import (
"net/netip"
"os"
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire"
"github.com/FerretDB/wire/wirebson"
"github.com/pmezard/go-difflib/difflib"
"go.opentelemetry.io/otel"
otelattribute "go.opentelemetry.io/otel/attribute"
otelcodes "go.opentelemetry.io/otel/codes"
oteltrace "go.opentelemetry.io/otel/trace"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/conninfo"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/handler/middleware"
"github.com/FerretDB/FerretDB/v2/internal/handler/proxy"
"github.com/FerretDB/FerretDB/v2/internal/mongoerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
"github.com/FerretDB/FerretDB/v2/internal/util/observability"
)
// conn represents client connection.
type conn struct {
netConn net.Conn
mode middleware.Mode
l *slog.Logger
h *handler.Handler
m *connmetrics.ConnMetrics
proxy *proxy.Handler
lastRequestID atomic.Int32
m *middleware.Middleware
testRecordsDir string // if empty, no records are created
}
// newConnOpts represents newConn options.
type newConnOpts struct {
netConn net.Conn
mode middleware.Mode
l *slog.Logger
handler *handler.Handler
connMetrics *connmetrics.ConnMetrics
proxyAddr string
proxyTLSCertFile string
proxyTLSKeyFile string
proxyTLSCAFile string
testRecordsDir string // if empty, no records are created
}
// newConn creates a new client connection for given net.Conn.
func newConn(opts *newConnOpts) (*conn, error) {
if opts.mode == "" {
panic("mode required")
}
if opts.handler == nil {
panic("handler required")
}
var p *proxy.Handler
if opts.mode != middleware.NormalMode {
var err error
p, err = proxy.New(opts.proxyAddr, opts.proxyTLSCertFile, opts.proxyTLSKeyFile, opts.proxyTLSCAFile)
if err != nil {
return nil, lazyerrors.Error(err)
}
}
return &conn{
netConn: opts.netConn,
mode: opts.mode,
l: opts.l,
h: opts.handler,
m: opts.connMetrics,
proxy: p,
testRecordsDir: opts.testRecordsDir,
}, nil
}
// run runs the client connection until ctx is canceled, client disconnects,
// or fatal error or panic is encountered.
//
@@ -119,54 +54,32 @@ func newConn(opts *newConnOpts) (*conn, error) {
//
// The caller is responsible for closing the underlying net.Conn.
func (c *conn) run(ctx context.Context) (err error) {
ctx, cancel := context.WithCancelCause(ctx)
defer func() {
cancel(lazyerrors.Errorf("run exits: %w", err))
}()
connInfo := conninfo.New()
defer connInfo.Close()
if c.netConn.RemoteAddr().Network() != "unix" {
connInfo.Peer, err = netip.ParseAddrPort(c.netConn.RemoteAddr().String())
if err != nil {
return
}
}
ctx = conninfo.Ctx(ctx, connInfo)
// That's not the best – it makes proxy handler very different from the main handler.
// Instead, proxy handler should map connections based on connInfo.
// TODO https://github.com/FerretDB/FerretDB/issues/4965
if c.proxy != nil {
go c.proxy.Run(ctx)
}
done := make(chan struct{})
// handle ctx cancellation
go func() {
select {
case <-done:
// nothing, let goroutine exit
case <-ctx.Done():
// unblocks ReadMessage below; any non-zero past value will do
if e := c.netConn.SetDeadline(time.Unix(0, 0)); e != nil {
c.l.WarnContext(ctx, fmt.Sprintf("Failed to set deadline: %s", e))
}
}
}()
ci := conninfo.New()
ctx, cancel := context.WithCancelCause(conninfo.Ctx(ctx, ci))
defer func() {
if p := recover(); p != nil {
c.l.LogAttrs(ctx, logging.LevelDPanic, fmt.Sprint(p), logging.Error(err))
err = errors.New("panic")
err = lazyerrors.New("panic")
}
// let goroutine above exit
close(done)
cancel(lazyerrors.Errorf("run exits: %w", err))
ci.Close()
}()
if c.netConn.RemoteAddr().Network() != "unix" {
ci.Peer, err = netip.ParseAddrPort(c.netConn.RemoteAddr().String())
if err != nil {
err = lazyerrors.Error(err)
return
}
}
go func() {
<-ctx.Done()
// unblocks ReadMessage in the processRequest below; any non-zero past value will do
_ = c.netConn.SetDeadline(time.Unix(0, 0))
}()
bufr := bufio.NewReader(c.netConn)
@@ -197,315 +110,43 @@ func (c *conn) run(ctx context.Context) (err error) {
bufw := bufio.NewWriter(c.netConn)
defer func() {
if e := bufw.Flush(); err == nil {
err = e
}
// c.netConn is closed by the caller
}()
for {
if err = c.processMessage(ctx, bufr, bufw); err != nil {
if err = c.processRequest(ctx, bufr, bufw); err != nil {
return
}
}
}
// processMessage reads the request, routes the request based on the operation mode
// and writes the response.
// processRequest reads the request, passes it to the middleware, and writes the response.
//
// Any error returned indicates the connection should be closed.
func (c *conn) processMessage(ctx context.Context, bufr *bufio.Reader, bufw *bufio.Writer) error {
func (c *conn) processRequest(ctx context.Context, bufr *bufio.Reader, bufw *bufio.Writer) error {
reqHeader, reqBody, err := wire.ReadMessage(bufr)
if err != nil {
return err
return lazyerrors.Error(err)
}
if c.l.Enabled(ctx, slog.LevelDebug) {
c.l.DebugContext(ctx, "Request header: "+reqHeader.String())
c.l.DebugContext(ctx, "Request message:\n"+reqBody.StringIndent())
req, err := middleware.RequestWire(reqHeader, reqBody)
if err != nil {
return lazyerrors.Error(err)
}
// diffLogLevel provides the level of logging for the diff between the "normal" and "proxy" responses.
// It is set to the highest level of logging used to log response.
diffLogLevel := slog.LevelDebug
// send request to proxy first (unless we are in normal mode)
// because FerretDB's handling could modify reqBody's documents,
// creating a data race
var proxyHeader *wire.MsgHeader
var proxyBody wire.MsgBody
if c.mode != middleware.NormalMode {
if c.proxy == nil {
panic("proxy addr was nil")
}
// TODO https://github.com/FerretDB/FerretDB/issues/1997
var resp *middleware.Response
resp, err = c.proxy.Handle(ctx, middleware.RequestWire(reqHeader, reqBody))
must.NoError(err)
proxyHeader = resp.WireHeader()
proxyBody = resp.WireBody()
resp := c.m.Handle(ctx, req)
if resp == nil {
return lazyerrors.New("middleware returned nil response")
}
// handle request unless we are in proxy mode
var resCloseConn bool
var resHeader *wire.MsgHeader
var resBody wire.MsgBody
if c.mode != middleware.ProxyMode {
resHeader, resBody, resCloseConn = c.route(ctx, reqHeader, reqBody)
if level := c.logResponse(ctx, "Response", resHeader, resBody, resCloseConn); level > diffLogLevel {
diffLogLevel = level
}
}
// log proxy response after the normal response to make it less confusing
if c.mode != middleware.NormalMode {
if level := c.logResponse(ctx, "Proxy response", proxyHeader, proxyBody, false); level > diffLogLevel {
diffLogLevel = level
}
}
// diff in diff mode
if c.l.Enabled(ctx, diffLogLevel) && (c.mode == middleware.DiffNormalMode || c.mode == middleware.DiffProxyMode) {
if err = c.logDiff(ctx, resHeader, proxyHeader, resBody, proxyBody, diffLogLevel); err != nil {
return err
}
}
// replace response with one from proxy in proxy and diff-proxy modes
if c.mode == middleware.ProxyMode || c.mode == middleware.DiffProxyMode {
resHeader = proxyHeader
resBody = proxyBody
}
if resHeader == nil || resBody == nil {
panic("no response to send to client")
}
if err = wire.WriteMessage(bufw, resHeader, resBody); err != nil {
c.l.DebugContext(ctx, "Failed to write message", logging.Error(err))
return err
if err = wire.WriteMessage(bufw, resp.WireHeader(), resp.WireBody()); err != nil {
return lazyerrors.Error(err)
}
if err = bufw.Flush(); err != nil {
c.l.DebugContext(ctx, "Failed to flush buffer", logging.Error(err))
return err
}
if resCloseConn {
err = errors.New("fatal error")
c.l.DebugContext(ctx, "Connection closed unexpectedly", logging.Error(err))
return err
return lazyerrors.Error(err)
}
return nil
}
// route sends request to a handler's command based on the op code provided in the request header.
//
// The passed context is canceled when the client disconnects.
//
// Handlers to which it routes, should not panic on bad input, but may do so in "impossible" cases.
// They also should not use recover(). That allows us to use fuzzing.
//
// Returned resBody can be nil.
func (c *conn) route(connCtx context.Context, reqHeader *wire.MsgHeader, reqBody wire.MsgBody) (resHeader *wire.MsgHeader, resBody wire.MsgBody, closeConn bool) { //nolint:lll // argument list is too long
var span oteltrace.Span
var command, result, argument string
defer func() {
if result == "" {
result = "panic"
}
if argument == "" {
argument = "unknown"
}
c.m.Responses.WithLabelValues(resHeader.OpCode.String(), command, argument, result).Inc()
must.NotBeZero(span)
if result != "ok" {
span.SetStatus(otelcodes.Error, result)
}
span.SetName(command)
span.SetAttributes(
otelattribute.String("db.ferretdb.opcode", resHeader.OpCode.String()),
otelattribute.Int("db.ferretdb.request_id", int(resHeader.ResponseTo)),
otelattribute.String("db.ferretdb.argument", argument),
)
span.End()
}()
resHeader = new(wire.MsgHeader)
var err error
switch reqHeader.OpCode {
case wire.OpCodeMsg:
msg := reqBody.(*wire.OpMsg)
resHeader.OpCode = wire.OpCodeMsg
var doc *wirebson.Document
if doc, err = msg.Section0(); err == nil {
command = doc.Command()
}
if err == nil {
comment, _ := doc.Get("comment").(string)
spanCtx, e := observability.SpanContextFromComment(comment)
if e == nil {
connCtx = oteltrace.ContextWithRemoteSpanContext(connCtx, spanCtx)
} else {
c.l.DebugContext(connCtx, "Failed to extract span context from comment", logging.Error(e))
}
}
connCtx, span = otel.Tracer("").Start(connCtx, "")
if err == nil {
var res *middleware.Response
if res, err = c.h.Handle(connCtx, middleware.RequestWire(reqHeader, msg)); res != nil {
resBody = res.OpMsg
}
}
case wire.OpCodeQuery:
query := reqBody.(*wire.OpQuery)
resHeader.OpCode = wire.OpCodeReply
var q *wirebson.Document
if q, err = query.Query(); err == nil {
command = q.Command()
}
connCtx, span = otel.Tracer("").Start(connCtx, "")
if err == nil {
var res *middleware.Response
if res, err = c.h.Handle(connCtx, middleware.RequestWire(reqHeader, query)); res != nil {
resBody = res.WireBody()
}
}
case wire.OpCodeReply:
fallthrough
case wire.OpCodeUpdate:
fallthrough
case wire.OpCodeInsert:
fallthrough
case wire.OpCodeGetByOID:
fallthrough
case wire.OpCodeGetMore:
fallthrough
case wire.OpCodeDelete:
fallthrough
case wire.OpCodeKillCursors:
fallthrough
case wire.OpCodeCompressed:
connCtx, span = otel.Tracer("").Start(connCtx, "")
err = lazyerrors.Errorf("unhandled OpCode %s", reqHeader.OpCode)
default:
connCtx, span = otel.Tracer("").Start(connCtx, "")
err = lazyerrors.Errorf("unexpected OpCode %s", reqHeader.OpCode)
}
if command == "" {
command = "unknown"
}
c.m.Requests.WithLabelValues(reqHeader.OpCode.String(), command).Inc()
// set body for error
if err != nil {
switch resHeader.OpCode {
case wire.OpCodeMsg:
protoErr := mongoerrors.Make(connCtx, err, "", c.l)
resBody = protoErr.Msg()
result = protoErr.Name
argument = protoErr.Argument
case wire.OpCodeReply:
protoErr := mongoerrors.Make(connCtx, err, "", c.l)
resBody = protoErr.Reply()
result = protoErr.Name
argument = protoErr.Argument
case wire.OpCodeQuery:
fallthrough
case wire.OpCodeUpdate:
fallthrough
case wire.OpCodeInsert:
fallthrough
case wire.OpCodeGetByOID:
fallthrough
case wire.OpCodeGetMore:
fallthrough
case wire.OpCodeDelete:
fallthrough
case wire.OpCodeKillCursors:
fallthrough
case wire.OpCodeCompressed:
// do not panic to make fuzzing easier
closeConn = true
result = "unhandled"
c.l.ErrorContext(
connCtx,
"Handler error for unhandled response opcode",
logging.Error(err),
slog.Any("opcode", resHeader.OpCode),
)
return
default:
// do not panic to make fuzzing easier
closeConn = true
result = "unexpected"
c.l.ErrorContext(
connCtx,
"Handler error for unexpected response opcode",
logging.Error(err),
slog.Any("opcode", resHeader.OpCode),
)
return
}
}
// Don't call MarshalBinary there. Fix header in the caller?
// TODO https://github.com/FerretDB/FerretDB/issues/273
b, err := resBody.MarshalBinary()
if err != nil {
result = ""
panic(err)
}
resHeader.MessageLength = int32(wire.MsgHeaderLen + len(b))
resHeader.RequestID = c.lastRequestID.Add(1)
resHeader.ResponseTo = reqHeader.RequestID
if result == "" {
result = "ok"
}
return
}
// renamePartialFile takes over an open file `f` and closes it.
// It uses the given error to check if the connection was closed by the client,
// if so the given file is renamed to a name generated by hash,
@@ -540,90 +181,3 @@ func (c *conn) renamePartialFile(ctx context.Context, f *os.File, h hash.Hash, e
c.l.WarnContext(ctx, "Failed to rename file", logging.Error(e))
}
}
// logResponse logs response's header and body and returns the log level that was used.
//
// The param `who` will be used in logs and should represent the type of the response,
// for example "Response" or "Proxy Response".
func (c *conn) logResponse(ctx context.Context, who string, resHeader *wire.MsgHeader, resBody wire.MsgBody, closeConn bool) slog.Level { //nolint:lll // for readability
level := slog.LevelDebug
if resHeader.OpCode == wire.OpCodeMsg {
msg := resBody.(*wire.OpMsg)
doc, _ := msg.Section0()
var ok bool
if doc != nil {
switch v := doc.Get("ok").(type) {
case float64:
ok = v == 1
case int32:
ok = v == 1
case int64:
ok = v == 1
}
}
if !ok {
level = slog.LevelWarn
}
}
if closeConn {
level = slog.LevelError
}
if c.l.Enabled(ctx, level) {
c.l.Log(ctx, level, who+" header: "+resHeader.String())
c.l.Log(ctx, level, who+" message:\n"+resBody.StringIndent())
}
return level
}
// logDiff logs the diff between the response and the proxy response.
func (c *conn) logDiff(ctx context.Context, resHeader, proxyHeader *wire.MsgHeader, resBody, proxyBody wire.MsgBody, logLevel slog.Level) error { //nolint:lll // for readability
diffHeader, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
A: difflib.SplitLines(resHeader.String()),
FromFile: "res header",
B: difflib.SplitLines(proxyHeader.String()),
ToFile: "proxy header",
Context: 1,
})
if err != nil {
return err
}
// resBody can be nil if we got a message we could not handle at all, like unsupported OpQuery.
var resBodyString, proxyBodyString string
if resBody != nil {
resBodyString = resBody.StringIndent()
}
if proxyBody != nil {
proxyBodyString = proxyBody.StringIndent()
}
diffBody, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
A: difflib.SplitLines(resBodyString),
FromFile: "res body",
B: difflib.SplitLines(proxyBodyString),
ToFile: "proxy body",
Context: 1,
})
if err != nil {
return err
}
if len(diffBody) > 0 {
// the diff control lines (those with ---, +++, or @@) are created with a trailing newline
diffBody = "\n" + strings.TrimSpace(diffBody)
}
c.l.Log(ctx, logLevel, "Header diff:\n"+diffHeader+"\nBody diff:"+diffBody)
return nil
}
+12 -1
View File
@@ -24,6 +24,7 @@ import (
)
// ConnInfo represents client connection information.
// It must be one-to-one mapped to a client connection.
type ConnInfo struct {
// the order of fields is weird to make the struct smaller due to alignment
@@ -33,7 +34,8 @@ type ConnInfo struct {
metadataRecv bool // protected by rw
steps int // protected by rw
token *resource.Token
token *resource.Token
onClose func(*ConnInfo)
}
// New creates a new ConnInfo.
@@ -49,9 +51,18 @@ func New() *ConnInfo {
// Close untracks ConnInfo.
func (ci *ConnInfo) Close() {
if ci.onClose != nil {
ci.onClose(ci)
}
resource.Untrack(ci, ci.token)
}
// OnClose sets a callback to be called when ConnInfo is [Close]'d.
func (ci *ConnInfo) OnClose(f func(*ConnInfo)) {
ci.onClose = f
}
// Conv returns SCRAM conversation.
func (ci *ConnInfo) Conv() *scram.Conv {
ci.rw.RLock()
+61 -48
View File
@@ -17,24 +17,23 @@ package clientconn
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log/slog"
"math/rand"
"net"
"os"
"sync"
"time"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire"
"github.com/prometheus/client_golang/prometheus"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/handler/middleware"
"github.com/FerretDB/FerretDB/v2/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/tlsutil"
)
// Listener listens on one or multiple interfaces (TCP, Unix, TLS sockets)
@@ -43,6 +42,7 @@ type Listener struct {
*ListenerOpts
ll *slog.Logger
lm *listenerMetrics
tcpListener net.Listener
unixListener net.Listener
@@ -53,9 +53,8 @@ type Listener struct {
// ListenerOpts represents listener configuration.
type ListenerOpts struct {
Handler *handler.Handler
Metrics *connmetrics.ListenerMetrics
Logger *slog.Logger
M *middleware.Middleware
Logger *slog.Logger
TCP string // empty value disables TCP listener
Unix string // empty value disables Unix listener
@@ -74,14 +73,57 @@ type ListenerOpts struct {
TestRecordsDir string // if empty, no records are created
}
// tlsConfig provides server TLS configuration for the given certificate and key files.
// Passing caFile enables client certificate verification.
func tlsConfig(certFile, keyFile, caFile string) (*tls.Config, error) {
if _, err := os.Stat(certFile); err != nil {
return nil, fmt.Errorf("TLS certificate file: %w", err)
}
if _, err := os.Stat(keyFile); err != nil {
return nil, fmt.Errorf("TLS key file: %w", err)
}
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("TLS file pair: %w", err)
}
config := &tls.Config{
Certificates: []tls.Certificate{cert},
}
if caFile != "" {
if _, err = os.Stat(caFile); err != nil {
return nil, fmt.Errorf("TLS CA file: %w", err)
}
var b []byte
if b, err = os.ReadFile(caFile); err != nil {
return nil, err
}
ca := x509.NewCertPool()
if ok := ca.AppendCertsFromPEM(b); !ok {
return nil, fmt.Errorf("TLS CA file: failed to parse")
}
config.ClientAuth = tls.RequireAndVerifyClientCert
config.ClientCAs = ca
}
return config, nil
}
// Listen creates a new listener and starts listening on configured interfaces.
// It takes over the passed handler.
// [Listener.Run] must be called on the returned value.
func Listen(opts *ListenerOpts) (l *Listener, err error) {
ll := logging.WithName(opts.Logger, "listener")
l = &Listener{
ListenerOpts: opts,
ll: ll,
lm: NewListenerMetrics(),
listenersClosed: make(chan struct{}),
}
@@ -115,7 +157,7 @@ func Listen(opts *ListenerOpts) (l *Listener, err error) {
if l.TLS != "" {
var config *tls.Config
if config, err = tlsutil.Config(l.TLSCertFile, l.TLSKeyFile, l.TLSCAFile); err != nil {
if config, err = tlsConfig(l.TLSCertFile, l.TLSKeyFile, l.TLSCAFile); err != nil {
err = lazyerrors.Error(err)
return
}
@@ -165,15 +207,6 @@ func (l *Listener) Listening() bool {
//
// When this method returns, listener and all connections are closed, and handler is stopped.
func (l *Listener) Run(ctx context.Context) {
// inherit ctx's values
handlerCtx, handlerCancel := context.WithCancel(context.WithoutCancel(ctx))
handlerDone := make(chan struct{})
go func() {
defer close(handlerDone)
l.Handler.Run(handlerCtx)
}()
var wg sync.WaitGroup
if l.tcpListener != nil {
@@ -219,11 +252,6 @@ func (l *Listener) Run(ctx context.Context) {
l.close()
l.ll.InfoContext(ctx, "Waiting for all connections to close")
wg.Wait()
// to properly handle last client commands like endSession,
// stop handler only after the last client disconnects
handlerCancel()
<-handlerDone
}
// acceptLoop runs listener's connection accepting loop until context is canceled.
@@ -237,7 +265,7 @@ func acceptLoop(ctx context.Context, listener net.Listener, wg *sync.WaitGroup,
return
}
l.Metrics.Accepts.WithLabelValues("1").Inc()
l.lm.accepts.WithLabelValues("1").Inc()
l.ll.WarnContext(ctx, "Failed to accept connection", logging.Error(err))
if !errors.Is(err, net.ErrClosed) {
@@ -248,7 +276,7 @@ func acceptLoop(ctx context.Context, listener net.Listener, wg *sync.WaitGroup,
}
wg.Add(1)
l.Metrics.Accepts.WithLabelValues("0").Inc()
l.lm.accepts.WithLabelValues("0").Inc()
go func() {
var connErr error
@@ -260,7 +288,7 @@ func acceptLoop(ctx context.Context, listener net.Listener, wg *sync.WaitGroup,
lv = "1"
}
l.Metrics.Durations.WithLabelValues(lv).Observe(time.Since(start).Seconds())
l.lm.durations.WithLabelValues(lv).Observe(time.Since(start).Seconds())
netConn.Close()
wg.Done()
}()
@@ -277,27 +305,14 @@ func acceptLoop(ctx context.Context, listener net.Listener, wg *sync.WaitGroup,
connID := fmt.Sprintf("%s -> %s", remoteAddr, netConn.LocalAddr())
opts := &newConnOpts{
netConn: netConn,
mode: l.Mode,
l: logging.WithName(l.ll, "// "+connID+" "), // derive from the original unnamed logger
handler: l.Handler,
connMetrics: l.Metrics.ConnMetrics, // share between all conns
proxyAddr: l.ProxyAddr,
proxyTLSCertFile: l.ProxyTLSCertFile,
proxyTLSKeyFile: l.ProxyTLSKeyFile,
proxyTLSCAFile: l.ProxyTLSCAFile,
//exhaustruct:enforce
conn := &conn{
netConn: netConn,
l: logging.WithName(l.ll, "// "+connID+" "),
m: l.M,
testRecordsDir: l.TestRecordsDir,
}
conn, connErr := newConn(opts)
if connErr != nil {
l.ll.WarnContext(connCtx, "Failed to create connection", slog.String("conn", connID), logging.Error(connErr))
return
}
l.ll.InfoContext(ctx, "Connection started", slog.String("conn", connID))
connErr = conn.run(connCtx)
@@ -343,14 +358,12 @@ func (l *Listener) TLSAddr() net.Addr {
// Describe implements [prometheus.Collector].
func (l *Listener) Describe(ch chan<- *prometheus.Desc) {
l.Metrics.Describe(ch)
l.Handler.Describe(ch)
l.lm.Describe(ch)
}
// Collect implements [prometheus.Collector].
func (l *Listener) Collect(ch chan<- prometheus.Metric) {
l.Metrics.Collect(ch)
l.Handler.Collect(ch)
l.lm.Collect(ch)
}
// check interfaces
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package connmetrics
package clientconn
import (
"time"
@@ -22,21 +22,21 @@ import (
// Parts of Prometheus metric names.
const (
// TODO https://github.com/FerretDB/FerretDB/issues/3420
namespace = "ferretdb"
subsystem = "client"
)
// ListenerMetrics represents listener metrics.
type ListenerMetrics struct {
Accepts *prometheus.CounterVec
Durations *prometheus.HistogramVec
ConnMetrics *ConnMetrics
// listenerMetrics represents listener metrics.
type listenerMetrics struct {
accepts *prometheus.CounterVec
durations *prometheus.HistogramVec
}
// NewListenerMetrics creates new listener metrics.
func NewListenerMetrics() *ListenerMetrics {
lm := &ListenerMetrics{
Accepts: prometheus.NewCounterVec(
func NewListenerMetrics() *listenerMetrics {
lm := &listenerMetrics{
accepts: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
@@ -45,7 +45,7 @@ func NewListenerMetrics() *ListenerMetrics {
},
[]string{"error"},
),
Durations: prometheus.NewHistogramVec(
durations: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
@@ -64,31 +64,27 @@ func NewListenerMetrics() *ListenerMetrics {
},
[]string{"error"},
),
ConnMetrics: newConnMetrics(),
}
lm.Accepts.WithLabelValues("0")
lm.Durations.WithLabelValues("0")
lm.accepts.WithLabelValues("0")
lm.durations.WithLabelValues("0")
return lm
}
// Describe implements [prometheus.Collector].
func (lm *ListenerMetrics) Describe(ch chan<- *prometheus.Desc) {
lm.Accepts.Describe(ch)
lm.Durations.Describe(ch)
lm.ConnMetrics.Describe(ch)
func (lm *listenerMetrics) Describe(ch chan<- *prometheus.Desc) {
lm.accepts.Describe(ch)
lm.durations.Describe(ch)
}
// Collect implements [prometheus.Collector].
func (lm *ListenerMetrics) Collect(ch chan<- prometheus.Metric) {
lm.Accepts.Collect(ch)
lm.Durations.Collect(ch)
lm.ConnMetrics.Collect(ch)
func (lm *listenerMetrics) Collect(ch chan<- prometheus.Metric) {
lm.accepts.Collect(ch)
lm.durations.Collect(ch)
}
// check interfaces
var (
_ prometheus.Collector = (*ListenerMetrics)(nil)
_ prometheus.Collector = (*listenerMetrics)(nil)
)
+34
View File
@@ -14,4 +14,38 @@
package api
import _ "embed"
// Spec is the embedded OpenAPI specification.
//
//go:embed openapi.json
var Spec []byte
//go:generate ../../../bin/oapi-codegen --config=./oapi-config.yml ./openapi.json
// Response types are used to represent the responses from the API.
//
//sumtype:decl
type Response interface {
sealed()
}
func (r *AggregateResponseBody) sealed() {}
func (r *DeleteResponseBody) sealed() {}
func (r *Error) sealed() {}
func (r *FindOneResponseBody) sealed() {}
func (r *FindManyResponseBody) sealed() {}
func (r *InsertOneResponseBody) sealed() {}
func (r *InsertManyResponseBody) sealed() {}
func (r *UpdateResponseBody) sealed() {}
var (
_ Response = (*AggregateResponseBody)(nil)
_ Response = (*DeleteResponseBody)(nil)
_ Response = (*Error)(nil)
_ Response = (*FindOneResponseBody)(nil)
_ Response = (*FindManyResponseBody)(nil)
_ Response = (*InsertManyResponseBody)(nil)
_ Response = (*InsertOneResponseBody)(nil)
_ Response = (*UpdateResponseBody)(nil)
)
+12 -12
View File
@@ -2,7 +2,7 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT.
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT.
package api
import (
@@ -39,7 +39,7 @@ type AggregateRequestBody struct {
// AggregateResponseBody defines model for AggregateResponseBody.
type AggregateResponseBody struct {
// Documents An array that contains the result set of the aggregation.
Documents []map[string]interface{} `json:"documents"`
Documents json.RawMessage `json:"documents"`
}
// DeleteRequestBody defines model for DeleteRequestBody.
@@ -80,8 +80,8 @@ type Error struct {
// ErrorMissingAuthenticationParameter defines model for ErrorMissingAuthenticationParameter.
type ErrorMissingAuthenticationParameter struct {
Error *interface{} `json:"error,omitempty"`
ErrorCode *interface{} `json:"error_code,omitempty"`
Error interface{} `json:"error,omitempty"`
ErrorCode interface{} `json:"error_code,omitempty"`
// Link A link to a [log entry](https://www.mongodb.com/docs/atlas/app-services/logs/endpoint/) for the failed operation.
Link string `json:"link,omitempty"`
@@ -89,8 +89,8 @@ type ErrorMissingAuthenticationParameter struct {
// ErrorNoAuthenticationSpecified defines model for ErrorNoAuthenticationSpecified.
type ErrorNoAuthenticationSpecified struct {
Error *interface{} `json:"error,omitempty"`
ErrorCode *interface{} `json:"error_code,omitempty"`
Error interface{} `json:"error,omitempty"`
ErrorCode interface{} `json:"error_code,omitempty"`
// Link A link to a [log entry](https://www.mongodb.com/docs/atlas/app-services/logs/endpoint/) for the failed operation.
Link string `json:"link,omitempty"`
@@ -98,8 +98,8 @@ type ErrorNoAuthenticationSpecified struct {
// ErrorUserNotFound defines model for ErrorUserNotFound.
type ErrorUserNotFound struct {
Error *interface{} `json:"error,omitempty"`
ErrorCode *interface{} `json:"error_code,omitempty"`
Error interface{} `json:"error,omitempty"`
ErrorCode interface{} `json:"error_code,omitempty"`
// Link A link to a [log entry](https://www.mongodb.com/docs/atlas/app-services/logs/endpoint/) for the failed operation.
Link string `json:"link,omitempty"`
@@ -144,7 +144,7 @@ type FindManyRequestBody struct {
// FindManyResponseBody The result of a find operation.
type FindManyResponseBody struct {
// Documents A list of documents that match the specified filter.
Documents *[]map[string]interface{} `json:"documents,omitempty"`
Documents *json.RawMessage `json:"documents,omitempty"`
}
// FindOneRequestBody defines model for FindOneRequestBody.
@@ -171,7 +171,7 @@ type FindOneRequestBody struct {
// FindOneResponseBody The result of a findOne operation.
type FindOneResponseBody struct {
// Document A document that matches the specified filter. If no documents match, this is `null`.
Document *map[string]interface{} `json:"document"`
Document *json.RawMessage `json:"document"`
}
// InsertManyRequestBody defines model for InsertManyRequestBody.
@@ -195,7 +195,7 @@ type InsertManyRequestBody struct {
// InsertManyResponseBody The result of an insertMany operation.
type InsertManyResponseBody struct {
// InsertedIds A list of the `_id` values of the inserted documents.
InsertedIds *[]string `json:"insertedIds,omitempty"`
InsertedIds *[]any `json:"insertedIds,omitempty"`
}
// InsertOneRequestBody defines model for InsertOneRequestBody.
@@ -219,7 +219,7 @@ type InsertOneRequestBody struct {
// InsertOneResponseBody The result of an insertOne operation.
type InsertOneResponseBody struct {
// InsertedId The `_id` value of the inserted document.
InsertedId *interface{} `json:"insertedId,omitempty"`
InsertedId *any `json:"insertedId,omitempty"`
}
// Limit defines model for Limit.
+5 -1
View File
@@ -1263,6 +1263,7 @@
"description": "The result of a findOne operation.",
"properties": {
"document": {
"x-go-type": "json.RawMessage",
"type": "object",
"nullable": true,
"description": "A document that matches the specified filter. If no documents match, this is `null`."
@@ -1404,6 +1405,7 @@
"properties": {
"documents": {
"type": "array",
"x-go-type": "json.RawMessage",
"items": {
"type": "object"
},
@@ -1438,6 +1440,7 @@
"type": "object",
"properties": {
"insertedId": {
"x-go-type": "any",
"description": "The `_id` value of the inserted document."
}
}
@@ -1475,7 +1478,7 @@
"type": "array",
"description": "A list of the `_id` values of the inserted documents.",
"items": {
"type": "string",
"x-go-type": "any",
"description": "The `_id` value of an inserted document."
}
}
@@ -1587,6 +1590,7 @@
"documents": {
"description": "An array that contains the result set of the aggregation.",
"type": "array",
"x-go-type": "json.RawMessage",
"items": {
"type": "object",
"description": "A document included in the result set of the aggregation."
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2021 FerretDB Inc.
//
// 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.
// Package dataapi provides a Data API wrapper,
// which allows FerretDB to be used over HTTP instead of MongoDB wire protocol.
package dataapi
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"github.com/AlekSi/lazyerrors"
"github.com/prometheus/client_golang/prometheus"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/server"
"github.com/FerretDB/FerretDB/v2/internal/handler/middleware"
"github.com/FerretDB/FerretDB/v2/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
)
// Listener represents Data API TCP listener and HTTP handler.
type Listener struct {
opts *ListenOpts
lis net.Listener
h http.Handler
}
// ListenOpts represents [Listen] options.
type ListenOpts struct {
L *slog.Logger
M *middleware.Middleware
TCPAddr string
Auth bool
}
// Listen creates a new Data API handler and starts listener on the given TCP address.
// [Listener.Run] must be called on the returned value.
func Listen(opts *ListenOpts) (*Listener, error) {
lis, err := net.Listen("tcp", opts.TCPAddr)
if err != nil {
return nil, lazyerrors.Error(err)
}
s := server.New(opts.L, opts.M)
mux := http.NewServeMux()
mux.HandleFunc("GET /openapi.json", s.OpenAPISpec)
h := api.HandlerFromMux(s, mux)
if opts.Auth {
h = s.AuthMiddleware(h)
}
h = s.ConnInfoMiddleware(h)
return &Listener{
opts: opts,
lis: lis,
h: h,
}, nil
}
// Run runs Data API handler until ctx is canceled.
//
// It exits when handler is stopped and listener closed.
func (lis *Listener) Run(ctx context.Context) {
s := &http.Server{
Handler: lis.h,
ErrorLog: slog.NewLogLogger(lis.opts.L.Handler(), slog.LevelError),
BaseContext: func(net.Listener) context.Context {
return ctx
},
}
l := lis.opts.L
l.InfoContext(ctx, fmt.Sprintf("Starting Data API server on http://%s/", lis.Addr()))
l.InfoContext(ctx, fmt.Sprintf("http://%s/openapi.json - OpenAPI spec", lis.Addr()))
go func() {
if err := s.Serve(lis.lis); !errors.Is(err, http.ErrServerClosed) {
l.LogAttrs(ctx, logging.LevelDPanic, "Serve exited with unexpected error", logging.Error(err))
}
}()
<-ctx.Done()
// ctx is already canceled, but we want to inherit its values
shutdownCtx, shutdownCancel := ctxutil.WithDelay(ctx)
defer shutdownCancel(nil)
if err := s.Shutdown(shutdownCtx); err != nil {
l.LogAttrs(ctx, logging.LevelDPanic, "Shutdown exited with unexpected error", logging.Error(err))
}
if err := s.Close(); err != nil {
l.LogAttrs(ctx, logging.LevelDPanic, "Close exited with unexpected error", logging.Error(err))
}
l.InfoContext(ctx, "Data API server stopped")
}
// Addr returns TCP listener's address.
// It can be used to determine an actually used port, if it was zero.
func (lis *Listener) Addr() net.Addr {
return lis.lis.Addr()
}
// Describe implements [prometheus.Collector].
func (lis *Listener) Describe(ch chan<- *prometheus.Desc) {
}
// Collect implements [prometheus.Collector].
func (lis *Listener) Collect(ch chan<- prometheus.Metric) {
}
@@ -12,16 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package dataapi
package dataapi_test
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -29,12 +29,8 @@ import (
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"github.com/FerretDB/FerretDB/v2/internal/clientconn"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/connmetrics"
"github.com/FerretDB/FerretDB/v2/internal/documentdb"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/handler/middleware"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/setup"
"github.com/FerretDB/FerretDB/v2/internal/util/state"
"github.com/FerretDB/FerretDB/v2/internal/util/testutil"
)
@@ -61,6 +57,22 @@ func TestSmokeDataAPI(t *testing.T) {
assert.JSONEq(t, `{"documents":[]}`, string(body))
})
t.Run("FindOneEmpty", func(t *testing.T) {
jsonBody := `{
"database": "` + db + `",
"collection": "` + coll + `",
"filter": {}
}`
res, err := postJSON(t, "http://"+addr+"/action/findOne", jsonBody)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.JSONEq(t, `{"document":null}`, string(body))
})
docs := []string{
`{"_id":1,"v":"foo"}`,
`{"_id":2,"v":42}`,
@@ -80,7 +92,7 @@ func TestSmokeDataAPI(t *testing.T) {
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.JSONEq(t, `{"n":1}`, string(body))
assert.JSONEq(t, `{"insertedId":1}`, string(body))
})
t.Run("InsertMany", func(t *testing.T) {
@@ -96,7 +108,7 @@ func TestSmokeDataAPI(t *testing.T) {
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.JSONEq(t, `{"n":1}`, string(body))
assert.JSONEq(t, `{"insertedIds": [2, 3]}`, string(body))
})
t.Run("UpdateOne", func(t *testing.T) {
@@ -212,6 +224,144 @@ func TestSmokeDataAPI(t *testing.T) {
require.NoError(t, err)
assert.JSONEq(t, `{"documents":[`+docs[2]+`]}`, string(body))
})
t.Run("InsertOneEmptyId", func(t *testing.T) {
jsonBody := `{
"database": "` + db + `",
"collection": "` + coll + `",
"document": ` + `{"v":"foo"}` + `
}`
res, err := postJSON(t, "http://"+addr+"/action/insertOne", jsonBody)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
bodyStr := strings.TrimSpace(string(body))
assert.Regexp(t,
`^{"insertedId":"[0-9a-f]{24}"}$`,
bodyStr,
"expected %q, got %q",
`{"insertedId":"<ObjectID>"}`,
string(body),
)
})
t.Run("UpsertOne", func(t *testing.T) {
jsonBody := `{
"database": "` + db + `",
"collection": "` + coll + `",
"filter": {"v":"non-existent"},
"update": {"$set":{"v":"foo"}},
"upsert": true
}`
res, err := postJSON(t, "http://"+addr+"/action/updateOne", jsonBody)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
bodyStr := strings.TrimSpace(string(body))
assert.Regexp(t,
`^{"matchedCount":1,"modifiedCount":0,"upsertedId":".*[0-9a-f]{24}.*"}$`,
bodyStr,
"expected %q, got %q",
`{"upsertedId":"<ObjectID>"}`,
string(body),
)
})
t.Run("UpsertOneStringId", func(t *testing.T) {
jsonBody := `{
"database": "` + db + `",
"collection": "` + coll + `",
"filter": {"v":"non-existent"},
"update": {"$set":{"_id":"string_id","v":"foo"}},
"upsert": true
}`
res, err := postJSON(t, "http://"+addr+"/action/updateOne", jsonBody)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.NoError(t, err)
assert.JSONEq(t, `{"matchedCount":1,"modifiedCount":0,"upsertedId":"string_id"}`, string(body))
})
t.Run("UpsertMany", func(t *testing.T) {
jsonBody := `{
"database": "` + db + `",
"collection": "` + coll + `",
"filter": {"v":"non-existent"},
"update": {"$set":{"v":"foo"}},
"upsert": true
}`
res, err := postJSON(t, "http://"+addr+"/action/updateMany", jsonBody)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
bodyStr := strings.TrimSpace(string(body))
assert.Regexp(t,
`^{"matchedCount":1,"modifiedCount":0,"upsertedId":".*[0-9a-f]{24}.*"}$`,
bodyStr,
"expected %q, got %q",
`{"upsertedId":"<ObjectID>"}`,
string(body),
)
})
}
func TestOpenAPI(t *testing.T) {
t.Parallel()
addr, _ := setupDataAPI(t, false)
t.Run("Spec", func(t *testing.T) {
resp, err := http.Get("http://" + addr + "/openapi.json")
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, resp.Body.Close())
})
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
assert.Equal(t, "53813", resp.Header.Get("Content-Length"))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var spec map[string]any
err = json.Unmarshal(body, &spec)
require.NoError(t, err)
assert.Equal(t, "3.0.0", spec["openapi"])
assert.Contains(t, spec, "info")
assert.Contains(t, spec, "paths")
info := spec["info"].(map[string]any)
assert.Equal(t, "FerretDB Data API", info["title"])
})
t.Run("MethodNotAllowed", func(t *testing.T) {
resp, err := http.Post("http://"+addr+"/openapi.json", "", nil)
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, resp.Body.Close())
})
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
})
}
// postJSON sends POST request with provided JSON to data API under provided uri.
@@ -240,65 +390,55 @@ func setupDataAPI(tb testing.TB, auth bool) (addr string, dbName string) {
l := testutil.Logger(tb)
p, err := documentdb.NewPool(uri, logging.WithName(l, "pool"), sp)
require.NoError(tb, err)
handlerOpts := &handler.NewOpts{
Pool: p,
Auth: auth,
L: logging.WithName(l, "handler"),
//exhaustruct:enforce
res := setup.Setup(tb.Context(), &setup.SetupOpts{
Logger: l,
StateProvider: sp,
}
Metrics: middleware.NewMetrics(),
h, err := handler.New(handlerOpts)
require.NoError(tb, err)
PostgreSQLURL: uri,
Auth: auth,
ReplSetName: "",
SessionCleanupInterval: 0,
listenerOpts := clientconn.ListenerOpts{
Handler: h,
Metrics: connmetrics.NewListenerMetrics(),
Logger: logging.WithName(l, "listener"),
TCP: "127.0.0.1:0",
Mode: middleware.NormalMode,
}
ProxyAddr: "",
ProxyTLSCertFile: "",
ProxyTLSKeyFile: "",
ProxyTLSCAFile: "",
lis, err := clientconn.Listen(&listenerOpts)
require.NoError(tb, err)
TCPAddr: "127.0.0.1:0",
UnixAddr: "",
TLSAddr: "",
TLSCertFile: "",
TLSKeyFile: "",
TLSCAFile: "",
Mode: middleware.NormalMode,
TestRecordsDir: "",
var apiLis *Listener
apiLis, err = Listen(&ListenOpts{
TCPAddr: "127.0.0.1:0",
L: logging.WithName(l, "dataapi"),
Handler: h,
DataAPIAddr: "127.0.0.1:0",
MCPAddr: "",
})
require.NoError(tb, err)
require.NotNil(tb, res)
ctx, cancel := context.WithCancel(testutil.Ctx(tb))
var wg sync.WaitGroup
runDone := make(chan struct{})
go func() {
defer close(runDone)
res.Run(ctx)
}()
// ensure that all listener's and handler's logs are written before test ends
tb.Cleanup(func() {
cancel()
wg.Wait()
<-runDone
})
wg.Add(1)
go func() {
defer wg.Done()
lis.Run(ctx)
}()
wg.Add(1)
go func() {
defer wg.Done()
apiLis.Run(ctx)
}()
u := &url.URL{
Scheme: "mongodb",
Host: lis.TCPAddr().String(),
Host: res.WireListener.TCPAddr().String(),
Path: "/",
}
@@ -309,7 +449,7 @@ func setupDataAPI(tb testing.TB, auth bool) (addr string, dbName string) {
client, err := mongo.Connect(options.Client().ApplyURI(u.String()))
require.NoError(tb, err)
addr = apiLis.lis.Addr().String()
addr = res.DataAPIListener.Addr().String()
dbName = testutil.DatabaseName(tb)
err = client.Database(dbName).Drop(ctx)
-90
View File
@@ -1,90 +0,0 @@
// Copyright 2021 FerretDB Inc.
//
// 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.
// Package dataapi provides a Data API wrapper,
// which allows FerretDB to be used over HTTP instead of MongoDB wire protocol.
package dataapi
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/server"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
)
// Listener represents dataapi listener.
type Listener struct {
opts *ListenOpts
lis net.Listener
srv *server.Server
}
// ListenOpts represents [Listen] options.
type ListenOpts struct {
L *slog.Logger
Handler *handler.Handler
TCPAddr string
}
// Listen creates a new dataapi handler and starts listener on the given TCP address.
func Listen(opts *ListenOpts) (*Listener, error) {
lis, err := net.Listen("tcp", opts.TCPAddr)
if err != nil {
return nil, lazyerrors.Error(err)
}
return &Listener{
opts: opts,
lis: lis,
srv: server.New(opts.L, opts.Handler),
}, nil
}
// Run runs dataapi handler until ctx is canceled.
//
// It exits when handler is stopped and listener closed.
func (lis *Listener) Run(ctx context.Context) {
srvHandler := api.HandlerFromMux(lis.srv, http.NewServeMux())
if lis.opts.Handler.Auth {
srvHandler = lis.srv.AuthMiddleware(srvHandler)
}
srv := &http.Server{
Handler: lis.srv.ConnInfoMiddleware(srvHandler),
ErrorLog: slog.NewLogLogger(lis.opts.L.Handler(), slog.LevelError),
BaseContext: func(net.Listener) context.Context {
return ctx
},
}
lis.opts.L.InfoContext(ctx, fmt.Sprintf("Starting DataAPI server on http://%s/", lis.lis.Addr()))
go func() {
if err := srv.Serve(lis.lis); !errors.Is(err, http.ErrServerClosed) {
lis.opts.L.LogAttrs(ctx, logging.LevelDPanic, "Serve exited with unexpected error", logging.Error(err))
}
}()
// TODO https://github.com/FerretDB/FerretDB/issues/4848
<-ctx.Done()
}
+23 -13
View File
@@ -20,15 +20,15 @@ import (
"net/http"
"net/http/httputil"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// Aggregate implements [ServerInterface].
func (s *Server) Aggregate(w http.ResponseWriter, r *http.Request) {
func (s *Server) Aggregate(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if s.l.Enabled(ctx, slog.LevelDebug) {
@@ -37,7 +37,7 @@ func (s *Server) Aggregate(w http.ResponseWriter, r *http.Request) {
var req api.AggregateRequestBody
if err := decodeJSONRequest(r, &req); err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
@@ -48,23 +48,33 @@ func (s *Server) Aggregate(w http.ResponseWriter, r *http.Request) {
"cursor", wirebson.MustDocument(),
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
resMsg, err := s.handler.Handle(ctx, msg)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
resp := s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
resRaw := must.NotFail(resMsg.OpMsg.DocumentRaw())
cursor := must.NotFail(resRaw.Decode()).Get("cursor").(wirebson.AnyDocument)
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
cursor := resp.Document().Get("cursor").(wirebson.AnyDocument)
firstBatch := must.NotFail(cursor.Decode()).Get("firstBatch").(wirebson.AnyArray)
res := wirebson.MustDocument(
"documents", firstBatch,
)
b, err := marshalSingleJSON(firstBatch)
if err != nil {
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
s.writeJSONResponse(ctx, w, res)
res := api.AggregateResponseBody{
Documents: b,
}
s.writeJSONResponse(ctx, rw, &res)
}
+16 -13
View File
@@ -20,15 +20,15 @@ import (
"net/http"
"net/http/httputil"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// DeleteMany implements [ServerInterface].
func (s *Server) DeleteMany(w http.ResponseWriter, r *http.Request) {
func (s *Server) DeleteMany(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if s.l.Enabled(ctx, slog.LevelDebug) {
@@ -37,7 +37,7 @@ func (s *Server) DeleteMany(w http.ResponseWriter, r *http.Request) {
var req api.DeleteRequestBody
if err := decodeJSONRequest(r, &req); err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
@@ -46,7 +46,7 @@ func (s *Server) DeleteMany(w http.ResponseWriter, r *http.Request) {
"limit", float64(0),
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
@@ -56,21 +56,24 @@ func (s *Server) DeleteMany(w http.ResponseWriter, r *http.Request) {
"deletes", wirebson.MustArray(deleteDoc),
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
resMsg, err := s.handler.Handle(ctx, msg)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
resp := s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
resDoc := must.NotFail(must.NotFail(resMsg.OpMsg.DocumentRaw()).Decode())
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
res := wirebson.MustDocument(
"deletedCount", resDoc.Get("n"),
)
res := api.DeleteResponseBody{
DeletedCount: resp.Document().Get("n"),
}
s.writeJSONResponse(ctx, w, res)
s.writeJSONResponse(ctx, rw, &res)
}
+16 -13
View File
@@ -20,15 +20,15 @@ import (
"net/http"
"net/http/httputil"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// DeleteOne implements [ServerInterface].
func (s *Server) DeleteOne(w http.ResponseWriter, r *http.Request) {
func (s *Server) DeleteOne(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if s.l.Enabled(ctx, slog.LevelDebug) {
@@ -37,7 +37,7 @@ func (s *Server) DeleteOne(w http.ResponseWriter, r *http.Request) {
var req api.DeleteRequestBody
if err := decodeJSONRequest(r, &req); err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
@@ -46,7 +46,7 @@ func (s *Server) DeleteOne(w http.ResponseWriter, r *http.Request) {
"limit", float64(1),
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
@@ -56,21 +56,24 @@ func (s *Server) DeleteOne(w http.ResponseWriter, r *http.Request) {
"deletes", wirebson.MustArray(deleteDoc),
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
resMsg, err := s.handler.Handle(ctx, msg)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
resp := s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
resDoc := must.NotFail(must.NotFail(resMsg.OpMsg.DocumentRaw()).Decode())
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
res := wirebson.MustDocument(
"deletedCount", resDoc.Get("n"),
)
res := api.DeleteResponseBody{
DeletedCount: resp.Document().Get("n"),
}
s.writeJSONResponse(ctx, w, res)
s.writeJSONResponse(ctx, rw, &res)
}
+6 -5
View File
@@ -38,10 +38,11 @@ var (
// writeError encodes [api.Error] into JSON and writes it to w
// with provided HTTP status code.
func writeError(w http.ResponseWriter, err api.Error, code int) {
w.WriteHeader(code)
w.Header().Set("Content-Type", "application/json")
// TODO https://github.com/FerretDB/FerretDB/issues/4965
func writeError(rw http.ResponseWriter, err api.Error, code int) {
rw.Header().Set("Content-Type", "application/json")
// ignore error, as writing to connection may fail if the client disconnects
_ = json.NewEncoder(w).Encode(err)
rw.WriteHeader(code)
_ = json.NewEncoder(rw).Encode(err)
}
+24 -13
View File
@@ -20,15 +20,15 @@ import (
"net/http"
"net/http/httputil"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// Find implements [ServerInterface].
func (s *Server) Find(w http.ResponseWriter, r *http.Request) {
func (s *Server) Find(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if s.l.Enabled(ctx, slog.LevelDebug) {
@@ -37,7 +37,7 @@ func (s *Server) Find(w http.ResponseWriter, r *http.Request) {
var req api.FindManyRequestBody
if err := decodeJSONRequest(r, &req); err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
@@ -51,22 +51,33 @@ func (s *Server) Find(w http.ResponseWriter, r *http.Request) {
"sort", req.Sort,
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
resMsg, err := s.handler.Handle(ctx, msg)
resp := s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
cursor := resp.Document().Get("cursor").(wirebson.AnyDocument)
firstBatch := must.NotFail(cursor.Decode()).Get("firstBatch").(wirebson.AnyArray)
b, err := marshalSingleJSON(firstBatch)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
resRaw := must.NotFail(resMsg.OpMsg.DocumentRaw())
cursor := must.NotFail(resRaw.Decode()).Get("cursor").(wirebson.AnyDocument)
res := api.FindManyResponseBody{
Documents: &b,
}
res := wirebson.MustDocument(
"documents", must.NotFail(cursor.Decode()).Get("firstBatch"),
)
s.writeJSONResponse(ctx, w, res)
s.writeJSONResponse(ctx, rw, &res)
}
+27 -16
View File
@@ -20,15 +20,15 @@ import (
"net/http"
"net/http/httputil"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// FindOne implements [ServerInterface].
func (s *Server) FindOne(w http.ResponseWriter, r *http.Request) {
func (s *Server) FindOne(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if s.l.Enabled(ctx, slog.LevelDebug) {
@@ -37,7 +37,7 @@ func (s *Server) FindOne(w http.ResponseWriter, r *http.Request) {
var req api.FindOneRequestBody
if err := decodeJSONRequest(r, &req); err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
@@ -49,30 +49,41 @@ func (s *Server) FindOne(w http.ResponseWriter, r *http.Request) {
"limit", float64(1),
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
resMsg, err := s.handler.Handle(ctx, msg)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
resp := s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
resRaw := must.NotFail(resMsg.OpMsg.DocumentRaw())
cursor := must.NotFail(resRaw.Decode()).Get("cursor").(wirebson.AnyDocument)
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
cursor := resp.Document().Get("cursor").(wirebson.AnyDocument)
firstBatch := must.NotFail(cursor.Decode()).Get("firstBatch").(wirebson.AnyArray)
var doc wirebson.RawDocument
var res api.FindOneResponseBody
docs := must.NotFail(firstBatch.Decode())
if docs.Len() > 0 {
doc = must.NotFail(docs.Get(0).(wirebson.AnyDocument).Encode())
if docs.Len() == 0 {
s.writeJSONResponse(ctx, rw, &res)
return
}
res := wirebson.MustDocument(
"document", doc,
)
doc := docs.Get(0).(wirebson.AnyDocument)
s.writeJSONResponse(ctx, w, res)
b, err := marshalSingleJSON(doc)
if err != nil {
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
res.Document = &b
s.writeJSONResponse(ctx, rw, &res)
}
+62 -12
View File
@@ -20,15 +20,15 @@ import (
"net/http"
"net/http/httputil"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// InsertMany implements [ServerInterface].
func (s *Server) InsertMany(w http.ResponseWriter, r *http.Request) {
func (s *Server) InsertMany(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if s.l.Enabled(ctx, slog.LevelDebug) {
@@ -37,29 +37,79 @@ func (s *Server) InsertMany(w http.ResponseWriter, r *http.Request) {
var req api.InsertManyRequestBody
if err := decodeJSONRequest(r, &req); err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
docsArr, err := unmarshalSingleJSON(&req.Documents)
if err != nil {
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
documents, err := docsArr.(wirebson.RawArray).Decode()
if err != nil {
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
var insertedIds []any
for i, v := range documents.All() {
v, ok := v.(wirebson.AnyDocument)
if !ok {
http.Error(rw, fmt.Sprintf("document %d is not a valid BSON document", i), http.StatusBadRequest)
return
}
var doc *wirebson.Document
doc, err = ensureID(v)
if err != nil {
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
var insertedId any
insertedId, err = wirebson.ToDriver(doc.Get("_id"))
if err != nil {
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
if err = documents.Replace(i, doc); err != nil {
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
insertedIds = append(insertedIds, insertedId)
}
msg, err := prepareRequest(
"insert", req.Collection,
"$db", req.Database,
"documents", req.Documents,
"documents", documents,
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
_, err = s.handler.Handle(ctx, msg)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
resp := s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
res := wirebson.MustDocument(
"n", float64(1),
)
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
s.writeJSONResponse(ctx, w, res)
res := api.InsertManyResponseBody{
InsertedIds: &insertedIds,
}
s.writeJSONResponse(ctx, rw, &res)
}
+64 -15
View File
@@ -20,15 +20,16 @@ import (
"net/http"
"net/http/httputil"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"go.mongodb.org/mongo-driver/v2/bson"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// InsertOne implements [ServerInterface].
func (s *Server) InsertOne(w http.ResponseWriter, r *http.Request) {
func (s *Server) InsertOne(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if s.l.Enabled(ctx, slog.LevelDebug) {
@@ -37,40 +38,88 @@ func (s *Server) InsertOne(w http.ResponseWriter, r *http.Request) {
var req api.InsertOneRequestBody
if err := decodeJSONRequest(r, &req); err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
insertDoc, err := unmarshalSingleJSON(&req.Document)
insert, err := unmarshalSingleJSON(&req.Document)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
var documents any
if insertDoc != nil {
documents = wirebson.MustArray(insertDoc)
insertDoc, ok := insert.(wirebson.AnyDocument)
if !ok {
http.Error(rw, lazyerrors.New("document must be a BSON document").Error(), http.StatusInternalServerError)
return
}
var doc *wirebson.Document
doc, err = ensureID(insertDoc)
if err != nil {
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
documents := wirebson.MustArray(doc)
msg, err := prepareRequest(
"insert", req.Collection,
"$db", req.Database,
"documents", documents,
)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
_, err = s.handler.Handle(ctx, msg)
resp := s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
insertedId, err := wirebson.ToDriver(doc.Get("_id"))
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
res := wirebson.MustDocument(
"n", float64(1),
)
res := api.InsertOneResponseBody{
InsertedId: &insertedId,
}
s.writeJSONResponse(ctx, w, res)
s.writeJSONResponse(ctx, rw, &res)
}
// ensureID ensures that inserted document has an "_id" field.
func ensureID(doc wirebson.AnyDocument) (*wirebson.Document, error) {
decoded, err := doc.Decode()
if err != nil {
return nil, lazyerrors.Error(err)
}
for field := range decoded.Fields() {
if field == "_id" {
return decoded, nil
}
}
id, err := wirebson.FromDriver(bson.NewObjectID())
if err != nil {
return nil, lazyerrors.Error(err)
}
err = decoded.Add("_id", id)
if err != nil {
return nil, lazyerrors.Error(err)
}
return decoded, err
}
+31 -9
View File
@@ -15,25 +15,47 @@
package server
import (
"bytes"
"encoding/json"
"fmt"
"io"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"go.mongodb.org/mongo-driver/v2/bson"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
)
// marshalJSON encodes wirebson.RawDocument into extended JSON.
func marshalJSON(raw wirebson.RawDocument, jsonDst io.Writer) error {
encoder := bson.NewEncoder(bson.NewExtJSONValueWriter(jsonDst, false, false))
// marshalSingleJSON converts wirebson value to the standard JSON representation.
func marshalSingleJSON(v any) (json.RawMessage, error) {
var err error
if err := encoder.Encode(bson.Raw(raw)); err != nil {
return lazyerrors.Error(err)
switch vt := v.(type) {
case wirebson.RawDocument:
v, err = vt.DecodeDeep()
if err != nil {
return nil, lazyerrors.Error(err)
}
case wirebson.RawArray:
v, err = vt.DecodeDeep()
if err != nil {
return nil, lazyerrors.Error(err)
}
default:
// no need to decode other types
}
return nil
bv, err := wirebson.ToDriver(v)
if err != nil {
return nil, lazyerrors.Error(err)
}
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(bv)
if err != nil {
return nil, lazyerrors.Error(err)
}
return json.RawMessage(buf.Bytes()), nil
}
// unmarshalSingleJSON takes extended JSON object and unmarshals it into the wirebson composite.
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2021 FerretDB Inc.
//
// 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.
package server
import (
"net/http"
"strconv"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
)
// OpenAPISpec serves the OpenAPI specification.
func (s *Server) OpenAPISpec(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set("Content-Length", strconv.Itoa(len(api.Spec)))
if _, err := rw.Write(api.Spec); err != nil {
s.l.WarnContext(r.Context(), "Failed to write OpenAPI spec", logging.Error(err))
}
}
+65 -50
View File
@@ -25,52 +25,51 @@ import (
"net/http"
"strings"
"github.com/AlekSi/lazyerrors"
"github.com/FerretDB/wire/wirebson"
"github.com/xdg-go/scram"
"github.com/FerretDB/FerretDB/v2/internal/clientconn/conninfo"
"github.com/FerretDB/FerretDB/v2/internal/dataapi/api"
"github.com/FerretDB/FerretDB/v2/internal/handler"
"github.com/FerretDB/FerretDB/v2/internal/handler/middleware"
"github.com/FerretDB/FerretDB/v2/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/v2/internal/util/logging"
"github.com/FerretDB/FerretDB/v2/internal/util/must"
)
// New creates a new Server.
func New(l *slog.Logger, handler *handler.Handler) *Server {
func New(l *slog.Logger, handler *middleware.Middleware) *Server {
return &Server{
l: l,
handler: handler,
l: l,
m: handler,
}
}
// Server implements services described by OpenAPI description file.
type Server struct {
l *slog.Logger
handler *handler.Handler
l *slog.Logger
m *middleware.Middleware
}
// AuthMiddleware handles SCRAM authentication based on the username and password specified in request.
// After a successful handshake it calls the next handler.
func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
username, password, ok := r.BasicAuth()
if !ok {
writeError(w, errorNoAuthenticationSpecified, http.StatusBadRequest)
writeError(rw, errorNoAuthenticationSpecified, http.StatusBadRequest)
return
}
if password == "" || username == "" {
writeError(w, errorMissingAuthenticationParameter, http.StatusBadRequest)
writeError(rw, errorMissingAuthenticationParameter, http.StatusBadRequest)
return
}
client, err := scram.SHA256.NewClient(username, password, "")
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusBadRequest)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusBadRequest)
return
}
@@ -78,7 +77,7 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
payload, err := conv.Step("")
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusBadRequest)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusBadRequest)
return
}
@@ -91,20 +90,23 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
"$db", "admin",
))
res, err := s.handler.Handle(ctx, msg)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusUnauthorized)
resp := s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
resDoc := must.NotFail(must.NotFail(res.OpMsg.DocumentRaw()).Decode())
convID := resDoc.Get("conversationId").(int32)
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
payloadBytes := resDoc.Get("payload").(wirebson.Binary).B
convID := resp.Document().Get("conversationId").(int32)
payloadBytes := resp.Document().Get("payload").(wirebson.Binary).B
payload, err = conv.Step(string(payloadBytes))
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusUnauthorized)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusUnauthorized)
return
}
@@ -115,77 +117,90 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
"$db", "admin",
))
res, err = s.handler.Handle(ctx, msg)
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusUnauthorized)
resp = s.m.Handle(ctx, msg)
if resp == nil {
http.Error(rw, "internal error", http.StatusInternalServerError)
return
}
resDoc = must.NotFail(must.NotFail(res.OpMsg.DocumentRaw()).Decode())
if !resDoc.Get("done").(bool) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
if !resp.OK() {
s.writeJSONError(ctx, rw, resp)
return
}
payloadBytes = resDoc.Get("payload").(wirebson.Binary).B
if !resp.Document().Get("done").(bool) {
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
payloadBytes = resp.Document().Get("payload").(wirebson.Binary).B
if _, err = conv.Step(string(payloadBytes)); err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusUnauthorized)
http.Error(rw, lazyerrors.Error(err).Error(), http.StatusUnauthorized)
return
}
if !conv.Valid() {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r.WithContext(ctx))
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
// ConnInfoMiddleware returns a handler function that creates a new [*conninfo.ConnInfo],
// calls the next handler, and closes the connection info after the request is done.
func (s *Server) ConnInfoMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
connInfo := conninfo.New()
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ci := conninfo.New()
defer connInfo.Close()
defer ci.Close()
next.ServeHTTP(w, r.WithContext(conninfo.Ctx(r.Context(), connInfo)))
next.ServeHTTP(rw, r.WithContext(conninfo.Ctx(r.Context(), ci)))
})
}
// writeJSONResponse marshals provided res document into extended JSON and
// writes it to provided [http.ResponseWriter].
func (s *Server) writeJSONResponse(ctx context.Context, w http.ResponseWriter, res wirebson.AnyDocument) {
l := s.l
func (s *Server) writeJSONResponse(ctx context.Context, rw http.ResponseWriter, res api.Response) {
rw.Header().Set("Content-Type", "application/json")
resRaw, err := res.Encode()
if err != nil {
http.Error(w, lazyerrors.Error(err).Error(), http.StatusInternalServerError)
return
}
var resWriter io.Writer = rw
w.Header().Set("Content-Type", "application/json")
var resWriter io.Writer = w
if l.Enabled(ctx, slog.LevelDebug) {
if s.l.Enabled(ctx, slog.LevelDebug) {
buf := new(bytes.Buffer)
resWriter = io.MultiWriter(w, buf)
resWriter = io.MultiWriter(rw, buf)
defer func() {
// extended JSON value writer always finish with '\n' character
l.DebugContext(ctx, fmt.Sprintf("Results:\n%s", strings.TrimSpace(buf.String())))
s.l.DebugContext(ctx, fmt.Sprintf("Results:\n%s", strings.TrimSpace(buf.String())))
}()
}
if err = marshalJSON(resRaw, resWriter); err != nil {
l.ErrorContext(ctx, "marshalJSON failed", logging.Error(err))
err := json.NewEncoder(resWriter).Encode(res)
if err != nil {
s.l.ErrorContext(ctx, "marshalJSON failed", logging.Error(err))
}
}
// TODO https://github.com/FerretDB/FerretDB/issues/4965
func (s *Server) writeJSONError(ctx context.Context, rw http.ResponseWriter, resp *middleware.Response) {
doc := resp.Document()
errmsg := doc.Get("errmsg").(string)
codeName := doc.Get("codeName").(string)
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusInternalServerError)
s.writeJSONResponse(ctx, rw, &api.Error{
Error: errmsg,
ErrorCode: codeName,
})
}
// prepareDocument creates a new bson document from the given pairs of
// field names and values, which can be used as handler command msg.
//
@@ -257,7 +272,7 @@ func prepareRequest(pairs ...any) (*middleware.Request, error) {
return nil, lazyerrors.Error(err)
}
return middleware.RequestDoc(doc), nil
return middleware.RequestDoc(doc)
}
// decodeJSONRequest takes request with JSON body and decodes it into
Loaded 100 of 498 files, more files were not shown because too many files have changed in this diff. Show more