diff --git a/.gitignore b/.gitignore index 6548bda..5b491e4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ .idea /.venv /site +docker-compose-prod.yml +.netrc diff --git a/README.md b/README.md index 5614d01..d2a7641 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,66 @@ To view a dump of the LMDB contents: You can browse the snapshots in MinIO at (login with minioadmin / minioadmin). +## Using Azure Blob storage +Lightning Stream supports Azure Blob Storage as a backend for storing snapshots. You can configure it to use either static credentials (account name and key) or Azure service principal authentication. + +### Basic configuration with static credentials + +```yaml +storage: + type: azure + options: + account_name: myaccountname + account_key: myaccountkey + use_shared_key: true + container: lightningstream + create_container: true +``` + +### Configuration with Azure service principal (recommended for production) + +When `account_key` is not set, the backend uses [`DefaultAzureCredential`](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential), which automatically picks up service principal credentials from environment variables: +- `AZURE_CLIENT_ID` +- `AZURE_TENANT_ID` +- `AZURE_CLIENT_SECRET` + +```yaml +storage: + type: azure + options: + container: lightningstream + endpoint_url: https://myaccount.blob.core.windows.net/ + create_container: true +``` + +### Available options + +| Option | Type | Summary | +|--------|------|---------| +| account_name | string | Azure storage account name (required for shared key auth) | +| account_key | string | Azure storage account key | +| use_shared_key | bool | Use shared key (account name + key) authentication; if false, `DefaultAzureCredential` is used | +| container | string | Azure blob container name (required) | +| create_container | bool | Create container if it does not exist | +| endpoint_url | string | Custom endpoint URL (defaults to `https://.blob.core.windows.net/`) | +| global_prefix | string | Transparently apply a global prefix to all blob names | +| disable_send_content_md5 | bool | Disable sending the Content-MD5 header | +| tls | [tlsconfig.Config](https://github.com/PowerDNS/go-tlsconfig) | TLS configuration | +| init_timeout | duration | Time allowed for initialisation (default: "20s") | +| use_update_marker | bool | Reduce LIST commands by using an update marker (see below) | +| update_marker_force_list_interval | duration | Force full LIST sync at this interval (default: "5m") | +| concurrency | int | Max number of concurrent uploads (default: 1) | + +The `use_update_marker` option can reduce Azure costs by replacing LIST operations (which are more expensive) with GET operations. However, it cannot be used if the container itself is replicated in an active-active fashion between data centers. + +You can see a working example in the docker-compose setup, which uses [Azurite](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite) (Azure Storage Emulator): + +```bash +docker-compose up +``` + +For all available Azure backend options with full descriptions, see [Simpleblob's Azure backend Options struct](https://github.com/PowerDNS/simpleblob/blob/main/backends/azure/azure.go). ## Open Source @@ -123,5 +182,3 @@ For more information on how we provide support for Open Source products, please PowerDNS also offers an Enterprise edition of Lightning Stream that includes professional support, advanced features, deployment tooling for large deployments, Kubernetes integration, and more. - - diff --git a/cmd/lightningstream/main.go b/cmd/lightningstream/main.go index fcf01ce..36db945 100644 --- a/cmd/lightningstream/main.go +++ b/cmd/lightningstream/main.go @@ -4,6 +4,7 @@ import ( "github.com/PowerDNS/lightningstream/cmd/lightningstream/commands" // Register storage backends + _ "github.com/PowerDNS/simpleblob/backends/azure" _ "github.com/PowerDNS/simpleblob/backends/fs" _ "github.com/PowerDNS/simpleblob/backends/memory" _ "github.com/PowerDNS/simpleblob/backends/s3" diff --git a/config/config.go b/config/config.go index 91f4b59..a444347 100644 --- a/config/config.go +++ b/config/config.go @@ -302,7 +302,7 @@ type DBIOptions struct { } type Storage struct { - Type string `yaml:"type"` // "fs", "s3", "memory" + Type string `yaml:"type"` // "fs", "s3", "memory", "azure" Options map[string]interface{} `yaml:"options"` // backend specific // FIXME: Configure per LMDB instead, since we run a cleaner per LMDB? diff --git a/docker-compose.yml b/docker-compose.yml index d3693b4..506af12 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ version: '2.4' volumes: lmdb: driver: local + azurite_data: minio: driver: local #snapshots: @@ -21,13 +22,28 @@ services: command: server /data --console-address :9001 volumes: - "minio:/data" - + # https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite + azurite: + image: mcr.microsoft.com/azure-storage/azurite + container_name: azurite + hostname: azurite + restart: always + ports: + - "${DEVENV_BIND_IP:-127.0.0.1}:10000:10000" + - "${DEVENV_BIND_IP:-127.0.0.1}:10001:10001" + - "${DEVENV_BIND_IP:-127.0.0.1}:10002:10002" + command: azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --silent + volumes: + - azurite_data:/data + - ./azure/certs:/certs + environment: + - AZURITE_ACCOUNTS=devstoreaccount1:key1 auth1: image: powerdns/pdns-auth-49 environment: - instance: 1 - port: 53 - webserver_port: 8081 + - instance=1 + - port=53 + - webserver_port=8081 ports: - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}51:53/tcp" - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}51:53/udp" @@ -42,9 +58,9 @@ services: auth2: image: powerdns/pdns-auth-49 environment: - instance: 2 - port: 53 - webserver_port: 8081 + - instance=2 + - port=53 + - webserver_port=8081 ports: - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}52:53/tcp" - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}52:53/udp" @@ -59,9 +75,9 @@ services: auth3: image: powerdns/pdns-auth-49 environment: - instance: 3 - port: 53 - webserver_port: 8081 + - instance=3 + - port=53 + - webserver_port=8081 ports: - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}53:53/tcp" - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}53:53/udp" @@ -78,7 +94,7 @@ services: dockerfile: Dockerfile context: . environment: - instance: 1 # used in config file + - instance=1 # used in config file ports: - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}91:8500" volumes: @@ -94,7 +110,7 @@ services: dockerfile: Dockerfile context: . environment: - instance: 2 # used in config file + - instance=2 # used in config file ports: - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}92:8500" volumes: @@ -110,13 +126,12 @@ services: dockerfile: Dockerfile context: . environment: - instance: 3 # used in config file + - instance=3 # used in config file ports: - "${DEVENV_BIND_IP:-127.0.0.1}:${PORT_PREFIX:-47}93:8500" volumes: - "lmdb:/lmdb" - "./docker/pdns/lightningstream.yaml:/lightningstream.yaml:ro" - #- "snapshots:/snapshots" working_dir: / user: "953" # pdns command: --minimum-pid 50 receive diff --git a/docker/pdns/lightningstream.yaml b/docker/pdns/lightningstream.yaml index 4dead10..64ef6a3 100644 --- a/docker/pdns/lightningstream.yaml +++ b/docker/pdns/lightningstream.yaml @@ -26,20 +26,31 @@ sweeper: storage: #type: fs - type: s3 - options: - #root_path: /snapshots - access_key: minioadmin - secret_key: minioadmin - region: us-east-1 - bucket: lightningstream - endpoint_url: http://minio:9000 - create_bucket: true + # type: s3 + # options: + # #root_path: /snapshots + # access_key: minioadmin + # secret_key: minioadmin + # region: us-east-1 + # bucket: lightningstream + # endpoint_url: http://minio:9000 + # create_bucket: true cleanup: enabled: true interval: 15m must_keep_interval: 24h remove_old_instances_interval: 168h + type: azure + options: + account_name: devstoreaccount1 + account_key: key1 + container: lightningstreamcontainer + endpoint_url: http://azurite:10000/devstoreaccount1 + create_container: true + use_update_marker: false + use_shared_key: true + +storage_poll_interval: 5s http: address: ":8500" diff --git a/docs/commands.md b/docs/commands.md index 4134907..5342916 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -361,6 +361,7 @@ lightningstream sync [flags] ``` -h, --help help for sync + --only-db stringArray Only sync this named db (can be repeated) --only-once Only do a single run and exit --wait-for-marker-file string Marker file to wait for in storage before starting syncers ``` diff --git a/docs/configuration.md b/docs/configuration.md index 0ca6e08..7a59d96 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -61,14 +61,13 @@ in a hostname is safe. ## Storage Lightning Stream uses our [Simpleblob](https://github.com/PowerDNS/simpleblob) library to support -different storage backends. At the moment of writing, it supports S3 and local filesystem -backends. +different storage backends. At the moment of writing, it supports S3, Azure Blob Storage, and local +filesystem backends. ### S3 backend -This is currently the only backend that makes sense for a production environment. It stores -snapshots in an S3 or compatible storage. We have tested it against Amazon AWS S3 and MinIO servers. +This is one option for a production environment. It stores snapshots in an S3 or compatible storage. We have tested it against Amazon AWS S3 and MinIO servers. MinIO example for testing without TLS: @@ -109,6 +108,104 @@ You can find all the available S3 options with full descriptions in [Simpleblob's S3 backend Options struct](https://github.com/PowerDNS/simpleblob/blob/main/backends/s3/s3.go#:~:text=Options%20struct). +### Azure Blob Storage backend + +Lightning Stream supports Azure Blob Storage as another production-ready backend. + +#### Authentication + +The backend supports two authentication methods: + +**Shared key** (static credentials): set `use_shared_key: true` and provide `account_name` and +`account_key`. This is appropriate for Azurite (local emulator) and simple deployments. + +```yaml +storage: + type: azure + options: + account_name: myaccount + account_key: myaccountkey== + use_shared_key: true + container: lightningstream + create_container: true +``` + +**DefaultAzureCredential** (recommended for production): omit `use_shared_key` (or set it to +`false`) and do not set `account_key`. The Azure SDK will automatically try, in order: environment +variables, workload identity, managed identity, Azure CLI, and other ambient credentials. + +To use a service principal, set these environment variables before starting Lightning Stream: + +``` +AZURE_CLIENT_ID= +AZURE_TENANT_ID= +AZURE_CLIENT_SECRET= +``` + +```yaml +storage: + type: azure + options: + container: lightningstream + endpoint_url: https://myaccount.blob.core.windows.net/ + create_container: true +``` + +!!! warning + + `DefaultAzureCredential` requires an HTTPS endpoint. It will refuse to authenticate over plain + HTTP. Use `use_shared_key: true` whenever your endpoint is HTTP (e.g. Azurite without TLS). + +#### Local testing with Azurite + +[Azurite](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite) is +Microsoft's open-source Azure Storage emulator. A working example is included in the +`docker-compose.yml` in this repository. + +```yaml +storage: + type: azure + options: + account_name: devstoreaccount1 + account_key: + use_shared_key: true + container: lightningstream + endpoint_url: http://azurite:10000/devstoreaccount1 + create_container: true +``` + +#### Available options + +| Option | Type | Summary | +|--------|------|---------| +| account_name | string | Azure storage account name (required for shared key auth) | +| account_key | string | Azure storage account key (required for shared key auth) | +| use_shared_key | bool | Use shared key authentication; if false, `DefaultAzureCredential` is used | +| container | string | Azure blob container name (required) | +| create_container | bool | Create the container if it does not exist | +| endpoint_url | string | Custom endpoint URL (defaults to `https://.blob.core.windows.net`) | +| global_prefix | string | Transparently apply a global prefix to all blob names | +| disable_send_content_md5 | bool | Disable sending the Content-MD5 header | +| tls | [tlsconfig.Config](https://github.com/PowerDNS/go-tlsconfig) | TLS configuration | +| init_timeout | duration | Time allowed for initialisation (default: "20s") | +| use_update_marker | bool | Reduce LIST operations using an update marker blob (see below) | +| update_marker_force_list_interval | duration | Force a full LIST after this interval (default: "5m") | +| concurrency | int | Max concurrent block uploads per Store call (default: 1) | + +The `use_update_marker` option can significantly reduce Azure Storage costs. GET operations are +approximately 12x cheaper than LIST on Azure. When enabled, Lightning Stream writes a small marker +blob on every store or delete, and uses it to skip LIST calls when nothing has changed. + +!!! warning + + `use_update_marker` must be enabled or disabled consistently across **all** instances sharing + the same container. It also cannot be used reliably when the container itself is replicated + in an active-active fashion between data centres. + +You can find all available options with full descriptions in +[Simpleblob's Azure backend Options struct](https://github.com/PowerDNS/simpleblob/blob/main/backends/azure/azure.go). + + ### Filesystem backend For local testing, it can be convenient to store all snapshots in a local directory instead of @@ -375,6 +472,72 @@ lmdbs: # records: # override_create_flags: 0 + +# Sweeper settings for the LMDB sweeper that removed deleted entries after +# a while, also known as the "tomb sweeper". +# +# The key consideration for these settings is how long instance can be +# expected to be disconnected from the storage (out of sync) before +# rejoining. If the retention interval is set too low, old records that +# have been removed during the downtime can reappear, which can cause +# major issues. +# +# When picking a value, also take into account development, testing and +# migration systems that only occasionally come online. +# +sweeper: + # Enabled controls if the sweeper is enabled. + # It is DISABLED by default, because of the important consistency + # considerations that depend on the kind of deployment. + # When disabled, the deleted entries will never actually be removed. + # Stats are only available when the sweeper is enabled. + #enabled: false + + # RetentionDays is the number of DAYS of retention. Unlike in most + # other places, this is specified in number of days instead of Duration + # because of the expected length of this. + # This is a float, so it is possible to use periods shorter than one day, + # but this is rarely a good idea. Best to set this as high as possible. + # Default: 370 (days, intentionally on the safe side) + #retention_days: 370 + + # Interval is the interval between sweeps of the whole database to enforce + # RetentionDays. + # As a guideline, on a fast server sweeping 1 million records takes + # about 1 second. + # Default: 6h + #interval: 6h + + # FirstInterval is the first Interval immediately after + # startup, to allow one soon after extended downtime. + # Default: 10m + #first_interval: 10m + + # LockDuration limits how long the sweeper may hold the exclusive write + # lock at one time. This effectively controls the maximum latency spike + # due to the sweeper for API calls that update the LMDB. + # This is not a hard quota, the sweeper may overrun it slightly. + # Default: 50ms + #lock_duration: 50ms + + # ReleaseDuration determines how long the sweeper must sleep before it + # is allowed to reacquire the exclusive write lock. + # If this is equal to LockDuration, it means that the sweeper can hold the + # LMDB at most half the time. + # Do not set this too high, as every sweep cycle will record a write + # transaction that can trigger a snapshot generation scan. It is best + # to get it over with in a short total sweep time. + # Default: 50ms + #release_duration: 50ms + + # RetentionLoadCutoffDuration is the time interval close to the RetentionDays + # limit where we will not load deletion markers from remote snapshots, + # because they would soon be eligible for removal by the sweeper anyway. + # Only set this if you understand the implications. + # Default: 1% of the duration corresponding to the retention_days setting. + #retention_load_cutoff_duration: 0 + + # Storage configures where LS stores its snapshots storage: # For the available backend types and options, please diff --git a/docs/configuration.template.md b/docs/configuration.template.md index 76a5ba3..af89ba8 100644 --- a/docs/configuration.template.md +++ b/docs/configuration.template.md @@ -56,8 +56,8 @@ in a hostname is safe. ## Storage Lightning Stream uses our [Simpleblob](https://github.com/PowerDNS/simpleblob) library to support -different storage backends. At the moment of writing, it supports S3 and local filesystem -backends. +different storage backends. At the moment of writing, it supports S3, Azure Blob Storage, and local +filesystem backends. ### S3 backend @@ -104,6 +104,104 @@ You can find all the available S3 options with full descriptions in [Simpleblob's S3 backend Options struct](https://github.com/PowerDNS/simpleblob/blob/main/backends/s3/s3.go#:~:text=Options%20struct). +### Azure Blob Storage backend + +Lightning Stream supports Azure Blob Storage as a production-ready backend. + +#### Authentication + +The backend supports two authentication methods: + +**Shared key** (static credentials): set `use_shared_key: true` and provide `account_name` and +`account_key`. This is appropriate for Azurite (local emulator) and simple deployments. + +```yaml +storage: + type: azure + options: + account_name: myaccount + account_key: myaccountkey== + use_shared_key: true + container: lightningstream + create_container: true +``` + +**DefaultAzureCredential** (recommended for production): omit `use_shared_key` (or set it to +`false`) and do not set `account_key`. The Azure SDK will automatically try, in order: environment +variables, workload identity, managed identity, Azure CLI, and other ambient credentials. + +To use a service principal, set these environment variables before starting Lightning Stream: + +``` +AZURE_CLIENT_ID= +AZURE_TENANT_ID= +AZURE_CLIENT_SECRET= +``` + +```yaml +storage: + type: azure + options: + container: lightningstream + endpoint_url: https://myaccount.blob.core.windows.net/ + create_container: true +``` + +!!! warning + + `DefaultAzureCredential` requires an HTTPS endpoint. It will refuse to authenticate over plain + HTTP. Use `use_shared_key: true` whenever your endpoint is HTTP (e.g. Azurite without TLS). + +#### Local testing with Azurite + +[Azurite](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite) is +Microsoft's open-source Azure Storage emulator. A working example is included in the +`docker-compose.yml` in this repository. + +```yaml +storage: + type: azure + options: + account_name: devstoreaccount1 + account_key: + use_shared_key: true + container: lightningstream + endpoint_url: http://azurite:10000/devstoreaccount1 + create_container: true +``` + +#### Available options + +| Option | Type | Summary | +|--------|------|---------| +| account_name | string | Azure storage account name (required for shared key auth) | +| account_key | string | Azure storage account key (required for shared key auth) | +| use_shared_key | bool | Use shared key authentication; if false, `DefaultAzureCredential` is used | +| container | string | Azure blob container name (required) | +| create_container | bool | Create the container if it does not exist | +| endpoint_url | string | Custom endpoint URL (defaults to `https://.blob.core.windows.net`) | +| global_prefix | string | Transparently apply a global prefix to all blob names | +| disable_send_content_md5 | bool | Disable sending the Content-MD5 header | +| tls | [tlsconfig.Config](https://github.com/PowerDNS/go-tlsconfig) | TLS configuration | +| init_timeout | duration | Time allowed for initialisation (default: "20s") | +| use_update_marker | bool | Reduce LIST operations using an update marker blob (see below) | +| update_marker_force_list_interval | duration | Force a full LIST after this interval (default: "5m") | +| concurrency | int | Max concurrent block uploads per Store call (default: 1) | + +The `use_update_marker` option can significantly reduce Azure Storage costs. GET operations are +approximately 12x cheaper than LIST on Azure. When enabled, Lightning Stream writes a small marker +blob on every store or delete, and uses it to skip LIST calls when nothing has changed. + +!!! warning + + `use_update_marker` must be enabled or disabled consistently across **all** instances sharing + the same container. It also cannot be used reliably when the container itself is replicated + in an active-active fashion between data centres. + +You can find all available options with full descriptions in +[Simpleblob's Azure backend Options struct](https://github.com/PowerDNS/simpleblob/blob/main/backends/azure/azure.go). + + ### Filesystem backend For local testing, it can be convenient to store all snapshots in a local directory instead of diff --git a/go.mod b/go.mod index 22d9ebe..6227d24 100644 --- a/go.mod +++ b/go.mod @@ -2,23 +2,25 @@ module github.com/PowerDNS/lightningstream go 1.24.0 +toolchain go1.24.5 + require ( github.com/CrowdStrike/csproto v0.35.0 github.com/PowerDNS/lmdb-go v1.9.3 - github.com/PowerDNS/simpleblob v0.3.2 + github.com/PowerDNS/simpleblob v0.4.0 github.com/bufbuild/buf v1.61.0 github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 github.com/gogo/protobuf v1.3.2 github.com/klauspost/compress v1.18.2 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 - github.com/samber/lo v1.52.0 - github.com/sirupsen/logrus v1.9.3 + github.com/samber/lo v1.37.0 + github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 github.com/wojas/go-healthz v0.2.0 - go.uber.org/atomic v1.11.0 - golang.org/x/sync v0.18.0 + go.uber.org/atomic v1.10.0 + golang.org/x/sync v0.19.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -37,10 +39,15 @@ require ( buf.build/go/protoyaml v0.6.0 // indirect buf.build/go/spdx v0.2.0 // indirect buf.build/go/standard v0.1.0 // indirect - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect connectrpc.com/connect v1.19.1 // indirect connectrpc.com/otelconnect v0.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/PowerDNS/go-tlsconfig v0.0.0-20221101135152-0956853b28df // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect @@ -67,21 +74,24 @@ require ( github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/goccy/go-json v0.10.5 // indirect github.com/gofrs/flock v0.13.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/cel-go v0.26.1 // indirect github.com/google/go-containerregistry v0.20.6 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jdx/go-netrc v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/minio/crc64nvme v1.0.2 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minio-go/v7 v7.0.95 // indirect + github.com/minio/minio-go/v7 v7.0.98 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.2 // indirect @@ -91,9 +101,10 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect github.com/philhofer/fwd v1.2.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.56.0 // indirect @@ -107,7 +118,7 @@ require ( github.com/stoewer/go-strcase v1.3.1 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/tidwall/btree v1.8.1 // indirect - github.com/tinylib/msgp v1.3.0 // indirect + github.com/tinylib/msgp v1.6.1 // indirect github.com/vbatts/tar-split v0.12.1 // indirect go.lsp.dev/jsonrpc2 v0.10.0 // indirect go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect @@ -115,20 +126,24 @@ require ( go.lsp.dev/uri v0.3.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.45.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect + google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect pluginrpc.com/pluginrpc v0.5.0 // indirect diff --git a/go.sum b/go.sum index 8b7c8f1..788d2bf 100644 --- a/go.sum +++ b/go.sum @@ -26,18 +26,34 @@ buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= buf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U= buf.build/go/standard v0.1.0/go.mod h1:PiqpHz/7ZFq+kqvYhc/SK3lxFIB9N/aiH2CFC2JHIQg= -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= connectrpc.com/otelconnect v0.8.0 h1:a4qrN4H8aEE2jAoCxheZYYfEjXMgVPyL9OzPQLBEFXU= connectrpc.com/otelconnect v0.8.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -46,8 +62,8 @@ github.com/PowerDNS/go-tlsconfig v0.0.0-20221101135152-0956853b28df h1:WMUClevRP github.com/PowerDNS/go-tlsconfig v0.0.0-20221101135152-0956853b28df/go.mod h1:aKP0MVHWl7U3ruSXqAmzsoVVFm8HhYIpOmm1MwkDyDY= github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg= github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU= -github.com/PowerDNS/simpleblob v0.3.2 h1:dIct2RFl9lwcgmB90cbJ8KCbhibLdK+LZ7Ci7sqXbKk= -github.com/PowerDNS/simpleblob v0.3.2/go.mod h1:Bcnf5Vpr2J36fUKzet4BKKOCm2Zvp4tsuC5UR3GCNes= +github.com/PowerDNS/simpleblob v0.4.0 h1:rUxKElgqSY1v7knEbASAoBNLZDkzFFagrJbXWCDtmdM= +github.com/PowerDNS/simpleblob v0.4.0/go.mod h1:oqFdmcwTiO9nkmxfyQqOhXYEct4XI6CwM3K8p8/kuZ4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= @@ -135,12 +151,12 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -173,16 +189,17 @@ github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8= github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= @@ -190,6 +207,8 @@ github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxh github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -206,12 +225,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg= -github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= -github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= +github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0= +github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -242,6 +261,8 @@ github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGg github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -253,8 +274,8 @@ github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4= @@ -276,16 +297,16 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= +github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= -github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= -github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= @@ -298,22 +319,23 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= -github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= -github.com/testcontainers/testcontainers-go/modules/minio v0.38.0 h1:iBxk0f9YEVZkC0CoiI8UsHg+zC9eWQudng7nBFkVkzU= -github.com/testcontainers/testcontainers-go/modules/minio v0.38.0/go.mod h1:LAxD0g8YUvs08zyLlEzpD81lTJSyADAYsEGPlEI6diY= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/azure v0.40.0 h1:a4Qn4UEgL3uzpY1Hhuzh2c87u/CuSoTaV12timQfHQU= +github.com/testcontainers/testcontainers-go/modules/azure v0.40.0/go.mod h1:047cjSoIxghqTQt8OVeLwLO918jOTrRnKYSEG5L6paQ= +github.com/testcontainers/testcontainers-go/modules/minio v0.40.0 h1:M+Ib1mIXq/hEcH8tyEvBnOZ7NJi03zY+P1gYO5GGp6o= +github.com/testcontainers/testcontainers-go/modules/minio v0.40.0/go.mod h1:ON0MxxS/pME0SJOKLImw/D9R1L7apYsxIZrM/uEqORA= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= -github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= -github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -340,25 +362,25 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= 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.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -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/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= @@ -367,13 +389,15 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +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= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= @@ -382,8 +406,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -395,8 +419,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -405,8 +429,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -416,18 +440,18 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -438,8 +462,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -459,8 +483,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/syncer/metrics.go b/syncer/metrics.go index 8f13eac..2dc1d41 100644 --- a/syncer/metrics.go +++ b/syncer/metrics.go @@ -22,6 +22,13 @@ var ( }, []string{"lmdb"}, ) + metricSnapshotsLastAge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "lightningstream_syncer_last_snapshot_age_in_seconds", + Help: "Age of last generated snapshot in seconds", + }, + []string{"lmdb"}, + ) metricSnapshotsLastSize = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "lightningstream_syncer_snapshots_generated_last_size_bytes", @@ -55,6 +62,13 @@ var ( Help: "Number of bytes stored successfully", }, ) + metricSnapshotsTimeStamp = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "lightningstream_syncer_snapshots_last_sent_unix_seconds", + Help: "UNIX timestamp of last snapshot sent by this instance", + }, + []string{"lmdb", "syncer_instance"}, + ) ) func init() { @@ -63,9 +77,11 @@ func init() { prometheus.MustRegister(metricSnapshotsLoaded) prometheus.MustRegister(metricSnapshotsLastTimestamp) + prometheus.MustRegister(metricSnapshotsLastAge) prometheus.MustRegister(metricSnapshotsLastSize) prometheus.MustRegister(metricSnapshotsStoreFailed) prometheus.MustRegister(metricSnapshotsStoreFailedPermanently) prometheus.MustRegister(metricSnapshotsStoreCalls) prometheus.MustRegister(metricSnapshotsStoreBytes) + prometheus.MustRegister(metricSnapshotsTimeStamp) } diff --git a/syncer/receiver/downloader.go b/syncer/receiver/downloader.go index 16646bb..42dbae6 100644 --- a/syncer/receiver/downloader.go +++ b/syncer/receiver/downloader.go @@ -102,6 +102,12 @@ func (d *Downloader) LoadOnce(ctx context.Context, ni snapshot.NameInfo) error { return err } + // Store the size of the snapshot in bytes + metricSnapshotsStorageBytes.WithLabelValues(d.r.lmdbname).Set(float64(len(data))) + + metricSnapshotsTimestampString.WithLabelValues(d.r.lmdbname, d.instance).Set(float64(ni.Timestamp.UnixNano()) / 1e9) + d.l.Debugf("GenerationID of downloaded snapshot: %s", ni.GenerationID) + // Signal success to health tracker d.r.storageLoadHealth.AddSuccess() @@ -162,12 +168,24 @@ func (d *Downloader) LoadOnce(ctx context.Context, ni snapshot.NameInfo) error { t2 := time.Now() d.l.WithFields(logrus.Fields{ - "timestamp": ni.TimestampString, - //"generation": ni.GenerationID, + "timestamp": ni.TimestampString, + "generation": ni.GenerationID, "shorthash": ni.ShortHash(), "time_load_storage": utils.TimeDiff(t1, t0), "time_load_total": utils.TimeDiff(t2, t0), }).Info("Snapshot downloaded") + // Emit seconds since last snapshot was received as a metric. + // Datadog doesn't work well with UNIX timestamps, so we use seconds + snapshotAge := time.Since(ni.Timestamp).Seconds() + metricSnapshotsLastDownloadedSeconds.WithLabelValues(d.r.lmdbname, d.instance).Set(snapshotAge) + + // Log time taken to download the snapshot from storage + metricSnapshotsTimeToDownloadFromStorage.WithLabelValues(d.r.lmdbname, d.instance). + Set(utils.TimeDiff(t1, t0).Seconds()) + + metricSnapshotsTimeToDownloadTotal.WithLabelValues(d.r.lmdbname, d.instance). + Set(utils.TimeDiff(t2, t0).Seconds()) + return nil } diff --git a/syncer/receiver/metrics.go b/syncer/receiver/metrics.go index 1a13668..e608318 100644 --- a/syncer/receiver/metrics.go +++ b/syncer/receiver/metrics.go @@ -11,6 +11,40 @@ const ( ) var ( + // called when we receive a new snapshot and download it to the instance + metricSnapshotsLastReceivedSeconds = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "lightningstream_receiver_snapshots_last_received_seconds_diff", + Help: "Seconds since last received snapshot by instance", + }, + []string{"lmdb", "syncer_instance"}, + ) + + // Called when download a snapshot from storage. This is called all the time, since it's in the syncLoop + metricSnapshotsLastDownloadedSeconds = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "lightningstream_receiver_snapshots_last_downloaded_seconds_diff", + Help: "Seconds since last downloaded snapshot by instance", + }, + []string{"lmdb", "syncer_instance"}, + ) + + metricSnapshotsTimeToDownloadFromStorage = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "lightningstream_receiver_snapshots_time_to_download_from_storage_seconds", + Help: "Time taken to download snapshot from storage by instance", + }, + []string{"lmdb", "syncer_instance"}, + ) + + metricSnapshotsTimeToDownloadTotal = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "lightningstream_receiver_snapshots_time_to_download_total_seconds", + Help: "Total time taken to download snapshots by instance", + }, + []string{"lmdb", "syncer_instance"}, + ) + metricSnapshotsLastReceivedTimestamp = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "lightningstream_receiver_snapshots_last_received_seconds", @@ -77,10 +111,22 @@ var ( }, []string{"lmdb"}, ) + + metricSnapshotsTimestampString = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "lightningstream_receiver_snapshots_last_downloaded_unix_seconds", + Help: "UNIX timestamp of the last downloaded snapshot by instance", + }, + []string{"lmdb", "syncer_instance"}, + ) ) func init() { prometheus.MustRegister(metricSnapshotsLastReceivedTimestamp) + prometheus.MustRegister(metricSnapshotsLastReceivedSeconds) + prometheus.MustRegister(metricSnapshotsLastDownloadedSeconds) + prometheus.MustRegister(metricSnapshotsTimeToDownloadFromStorage) + prometheus.MustRegister(metricSnapshotsTimeToDownloadTotal) prometheus.MustRegister(metricSnapshotsLastReceivedAge) prometheus.MustRegister(metricSnapshotsLoadCalls) prometheus.MustRegister(metricSnapshotsListCalls) @@ -89,4 +135,5 @@ func init() { prometheus.MustRegister(metricSnapshotsLoadBytes) prometheus.MustRegister(metricSnapshotsStorageCount) prometheus.MustRegister(metricSnapshotsStorageBytes) + prometheus.MustRegister(metricSnapshotsTimestampString) } diff --git a/syncer/receiver/receiver.go b/syncer/receiver/receiver.go index f53d7a3..11acc19 100644 --- a/syncer/receiver/receiver.go +++ b/syncer/receiver/receiver.go @@ -176,7 +176,7 @@ func (r *Receiver) Run(ctx context.Context) error { } func (r *Receiver) RunOnce(ctx context.Context, includingOwn bool) error { - //r.l.Debug("RunOnce") + r.l.Debug("RunOnce called") st := r.st prefix := r.prefix @@ -274,6 +274,12 @@ func (r *Receiver) RunOnce(ctx context.Context, includingOwn bool) error { metricSnapshotsLastReceivedTimestamp.WithLabelValues(r.lmdbname, inst). Set(float64(ni.Timestamp.UnixNano()) / 1e9) + + // Emit seconds since last snapshot was received as a metric. + // Datadog doesn't work well with UNIX timestamps, so we use seconds + snapshotAge := time.Since(ni.Timestamp).Seconds() + metricSnapshotsLastReceivedSeconds.WithLabelValues(r.lmdbname, inst).Set(snapshotAge) + metricSnapshotsLastReceivedAge.WithLabelValues(r.lmdbname, inst). Observe(float64(age) / float64(time.Second)) diff --git a/syncer/send.go b/syncer/send.go index be41c36..84757d2 100644 --- a/syncer/send.go +++ b/syncer/send.go @@ -182,12 +182,20 @@ func (s *Syncer) SendOnce(ctx context.Context, env *lmdb.Env) (txnID header.TxnI } tDumpedData := time.Now() + // Used in logging + generationID := string(msg.Meta.GenerationID) + nameTimeStamp := snapshot.NameTimestamp(ts) + meta := msg.Meta // keep a copy of metadata for events msg = nil // no longer needed timeGC := utils.GC() + unixTimestamp := float64(ts.UnixNano()) / 1e9 + metricSnapshotsLoaded.WithLabelValues(s.name).Inc() - metricSnapshotsLastTimestamp.WithLabelValues(s.name).Set(float64(ts.UnixNano()) / 1e9) + metricSnapshotsLastTimestamp.WithLabelValues(s.name).Set(unixTimestamp) + // Log the time in seconds since the snapshot was created + metricSnapshotsLastAge.WithLabelValues(s.name).Set(time.Since(ts).Seconds()) metricSnapshotsLastSize.WithLabelValues(s.name).Set(float64(len(out))) // Send it to storage @@ -206,7 +214,10 @@ func (s *Syncer) SendOnce(ctx context.Context, env *lmdb.Env) (txnID header.TxnI } continue } - s.l.Debug("Store succeeded") + + metricSnapshotsTimeStamp.WithLabelValues(s.name, s.instanceID()).Set(unixTimestamp) + + s.l.Debugf("Store succeeded with GenerationID %s", generationID) metricSnapshotsStoreBytes.Add(float64(len(out))) // UpdateStored hook and event @@ -241,7 +252,6 @@ func (s *Syncer) SendOnce(ctx context.Context, env *lmdb.Env) (txnID header.TxnI } s.l.WithFields(logrus.Fields{ - "time_acquire": utils.TimeDiff(tTxnAcquire, t0), "time_copy_shadow": tShadow.Sub(tTxnAcquire).Round(time.Millisecond), "time_dump": tDumped.Sub(tShadow).Round(time.Millisecond), "time_compress": dds.TCompressed.Round(time.Millisecond), @@ -254,6 +264,11 @@ func (s *Syncer) SendOnce(ctx context.Context, env *lmdb.Env) (txnID header.TxnI "snapshot_name": name, "snapshot_kind": ni.Kind, "txnID": txnID, + "timeStamp": nameTimeStamp, + "generationID": generationID, + "instanceID": s.instanceID(), + "unix_timestamp": unixTimestamp, + "time_acquire": utils.TimeDiff(tTxnAcquire, t0), }).Info("Stored snapshot") if ni.Kind == snapshot.KindSnapshot { diff --git a/syncer/send_test.go b/syncer/send_test.go index 401f4c4..58a56a8 100644 --- a/syncer/send_test.go +++ b/syncer/send_test.go @@ -9,12 +9,72 @@ import ( "github.com/PowerDNS/lightningstream/config" "github.com/PowerDNS/lightningstream/lmdbenv" "github.com/PowerDNS/lightningstream/lmdbenv/header" + "github.com/PowerDNS/lightningstream/snapshot" "github.com/PowerDNS/lmdb-go/lmdb" "github.com/PowerDNS/simpleblob/backends/memory" "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// TestSyncer_SendOnce_GenerationIDStable verifies that the GenerationID embedded +// in snapshot filenames does not change across multiple SendOnce calls on the +// same Syncer instance. It must stay stable for the lifetime of the process so +// that peers can identify which syncer produced each snapshot. +func TestSyncer_SendOnce_GenerationIDStable(t *testing.T) { + l, _ := test.NewNullLogger() + lc := config.LMDB{SchemaTracksChanges: true} + now := time.Now() + + val := make([]byte, header.MinHeaderSize, 50) + header.PutBasic(val, header.TimestampFromTime(now), 42, header.NoFlags) + val = append(val, "test-value"...) + + st := memory.New() + + err := lmdbenv.TestEnv(func(env *lmdb.Env) error { + s, err := New("test", env, st, config.Config{StorageRetryCount: 1}, lc, Options{}) + require.NoError(t, err) + s.l = l + + ctx := context.Background() + + // Write distinct data between sends so each SendOnce produces a new snapshot. + for i := 0; i < 3; i++ { + err = env.Update(func(txn *lmdb.Txn) error { + dbi, err := txn.OpenDBI("foo", lmdb.Create) + require.NoError(t, err) + return txn.Put(dbi, []byte(fmt.Sprintf("key-%d", i)), val, 0) + }) + require.NoError(t, err) + + _, err = s.SendOnce(ctx, env) + require.NoError(t, err) + } + return nil + }) + require.NoError(t, err) + + blobs, err := st.List(context.Background(), "") + require.NoError(t, err) + + var generationIDs []string + for _, name := range blobs.Names() { + ni, err := snapshot.ParseName(name) + if err != nil { + continue // skip non-snapshot files + } + generationIDs = append(generationIDs, string(ni.GenerationID)) + } + + require.NotEmpty(t, generationIDs, "expected at least one snapshot to be stored") + + first := generationIDs[0] + for i, gid := range generationIDs[1:] { + assert.Equal(t, first, gid, "snapshot %d has a different GenerationID", i+1) + } +} + func BenchmarkSyncer_SendOnce_native_100k(b *testing.B) { doBenchmarkSyncerSendOnce(b, true, false) } diff --git a/syncer/sync.go b/syncer/sync.go index b3c85ba..350d20f 100644 --- a/syncer/sync.go +++ b/syncer/sync.go @@ -46,6 +46,7 @@ func (s *Syncer) Sync(ctx context.Context) error { s.hooks, ) + s.l.Debug("Starting sync loop") return s.syncLoop(ctx, env, r) } diff --git a/syncer/syncer.go b/syncer/syncer.go index d36e190..49e6033 100644 --- a/syncer/syncer.go +++ b/syncer/syncer.go @@ -46,6 +46,7 @@ func New(name string, env *lmdb.Env, st simpleblob.Interface, c config.Config, l lc: lc, opt: opt, shadow: true, + generation: uint64(time.Now().UnixNano()), // Generate unique ID based on current time env: env, events: ev, hooks: h, @@ -70,17 +71,17 @@ func New(name string, env *lmdb.Env, st simpleblob.Interface, c config.Config, l } type Syncer struct { - name string // database name - st simpleblob.Interface - c config.Config - lc config.LMDB - opt Options - l logrus.FieldLogger - shadow bool // use shadow database for timestamps? - env *lmdb.Env - events *events.Events - hooks *hooks.Hooks - + name string // database name + st simpleblob.Interface + c config.Config + lc config.LMDB + opt Options + l logrus.FieldLogger + shadow bool // use shadow database for timestamps? + env *lmdb.Env + events *events.Events + hooks *hooks.Hooks + generation uint64 // lastByInstance tracks the last snapshot loaded by instance, so that the // cleaner can make safe decisions about when to remove stale snapshots. lastByInstance map[string]time.Time diff --git a/syncer/utils.go b/syncer/utils.go index ca93836..9d7fdfc 100644 --- a/syncer/utils.go +++ b/syncer/utils.go @@ -83,7 +83,8 @@ func (s *Syncer) instanceID() string { // filenames, so we just return the minimum requirement (start with 'G', // followed by a value 'X'). func (s *Syncer) generationID() string { - return "GX" + out := fmt.Sprintf("G-%016x", s.generation) + return out } // readDBI reads a DBI into a snapshot DBI. diff --git a/syncer/utils_test.go b/syncer/utils_test.go new file mode 100644 index 0000000..f95dc26 --- /dev/null +++ b/syncer/utils_test.go @@ -0,0 +1,61 @@ +package syncer + +import ( + "regexp" + "testing" + + "github.com/PowerDNS/lightningstream/config" + "github.com/PowerDNS/lightningstream/lmdbenv" + "github.com/PowerDNS/lmdb-go/lmdb" + "github.com/PowerDNS/simpleblob/backends/memory" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var reGenerationID = regexp.MustCompile(`^G-[0-9a-f]{16}$`) + +// TestGenerationID_Format verifies that generationID returns a string matching +// the expected format: "G-" followed by exactly 16 lowercase hex digits. +// The format is part of the snapshot filename spec and must not regress to the +// old hardcoded "GX" value. +func TestGenerationID_Format(t *testing.T) { + l, _ := test.NewNullLogger() + err := lmdbenv.TestEnv(func(env *lmdb.Env) error { + s, err := New("test", env, memory.New(), config.Config{}, config.LMDB{}, Options{}) + require.NoError(t, err) + s.l = l + + gid := s.generationID() + assert.Regexp(t, reGenerationID, gid, + "generationID must match G-<16 hex digits>, got %q", gid) + return nil + }) + require.NoError(t, err) +} + +// TestGenerationID_UniquePerInstance verifies that two independently created +// Syncer instances receive different generation IDs. The ID is seeded from +// time.Now().UnixNano() at construction, so two instances created in the same +// process should never collide. +func TestGenerationID_UniquePerInstance(t *testing.T) { + l, _ := test.NewNullLogger() + var gid1, gid2 string + + err := lmdbenv.TestEnv(func(env *lmdb.Env) error { + s1, err := New("test", env, memory.New(), config.Config{}, config.LMDB{}, Options{}) + require.NoError(t, err) + s1.l = l + gid1 = s1.generationID() + + s2, err := New("test", env, memory.New(), config.Config{}, config.LMDB{}, Options{}) + require.NoError(t, err) + s2.l = l + gid2 = s2.generationID() + return nil + }) + require.NoError(t, err) + + assert.NotEqual(t, gid1, gid2, + "two Syncer instances must have different generation IDs") +} diff --git a/test-in-docker.sh b/test-in-docker.sh index d105bb5..f7d4361 100755 --- a/test-in-docker.sh +++ b/test-in-docker.sh @@ -5,5 +5,7 @@ set -ex image=lightningstream-test docker build --target=builder -t "$image" . -docker run -w /src --entrypoint '' "$image" /src/test.sh "$@" +docker run \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -w /src --entrypoint '' "$image" /src/test.sh "$@" diff --git a/test.sh b/test.sh index 374cae5..4b96870 100755 --- a/test.sh +++ b/test.sh @@ -1,12 +1,12 @@ #!/bin/sh -if [ ! -z "$SIMPLEBLOB_TEST_S3_CONFIG" ]; then - echo "* Using existing SIMPLEBLOB_TEST_S3_CONFIG=$SIMPLEBLOB_TEST_S3_CONFIG" -elif curl -v --connect-timeout 2 http://localhost:4730/ 2>&1 | grep --silent MinIO ; then - echo "* Using MinIO on localhost for tests" - export SIMPLEBLOB_TEST_S3_CONFIG="$PWD/docker/test-minio.json" +# S3 and Azure backend tests in simpleblob use testcontainers-go to spin up +# MinIO and Azurite automatically. They require a reachable Docker socket and +# will self-skip via testcontainers.SkipIfProviderIsNotHealthy if one is not available. +if [ -S /var/run/docker.sock ] || docker info > /dev/null 2>&1; then + echo "* Docker socket available, S3 (MinIO) and Azure (Azurite) backend tests will run via testcontainers" else - echo "* MinIO not running on localhost, skipping S3 tests" + echo "* Docker socket not available, S3 and Azure backend tests will be skipped" fi set -ex