From 8c05c57ea2533c5d6ffefbcf0b654c1f03aba710 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 5 Mar 2026 21:36:06 -0500 Subject: [PATCH] dco Signed-off-by: Kobbi Gal --- .build-tools/README.md | 20 + .../cmd/cmd-check-component-registrations.go | 786 ++++++ .build-tools/cmd/cmd-gen-component-schema.go | 2 +- .build-tools/go.mod | 4 +- .build-tools/go.sum | 8 +- .build-tools/pkg/metadataanalyzer/utils.go | 3 +- .build-tools/pkg/metadataschema/schema.go | 2 +- .github/dependabot.yml | 73 + .../docker-compose-cassandra.yml | 2 +- .../infrastructure/docker-compose-etcd.yml | 4 +- .../infrastructure/docker-compose-ravendb.yml | 14 + .github/scripts/create-release.sh | 58 + .github/scripts/dapr_bot.js | 22 +- .github/scripts/test-info.mjs | 38 + .github/workflows/backport.yaml | 37 + .github/workflows/certification.yml | 14 +- .github/workflows/components-contrib-all.yml | 4 +- .github/workflows/components-contrib.yml | 35 +- .github/workflows/conformance.yml | 23 +- .github/workflows/coverage-reports.yml | 6 +- .github/workflows/create-release.yaml | 49 + .github/workflows/dapr-bot-schedule.yml | 4 +- .github/workflows/dapr-bot.yml | 4 +- .github/workflows/dependabot-tidy.yml | 62 + .github/workflows/fossa.yml | 6 +- .../generate-component-metadata-for-tag.yml | 14 +- RELEASE.md | 45 + .../alicloud/dingtalk/webhook/metadata.yaml | 31 + bindings/alicloud/oss/metadata.yaml | 48 + bindings/alicloud/sls/metadata.yaml | 52 + bindings/alicloud/sls/sls.go | 1 + bindings/alicloud/tablestore/metadata.yaml | 48 + bindings/alicloud/tablestore/tablestore.go | 4 +- bindings/apns/apns.go | 2 + bindings/apns/metadata.yaml | 40 + bindings/aws/dynamodb/dynamodb.go | 37 +- bindings/aws/dynamodb/metadata.yaml | 48 + bindings/aws/kinesis/kinesis.go | 171 +- bindings/aws/kinesis/kinesis_test.go | 20 + bindings/aws/kinesis/metadata.yaml | 66 + bindings/aws/s3/s3.go | 180 +- bindings/aws/s3/s3_test.go | 59 + bindings/aws/ses/metadata.yaml | 60 + bindings/aws/ses/ses.go | 72 +- bindings/aws/sns/sns.go | 38 +- bindings/aws/sqs/metadata.yaml | 50 + bindings/aws/sqs/sqs.go | 96 +- .../gremlinapi}/cosmosdbgremlinapi.go | 0 .../gremlinapi}/cosmosdbgremlinapi_test.go | 0 .../gremlinapi}/metadata.yaml | 6 +- bindings/cloudflare/queues/metadata.yaml | 68 + bindings/commercetools/commercetools.go | 12 +- bindings/commercetools/metadata.yaml | 50 + bindings/dubbo/metadata.yaml | 41 + bindings/gcp/bucket/bucket.go | 18 +- bindings/gcp/bucket/bucket_test.go | 36 + bindings/gcp/bucket/metadata.yaml | 10 +- bindings/gcp/pubsub/metadata.yaml | 29 + bindings/gcp/pubsub/pubsub.go | 68 +- bindings/gcp/pubsub/pubsub_test.go | 53 +- bindings/graphql/metadata.yaml | 21 + bindings/huawei/obs/metadata.yaml | 43 + bindings/influx/metadata.yaml | 43 + bindings/kafka/metadata.yaml | 93 +- bindings/kitex/kitex_output_test.go | 8 +- bindings/kitex/metadata.yaml | 33 + bindings/kubemq/metadata.yaml | 56 + bindings/kubernetes/kubernetes.go | 18 +- bindings/kubernetes/metadata.yaml | 33 + bindings/localstorage/metadata.yaml | 25 + bindings/mqtt3/metadata.go | 18 +- bindings/mqtt3/metadata.yaml | 64 + bindings/mysql/mysql.go | 8 + bindings/mysql/mysql_integration_test.go | 70 +- bindings/postgres/metadata.go | 2 +- bindings/postgres/postgres.go | 28 +- bindings/postmark/metadata.yaml | 45 + bindings/redis/metadata.yaml | 16 +- bindings/redis/redis_test.go | 20 + bindings/rethinkdb/statechange/metadata.yaml | 158 ++ bindings/rethinkdb/statechange/statechange.go | 91 +- bindings/rocketmq/metadata.yaml | 77 + bindings/rocketmq/settings.go | 2 +- bindings/sftp/client.go | 318 +++ bindings/sftp/client_test.go | 127 + bindings/sftp/docker-compose.yaml | 11 + bindings/sftp/metadata.yaml | 84 + bindings/sftp/sftp.go | 71 +- bindings/sftp/sftp_integration_test.go | 303 +++ bindings/sftp/upload/test.txt | 1 + bindings/smtp/metadata.yaml | 70 + bindings/twilio/sendgrid/metadata.yaml | 63 + bindings/twilio/sms/metadata.yaml | 45 + bindings/twilio/sms/sms.go | 4 - bindings/wasm/metadata.yaml | 26 + common/authentication/aws/aws.go | 105 - common/authentication/aws/aws_test.go | 44 - common/authentication/aws/client.go | 210 -- common/authentication/aws/client_fake.go | 79 - common/authentication/aws/client_test.go | 265 -- common/authentication/aws/static.go | 386 --- common/authentication/aws/static_test.go | 72 - common/authentication/aws/x509.go | 571 ---- common/authentication/aws/x509_test.go | 127 - common/authentication/azure/auth.go | 29 +- common/authentication/azure/spiffe.go | 92 + .../oauth2/clientcredentials.go | 131 +- .../oauth2/clientcredentials_test.go | 202 ++ common/authentication/postgresql/metadata.go | 22 +- common/aws/auth/auth.go | 14 + common/aws/deprecated.go | 13 + common/aws/dynamodb_client.go | 29 + common/aws/helpers_common_test.go | 20 + common/aws/kinesis.go | 48 + common/aws/kinesis_worker_test.go | 34 + common/aws/mock/dynamodb_client.go | 45 + common/aws/mock/secretsmanager_client.go | 34 + common/aws/mock/ssm_client.go | 35 + common/aws/postgres.go | 38 + common/aws/postgres_iam_test.go | 38 + common/aws/secretsmanager_client.go | 27 + common/aws/ssm_client.go | 27 + common/aws/stream_arn_test.go | 15 + common/component/azure/blobstorage/client.go | 1 - .../azure/servicebus/subscription.go | 17 +- .../cloudflare/worker-src/package-lock.json | 2333 ++++++++++++----- .../cloudflare/worker-src/package.json | 6 +- .../cloudflare/workers/code/worker.js | 2 +- .../cloudflare/workers/code/worker.js.map | 8 +- common/component/kafka/auth.go | 17 + common/component/kafka/auth_test.go | 55 + common/component/kafka/clients.go | 10 +- common/component/kafka/kafka.go | 49 +- common/component/kafka/kafka_test.go | 302 ++- common/component/kafka/metadata.go | 94 +- common/component/kafka/metadata_test.go | 39 +- .../kafka/mocks/mock_ISchemaRegistryClient.go | 45 +- common/component/kafka/sasl_oauthbearer.go | 28 +- .../kafka/sasl_oauthbearer_private_key_jwt.go | 232 ++ common/component/kafka/subscriber_test.go | 4 +- common/component/postgresql/v1/metadata.go | 2 +- common/component/postgresql/v1/postgresql.go | 120 +- .../postgresql/v1/postgresql_query.go | 7 +- .../postgresql/v1/postgresql_test.go | 20 + common/component/redis/redis.go | 2 +- common/component/redis/redis_test.go | 2 +- common/component/redis/settings.go | 68 + common/component/redis/settings_test.go | 217 ++ common/component/redis/v8client.go | 5 +- common/component/redis/v9client.go | 5 +- common/proto/state/sqlserver/test.pb.go | 162 ++ common/proto/state/sqlserver/test.proto | 10 + component-metadata-schema.json | 1 + configuration/postgres/metadata.go | 7 +- configuration/postgres/postgres.go | 74 +- configuration/postgres/postgres_test.go | 132 + configuration/redis/metadata.yaml | 16 +- configuration/redis/redis_test.go | 4 +- conversation/anthropic/anthropic.go | 22 +- conversation/anthropic/metadata.yaml | 6 +- conversation/aws/bedrock/bedrock.go | 45 +- conversation/aws/bedrock/bedrock_test.go | 166 ++ conversation/aws/bedrock/metadata.yaml | 10 +- conversation/converse.go | 28 +- conversation/deepseek/deepseek.go | 7 +- conversation/deepseek/metadata.go | 9 +- conversation/echo/echo.go | 46 +- conversation/echo/echo_test.go | 63 + conversation/googleai/googleai.go | 30 +- conversation/googleai/metadata.yaml | 6 +- conversation/huggingface/huggingface.go | 20 +- conversation/huggingface/metadata.yaml | 2 +- conversation/langchaingokit/model.go | 120 +- conversation/langchaingokit/translate.go | 162 ++ conversation/langchaingokit/translate_test.go | 339 +++ conversation/langchaingokit/workarounds.go | 190 ++ .../langchaingokit/workarounds_test.go | 548 ++++ conversation/metadata.go | 14 +- conversation/metadata_test.go | 12 +- conversation/mistral/metadata.yaml | 2 +- conversation/mistral/mistral.go | 86 +- conversation/models.go | 85 + conversation/ollama/metadata.yaml | 2 +- conversation/ollama/ollama.go | 31 +- conversation/openai/metadata.go | 4 +- conversation/openai/metadata.yaml | 11 +- conversation/openai/metadata_test.go | 12 +- conversation/openai/openai.go | 66 +- conversation/openai/openai_test.go | 15 +- conversation/opts.go | 49 +- conversation/usage.go | 37 + crypto/azure/keyvault/metadata.yaml | 2 +- crypto/jwks/metadata.yaml | 2 +- crypto/kubernetes/secrets/metadata.yaml | 2 +- crypto/localstorage/metadata.yaml | 2 +- crypto/pubkey_cache_test.go | 18 +- docs/developing-component.md | 16 + go.mod | 280 +- go.sum | 758 +++--- lock/redis/metadata.yaml | 27 +- middleware/http/oauth2/oauth2_middleware.go | 8 +- .../http/oauth2/oauth2_middleware_test.go | 59 + .../oauth2clientcredentials/metadata.yaml | 12 +- middleware/http/opa/metadata.yaml | 6 +- middleware/http/opa/middleware.go | 9 + middleware/http/ratelimit/metadata.yaml | 4 +- middleware/http/wasm/internal/e2e_test.go | 28 +- nameresolution/aws/cloudmap/README.md | 140 + nameresolution/aws/cloudmap/cloudmap.go | 260 ++ nameresolution/aws/cloudmap/cloudmap_test.go | 388 +++ nameresolution/aws/cloudmap/metadata.go | 46 + nameresolution/aws/cloudmap/metadata.yaml | 28 + .../{ => hashicorp}/consul/README.md | 0 .../{ => hashicorp}/consul/configuration.go | 2 +- .../{ => hashicorp}/consul/consul.go | 0 .../{ => hashicorp}/consul/consul_test.go | 0 nameresolution/hashicorp/consul/metadata.yaml | 157 ++ .../{ => hashicorp}/consul/watcher.go | 0 nameresolution/kubernetes/metadata.yaml | 11 + nameresolution/mdns/metadata.yaml | 11 + nameresolution/nameformat/README.md | 39 + nameresolution/nameformat/metadata.yaml | 17 + nameresolution/nameformat/nameformat.go | 94 + nameresolution/nameformat/nameformat_test.go | 142 + nameresolution/sqlite/metadata.yaml | 11 + pubsub/aws/snssqs/metadata.go | 35 +- pubsub/aws/snssqs/snssqs.go | 2 +- pubsub/aws/snssqs/subscription_mgmt.go | 6 +- .../servicebus/topics/servicebus_test.go | 376 +++ pubsub/in-memory/in-memory.go | 26 +- pubsub/in-memory/in-memory_test.go | 33 +- pubsub/jetstream/metadata.yaml | 41 +- pubsub/kafka/metadata.yaml | 93 +- pubsub/kubemq/metadata.yaml | 8 +- pubsub/pulsar/metadata.go | 5 + pubsub/pulsar/metadata.yaml | 38 +- pubsub/pulsar/pulsar.go | 145 +- pubsub/pulsar/pulsar_test.go | 1226 ++++++++- pubsub/rabbitmq/rabbitmq.go | 4 +- pubsub/redis/metadata.yaml | 15 +- pubsub/redis/redis.go | 22 +- pubsub/redis/redis_test.go | 275 +- pubsub/rocketmq/metadata.yaml | 44 +- secretstores/akeyless/README.md | 231 ++ secretstores/akeyless/akeyless.go | 988 +++++++ secretstores/akeyless/akeyless_test.go | 1325 ++++++++++ secretstores/akeyless/metadata.yaml | 101 + secretstores/akeyless/utils.go | 344 +++ .../aws/parameterstore/parameterstore.go | 47 +- .../aws/parameterstore/parameterstore_test.go | 178 +- secretstores/aws/secretmanager/metadata.yaml | 8 +- .../aws/secretmanager/secretmanager.go | 106 +- .../aws/secretmanager/secretmanager_test.go | 370 ++- secretstores/local/file/metadata.yaml | 4 +- state/aws/dynamodb/dynamodb.go | 159 +- state/aws/dynamodb/dynamodb_test.go | 855 +++--- state/azure/blobstorage/v2/metadata.yaml | 18 + state/cockroachdb/cockroachdb.go | 95 +- state/couchbase/metadata.yaml | 12 +- state/errors.go | 2 + state/etcd/etcd.go | 216 ++ state/etcd/metadata.yaml | 6 +- state/feature.go | 2 + state/gcp/firestore/metadata.yaml | 4 +- state/in-memory/in_memory.go | 158 +- state/in-memory/in_memory_test.go | 6 +- state/in-memory/keys.go | 36 + state/mongodb/mongodb.go | 102 + state/mysql/mysql.go | 148 +- state/mysql/mysql_test.go | 7 + state/oci/objectstorage/metadata.yaml | 8 +- state/oracledatabase/oracledatabaseaccess.go | 128 +- .../oracledatabaseaccess_test.go | 416 ++- state/postgresql/v1/migrations.go | 117 +- .../v1/postgresql_integration_test.go | 6 + state/postgresql/v2/metadata.go | 4 +- state/postgresql/v2/postgresql.go | 230 +- .../v2/postgresql_integration_test.go | 7 + state/ravendb/metadata.yaml | 51 + state/ravendb/ravendb.go | 520 ++++ state/ravendb/ravendb_test.go | 129 + state/redis/metadata.yaml | 16 +- state/redis/redis.go | 213 +- state/redis/redis_test.go | 64 + state/requests.go | 13 + state/responses.go | 10 + state/rethinkdb/metadata.yaml | 28 +- state/sqlite/metadata.yaml | 4 +- state/sqlite/sqlite.go | 5 + state/sqlite/sqlite_dbaccess.go | 91 + state/sqlite/sqlite_test.go | 12 + state/sqlserver/sqlserver.go | 74 + state/sqlserver/v2/metadata.go | 210 ++ state/sqlserver/v2/metadata.yaml | 105 + state/sqlserver/v2/migration.go | 351 +++ state/sqlserver/v2/sqlserver.go | 420 +++ .../v2/sqlserver_integration_test.go | 661 +++++ state/sqlserver/v2/sqlserver_test.go | 556 ++++ state/store.go | 6 + state/zookeeper/metadata.yaml | 12 +- .../certification/bindings/aws/s3/s3_test.go | 2 +- .../kafka/components/consumer1/kafka.yaml | 2 + .../kafka/components/consumer2/kafka.yaml | 2 + .../sasl-password/kafka-binding.yaml | 2 + tests/certification/bindings/mysql/README.md | 25 + .../mysql/components/standard/mysql.yaml | 10 + .../certification/bindings/mysql/config.yaml | 6 + .../bindings/mysql/docker-compose.yml | 13 + .../bindings/mysql/mysql_test.go | 412 +++ .../zeebe/command/create_instance_test.go | 4 +- tests/certification/embedded/components.go | 2 +- tests/certification/embedded/embedded.go | 1 + .../flow/dockercompose/dockercompose.go | 1 + tests/certification/flow/sidecar/sidecar.go | 16 + tests/certification/go.mod | 229 +- tests/certification/go.sum | 627 +++-- .../pubsub/aws/snssqs/snssqs_helper.go | 101 +- .../pubsub/azure/servicebus/queues/README.md | 206 ++ .../authentication/localsecrets.yaml | 9 + .../authentication/service_bus.yaml | 27 + .../components/consumer_one/localsecrets.yaml | 9 + .../components/consumer_one/service_bus.yaml | 17 + .../components/consumer_two/localsecrets.yaml | 9 + .../components/consumer_two/service_bus.yaml | 17 + .../components/default_ttl/localsecrets.yaml | 9 + .../components/default_ttl/service_bus.yaml | 19 + .../components/entity_mgmt/localsecrets.yaml | 9 + .../components/entity_mgmt/service_bus.yaml | 17 + .../azure/servicebus/queues/config.yaml | 2 + .../servicebus/queues/servicebus_test.go | 1369 ++++++++++ .../servicebus/topics/servicebus_test.go | 281 +- .../components/auth_oidc_certs/kafka.yaml | 37 + .../auth_oidc_certs/localsecrets.yaml | 8 + .../auth_oidc_secret_key/kafka.yaml | 22 + .../kafka/components/consumer1/kafka.yaml | 2 + .../kafka/components/consumer2/kafka.yaml | 4 +- .../kafka/components/consumerAvro/kafka.yaml | 4 +- .../pubsub/kafka/data/realm-export.json | 43 + .../pubsub/kafka/docker-compose.auth.yml | 73 + .../certification/pubsub/kafka/kafka_test.go | 148 +- .../consumer_eight/pulsar.yml.tmpl | 24 + .../consumer_seven/pulsar.yml.tmpl | 26 + .../pubsub/pulsar/pulsar_test.go | 128 +- .../state/aws/dynamodb/README.md | 46 + .../state/aws/dynamodb/dynamodb_test.go | 23 +- .../cassandra/docker-compose-cluster.yml | 4 +- .../state/cassandra/docker-compose-single.yml | 2 +- tests/certification/state/mysql/mysql_test.go | 4 +- .../state/postgresql/v1/postgresql_test.go | 2 +- .../state/postgresql/v2/postgresql_test.go | 2 +- tests/certification/state/ravendb/README.md | 17 + .../ravendb/components/default/ravendb.yaml | 14 + tests/certification/state/ravendb/config.yaml | 6 + .../state/ravendb/docker-compose.yml | 14 + .../state/ravendb/ravendb_test.go | 270 ++ tests/certification/state/redis/redis_test.go | 13 +- .../state/sqlserver/v2/README.md | 46 + .../v2/components/azure/localsecrets.yaml | 9 + .../v2/components/azure/sqlserver.yaml | 24 + .../customschemawithindex/sqlserver.yaml | 18 + .../components/docker/default/sqlserver.yaml | 11 + .../state/sqlserver/v2/config.yaml | 6 + .../state/sqlserver/v2/docker-compose.yml | 9 + .../state/sqlserver/v2/sqlserver_test.go | 664 +++++ tests/config/conversation/README.md | 59 +- .../conversation/anthropic/anthropic.yml | 2 +- .../config/conversation/googleai/googleai.yml | 2 +- .../conversation/huggingface/huggingface.yml | 2 +- tests/config/conversation/mistral/mistral.yml | 3 +- tests/config/conversation/ollama/ollama.yml | 2 +- .../conversation/openai/azure/openai.yml | 2 +- .../conversation/openai/openai/openai.yml | 2 +- tests/config/conversation/tests.yml | 1 + tests/config/state/ravendb/statestore.yaml | 14 + .../state/sqlserver/docker/statestore.yml | 11 + .../state/sqlserver/v2/docker/statestore.yml | 11 + .../config/state/sqlserver/v2/statestore.yml | 11 + tests/config/state/tests.yml | 49 +- tests/conformance/README.md | 5 + .../conformance/conversation/conversation.go | 309 ++- tests/conformance/conversation_test.go | 3 + tests/conformance/pubsub/pubsub.go | 21 +- tests/conformance/state/state.go | 421 +++ tests/conformance/state_test.go | 10 + tests/e2e/bindings/kitex/EchoKitexServer.go | 7 +- tests/e2e/pubsub/jetstream/go.mod | 21 +- tests/e2e/pubsub/jetstream/go.sum | 58 +- tests/utils/sftpproxy/proxy.go | 121 + 388 files changed, 29647 insertions(+), 5687 deletions(-) create mode 100644 .build-tools/cmd/cmd-check-component-registrations.go create mode 100644 .github/dependabot.yml create mode 100644 .github/infrastructure/docker-compose-ravendb.yml create mode 100755 .github/scripts/create-release.sh create mode 100644 .github/workflows/backport.yaml create mode 100644 .github/workflows/create-release.yaml create mode 100644 .github/workflows/dependabot-tidy.yml create mode 100644 RELEASE.md create mode 100644 bindings/alicloud/dingtalk/webhook/metadata.yaml create mode 100644 bindings/alicloud/oss/metadata.yaml create mode 100644 bindings/alicloud/sls/metadata.yaml create mode 100644 bindings/alicloud/tablestore/metadata.yaml create mode 100644 bindings/apns/metadata.yaml create mode 100644 bindings/aws/dynamodb/metadata.yaml create mode 100644 bindings/aws/kinesis/metadata.yaml create mode 100644 bindings/aws/ses/metadata.yaml create mode 100644 bindings/aws/sqs/metadata.yaml rename bindings/azure/{cosmosdbgremlinapi => cosmosdb/gremlinapi}/cosmosdbgremlinapi.go (100%) rename bindings/azure/{cosmosdbgremlinapi => cosmosdb/gremlinapi}/cosmosdbgremlinapi_test.go (100%) rename bindings/azure/{cosmosdbgremlinapi => cosmosdb/gremlinapi}/metadata.yaml (86%) create mode 100644 bindings/cloudflare/queues/metadata.yaml create mode 100644 bindings/commercetools/metadata.yaml create mode 100644 bindings/dubbo/metadata.yaml create mode 100644 bindings/gcp/pubsub/metadata.yaml create mode 100644 bindings/graphql/metadata.yaml create mode 100644 bindings/huawei/obs/metadata.yaml create mode 100644 bindings/influx/metadata.yaml create mode 100644 bindings/kitex/metadata.yaml create mode 100644 bindings/kubemq/metadata.yaml create mode 100644 bindings/kubernetes/metadata.yaml create mode 100644 bindings/localstorage/metadata.yaml create mode 100644 bindings/mqtt3/metadata.yaml create mode 100644 bindings/postmark/metadata.yaml create mode 100644 bindings/rethinkdb/statechange/metadata.yaml create mode 100644 bindings/rocketmq/metadata.yaml create mode 100644 bindings/sftp/client.go create mode 100644 bindings/sftp/client_test.go create mode 100644 bindings/sftp/docker-compose.yaml create mode 100644 bindings/sftp/metadata.yaml create mode 100644 bindings/sftp/sftp_integration_test.go create mode 100644 bindings/sftp/upload/test.txt create mode 100644 bindings/smtp/metadata.yaml create mode 100644 bindings/twilio/sendgrid/metadata.yaml create mode 100644 bindings/twilio/sms/metadata.yaml create mode 100644 bindings/wasm/metadata.yaml delete mode 100644 common/authentication/aws/aws.go delete mode 100644 common/authentication/aws/aws_test.go delete mode 100644 common/authentication/aws/client.go delete mode 100644 common/authentication/aws/client_fake.go delete mode 100644 common/authentication/aws/client_test.go delete mode 100644 common/authentication/aws/static.go delete mode 100644 common/authentication/aws/static_test.go delete mode 100644 common/authentication/aws/x509.go delete mode 100644 common/authentication/aws/x509_test.go create mode 100644 common/authentication/azure/spiffe.go create mode 100644 common/aws/deprecated.go create mode 100644 common/aws/dynamodb_client.go create mode 100644 common/aws/helpers_common_test.go create mode 100644 common/aws/kinesis.go create mode 100644 common/aws/kinesis_worker_test.go create mode 100644 common/aws/mock/dynamodb_client.go create mode 100644 common/aws/mock/secretsmanager_client.go create mode 100644 common/aws/mock/ssm_client.go create mode 100644 common/aws/postgres.go create mode 100644 common/aws/postgres_iam_test.go create mode 100644 common/aws/secretsmanager_client.go create mode 100644 common/aws/ssm_client.go create mode 100644 common/aws/stream_arn_test.go create mode 100644 common/component/kafka/sasl_oauthbearer_private_key_jwt.go create mode 100644 common/proto/state/sqlserver/test.pb.go create mode 100644 common/proto/state/sqlserver/test.proto create mode 100644 conversation/aws/bedrock/bedrock_test.go create mode 100644 conversation/langchaingokit/translate.go create mode 100644 conversation/langchaingokit/translate_test.go create mode 100644 conversation/langchaingokit/workarounds.go create mode 100644 conversation/langchaingokit/workarounds_test.go create mode 100644 conversation/models.go create mode 100644 conversation/usage.go create mode 100644 nameresolution/aws/cloudmap/README.md create mode 100644 nameresolution/aws/cloudmap/cloudmap.go create mode 100644 nameresolution/aws/cloudmap/cloudmap_test.go create mode 100644 nameresolution/aws/cloudmap/metadata.go create mode 100644 nameresolution/aws/cloudmap/metadata.yaml rename nameresolution/{ => hashicorp}/consul/README.md (100%) rename nameresolution/{ => hashicorp}/consul/configuration.go (99%) rename nameresolution/{ => hashicorp}/consul/consul.go (100%) rename nameresolution/{ => hashicorp}/consul/consul_test.go (100%) create mode 100644 nameresolution/hashicorp/consul/metadata.yaml rename nameresolution/{ => hashicorp}/consul/watcher.go (100%) create mode 100644 nameresolution/kubernetes/metadata.yaml create mode 100644 nameresolution/mdns/metadata.yaml create mode 100644 nameresolution/nameformat/README.md create mode 100644 nameresolution/nameformat/metadata.yaml create mode 100644 nameresolution/nameformat/nameformat.go create mode 100644 nameresolution/nameformat/nameformat_test.go create mode 100644 nameresolution/sqlite/metadata.yaml create mode 100644 pubsub/azure/servicebus/topics/servicebus_test.go create mode 100644 secretstores/akeyless/README.md create mode 100644 secretstores/akeyless/akeyless.go create mode 100644 secretstores/akeyless/akeyless_test.go create mode 100644 secretstores/akeyless/metadata.yaml create mode 100644 secretstores/akeyless/utils.go create mode 100644 state/in-memory/keys.go create mode 100644 state/ravendb/metadata.yaml create mode 100644 state/ravendb/ravendb.go create mode 100644 state/ravendb/ravendb_test.go create mode 100644 state/sqlserver/v2/metadata.go create mode 100644 state/sqlserver/v2/metadata.yaml create mode 100644 state/sqlserver/v2/migration.go create mode 100644 state/sqlserver/v2/sqlserver.go create mode 100644 state/sqlserver/v2/sqlserver_integration_test.go create mode 100644 state/sqlserver/v2/sqlserver_test.go create mode 100644 tests/certification/bindings/mysql/README.md create mode 100644 tests/certification/bindings/mysql/components/standard/mysql.yaml create mode 100644 tests/certification/bindings/mysql/config.yaml create mode 100644 tests/certification/bindings/mysql/docker-compose.yml create mode 100644 tests/certification/bindings/mysql/mysql_test.go create mode 100644 tests/certification/pubsub/azure/servicebus/queues/README.md create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/authentication/localsecrets.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/authentication/service_bus.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/consumer_one/localsecrets.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/consumer_one/service_bus.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/consumer_two/localsecrets.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/consumer_two/service_bus.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/default_ttl/localsecrets.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/default_ttl/service_bus.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/entity_mgmt/localsecrets.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/components/entity_mgmt/service_bus.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/config.yaml create mode 100644 tests/certification/pubsub/azure/servicebus/queues/servicebus_test.go create mode 100644 tests/certification/pubsub/kafka/components/auth_oidc_certs/kafka.yaml create mode 100644 tests/certification/pubsub/kafka/components/auth_oidc_certs/localsecrets.yaml create mode 100644 tests/certification/pubsub/kafka/components/auth_oidc_secret_key/kafka.yaml create mode 100644 tests/certification/pubsub/kafka/data/realm-export.json create mode 100644 tests/certification/pubsub/kafka/docker-compose.auth.yml create mode 100644 tests/certification/pubsub/pulsar/components/auth-oauth2/consumer_eight/pulsar.yml.tmpl create mode 100644 tests/certification/pubsub/pulsar/components/auth-oauth2/consumer_seven/pulsar.yml.tmpl create mode 100644 tests/certification/state/ravendb/README.md create mode 100644 tests/certification/state/ravendb/components/default/ravendb.yaml create mode 100644 tests/certification/state/ravendb/config.yaml create mode 100644 tests/certification/state/ravendb/docker-compose.yml create mode 100644 tests/certification/state/ravendb/ravendb_test.go create mode 100644 tests/certification/state/sqlserver/v2/README.md create mode 100644 tests/certification/state/sqlserver/v2/components/azure/localsecrets.yaml create mode 100644 tests/certification/state/sqlserver/v2/components/azure/sqlserver.yaml create mode 100644 tests/certification/state/sqlserver/v2/components/docker/customschemawithindex/sqlserver.yaml create mode 100644 tests/certification/state/sqlserver/v2/components/docker/default/sqlserver.yaml create mode 100644 tests/certification/state/sqlserver/v2/config.yaml create mode 100644 tests/certification/state/sqlserver/v2/docker-compose.yml create mode 100644 tests/certification/state/sqlserver/v2/sqlserver_test.go create mode 100644 tests/config/state/ravendb/statestore.yaml create mode 100644 tests/config/state/sqlserver/docker/statestore.yml create mode 100644 tests/config/state/sqlserver/v2/docker/statestore.yml create mode 100644 tests/config/state/sqlserver/v2/statestore.yml create mode 100644 tests/utils/sftpproxy/proxy.go diff --git a/.build-tools/README.md b/.build-tools/README.md index b760464fc6..cbc014b75b 100644 --- a/.build-tools/README.md +++ b/.build-tools/README.md @@ -16,3 +16,23 @@ You have two ways to run the CLI: The list of available commands in this CLI is dynamic and is subject to change at any time. Each command, including the "root" one (no sub-command), are self-documented in the CLI, and you can read the help page by adding `--help`. For example, `./build-tools --help` shows the full list of commands the CLI offers. + +### check-component-registrations + +Checks that all components in components-contrib are properly registered in dapr/dapr. This includes checking for: + +- Registry files in dapr/pkg/components/ +- Registration of specific components in dapr/cmd/daprd/components/ +- Metadata files in component directories + +Usage: +```bash +# Run from build-tools directory +go run . check-component-registrations + +# Or using the compiled binary +./build-tools check-component-registrations +``` + +This command will scan all component types (conversation, state, secretstores, pubsub, bindings, configuration, nameresolution, middleware, cryptography, lock) and report any missing registrations. +This is part of the release endgame tasking to ensure all components properly register within runtime as expected. diff --git a/.build-tools/cmd/cmd-check-component-registrations.go b/.build-tools/cmd/cmd-check-component-registrations.go new file mode 100644 index 0000000000..c7d03b387e --- /dev/null +++ b/.build-tools/cmd/cmd-check-component-registrations.go @@ -0,0 +1,786 @@ +/* +Copyright 2025 The Dapr Authors +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 cmd + +import ( + "fmt" + "os/exec" + "slices" + "strings" + + "github.com/spf13/cobra" +) + +var verbose bool + +// checkComponentRegistrationsCmd represents the check-component-registrations command +// go run . check-component-registrations +// This automates an endgame task that must be completed before a release. +// It checks that all components in components-contrib are properly registered in dapr/dapr. +var checkComponentRegistrationsCmd = &cobra.Command{ + Use: "check-component-registrations", + Short: "Checks that all components are properly registered", + Long: `Checks that all components in components-contrib are properly registered in dapr/dapr. +This includes checking for: +- Registry files in dapr/pkg/components/ +- Registration of specific components in dapr/cmd/daprd/components/ +- Metadata files in component directories + +This is a required step before an official Dapr release.`, + Run: func(cmd *cobra.Command, args []string) { + // Navigate to the root of the repo + err := cwdToRepoRoot() + if err != nil { + panic(err) + } + + fmt.Println("Checking Dapr Component Registrations across runtime in dapr/dapr and components-contrib") + fmt.Println("========================================================================================") + + checkConversationComponents() + checkStateComponents() + checkPubSubComponents() + checkSecretStoreComponents() + checkBindingComponents() + checkConfigurationComponents() + checkLockComponents() + checkCryptographyComponents() + checkNameResolutionComponents() + checkMiddlewareComponents() + + fmt.Println("\nCheck completed!") + }, +} + +func init() { + checkComponentRegistrationsCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") + rootCmd.AddCommand(checkComponentRegistrationsCmd) +} + +func checkConversationComponents() { + fmt.Println("\nChecking conversation components...") + checkComponents("conversation", []string{}, []string{}) +} + +func checkStateComponents() { + fmt.Println("\nChecking state components...") + + // Yugabyte are supported via the postgres component, so in contrib this is covered by the postgresql component in the contrib list. + // Also, postgres = postgresql, so we can ignore postgres. + // also sqlite3 is an alias for sqlite so we can ignore one. + ignoreDaprComponents := []string{"yugabyte", "yugabytedb", "postgres", "sqlite3"} + ignoreContribComponents := []string{"azure.blobstorage.internal"} + checkComponents("state", ignoreDaprComponents, ignoreContribComponents) +} + +func checkPubSubComponents() { + fmt.Println("\nChecking pubsub components...") + + // mqtt3 = mqtt, so ignore mqtt (keep mqtt3 since it exists in contrib) + // azure.servicebusqueues is an alias for azure.servicebus.queues (keep azure.servicebus.queues) so ignore it + // azure.servicebus is an alias for azure.servicebus.topics (keep azure.servicebus.topics) so ignore it + ignoreDaprComponents := []string{"mqtt", "azure.servicebusqueues", "azure.servicebus"} + ignoreContribComponents := []string{} + checkComponents("pubsub", ignoreDaprComponents, ignoreContribComponents) +} + +func checkSecretStoreComponents() { + fmt.Println("\nChecking secretstore components...") + checkComponents("secretstores", []string{}, []string{}) +} + +func checkBindingComponents() { + fmt.Println("\nChecking bindings components...") + // ignore servicebus.queues as runtime has an alias on this so we're checking for servicebusqueues + ignoreDaprComponents := []string{"mqtt3", "azure.servicebus.queues", "postgresql"} + ignoreContribComponents := []string{} + checkComponents("bindings", ignoreDaprComponents, ignoreContribComponents) +} + +func checkConfigurationComponents() { + fmt.Println("\nChecking configuration components...") + ignoreDaprComponents := []string{"postgresql"} + checkComponents("configuration", ignoreDaprComponents, []string{}) +} + +func checkLockComponents() { + fmt.Println("\nChecking lock components...") + checkComponents("lock", []string{}, []string{}) +} + +func checkCryptographyComponents() { + fmt.Println("\nChecking cryptography components...") + // below is not actually a component. Cryptography section in contrib needs quite a bit of clean up/organization to clean this up so I don't have to "ignore" it. + ignoreContribComponents := []string{"pubkey_cache"} + // For crypto, runtime components are registered with a dapr.* prefix (for example, dapr.jwks) + // while contrib components use the unprefixed name (for example, jwks). + // Instead of ignoring the dapr.* names, we treat "dapr" as a vendor prefix in normalization + // so counts still line up. + ignoreDaprComponents := []string{} + // TODO: in future update this to cryptography once we have a cryptography component in contrib and not crypto components + checkComponents("crypto", ignoreDaprComponents, ignoreContribComponents) +} + +func checkNameResolutionComponents() { + fmt.Println("\nChecking name resolution components...") + checkComponents("nameresolution", []string{}, []string{}) +} + +func checkMiddlewareComponents() { + fmt.Println("\nChecking middleware components...") + // uppercase is a component only in runtime which doesn't make sense give the nameresolution components have no real metadata of their own for the most part; + // however, it's definition is only in runtime and not in contrib so we must ignore it. + ignoreDaprComponents := []string{"uppercase"} + ignoreContribComponents := []string{} + checkComponents("middleware", ignoreDaprComponents, ignoreContribComponents) +} + +// Note: because this cli cmd changes to the working directory to the root of the repo so pathing is relative to that. +func checkComponents(componentType string, ignoreDaprComponents []string, ignoreContribComponents []string) { + if err := checkRegistry(componentType); err != nil { + fmt.Printf("Registry check failed: %v\n", err) + return + } + + contribComponents, daprComponents, err := findComponentsInBothRepos(componentType, ignoreContribComponents) + if err != nil { + fmt.Printf("Component discovery across repos failed: %v\n", err) + return + } + + if verbose { + fmt.Printf("Components to ignore: %v\n", ignoreDaprComponents) + fmt.Printf("Dapr components before filtering: %v\n", daprComponents) + } + + // Filter out components to ignore + filteredDaprComponents := make([]string, 0, len(daprComponents)) + for _, comp := range daprComponents { + if !slices.Contains(ignoreDaprComponents, comp) { + filteredDaprComponents = append(filteredDaprComponents, comp) + } else { + if verbose { + fmt.Printf("Ignoring component: %s\n", comp) + } + } + } + daprComponents = filteredDaprComponents + + filteredContribComponents := make([]string, 0, len(contribComponents)) + for _, comp := range contribComponents { + if !slices.Contains(ignoreContribComponents, comp) { + filteredContribComponents = append(filteredContribComponents, comp) + } else { + if verbose { + fmt.Printf("Ignoring component: %s\n", comp) + } + } + } + contribComponents = filteredContribComponents + + // Apply vendor prefix mapping and deduplication to both lists. + // This removes things like the CSP and/or vendor prefixing. + mappedContribComponents := mapAndDeduplicateComponents(contribComponents) + mappedDaprComponents := mapAndDeduplicateComponents(daprComponents) + + fmt.Printf("Components in contrib: %d\n", len(mappedContribComponents)) + fmt.Printf("Components registered in runtime: %d\n", len(mappedDaprComponents)) + + if len(mappedContribComponents) != len(mappedDaprComponents) { + fmt.Println("\nNumber of components in contrib and dapr/dapr do not match") + fmt.Printf("Contrib: %v\n\n", mappedContribComponents) + fmt.Printf("Dapr: %v\n", mappedDaprComponents) + return + } + + missingRegistrations, missingBuildTags, missingMetadata, err := validateComponents(contribComponents, componentType) + if err != nil { + fmt.Printf("Component validation failed: %v\n", err) + return + } + + reportResults(missingRegistrations, missingBuildTags, missingMetadata, componentType) +} + +// getRegistryPath is needed to get the correct registry file for the component, +// and middleware components are nested under a specific http dir, so we must handle this case. +func getRegistryPath(componentType string) string { + if componentType == "middleware" { + return fmt.Sprintf("../dapr/pkg/components/%s/http/registry.go", componentType) + } + return fmt.Sprintf("../dapr/pkg/components/%s/registry.go", componentType) +} + +func checkRegistry(componentType string) error { + registryPath := getRegistryPath(componentType) + + // Check for default registry singleton + registryCmd := exec.Command("grep", "-r", `var DefaultRegistry \*Registry = NewRegistry\(\)`, registryPath) + _, err := registryCmd.Output() + if err != nil { + return fmt.Errorf("could not find default registry: %v", err) + } + + // Check for RegisterComponent() + if componentType != "bindings" { + registerComponentCmd := exec.Command("grep", "-r", `Registry) RegisterComponent(componentFactory`, registryPath) + _, err := registerComponentCmd.Output() + if err != nil { + return fmt.Errorf("could not find RegisterComponent method: %v", err) + } + + // Check for Create() + createCmd := exec.Command("grep", "-r", `Registry) Create(name, version`, registryPath) + _, err = createCmd.Output() + if err != nil { + return fmt.Errorf("could not find Create method: %v", err) + } + } else { + registerInputBindingCmd := exec.Command("grep", "-r", `Registry) RegisterInputBinding(componentFactory func(logger.Logger)`, registryPath) + _, err := registerInputBindingCmd.Output() + if err != nil { + return fmt.Errorf("could not find registerInputBindingCmd method: %v", err) + } + + registerOutputBindingCmd := exec.Command("grep", "-r", `Registry) RegisterOutputBinding(componentFactory func(logger.Logger)`, registryPath) + _, err = registerOutputBindingCmd.Output() + if err != nil { + return fmt.Errorf("could not find registerOutputBindingCmd method: %v", err) + } + + // Check for Creates + createInputBindingCmd := exec.Command("grep", "-r", `Registry) CreateInputBinding(name, version, logName string)`, registryPath) + _, err = createInputBindingCmd.Output() + if err != nil { + return fmt.Errorf("could not find CreateInputBinding method: %v", err) + } + + createOutputBindingCmd := exec.Command("grep", "-r", `Registry) CreateOutputBinding(name, version, logName string)`, registryPath) + _, err = createOutputBindingCmd.Output() + if err != nil { + return fmt.Errorf("could not find CreateInputBinding method: %v", err) + } + } + + return nil +} + +func findComponentsInBothRepos(componentType string, ignoreContribComponents []string) ([]string, []string, error) { + // Find all components in components-contrib, excluding utility files + // Configure exclude list based on component type + var excludeFiles []string + // we have to exclude certain files that match the grep, but are not components. + // In future, this can be cleaned up if files are moved to proper pkg like directories. + switch componentType { + case "state": + excludeFiles = []string{"--exclude=errors.go", "--exclude=bulk.go", "--exclude=query.go"} + case "pubsub": + excludeFiles = []string{"--exclude=envelope.go", "--exclude=responses.go"} + case "bindings": + excludeFiles = []string{"--exclude=client.go"} + case "crypto": + excludeFiles = []string{"--exclude=key.go", "--exclude=pubkey_cache.go"} + case "middleware": + excludeFiles = []string{"--exclude=mock*"} + default: + excludeFiles = []string{} + } + + grepArgs := []string{"-rl", "--include=*.go"} + grepArgs = append(grepArgs, excludeFiles...) + grepArgs = append(grepArgs, "func New", componentType) + + contribCmd := exec.Command("grep", grepArgs...) + contribOutput, err := contribCmd.Output() + if err != nil { + return nil, nil, fmt.Errorf("could not find components within components-contrib: %v", err) + } + + // Find all registered components in dapr/dapr + var registeredOutput []byte + if componentType != "bindings" { + var searchPattern string + // bc middleware components are nested under a specific http dir, we must handle this case. + if componentType == "middleware" { + searchPattern = "../dapr/cmd/daprd/components/middleware_http_*.go" + } else { + searchPattern = fmt.Sprintf("../dapr/cmd/daprd/components/%s_*.go", componentType) + } + registeredCmd := exec.Command("sh", "-c", fmt.Sprintf(`grep -r "RegisterComponent" %s`, searchPattern)) + registeredOutput, err = registeredCmd.Output() + if err != nil { + return nil, nil, fmt.Errorf("could not find all registered components in dapr/dapr: %v", err) + } + } else { + // For bindings, capture both RegisterInputBinding and RegisterOutputBinding. + // Also include files that are disabled but still present for registration (bindings_kitex.go_disabled) + registeredCmd := exec.Command("sh", "-c", fmt.Sprintf(`grep -r "RegisterInputBinding\|RegisterOutputBinding" ../dapr/cmd/daprd/components/%s_*.go*`, componentType)) + registeredOutput, err = registeredCmd.Output() + if err != nil { + return nil, nil, fmt.Errorf("could not find all registered components in dapr/dapr: %v", err) + } + } + + contribComponents := parseContribComponents(string(contribOutput), componentType, ignoreContribComponents) + registeredComponents := parseRegisteredComponents(string(registeredOutput), componentType) + + return contribComponents, registeredComponents, nil +} + +func validateComponents(contribComponents []string, componentType string) ([]string, []string, []string, error) { + missingRegistrations := []string{} + missingBuildTags := []string{} + missingMetadata := []string{} + + for _, contrib := range contribComponents { + registrationErr := checkComponentRegistration(contrib, componentType) + buildTagErr := checkBuildTag(contrib, componentType) + metadataErr := checkMetadataFile(contrib, componentType) + + if registrationErr != nil { + missingRegistrations = append(missingRegistrations, contrib) + } + if buildTagErr != nil { + missingBuildTags = append(missingBuildTags, contrib) + } + if metadataErr != nil { + missingMetadata = append(missingMetadata, contrib) + } + } + + return missingRegistrations, missingBuildTags, missingMetadata, nil +} + +func checkComponentRegistration(contrib, componentType string) error { + // For registration files, resolve the actual registration filename. + // This supports both the normal filename (e.g. bindings_kitex.go) and + // disabled variants that share the same prefix (e.g. bindings_kitex.go_disabled). + compFileName, err := getRegistrationFilePath(contrib, componentType) + if err != nil { + return err + } + + // Check if this is a versioned component + // EX: postgresql.v1, azure.blobstorage.v2 + parts := strings.Split(contrib, ".") + if len(parts) >= 2 { + lastPart := parts[len(parts)-1] + if strings.HasPrefix(lastPart, "v") && len(lastPart) <= 3 { + // This is a versioned component, check if the base component is registered + baseComponent := strings.Join(parts[:len(parts)-1], ".") + if err := checkComponentIsActuallyRegisteredInFile(baseComponent, componentType, compFileName); err != nil { + return fmt.Errorf("versioned component '%s' base component '%s' not registered in %s: %v", contrib, baseComponent, compFileName, err) + } + if verbose { + fmt.Printf("Versioned component '%s' properly registered via base component '%s' in %s\n", contrib, baseComponent, compFileName) + } + return nil + } + } + + if err := checkComponentIsActuallyRegisteredInFile(contrib, componentType, compFileName); err != nil { + return fmt.Errorf("component '%s' not registered in %s: %v", contrib, compFileName, err) + } + + if verbose { + fmt.Printf("Component '%s' properly registered in %s\n", contrib, compFileName) + } + return nil +} + +// checkComponentIsActuallyRegisteredInFile basically checks if the component name string is within the file +// to ensure it is properly registered within runtime. +func checkComponentIsActuallyRegisteredInFile(contrib, componentType, registrationFile string) error { + namesToCheck := []string{contrib} + + // Crypto providers are registered with a dapr.* prefix in runtime (for example, dapr.jwks), + // while contrib uses the unprefixed name (for example, jwks). Treat either as valid. + if componentType == "crypto" { + namesToCheck = append(namesToCheck, fmt.Sprintf("dapr.%s", contrib)) + } + + for _, name := range namesToCheck { + grepCmd := exec.Command("grep", "-q", fmt.Sprintf(`"%s"`, name), registrationFile) + if _, err := grepCmd.Output(); err == nil { + return nil + } + } + + return fmt.Errorf("component '%s' (or dapr.%s) not found in registration file '%s'", contrib, contrib, registrationFile) +} + +func checkBuildTag(contrib, componentType string) error { + compFileName, err := getRegistrationFilePath(contrib, componentType) + if err != nil { + return err + } + + // Check for "go:build allcomponents" + buildTagCmd := exec.Command("grep", "-q", "allcomponents", compFileName) + _, err = buildTagCmd.Output() + if err != nil { + return fmt.Errorf("build tag for 'allcomponents' not found in %s", compFileName) + } + + // TODO: in future, add check for stable components + return nil +} + +// normalizeComponentName strips vendor prefixes and versions from component names +// EX: aws.bedrock -> bedrock +// EX: postgresql.v1 -> postgresql +// EX: azure.blobstorage.v1 -> blobstorage +func normalizeComponentName(contrib string) string { + if !strings.Contains(contrib, ".") { + return contrib + } + + parts := strings.Split(contrib, ".") + // Note: I am putting http as a vendor prefix, but that should be removed and all http middleware components should be http.blah bc + // in future we could add grpc middleware components. + vendorPrefixes := []string{"hashicorp", "aws", "azure", "gcp", "alicloud", "oci", "cloudflare", "ibm", "tencentcloud", "huaweicloud", "twilio", "http", "dapr"} + versionSuffixes := []string{"v1", "v2", "internal"} + + // Handle 2-part names (vendor.component) + if len(parts) == 2 { + // Strip vendor prefixes + if slices.Contains(vendorPrefixes, parts[0]) { + return parts[1] + } + // Strip version suffixes + if slices.Contains(versionSuffixes, parts[1]) { + return parts[0] + } + } + + // Handle 3+ part names (vendor.component.version) + if len(parts) >= 3 { + if slices.Contains(vendorPrefixes, parts[0]) { + // Check if last part is a version suffix + if slices.Contains(versionSuffixes, parts[len(parts)-1]) { + // Return middle parts: azure.blobstorage.v1 -> blobstorage + return strings.Join(parts[1:len(parts)-1], ".") + } + // Return all parts except vendor: azure.cosmosdb -> cosmosdb + return strings.Join(parts[1:], ".") + } + // Check if last part is a version suffix + if slices.Contains(versionSuffixes, parts[len(parts)-1]) { + // Return all parts except version: component.v1 -> component + return strings.Join(parts[:len(parts)-1], ".") + } + } + + return contrib +} + +func mapAndDeduplicateComponents(components []string) []string { + mappedComponents := make([]string, 0, len(components)) + seen := make(map[string]bool) + + for _, component := range components { + simpleName := normalizeComponentName(component) + if !seen[simpleName] { + mappedComponents = append(mappedComponents, simpleName) + seen[simpleName] = true + } + } + + return mappedComponents +} + +func checkMetadataFile(contrib, componentType string) error { + metadataFile := getMetadataFilePath(contrib, componentType) + + metadataExistsCmd := exec.Command("ls", metadataFile) + _, err := metadataExistsCmd.Output() + if err != nil { + return fmt.Errorf("metadata file for '%s' not found: %s", contrib, metadataFile) + } + + if verbose { + fmt.Printf("Metadata file for '%s' found: %s\n", contrib, metadataFile) + } + return nil +} + +func getMetadataFilePath(contrib, componentType string) string { + // Special handling for HTTP middleware components + // The metadata files are located at middleware/http/componentname/metadata.yaml + if componentType == "middleware" { + return fmt.Sprintf("%s/http/%s/metadata.yaml", componentType, contrib) + } + + if strings.Contains(contrib, ".") { + // For nested components like "aws.bedrock", split and join with "/" + parts := strings.Split(contrib, ".") + return fmt.Sprintf("%s/%s/metadata.yaml", componentType, strings.Join(parts, "/")) + } + // For simple components like "echo" + return fmt.Sprintf("%s/%s/metadata.yaml", componentType, contrib) +} + +func reportResults(missingRegistrations, missingBuildTags, missingMetadata []string, componentType string) { + totalIssues := len(missingRegistrations) + len(missingBuildTags) + len(missingMetadata) + + if totalIssues > 0 { + fmt.Printf("\nValidation Results for %s components:\n", componentType) + + if len(missingRegistrations) > 0 { + fmt.Printf("Missing registrations: %d\n", len(missingRegistrations)) + for _, m := range missingRegistrations { + fmt.Printf(" - %s\n", m) + } + } + + if len(missingBuildTags) > 0 { + fmt.Printf("Missing build tags: %d\n", len(missingBuildTags)) + for _, m := range missingBuildTags { + fmt.Printf(" - %s\n", m) + } + } + + if len(missingMetadata) > 0 { + fmt.Printf("Missing metadata files: %d\n", len(missingMetadata)) + for _, m := range missingMetadata { + fmt.Printf(" - %s\n", m) + } + } + } else { + fmt.Printf("All %s components are properly configured\n", componentType) + } +} + +func parseContribComponents(output, componentType string, ignoreContribComponents []string) []string { + components := []string{} + lines := strings.FieldsFunc(output, func(r rune) bool { + return r == '\n' + }) + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + componentName := extractComponentNameFromPath(line, componentType) + if componentName != "" { + if !slices.Contains(ignoreContribComponents, componentName) { + components = append(components, componentName) + } + } + } + + return components +} + +// extractComponentNameFromPath extracts a component name from file path +// EX: conversation/echo/echo.go -> echo +// EX: conversation/aws/bedrock/bedrock.go -> aws.bedrock +func extractComponentNameFromPath(filePath, componentType string) string { + // Find the component type directory in the path + componentTypeIndex := strings.Index(filePath, componentType+"/") + if componentTypeIndex < 0 { + return "" + } + + // Extract the path after componentType/ + pathAfterComponentType := filePath[componentTypeIndex+len(componentType+"/"):] + pathWithoutExt := strings.TrimSuffix(pathAfterComponentType, ".go") + + // Convert directory structure to component name + // Replace slashes with dots, but only up to the last directory + parts := strings.Split(pathWithoutExt, "/") + if len(parts) >= 2 { + // For nested components: aws/bedrock/bedrock.go -> aws.bedrock + // Take all parts except the last (which is the filename) + dirParts := parts[:len(parts)-1] + + // Special handling for HTTP middleware components + // middleware/http/componentname/ -> componentname (not http.componentname) + if componentType == "middleware" && len(dirParts) >= 2 && dirParts[0] == "http" { + return strings.Join(dirParts[1:], ".") + } + + return strings.Join(dirParts, ".") + } else if len(parts) == 1 { + // For simple components: echo/echo.go -> echo + // The part before the slash is the component name + return parts[0] + } + + return "" +} + +// getRegistrationFileName converts a component name to its registration file name. +// Note: runtime does not deliniate versions in file names so we ignore that in pathing. +// EX: aws.bedrock -> ../dapr/cmd/daprd/components/conversation_bedrock.go +// EX: alicloud.tablestore -> ../dapr/cmd/daprd/components/state_alicloud_tablestore.go +// EX: postgresql.v1 -> ../dapr/cmd/daprd/components/state_postgres.go +func getRegistrationFileName(contrib, componentType string) string { + // For versioned components, strip the version suffix + parts := strings.Split(contrib, ".") + if len(parts) >= 2 { + // Check if the last part is a version (v1, v2, etc.) + lastPart := parts[len(parts)-1] + if strings.HasPrefix(lastPart, "v") && len(lastPart) <= 3 { + // Remove the version part + baseName := strings.Join(parts[:len(parts)-1], ".") + fileName := strings.ReplaceAll(baseName, ".", "_") + // TODO: update runtime file names to match this + if fileName == "postgresql" { + fileName = "postgres" + } + return fmt.Sprintf("../dapr/cmd/daprd/components/%s_%s.go", componentType, fileName) + } + } + + fileName := strings.ReplaceAll(contrib, ".", "_") + + // Special handling for HTTP middleware components + // The registration files are named middleware_http_componentname.go + if componentType == "middleware" { + return fmt.Sprintf("../dapr/cmd/daprd/components/%s_http_%s.go", componentType, fileName) + } + + return fmt.Sprintf("../dapr/cmd/daprd/components/%s_%s.go", componentType, fileName) +} + +// getRegistrationFilePath returns the actual registration file path on disk. +// It first looks for the exact expected filename (something like bindings_kitex.go). +// If that does not exist, it falls back to any file that has the same prefix, +// which allows handling disabled files like bindings_kitex.go_disabled. +func getRegistrationFilePath(contrib, componentType string) (string, error) { + compFileName := getRegistrationFileName(contrib, componentType) + + // First try the exact filename. + fileExistsCmd := exec.Command("ls", compFileName) + if _, err := fileExistsCmd.Output(); err == nil { + return compFileName, nil + } + + // Fallback: treat the expected filename as a prefix and look for any match. + prefixCmd := exec.Command("sh", "-c", fmt.Sprintf("ls %s*", compFileName)) + output, err := prefixCmd.Output() + if err != nil { + return "", fmt.Errorf("registration file for '%s' not found: %s", contrib, compFileName) + } + + paths := strings.Fields(string(output)) + if len(paths) == 0 { + return "", fmt.Errorf("registration file for '%s' not found: %s", contrib, compFileName) + } + + return paths[0], nil +} + +// parseRegisteredComponents parses the output of the grep command to find all registered components +func parseRegisteredComponents(output string, componentType string) []string { + components := []string{} + lines := strings.FieldsFunc(output, func(r rune) bool { + return r == '\n' + }) + + // Extract unique file paths from grep output + uniqueFiles := make(map[string]bool) + for _, line := range lines { + colonIndex := strings.Index(line, ":") + if colonIndex == -1 { + continue + } + filePath := line[:colonIndex] + uniqueFiles[filePath] = true + } + + // Process each registration file + for filePath := range uniqueFiles { + // Skip uppercase component as it contains many magic strings that aren't component names + if strings.Contains(filePath, "uppercase") { + continue + } + + // Read the entire file content + cmd := exec.Command("cat", filePath) + fileContent, err := cmd.Output() + if err != nil { + continue + } + + fileLines := strings.FieldsFunc(string(fileContent), func(r rune) bool { + return r == '\n' + }) + + // Check if this file contains registration calls and find the starting line + hasRegistrationCalls := false + lineToContinueFrom := 0 + for i, line := range fileLines { + if componentType == "bindings" { + if strings.Contains(line, "RegisterInputBinding") || strings.Contains(line, "RegisterOutputBinding") { + hasRegistrationCalls = true + lineToContinueFrom = i + break + } + } else { + if strings.Contains(line, "RegisterComponent") { + hasRegistrationCalls = true + lineToContinueFrom = i + break + } + } + } + + // Extract component names from lines starting from the first registration call + if hasRegistrationCalls { + for i, line := range fileLines { + if i < lineToContinueFrom { + continue + } + quotedStrings := extractAllQuotedStrings(line) + if len(quotedStrings) > 0 { + components = append(components, quotedStrings...) + } + } + } + } + + return components +} + +// extractAllQuotedStrings extracts all quoted strings from a line +func extractAllQuotedStrings(line string) []string { + var quotedStrings []string + start := 0 + for { + quoteIndex := strings.Index(line[start:], "\"") + if quoteIndex == -1 { + break + } + quoteIndex += start + + rest := line[quoteIndex+1:] + endQuoteIndex := strings.Index(rest, "\"") + if endQuoteIndex == -1 { + break + } + + quotedString := rest[:endQuoteIndex] + if quotedString != "v1" && quotedString != "v2" { + quotedStrings = append(quotedStrings, quotedString) + } + + start = quoteIndex + 1 + endQuoteIndex + 1 + } + + return quotedStrings +} diff --git a/.build-tools/cmd/cmd-gen-component-schema.go b/.build-tools/cmd/cmd-gen-component-schema.go index 7be0c8005f..3199670ef5 100644 --- a/.build-tools/cmd/cmd-gen-component-schema.go +++ b/.build-tools/cmd/cmd-gen-component-schema.go @@ -39,7 +39,7 @@ The result is written to stdout.`, } res := reflector.Reflect(&metadataschema.ComponentMetadata{}) res.Title = "ComponentMetadata" - // Print resut to stdout + // Print result to stdout enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") _ = enc.Encode(res) diff --git a/.build-tools/go.mod b/.build-tools/go.mod index b5bd060f10..aa2c1130fd 100644 --- a/.build-tools/go.mod +++ b/.build-tools/go.mod @@ -1,6 +1,6 @@ module github.com/dapr/components-contrib/build-tools -go 1.24.4 +go 1.24.13 require ( github.com/dapr/components-contrib v0.0.0 @@ -12,7 +12,7 @@ require ( ) require ( - github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 // indirect + github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect diff --git a/.build-tools/go.sum b/.build-tools/go.sum index 8b0fb3bb78..c8f07ff7ef 100644 --- a/.build-tools/go.sum +++ b/.build-tools/go.sum @@ -1,6 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 h1:Q26gmPxs6WnnBYoudOlznPHsmrbTawcYEpHg4VoB7v8= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= +github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d h1:csljij9d1IO6u9nqbg+TuSRmTZ+OXT8G49yh6zie1yI= +github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -43,8 +43,8 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An 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.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/.build-tools/pkg/metadataanalyzer/utils.go b/.build-tools/pkg/metadataanalyzer/utils.go index 91b26a2bdf..40f8751412 100644 --- a/.build-tools/pkg/metadataanalyzer/utils.go +++ b/.build-tools/pkg/metadataanalyzer/utils.go @@ -120,7 +120,7 @@ func GenerateMetadataAnalyzer(contribRoot string, componentFolders []string, out if methodFinderErr == nil { methodFound = true } - case "crypto": + case "cryptography": method, methodFinderErr = getConstructorMethod("contribCrypto.SubtleCrypto", parsedFile) if methodFinderErr == nil { methodFound = true @@ -136,6 +136,7 @@ func GenerateMetadataAnalyzer(contribRoot string, componentFolders []string, out methodFound = true } } + // TODO: add conversation, nameresolution if methodFound { pkgs[packageName] = PkgInfo{ diff --git a/.build-tools/pkg/metadataschema/schema.go b/.build-tools/pkg/metadataschema/schema.go index bc7b0f4ac3..6ce6401476 100644 --- a/.build-tools/pkg/metadataschema/schema.go +++ b/.build-tools/pkg/metadataschema/schema.go @@ -20,7 +20,7 @@ type ComponentMetadata struct { // Version of the component metadata schema. SchemaVersion string `json:"schemaVersion" yaml:"schemaVersion" jsonschema:"enum=v1"` // Component type, of one of the allowed values. - Type string `json:"type" yaml:"type" jsonschema:"enum=bindings,enum=state,enum=secretstores,enum=pubsub,enum=workflows,enum=configuration,enum=lock,enum=middleware,enum=crypto,enum=conversation"` + Type string `json:"type" yaml:"type" jsonschema:"enum=bindings,enum=state,enum=secretstores,enum=pubsub,enum=workflows,enum=configuration,enum=lock,enum=middleware,enum=crypto,enum=nameresolution,enum=conversation"` // Name of the component (without the inital type, e.g. "http" instead of "bindings.http"). Name string `json:"name" yaml:"name"` // Version of the component, with the leading "v", e.g. "v1". diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..85a7344032 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,73 @@ +version: 2 +updates: + # Git + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + # gomod main pkg + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + # gomod build-tools + - package-ecosystem: "gomod" + directory: "/build-tools" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + # gomod middleware + - package-ecosystem: "gomod" + directories: + - "/middleware/http/was/example" + - "/middleware/http/wasm/internal/e2e-guests/rewrite" + - "/middleware/http/wasm/internal/e2e-guests/config" + - "/middleware/http/wasm/internal/e2e-guests/output" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + # gomod bindings + - package-ecosystem: "gomod" + directories: + - "/bindings/wasm/testdata/example" + - "/bindings/wasm/testdata/http" + - "/bindings/wasm/testdata/args" + - "/bindings/wasm/testdata/loop" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + # gomod tests + - package-ecosystem: "gomod" + directories: + - "/tests/e2e/pubsub/jetstream" + - "/tests/certification" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + \ No newline at end of file diff --git a/.github/infrastructure/docker-compose-cassandra.yml b/.github/infrastructure/docker-compose-cassandra.yml index 4cb9d80a4d..40845b5acc 100644 --- a/.github/infrastructure/docker-compose-cassandra.yml +++ b/.github/infrastructure/docker-compose-cassandra.yml @@ -2,7 +2,7 @@ version: '2' services: cassandra: - image: docker.io/bitnami/cassandra:4.0.1 + image: docker.io/bitnamilegacy/cassandra:4.1 ports: - '7000:7000' - '9042:9042' diff --git a/.github/infrastructure/docker-compose-etcd.yml b/.github/infrastructure/docker-compose-etcd.yml index 9bcd6754ad..f301d623ff 100644 --- a/.github/infrastructure/docker-compose-etcd.yml +++ b/.github/infrastructure/docker-compose-etcd.yml @@ -1,7 +1,7 @@ version: '2' services: etcd: - image: gcr.io/etcd-development/etcd:v3.4.20 + image: gcr.io/etcd-development/etcd:v3.5.21 ports: - "12379:2379" - command: etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379 \ No newline at end of file + command: etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379 diff --git a/.github/infrastructure/docker-compose-ravendb.yml b/.github/infrastructure/docker-compose-ravendb.yml new file mode 100644 index 0000000000..a84c4efc8f --- /dev/null +++ b/.github/infrastructure/docker-compose-ravendb.yml @@ -0,0 +1,14 @@ +services: + ravendb: + image: ravendb/ravendb + container_name: RavenDb + ports: + - "8080:8080" + environment: + - RAVEN_LICENSE={"Id":"b75b6995-5f1f-440f-bcaa-40f7388ecf90","Name":"Vega IT","Keys":["jwX1/epkfQHHLKF0UX8+DRCGP","zwoUaohApGfVHZ0rmZ4Cvaj4a","653+JiPtKVTANiZw6wxdD7XB6","3OfgLC8++bvE8T8JhuTD12PQt","nFzHL2zUPhlP5q+9mI8NmAzbu","tTXscNl13tFX1GLgcWkZYNN80","H7RrWQXwKc+HJ4q1d2catABYE","DNy4xBSYoSQMqKywtLi8wJzEy","MzQVFjc4OTo7PD0+nwIfIJ8CI","CCfAiEgnwIjIJ8CJCCfAiUgnw","ImIJ8CJyCfAiggnwIpIJ8CKiC","fAisgnwIsIJ8CLSCfAi4gnwIv","IJ8CMCCfAzZAAZ8CQiCfAkMgn","wJEIJ8CRSCfAkYgnwJHIJ8CSA","BDJEQJYgVdnwRBYAJd"]} + - RAVEN_DATABASE=testdapr + - RAVEN_Setup_Mode=None + - RAVEN_License_Eula_Accepted=true + - RAVEN_Security_UnsecuredAccessAllowed=PrivateNetwork + - RAVEN_License_ThrowOnInvalidOrMissingLicense=true + restart: unless-stopped diff --git a/.github/scripts/create-release.sh b/.github/scripts/create-release.sh new file mode 100755 index 0000000000..f5e49047d6 --- /dev/null +++ b/.github/scripts/create-release.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# +# Copyright 2025 The Dapr Authors +# 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. + +set -ue + +# Thanks to https://ihateregex.io/expr/semver/ +SEMVER_REGEX='^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' + +REL_VERSION=`echo $1 | sed -r 's/^[vV]?([0-9].+)$/\1/'` + +if [ `echo $REL_VERSION | pcre2grep "$SEMVER_REGEX"` ]; then + echo "$REL_VERSION is a valid semantic version." +else + echo "$REL_VERSION is not a valid semantic version." + exit 1 +fi + +MAJOR_MINOR_VERSION=`echo $REL_VERSION | cut -d. -f1,2` +RELEASE_BRANCH="release-$MAJOR_MINOR_VERSION" +RELEASE_TAG="v$REL_VERSION" +SUFFIX=`echo $REL_VERSION | grep \- | cut -d- -f2 | cut -d. -f1` +if [ "$SUFFIX" == "alpha" ]; then + # Alpha releases come from the master branch as they are not complete for an RC yet. + RELEASE_BRANCH="master" +fi + +if [ `git rev-parse --verify origin/$RELEASE_BRANCH 2>/dev/null` ]; then + echo "$RELEASE_BRANCH branch already exists, checking it out ..." + git checkout $RELEASE_BRANCH +else + echo "$RELEASE_BRANCH does not exist, creating ..." + git checkout -b $RELEASE_BRANCH + git push origin $RELEASE_BRANCH +fi +echo "$RELEASE_BRANCH branch is ready." + +if [ `git rev-parse --verify $RELEASE_TAG 2>/dev/null` ]; then + echo "$RELEASE_TAG tag already exists, aborting ..." + exit 2 +fi + +echo "Tagging $RELEASE_TAG ..." +git tag $RELEASE_TAG +echo "$RELEASE_TAG is tagged." + +echo "Pushing $RELEASE_TAG tag ..." +git push origin $RELEASE_TAG +echo "$RELEASE_TAG tag is pushed." diff --git a/.github/scripts/dapr_bot.js b/.github/scripts/dapr_bot.js index 328972d27e..5d1c958abf 100644 --- a/.github/scripts/dapr_bot.js +++ b/.github/scripts/dapr_bot.js @@ -1,32 +1,18 @@ // list of owner who can control dapr-bot workflow // TODO: Read owners from OWNERS file. const owners = [ - 'addjuarez', - 'amuluyavarote', - 'artursouza', + 'acroca', 'berndverst', + 'cicoyle', 'daixiang0', - 'DeepanshuA', - 'elena-kolevska', - 'halspang', - 'ItalyPaleAle', + 'javier-aliaga', 'jjcollinge', 'joshvanl', 'mikeee', 'msfussell', - 'mukundansundar', - 'pkedy', - 'pravinpushkar', - 'robertojrojas', - 'RyanLettieri', - 'shivamkm07', - 'shubham1172', + 'nelson-parente', 'sicoyle', - 'skyao', - 'Taction', - 'tmacam', 'yaron2', - 'yash-nisar', ] const docsIssueBodyTpl = ( diff --git a/.github/scripts/test-info.mjs b/.github/scripts/test-info.mjs index 9a659db6f6..75069e4ab2 100644 --- a/.github/scripts/test-info.mjs +++ b/.github/scripts/test-info.mjs @@ -172,6 +172,16 @@ const components = { conformanceSetup: 'docker-compose.sh vernemq', sourcePkg: ['bindings/mqtt3'], }, + 'bindings.mysql': { + certification: true, + }, + 'bindings.mysql.docker': { + conformance: true, + conformanceSetup: 'docker-compose.sh mysql', + sourcePkg: [ + 'bindings/mysql', + ], + }, 'bindings.postgres': { certification: true, }, @@ -795,6 +805,23 @@ const components = { requiredSecrets: ['AzureSqlServerConnectionString'], sourcePkg: ['state/sqlserver', 'common/component/sql'], }, + 'state.sqlserver.v2': { + conformance: true, + certification: true, + conformanceSetup: 'docker-compose.sh sqlserver', + requiredSecrets: ['AzureSqlServerConnectionString'], + sourcePkg: ['state/sqlserver/v2', 'common/component/sql'], + }, + 'state.sqlserver.docker': { + conformance: true, + conformanceSetup: 'docker-compose.sh sqlserver', + sourcePkg: ['state/sqlserver', 'common/component/sql'], + }, + 'state.sqlserver.v2.docker': { + conformance: true, + conformanceSetup: 'docker-compose.sh sqlserver', + sourcePkg: ['state/sqlserver/v2', 'common/component/sql'], + }, // 'state.gcp.firestore.docker': { // conformance: true, // requireDocker: true, @@ -810,6 +837,12 @@ const components = { requireGCPCredentials: true, certificationSetup: 'certification-state.gcp.firestore-setup.sh', }, + 'state.ravendb': { + conformance: true, + certification: true, + conformanceSetup: 'docker-compose.sh ravendb', + requireRavenDBCredentials: true, + }, } /** @@ -822,6 +855,7 @@ const components = { * @property {boolean?} requireAWSCredentials If true, requires AWS credentials and makes the test "cloud-only" * @property {boolean?} requireGCPCredentials If true, requires GCP credentials and makes the test "cloud-only" * @property {boolean?} requireCloudflareCredentials If true, requires Cloudflare credentials and makes the test "cloud-only" + * @property {boolean?} requireRavenDBCredentials If true, requires RavenDB credentials * @property {boolean?} requireTerraform If true, requires Terraform * @property {boolean?} requireKind If true, requires KinD * @property {string?} conformanceSetup Setup script for conformance tests @@ -843,6 +877,7 @@ const components = { * @property {boolean?} require-aws-credentials Requires AWS credentials * @property {boolean?} require-gcp-credentials Requires GCP credentials * @property {boolean?} require-cloudflare-credentials Requires Cloudflare credentials + * @property {boolean?} require-ravendb-credentials Requires RavenDB credentials * @property {boolean?} require-terraform Requires Terraform * @property {boolean?} require-kind Requires KinD * @property {string?} setup-script Setup script @@ -914,6 +949,9 @@ function GenerateMatrix(testKind, enableCloudTests) { 'require-cloudflare-credentials': comp.requireCloudflareCredentials ? 'true' : undefined, + 'require-ravendb-credentials': comp.requireRavenDBCredentials + ? 'true' + : undefined, 'require-terraform': comp.requireTerraform ? 'true' : undefined, 'require-kind': comp.requireKind ? 'true' : undefined, 'setup-script': comp[testKind + 'Setup'] || undefined, diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml new file mode 100644 index 0000000000..71b9716cbe --- /dev/null +++ b/.github/workflows/backport.yaml @@ -0,0 +1,37 @@ +# +# Copyright 2026 The Dapr Authors +# 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. +# + +name: Backport +on: + pull_request_target: + types: + - closed + - labeled + +jobs: + backport: + name: Backport + runs-on: ubuntu-latest + if: > + github.event.pull_request.merged + && ( + github.event.action == 'closed' + || ( + github.event.action == 'labeled' + && contains(github.event.label.name, 'backport') + ) + ) + steps: + - uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e + with: + github_token: ${{ secrets.DAPR_BOT_TOKEN }} diff --git a/.github/workflows/certification.yml b/.github/workflows/certification.yml index 0e0a7be7a5..98d6fff871 100644 --- a/.github/workflows/certification.yml +++ b/.github/workflows/certification.yml @@ -44,7 +44,7 @@ jobs: fi - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.CHECKOUT_REPO }} ref: ${{ env.CHECKOUT_REF }} @@ -120,7 +120,7 @@ jobs: fi - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.CHECKOUT_REPO }} ref: ${{ env.CHECKOUT_REF }} @@ -174,7 +174,7 @@ jobs: - id: 'auth' if: matrix.require-gcp-credentials == 'true' name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v3' with: token_format: 'access_token' workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER_NAME }} @@ -215,7 +215,7 @@ jobs: echo "AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }}" >> $GITHUB_ENV - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v5 if: matrix.require-aws-credentials == 'true' with: aws-access-key-id: "${{ secrets.AWS_ACCESS_KEY }}" @@ -292,7 +292,7 @@ jobs: fi - name: Upload Cert Coverage Report File - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: github.event_name == 'schedule' with: name: ${{ matrix.component }}_cert_code_cov @@ -311,7 +311,7 @@ jobs: fi - name: Upload result files - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() with: name: ${{ matrix.component }}_result_files @@ -359,7 +359,7 @@ jobs: - name: Build message if: always() && env.PR_NUMBER != '' # Abusing of the github-script action to be able to write this in JS - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: | const allComponents = JSON.parse('${{ needs.generate-matrix.outputs.test-matrix }}') diff --git a/.github/workflows/components-contrib-all.yml b/.github/workflows/components-contrib-all.yml index 332a14bc01..026ca751c9 100644 --- a/.github/workflows/components-contrib-all.yml +++ b/.github/workflows/components-contrib-all.yml @@ -21,11 +21,13 @@ on: - cron: '0 */6 * * *' push: branches: + - main - release-* tags: - v* pull_request: branches: + - main - release-* jobs: post-comment: @@ -98,7 +100,7 @@ jobs: fi - name: Check out code into the Go module directory if: ${{ steps.skip_check.outputs.should_skip != 'true' }} - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.CHECKOUT_REPO }} ref: ${{ env.CHECKOUT_REF }} diff --git a/.github/workflows/components-contrib.yml b/.github/workflows/components-contrib.yml index 2ce6d87850..50ceedd0b1 100644 --- a/.github/workflows/components-contrib.yml +++ b/.github/workflows/components-contrib.yml @@ -14,10 +14,10 @@ name: "Build, Lint, Unit Test - Linux AMD64 Only" on: + workflow_dispatch: push: branches: - feature/* - - gh-readonly-queue/main/* pull_request: branches: - main @@ -37,7 +37,7 @@ jobs: steps: - name: Check out code into the Go module directory if: ${{ steps.skip_check.outputs.should_skip != 'true' }} - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go id: setup-go if: ${{ steps.skip_check.outputs.should_skip != 'true' }} @@ -85,3 +85,34 @@ jobs: - name: Codecov if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux' uses: codecov/codecov-action@v3 + + check-metadata-bundle-changes: + name: Detect metadata file changes + runs-on: ubuntu-latest + outputs: + metadata: ${{ steps.filter.outputs.metadata }} + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + metadata: + - '**/metadata.yaml' + - 'component-metadata-schema.json' + + check-metadata-bundle: + name: Check component metadata bundle + needs: check-metadata-bundle-changes + if: needs.check-metadata-bundle-changes.outputs.metadata == 'true' + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' + cache: 'false' + - name: Check component metadata bundle can be generated + run: make bundle-component-metadata diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index ed98baa84e..ece84162a8 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -22,7 +22,6 @@ on: push: branches: - 'release-*' - - 'gh-readonly-queue/main/*' pull_request: branches: - 'main' @@ -47,7 +46,7 @@ jobs: fi - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.CHECKOUT_REPO }} ref: ${{ env.CHECKOUT_REF }} @@ -126,7 +125,7 @@ jobs: fi - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.CHECKOUT_REPO }} ref: ${{ env.CHECKOUT_REF }} @@ -180,7 +179,7 @@ jobs: - id: 'auth' if: matrix.require-gcp-credentials == 'true' name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v3' with: token_format: 'access_token' workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER_NAME }} @@ -220,7 +219,7 @@ jobs: - name: Configure AWS Credentials if: matrix.require-aws-credentials == 'true' - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v5 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} @@ -228,7 +227,7 @@ jobs: - name: Start MongoDB if: matrix.mongodb-version != '' - uses: supercharge/mongodb-github-action@1.8.0 + uses: supercharge/mongodb-github-action@1.12.1 with: mongodb-version: ${{ matrix.mongodb-version }} mongodb-replica-set: test-rs @@ -241,12 +240,12 @@ jobs: - name: Install Node.js ${{ matrix.nodejs-version }} if: matrix.nodejs-version != '' - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.nodejs-version }} - name: Start KinD - uses: helm/kind-action@v1.5.0 + uses: helm/kind-action@v1.13.0 if: matrix.require-kind == 'true' - name: Download Go dependencies @@ -333,7 +332,7 @@ jobs: fi - name: Upload result files - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() with: name: ${{ matrix.component }}_result_files @@ -347,7 +346,7 @@ jobs: cp cover.out tmp/conf_code_cov/${{ env.SOURCE_PATH_LINEAR }}.out - name: Upload coverage report file - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: github.event_name == 'schedule' with: name: ${{ matrix.component }}_conf_code_cov @@ -357,7 +356,7 @@ jobs: # Upload logs for test analytics to consume - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ matrix.component }}_conformance_test path: ${{ env.TEST_OUTPUT_FILE_PREFIX }}_conformance.* @@ -395,7 +394,7 @@ jobs: - name: Build message if: always() && env.PR_NUMBER != '' # Abusing of the github-script action to be able to write this in JS - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: | const allComponents = JSON.parse('${{ needs.generate-matrix.outputs.test-matrix }}') diff --git a/.github/workflows/coverage-reports.yml b/.github/workflows/coverage-reports.yml index eb4dc375d9..880ad813c1 100644 --- a/.github/workflows/coverage-reports.yml +++ b/.github/workflows/coverage-reports.yml @@ -30,7 +30,7 @@ jobs: GOCOVMERGE_VER: "b5bfa59" steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.CHECKOUT_REPO }} ref: ${{ env.CHECKOUT_REF }} @@ -48,7 +48,7 @@ jobs: go install github.com/wadey/gocovmerge@${{ env.GOCOVMERGE_VER }} - name: Download workflow artifact - uses: dawidd6/action-download-artifact@v2.24.3 + uses: dawidd6/action-download-artifact@v12 with: workflow: certification.yml workflow_conclusion: "success" @@ -59,7 +59,7 @@ jobs: path: tmp/cert_code_cov - name: Download workflow artifact - uses: dawidd6/action-download-artifact@v2.24.3 + uses: dawidd6/action-download-artifact@v12 with: workflow: conformance.yml workflow_conclusion: "success" diff --git a/.github/workflows/create-release.yaml b/.github/workflows/create-release.yaml new file mode 100644 index 0000000000..05e893fcaa --- /dev/null +++ b/.github/workflows/create-release.yaml @@ -0,0 +1,49 @@ +# +# Copyright 2025 The Dapr Authors +# 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. +# + +name: Create a release + +on: + workflow_dispatch: + inputs: + rel_version: + description: 'Release version (examples: 1.16.0-rc.1, 1.16.0)' + required: true + type: string + +permissions: {} + +jobs: + create-release: + name: Creates release branch and tag + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install required packages + run: | + sudo apt-get update + sudo apt-get install pcre2-utils + - name: Create release branch and tag + env: + GITHUB_TOKEN: ${{ secrets.DAPR_BOT_TOKEN }} + run: | + git config user.email "dapr@dapr.io" + git config user.name "Dapr Bot" + # Update origin with token + git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git + ./.github/scripts/create-release.sh ${{ inputs.rel_version }} \ No newline at end of file diff --git a/.github/workflows/dapr-bot-schedule.yml b/.github/workflows/dapr-bot-schedule.yml index 29fb8fc05a..4df8485988 100644 --- a/.github/workflows/dapr-bot-schedule.yml +++ b/.github/workflows/dapr-bot-schedule.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install dependencies run: pip install PyGithub - name: Automerge and update @@ -40,7 +40,7 @@ jobs: pull-requests: write steps: - name: Prune Stale - uses: actions/stale@v7.0.0 + uses: actions/stale@v10.1.1 with: repo-token: ${{ github.token }} days-before-stale: 30 diff --git a/.github/workflows/dapr-bot.yml b/.github/workflows/dapr-bot.yml index f0ca08d6ba..20175a95fa 100644 --- a/.github/workflows/dapr-bot.yml +++ b/.github/workflows/dapr-bot.yml @@ -27,9 +27,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 # required to make the script available for next step + uses: actions/checkout@v6 # required to make the script available for next step - name: Issue analyzer - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: github-token: ${{secrets.DAPR_BOT_TOKEN}} script: | diff --git a/.github/workflows/dependabot-tidy.yml b/.github/workflows/dependabot-tidy.yml new file mode 100644 index 0000000000..ea31a83315 --- /dev/null +++ b/.github/workflows/dependabot-tidy.yml @@ -0,0 +1,62 @@ +name: Dependabot modtidy-all +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + actions: write + +jobs: + modtidy-all: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} # ensure we are not checking out the merge commit, but the working branch instead + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + - name: Run go mod tidy for all modules + run: | + make modtidy-all + - name: Prepare repo and detect changes + id: detect_changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add . + # Exit status 0 => no changes. Set output for next steps. + if git diff --cached --quiet; then + echo "changes=false" >> $GITHUB_OUTPUT + else + echo "changes=true" >> $GITHUB_OUTPUT + fi + - name: Commit and push changes + if: steps.detect_changes.outputs.changes == 'true' + id: commit_push + run: | + git commit -m "chore(deps): run go mod tidy for all modules" + git push --set-upstream origin HEAD + echo "commit_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + - name: Trigger workflow runs on new commit + if: steps.detect_changes.outputs.changes == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ steps.commit_push.outputs.commit_sha }} + run: | + for WORKFLOW_FILE in components-contrib.yml conformance.yml certification.yml fossa.yml; do + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + https://api.github.com/repos/$REPO/actions/workflows/$WORKFLOW_FILE/dispatches \ + -d "{\"ref\": \"${SHA}\"}" + done + - name: No changes to commit + if: steps.detect_changes.outputs.changes == 'false' + run: echo "No changes to commit" diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 84c05aee1d..950a1200b1 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -32,16 +32,16 @@ jobs: FOSSA_API_KEY: b88e1f4287c3108c8751bf106fb46db6 # This is a push-only token that is safe to be exposed. steps: - name: "Checkout code" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Run FOSSA Scan" - uses: fossas/fossa-action@v1.3.1 # Use a specific version if locking is preferred + uses: fossas/fossa-action@v1.7.0 # Use a specific version if locking is preferred with: api-key: ${{ env.FOSSA_API_KEY }} # REMOVING THIS STEP AS FOSSA API HAS BEEN FAILING FOR MONTHS NOW # - name: "Run FOSSA Test" -# uses: fossas/fossa-action@v1.3.1 # Use a specific version if locking is preferred +# uses: fossas/fossa-action@v1.7.0 # Use a specific version if locking is preferred # with: # api-key: ${{ env.FOSSA_API_KEY }} # run-tests: true diff --git a/.github/workflows/generate-component-metadata-for-tag.yml b/.github/workflows/generate-component-metadata-for-tag.yml index ec4cda1b00..895417001a 100644 --- a/.github/workflows/generate-component-metadata-for-tag.yml +++ b/.github/workflows/generate-component-metadata-for-tag.yml @@ -5,12 +5,17 @@ on: tags: - '*' +permissions: {} + jobs: upload-bundle: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v5 with: @@ -19,7 +24,8 @@ jobs: - name: Build component-metadata-bundle.json run: make bundle-component-metadata - name: Upload component-metadata-bundle.json - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 with: - files: component-metadata-bundle.json \ No newline at end of file + token: ${{ secrets.DAPR_BOT_TOKEN }} + generate_release_notes: true + files: component-metadata-bundle.json diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..47bff87140 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,45 @@ +# Release Guide + +This document describes how to release Dapr Components Contrib along with associated artifacts. + +## Prerequisites + +Only maintainers and the release team should run releases. + +## Pre-release + +Pre-releases use tags like `v1.11.0-rc.0`, `-rc.1`, and so on. They are created through the GitHub Actions workflow. + +### Steps + +1. Update the Dapr runtime and dashboard versions in workflows or tests if needed. Merge that change to master. + +2. Open GitHub Actions and click the **create-release** workflow. + +3. Press the **Run workflow** button. + The workflow will: + + * create the `release-.` branch + * create the pre-release tag + * build the artifacts + +4. Test the produced build. + +5. If there are issues, fix them in the release branch and trigger the workflow again by creating a new pre-release tag (for example `-rc.1`). + +6. Repeat until the build is good. + +## Stable Release + +Create a stable tag without the rc suffix (for example `v1.11.0`). Ensure the new release is set to `latest` and not a `pre-release` +CI will build and publish the release. + +## Patch Releases + +Use the existing release branch. +Create a new pre-release tag like `v1.11.1-rc.0`, test it, and when ready tag the stable version `v1.11.1`. +CI will build and publish it. + +## Project Release Guidelines + +See [this document](https://github.com/dapr/community/blob/master/release-process.md) for the project's release process and guidelines. \ No newline at end of file diff --git a/bindings/alicloud/dingtalk/webhook/metadata.yaml b/bindings/alicloud/dingtalk/webhook/metadata.yaml new file mode 100644 index 0000000000..7cc7dc9f80 --- /dev/null +++ b/bindings/alicloud/dingtalk/webhook/metadata.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: alicloud.dingtalk.webhook +version: v1 +status: alpha +title: "AliCloud DingTalk Webhook" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/alicloud-dingtalk/ +binding: + output: true + input: true + operations: + - name: create + description: "Send a message to DingTalk webhook" + - name: read + description: "Receive messages from DingTalk webhook" +metadata: + - name: id + required: true + description: "The webhook ID" + example: "your-webhook-id" + - name: url + required: true + description: "The webhook URL" + example: '"https://oapi.dingtalk.com/robot/send?access_token=your-token"' + - name: secret + required: false + description: "The webhook secret for signature verification" + example: "your-webhook-secret" diff --git a/bindings/alicloud/oss/metadata.yaml b/bindings/alicloud/oss/metadata.yaml new file mode 100644 index 0000000000..b86983069d --- /dev/null +++ b/bindings/alicloud/oss/metadata.yaml @@ -0,0 +1,48 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: alicloud.oss +version: v1 +status: alpha +title: "AliCloud Object Storage Service (OSS)" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/oss/ +binding: + output: true + input: false + operations: + - name: create + description: "Upload file to OSS" +authenticationProfiles: + - title: "AliCloud Access Key Authentication" + description: | + Authenticate using AliCloud access key credentials. + metadata: + - name: accessKeyID + required: false + description: "The AliCloud access key ID" + example: '"your-access-key-id"' + sensitive: true + - name: accessKeySecret + required: false + sensitive: true + description: "The AliCloud access key secret" + example: '"your-access-key-secret"' + - name: accessKey + required: false + description: "The AliCloud access key" + example: "access-key" +metadata: + - name: endpoint + required: true + description: "The OSS endpoint" + example: "https://oss-cn-hangzhou.aliyuncs.com" + - name: bucket + required: true + description: "The OSS bucket name" + example: "your-bucket-name" + - name: objectKey + required: true + description: "The object key in the bucket" + example: "path/to/file.txt" diff --git a/bindings/alicloud/sls/metadata.yaml b/bindings/alicloud/sls/metadata.yaml new file mode 100644 index 0000000000..069ca26e9c --- /dev/null +++ b/bindings/alicloud/sls/metadata.yaml @@ -0,0 +1,52 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: alicloud.sls +version: v1 +status: alpha +title: "AliCloud Simple Log Storage (SLS)" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/alicloudsls/ +binding: + output: true + input: false + operations: + - name: create + description: "Send logs to SLS" +authenticationProfiles: + - title: "Access Key Authentication" + description: | + Authenticate using AliCloud access key credentials. + metadata: + - name: accessKeyID + required: false + sensitive: true + description: "The AliCloud access key ID" + example: "your-access-key-id" + - name: accessKeySecret + required: false + sensitive: true + description: "The AliCloud access key secret" + example: "your-access-key-secret" +metadata: + - name: endpoint + required: true + description: "The SLS endpoint" + example: "https://your-project.cn-hangzhou.log.aliyuncs.com" + - name: project + required: true + description: "The SLS project name" + example: '"your-project-name"' + - name: logstore + required: true + description: "The SLS logstore name" + example: "your-logstore-name" + - name: topic + required: true + description: "The SLS topic name" + example: "your-topic-name" + - name: source + required: true + description: "The SLS source name" + example: "your-source-name" diff --git a/bindings/alicloud/sls/sls.go b/bindings/alicloud/sls/sls.go index 42d7b4a55c..9cc983736f 100644 --- a/bindings/alicloud/sls/sls.go +++ b/bindings/alicloud/sls/sls.go @@ -60,6 +60,7 @@ func NewAliCloudSlsLogstorage(logger logger.Logger) bindings.OutputBinding { func (s *AliCloudSlsLogstorage) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { // verify the metadata property + // TODO: move these to the struct with proper tags if logProject := req.Metadata["project"]; logProject == "" { return nil, errors.New("SLS binding error: project property not supplied") } diff --git a/bindings/alicloud/tablestore/metadata.yaml b/bindings/alicloud/tablestore/metadata.yaml new file mode 100644 index 0000000000..481791183d --- /dev/null +++ b/bindings/alicloud/tablestore/metadata.yaml @@ -0,0 +1,48 @@ + +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: alicloud.tablestore +version: v1 +status: stable +title: "AliCloud Table Store" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/alicloudtablestore/ +binding: + output: true + input: false + operations: + - name: create + description: "Write data to Table Store" +authenticationProfiles: + - title: "Access Key Authentication" + description: | + Authenticate using AliCloud access key credentials. + metadata: + - name: accessKeyID + required: true + description: "The AliCloud access key ID" + example: "access-key-id" + - name: accessKeySecret + required: true + sensitive: true + description: "The AliCloud access key secret" + example: "access-key-secret" + - name: accessKey + required: true + description: "The AliCloud access key" + example: "access-key" +metadata: + - name: endpoint + required: true + description: "The Table Store endpoint" + example: '"https://your-instance.cn-hangzhou.ots.aliyuncs.com"' + - name: instanceName + required: true + description: "The Table Store instance name" + example: "instance-name" + - name: tableName + required: true + description: "The table name to write to" + example: "table-name" diff --git a/bindings/alicloud/tablestore/tablestore.go b/bindings/alicloud/tablestore/tablestore.go index c153f2c1e0..e8762e8487 100644 --- a/bindings/alicloud/tablestore/tablestore.go +++ b/bindings/alicloud/tablestore/tablestore.go @@ -137,7 +137,7 @@ func (s *AliCloudTableStore) get(req *bindings.InvokeRequest, resp *bindings.Inv pkNames := strings.Split(req.Metadata[primaryKeys], ",") pks := make([]*tablestore.PrimaryKeyColumn, len(pkNames)) - data := make(map[string]interface{}) + data := make(map[string]any) err := json.Unmarshal(req.Data, &data) if err != nil { return err @@ -313,7 +313,7 @@ func (s *AliCloudTableStore) unmarshal(pks []*tablestore.PrimaryKeyColumn, colum return nil, nil } - data := make(map[string]interface{}) + data := make(map[string]any) for _, pk := range pks { data[pk.ColumnName] = pk.Value diff --git a/bindings/apns/apns.go b/bindings/apns/apns.go index b0a08d4532..abb8a99b68 100644 --- a/bindings/apns/apns.go +++ b/bindings/apns/apns.go @@ -33,6 +33,7 @@ import ( kitmd "github.com/dapr/kit/metadata" ) +// TODO: these should be configured in the metadata.yaml file and be part of the metadata struct with proper json tags. const ( collapseIDKey = "apns-collapse-id" developmentKey = "development" @@ -67,6 +68,7 @@ type APNS struct { authorizationBuilder *authorizationBuilder } +// TODO: use proper tags type APNSmetadata struct { Development bool `mapstructure:"development"` KeyID string `mapstructure:"key-id"` diff --git a/bindings/apns/metadata.yaml b/bindings/apns/metadata.yaml new file mode 100644 index 0000000000..44b686a83e --- /dev/null +++ b/bindings/apns/metadata.yaml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: apns +version: v1 +status: alpha +title: "Apple Push Notification Service (APNS)" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/apns/ +binding: + output: true + input: false + operations: + - name: create + description: "Send push notification via APNS" +authenticationProfiles: + - title: "APNS Key Authentication" + description: | + Authenticate using APNS key credentials. + metadata: + - name: key-id + required: true + description: "The APNS key ID" + example: "ABC123DEF4" + - name: team-id + required: true + description: "The APNS team ID" + example: "DEF123GHI4" + - name: private-key + required: true + sensitive: true + description: "The APNS private key (P8 file content)" +metadata: + - name: development + type: bool + required: false + description: "The APNS environment is development or not" + example: "true" + default: "false" diff --git a/bindings/aws/dynamodb/dynamodb.go b/bindings/aws/dynamodb/dynamodb.go index 2096f22433..90f0efaea5 100644 --- a/bindings/aws/dynamodb/dynamodb.go +++ b/bindings/aws/dynamodb/dynamodb.go @@ -18,12 +18,12 @@ import ( "encoding/json" "reflect" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/dapr/components-contrib/bindings" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsCommonAuth "github.com/dapr/components-contrib/common/aws/auth" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" kitmd "github.com/dapr/kit/metadata" @@ -31,11 +31,12 @@ import ( // DynamoDB allows performing stateful operations on AWS DynamoDB. type DynamoDB struct { - authProvider awsAuth.Provider - table string - logger logger.Logger + dynamodbClient awsCommon.DynamoDBClient + table string + logger logger.Logger } +// TODO: the metadata fields need updating to use the builtin aws auth provider fully and reflect in metadata.yaml type dynamoDBMetadata struct { Region string `json:"region" mapstructure:"region"` Endpoint string `json:"endpoint" mapstructure:"endpoint"` @@ -57,7 +58,7 @@ func (d *DynamoDB) Init(ctx context.Context, metadata bindings.Metadata) error { return err } - opts := awsAuth.Options{ + opts := awsCommonAuth.Options{ Logger: d.logger, Properties: metadata.Properties, Region: meta.Region, @@ -67,11 +68,12 @@ func (d *DynamoDB) Init(ctx context.Context, metadata bindings.Metadata) error { SessionToken: meta.SessionToken, } - provider, err := awsAuth.NewProvider(ctx, opts, awsAuth.GetConfig(opts)) + awsConfig, err := awsCommon.NewConfig(ctx, opts) if err != nil { return err } - d.authProvider = provider + + d.dynamodbClient = dynamodb.NewFromConfig(awsConfig) d.table = meta.Table return nil @@ -88,14 +90,14 @@ func (d *DynamoDB) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bi return nil, err } - item, err := dynamodbattribute.MarshalMap(obj) + item, err := attributevalue.MarshalMap(obj) if err != nil { return nil, err } - _, err = d.authProvider.DynamoDB().DynamoDB.PutItemWithContext(ctx, &dynamodb.PutItemInput{ + _, err = d.dynamodbClient.PutItem(ctx, &dynamodb.PutItemInput{ Item: item, - TableName: aws.String(d.table), + TableName: &d.table, }) if err != nil { return nil, err @@ -117,13 +119,14 @@ func (d *DynamoDB) getDynamoDBMetadata(spec bindings.Metadata) (*dynamoDBMetadat // GetComponentMetadata returns the metadata of the component. func (d *DynamoDB) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := dynamoDBMetadata{} - metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) + if err := metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType); err != nil { + if d != nil && d.logger != nil { + d.logger.Errorf("error getting component metadata: %v", err) + } + } return } func (d *DynamoDB) Close() error { - if d.authProvider != nil { - return d.authProvider.Close() - } return nil } diff --git a/bindings/aws/dynamodb/metadata.yaml b/bindings/aws/dynamodb/metadata.yaml new file mode 100644 index 0000000000..488936bfa7 --- /dev/null +++ b/bindings/aws/dynamodb/metadata.yaml @@ -0,0 +1,48 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: aws.dynamodb +version: v1 +status: stable +title: "AWS DynamoDB" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/dynamodb/ +binding: + output: true + input: false + operations: + - name: create + description: "Write item to DynamoDB table" +authenticationProfiles: + - title: "AWS Access Key Authentication" + description: | + Authenticate using AWS access key credentials. + metadata: + - name: accessKey + required: true + description: "The AWS access key" + example: "AKIAIOSFODNN7EXAMPLE" + - name: secretKey + required: true + sensitive: true + description: "The AWS secret key" + example: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + - name: sessionToken + required: false + sensitive: true + description: "The AWS session token" + example: "TOKEN" + - name: region + required: true + description: "The AWS region" + example: "us-east-1" +metadata: + - name: table + required: true + description: "The DynamoDB table name" + example: "my-table" + - name: endpoint + required: false + description: "The DynamoDB endpoint URL" + example: "http://localhost:8000" diff --git a/bindings/aws/kinesis/kinesis.go b/bindings/aws/kinesis/kinesis.go index 0b84b44dcd..3f478f1c99 100644 --- a/bindings/aws/kinesis/kinesis.go +++ b/bindings/aws/kinesis/kinesis.go @@ -22,16 +22,18 @@ import ( "sync/atomic" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/kinesis" + "github.com/aws/aws-sdk-go-v2/service/kinesis/types" + "github.com/cenkalti/backoff/v4" "github.com/google/uuid" - "github.com/vmware/vmware-go-kcl/clientlibrary/interfaces" - "github.com/vmware/vmware-go-kcl/clientlibrary/worker" + "github.com/vmware/vmware-go-kcl-v2/clientlibrary/interfaces" + "github.com/vmware/vmware-go-kcl-v2/clientlibrary/worker" "github.com/dapr/components-contrib/bindings" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsCommonAuth "github.com/dapr/components-contrib/common/aws/auth" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" kitmd "github.com/dapr/kit/metadata" @@ -39,8 +41,7 @@ import ( // AWSKinesis allows receiving and sending data to/from AWS Kinesis stream. type AWSKinesis struct { - authProvider awsAuth.Provider - metadata *kinesisMetadata + metadata *kinesisMetadata worker *worker.Worker @@ -50,11 +51,15 @@ type AWSKinesis struct { logger logger.Logger consumerMode string + kinesisClient *kinesis.Client + awsCfg aws.Config + closed atomic.Bool closeCh chan struct{} wg sync.WaitGroup } +// TODO: we need to clean up the metadata fields here and update this binding to use the builtin aws auth provider and reflect in metadata.yaml type kinesisMetadata struct { StreamName string `json:"streamName" mapstructure:"streamName"` ConsumerName string `json:"consumerName" mapstructure:"consumerName"` @@ -73,7 +78,7 @@ const ( // SharedThroughput - shared throughput using checkpoint and monitoring. SharedThroughput = "shared" - partitionKeyName = "partitionKey" + partitionKeyName = "partitionKey" // TODO: mv to metadata field instead ) // recordProcessorFactory. @@ -117,20 +122,21 @@ func (a *AWSKinesis) Init(ctx context.Context, metadata bindings.Metadata) error a.consumerName = m.ConsumerName a.metadata = m - opts := awsAuth.Options{ + configOpts := awsCommonAuth.Options{ Logger: a.logger, Properties: metadata.Properties, Region: m.Region, + Endpoint: m.Endpoint, AccessKey: m.AccessKey, SecretKey: m.SecretKey, - SessionToken: "", + SessionToken: m.SessionToken, } - // extra configs needed per component type - provider, err := awsAuth.NewProvider(ctx, opts, awsAuth.GetConfig(opts)) + awsCfg, err := awsCommon.NewConfig(ctx, configOpts) if err != nil { - return err + return fmt.Errorf("error getting AWS config: %w", err) } - a.authProvider = provider + a.awsCfg = awsCfg + a.kinesisClient = kinesis.NewFromConfig(awsCfg) return nil } @@ -143,10 +149,10 @@ func (a *AWSKinesis) Invoke(ctx context.Context, req *bindings.InvokeRequest) (* if partitionKey == "" { partitionKey = uuid.New().String() } - _, err := a.authProvider.Kinesis().Kinesis.PutRecordWithContext(ctx, &kinesis.PutRecordInput{ - StreamName: &a.metadata.StreamName, + _, err := a.kinesisClient.PutRecord(ctx, &kinesis.PutRecordInput{ + StreamName: aws.String(a.metadata.StreamName), Data: req.Data, - PartitionKey: &partitionKey, + PartitionKey: aws.String(partitionKey), }) return nil, err @@ -158,30 +164,35 @@ func (a *AWSKinesis) Read(ctx context.Context, handler bindings.Handler) (err er } if a.metadata.KinesisConsumerMode == SharedThroughput { - // Configure the KCL worker with custom endpoints for LocalStack - config := a.authProvider.Kinesis().WorkerCfg(ctx, a.streamName, a.consumerName, a.consumerMode) + // Configure the KCL worker with custom endpoints for LocalStack. + config, cfgErr := awsCommon.NewKinesisWorkerConfig(a.awsCfg, a.streamName, a.consumerName, a.consumerMode) + if cfgErr != nil { + return fmt.Errorf("unable to build kinesis worker configuration: %w", cfgErr) + } if a.metadata.Endpoint != "" { config.KinesisEndpoint = a.metadata.Endpoint config.DynamoDBEndpoint = a.metadata.Endpoint } a.worker = worker.NewWorker(a.recordProcessorFactory(ctx, handler), config) - err = a.worker.Start() - if err != nil { + if err = a.worker.Start(); err != nil { return err } } else if a.metadata.KinesisConsumerMode == ExtendedFanout { - var stream *kinesis.DescribeStreamOutput - stream, err = a.authProvider.Kinesis().Kinesis.DescribeStream(&kinesis.DescribeStreamInput{StreamName: &a.metadata.StreamName}) + var streamResp *kinesis.DescribeStreamOutput + streamResp, err = a.kinesisClient.DescribeStream(ctx, &kinesis.DescribeStreamInput{StreamName: aws.String(a.metadata.StreamName)}) if err != nil { return err } - err = a.Subscribe(ctx, *stream.StreamDescription, handler) + if streamResp.StreamDescription == nil { + return fmt.Errorf("empty stream description for %s", a.metadata.StreamName) + } + err = a.Subscribe(ctx, *streamResp.StreamDescription, handler) if err != nil { return err } } - stream, err := a.authProvider.Kinesis().Stream(ctx, a.streamName) + stream, err := awsCommon.StreamARN(ctx, a.kinesisClient, a.streamName) if err != nil { return fmt.Errorf("failed to get kinesis stream arn: %v", err) } @@ -204,7 +215,9 @@ func (a *AWSKinesis) Read(ctx context.Context, handler bindings.Handler) (err er } // Subscribe to all shards. -func (a *AWSKinesis) Subscribe(ctx context.Context, streamDesc kinesis.StreamDescription, handler bindings.Handler) error { +func (a *AWSKinesis) Subscribe(ctx context.Context, streamDesc types.StreamDescription, + handler bindings.Handler, +) error { consumerARN, err := a.ensureConsumer(ctx, streamDesc.StreamARN) if err != nil { a.logger.Error(err) @@ -215,7 +228,7 @@ func (a *AWSKinesis) Subscribe(ctx context.Context, streamDesc kinesis.StreamDes a.wg.Add(len(streamDesc.Shards)) for i, shard := range streamDesc.Shards { - go func(idx int, s *kinesis.Shard) { + go func(idx int, s types.Shard) { defer a.wg.Done() // Reconnection backoff @@ -231,10 +244,10 @@ func (a *AWSKinesis) Subscribe(ctx context.Context, streamDesc kinesis.StreamDes return default: } - sub, err := a.authProvider.Kinesis().Kinesis.SubscribeToShardWithContext(ctx, &kinesis.SubscribeToShardInput{ + sub, err := a.kinesisClient.SubscribeToShard(ctx, &kinesis.SubscribeToShardInput{ ConsumerARN: consumerARN, ShardId: s.ShardId, - StartingPosition: &kinesis.StartingPosition{Type: aws.String(kinesis.ShardIteratorTypeLatest)}, + StartingPosition: &types.StartingPosition{Type: types.ShardIteratorTypeLatest}, }) if err != nil { wait := bo.NextBackOff() @@ -251,13 +264,13 @@ func (a *AWSKinesis) Subscribe(ctx context.Context, streamDesc kinesis.StreamDes bo.Reset() // Process events - for event := range sub.EventStream.Events() { - switch e := event.(type) { - case *kinesis.SubscribeToShardEvent: - for _, rec := range e.Records { - handler(ctx, &bindings.ReadResponse{ - Data: rec.Data, - }) + for ev := range sub.GetStream().Events() { + switch v := ev.(type) { + case *types.SubscribeToShardEventStreamMemberSubscribeToShardEvent: + if len(v.Value.Records) > 0 { + for _, rec := range v.Value.Records { + handler(ctx, &bindings.ReadResponse{Data: rec.Data}) + } } } } @@ -273,9 +286,6 @@ func (a *AWSKinesis) Close() error { close(a.closeCh) } a.wg.Wait() - if a.authProvider != nil { - return a.authProvider.Close() - } return nil } @@ -283,20 +293,23 @@ func (a *AWSKinesis) ensureConsumer(ctx context.Context, streamARN *string) (*st // Only set timeout on consumer call. conCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - consumer, err := a.authProvider.Kinesis().Kinesis.DescribeStreamConsumerWithContext(conCtx, &kinesis.DescribeStreamConsumerInput{ - ConsumerName: &a.metadata.ConsumerName, + consumer, err := a.kinesisClient.DescribeStreamConsumer(conCtx, &kinesis.DescribeStreamConsumerInput{ + ConsumerName: aws.String(a.metadata.ConsumerName), StreamARN: streamARN, }) if err != nil { return a.registerConsumer(ctx, streamARN) } - return consumer.ConsumerDescription.ConsumerARN, nil + if consumer.ConsumerDescription != nil { + return consumer.ConsumerDescription.ConsumerARN, nil + } + return nil, errors.New("empty consumer description") } func (a *AWSKinesis) registerConsumer(ctx context.Context, streamARN *string) (*string, error) { - consumer, err := a.authProvider.Kinesis().Kinesis.RegisterStreamConsumerWithContext(ctx, &kinesis.RegisterStreamConsumerInput{ - ConsumerName: &a.metadata.ConsumerName, + consumer, err := a.kinesisClient.RegisterStreamConsumer(ctx, &kinesis.RegisterStreamConsumerInput{ + ConsumerName: aws.String(a.metadata.ConsumerName), StreamARN: streamARN, }) if err != nil { @@ -304,26 +317,30 @@ func (a *AWSKinesis) registerConsumer(ctx context.Context, streamARN *string) (* } err = a.waitUntilConsumerExists(ctx, &kinesis.DescribeStreamConsumerInput{ - ConsumerName: &a.metadata.ConsumerName, + ConsumerName: aws.String(a.metadata.ConsumerName), StreamARN: streamARN, }) if err != nil { return nil, err } - return consumer.Consumer.ConsumerARN, nil + if consumer.Consumer != nil { + return consumer.Consumer.ConsumerARN, nil + } + + return nil, errors.New("empty consumer ARN after register") } func (a *AWSKinesis) deregisterConsumer(ctx context.Context, streamARN *string, consumerARN *string) error { if a.consumerARN != nil { // Use a background context because the running context may have been canceled already ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - _, err := a.authProvider.Kinesis().Kinesis.DeregisterStreamConsumerWithContext(ctx, &kinesis.DeregisterStreamConsumerInput{ + defer cancel() + _, err := a.kinesisClient.DeregisterStreamConsumer(ctx, &kinesis.DeregisterStreamConsumerInput{ ConsumerARN: consumerARN, StreamARN: streamARN, - ConsumerName: &a.metadata.ConsumerName, + ConsumerName: aws.String(a.metadata.ConsumerName), }) - cancel() return err } @@ -331,34 +348,34 @@ func (a *AWSKinesis) deregisterConsumer(ctx context.Context, streamARN *string, return nil } -func (a *AWSKinesis) waitUntilConsumerExists(ctx aws.Context, input *kinesis.DescribeStreamConsumerInput, opts ...request.WaiterOption) error { - w := request.Waiter{ - Name: "WaitUntilConsumerExists", - MaxAttempts: 18, - Delay: request.ConstantWaiterDelay(10 * time.Second), - Acceptors: []request.WaiterAcceptor{ - { - State: request.SuccessWaiterState, - Matcher: request.PathWaiterMatch, Argument: "ConsumerDescription.ConsumerStatus", - Expected: "ACTIVE", - }, - }, - NewRequest: func(opts []request.Option) (*request.Request, error) { - var inCpy *kinesis.DescribeStreamConsumerInput - if input != nil { - tmp := *input - inCpy = &tmp - } - req, _ := a.authProvider.Kinesis().Kinesis.DescribeStreamConsumerRequest(inCpy) - req.SetContext(ctx) - req.ApplyOptions(opts...) +func (a *AWSKinesis) waitUntilConsumerExists(ctx context.Context, input *kinesis.DescribeStreamConsumerInput) error { + bo := backoff.NewExponentialBackOff() + bo.InitialInterval = 10 * time.Second + bo.MaxElapsedTime = 3 * time.Minute - return req, nil - }, - } - w.ApplyOptions(opts...) + pollCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) + defer cancel() - return w.WaitWithContext(ctx) + for { + select { + case <-pollCtx.Done(): + return fmt.Errorf("timed out waiting for consumer to become ACTIVE: %w", pollCtx.Err()) + default: + resp, err := a.kinesisClient.DescribeStreamConsumer(ctx, input) + if err == nil && resp.ConsumerDescription != nil && resp.ConsumerDescription.ConsumerStatus == types.ConsumerStatusActive { + return nil + } + wait := bo.NextBackOff() + if wait == backoff.Stop { + return errors.New("consumer did not become active in time") + } + select { + case <-pollCtx.Done(): + return fmt.Errorf("timed out waiting for consumer to become ACTIVE: %w", pollCtx.Err()) + case <-time.After(wait): + } + } + } } func (a *AWSKinesis) parseMetadata(meta bindings.Metadata) (*kinesisMetadata, error) { @@ -387,7 +404,7 @@ func (r *recordProcessorFactory) CreateProcessor() interfaces.IRecordProcessor { } func (p *recordProcessor) Initialize(input *interfaces.InitializationInput) { - p.logger.Infof("Processing ShardId: %v at checkpoint: %v", input.ShardId, aws.StringValue(input.ExtendedSequenceNumber.SequenceNumber)) + p.logger.Infof("Processing ShardId: %v at checkpoint: %v", input.ShardId, aws.ToString(input.ExtendedSequenceNumber.SequenceNumber)) } func (p *recordProcessor) ProcessRecords(input *interfaces.ProcessRecordsInput) { diff --git a/bindings/aws/kinesis/kinesis_test.go b/bindings/aws/kinesis/kinesis_test.go index aaca0c3c0a..1b7dccea47 100644 --- a/bindings/aws/kinesis/kinesis_test.go +++ b/bindings/aws/kinesis/kinesis_test.go @@ -14,11 +14,14 @@ limitations under the License. package kinesis import ( + "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/dapr/components-contrib/bindings" ) @@ -46,3 +49,20 @@ func TestParseMetadata(t *testing.T) { assert.Equal(t, "token", meta.SessionToken) assert.Equal(t, "extended", meta.KinesisConsumerMode) } + +func TestReadConfigError(t *testing.T) { + kinesis := &AWSKinesis{ + metadata: &kinesisMetadata{KinesisConsumerMode: SharedThroughput}, + awsCfg: aws.Config{}, + streamName: "stream", + consumerName: "consumer", + consumerMode: SharedThroughput, + } + err := kinesis.Read(t.Context(), func(ctx context.Context, resp *bindings.ReadResponse) ([]byte, error) { + return nil, nil + }) + require.Error(t, err) + errStr := err.Error() + assert.Contains(t, errStr, "unable to build kinesis worker configuration") + assert.Contains(t, errStr, "region is required for Kinesis worker config") +} diff --git a/bindings/aws/kinesis/metadata.yaml b/bindings/aws/kinesis/metadata.yaml new file mode 100644 index 0000000000..b9563973cf --- /dev/null +++ b/bindings/aws/kinesis/metadata.yaml @@ -0,0 +1,66 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: aws.kinesis +version: v1 +status: alpha +title: "AWS Kinesis" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/kinesis/ +binding: + output: true + input: true + operations: + - name: create + description: "Send record to Kinesis stream" + - name: read + description: "Receive records from Kinesis stream" +authenticationProfiles: + - title: "AWS Access Key Authentication" + description: | + Authenticate using AWS access key credentials. + metadata: + - name: accessKey + required: true + description: "The AWS access key" + example: "AKIAIOSFODNN7EXAMPLE" + - name: secretKey + required: true + sensitive: true + description: "The AWS secret key" + example: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + - name: sessionToken + required: false + sensitive: true + description: "The AWS session token" + example: "TOKEN" + - name: region + required: true + description: "The AWS region" + example: "us-east-1" +metadata: + - name: streamName + required: true + description: "The Kinesis stream name" + example: "my-stream" + - name: consumerName + required: false + description: "The consumer name for input binding" + example: "my-consumer" + - name: mode + required: false + description: "The consumer mode" + example: "shared" + default: "shared" + allowedValues: + - "shared" + - "extended" + - name: partitionKey + required: false + description: "The partition key for the Kinesis stream" + example: "partition-key" + - name: endpoint + required: false + description: "The Kinesis endpoint URL" + example: "http://localhost:4566" diff --git a/bindings/aws/s3/s3.go b/bindings/aws/s3/s3.go index 5b82b05192..49216eed17 100644 --- a/bindings/aws/s3/s3.go +++ b/bindings/aws/s3/s3.go @@ -27,14 +27,17 @@ import ( "strings" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/smithy-go" "github.com/google/uuid" + "github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager" + tmtypes "github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager/types" + "github.com/dapr/components-contrib/bindings" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsCommonAuth "github.com/dapr/components-contrib/common/aws/auth" commonutils "github.com/dapr/components-contrib/common/utils" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" @@ -60,9 +63,11 @@ const ( // AWSS3 is a binding for an AWS S3 storage bucket. type AWSS3 struct { - metadata *s3Metadata - authProvider awsAuth.Provider - logger logger.Logger + metadata *s3Metadata + s3Client *s3.Client + tmClient *transfermanager.Client + presigner *s3.PresignClient + logger logger.Logger } type s3Metadata struct { @@ -106,26 +111,6 @@ func NewAWSS3(logger logger.Logger) bindings.OutputBinding { return &AWSS3{logger: logger} } -func (s *AWSS3) getAWSConfig(opts awsAuth.Options) *aws.Config { - cfg := awsAuth.GetConfig(opts).WithS3ForcePathStyle(s.metadata.ForcePathStyle).WithDisableSSL(s.metadata.DisableSSL) - - // Use a custom HTTP client to allow self-signed certs - if s.metadata.InsecureSSL { - customTransport := http.DefaultTransport.(*http.Transport).Clone() - customTransport.TLSClientConfig = &tls.Config{ - //nolint:gosec - InsecureSkipVerify: true, - } - client := &http.Client{ - Transport: customTransport, - } - cfg = cfg.WithHTTPClient(client) - - s.logger.Infof("aws s3: you are using 'insecureSSL' to skip server config verify which is unsafe!") - } - return cfg -} - // Init does metadata parsing and connection creation. func (s *AWSS3) Init(ctx context.Context, metadata bindings.Metadata) error { m, err := s.parseMetadata(metadata) @@ -134,7 +119,11 @@ func (s *AWSS3) Init(ctx context.Context, metadata bindings.Metadata) error { } s.metadata = m - opts := awsAuth.Options{ + if s.metadata.DisableSSL && s.metadata.Endpoint != "" && !strings.HasPrefix(s.metadata.Endpoint, "http://") && !strings.HasPrefix(s.metadata.Endpoint, "https://") { + s.metadata.Endpoint = "http://" + s.metadata.Endpoint + } + + configOpts := awsCommonAuth.Options{ Logger: s.logger, Properties: metadata.Properties, Region: m.Region, @@ -143,20 +132,38 @@ func (s *AWSS3) Init(ctx context.Context, metadata bindings.Metadata) error { SecretKey: m.SecretKey, SessionToken: m.SessionToken, } - // extra configs needed per component type - provider, err := awsAuth.NewProvider(ctx, opts, s.getAWSConfig(opts)) + + var awsCfg aws.Config + if s.metadata.InsecureSSL { + customTransport := http.DefaultTransport.(*http.Transport).Clone() + customTransport.TLSClientConfig = &tls.Config{ + //nolint:gosec + InsecureSkipVerify: true, + } + client := &http.Client{Transport: customTransport} + awsCfg, err = awsCommon.NewConfig(ctx, configOpts, awsCommon.WithHTTPClient(client)) + if err == nil { + s.logger.Infof("aws s3: you are using 'insecureSSL' to skip server config verify which is unsafe!") + } + } else { + awsCfg, err = awsCommon.NewConfig(ctx, configOpts) + } if err != nil { return err } - s.authProvider = provider + + s.s3Client = s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = s.metadata.ForcePathStyle + }) + // transfermanager for multipart/managed uploads + s.tmClient = transfermanager.New(s.s3Client) + s.presigner = s3.NewPresignClient(s.s3Client) return nil } func (s *AWSS3) Close() error { - if s.authProvider != nil { - return s.authProvider.Close() - } + // nothing to cleanup return nil } @@ -220,18 +227,46 @@ func (s *AWSS3) create(ctx context.Context, req *bindings.InvokeRequest) (*bindi storageClass = aws.String(metadata.StorageClass) } - resultUpload, err := s.authProvider.S3().Uploader.UploadWithContext(ctx, &s3manager.UploadInput{ - Bucket: ptr.Of(metadata.Bucket), - Key: ptr.Of(key), - Body: r, - ContentType: contentType, - StorageClass: storageClass, - Tagging: tagging, - }) + uploadIn := &transfermanager.UploadObjectInput{ + Bucket: ptr.Of(metadata.Bucket), + Key: ptr.Of(key), + Body: r, + } + if contentType != nil { + uploadIn.ContentType = contentType + } + if storageClass != nil { + uploadIn.StorageClass = tmtypes.StorageClass(*storageClass) + } + if tagging != nil { + uploadIn.Tagging = tagging + } + + uploadOut, err := s.tmClient.UploadObject(ctx, uploadIn) if err != nil { return nil, fmt.Errorf("s3 binding error: uploading failed: %w", err) } + // location/path construction + var location string + if uploadOut != nil && uploadOut.Location != nil && *uploadOut.Location != "" { + location = *uploadOut.Location + } else if s.metadata.Endpoint != "" { + ep := strings.TrimRight(s.metadata.Endpoint, "/") + location = fmt.Sprintf("%s/%s/%s", ep, metadata.Bucket, key) + } else { + if s.metadata.ForcePathStyle { + location = fmt.Sprintf("https://s3.amazonaws.com/%s/%s", metadata.Bucket, key) + } else { + location = fmt.Sprintf("https://%s.s3.amazonaws.com/%s", metadata.Bucket, key) + } + } + + resultLocation := location + var versionID *string + if uploadOut != nil { + versionID = uploadOut.VersionID + } var presignURL string if metadata.PresignTTL != "" { url, presignErr := s.presignObject(ctx, metadata.Bucket, key, metadata.PresignTTL) @@ -243,8 +278,8 @@ func (s *AWSS3) create(ctx context.Context, req *bindings.InvokeRequest) (*bindi } jsonResponse, err := json.Marshal(createResponse{ - Location: resultUpload.Location, - VersionID: resultUpload.VersionID, + Location: resultLocation, + VersionID: versionID, PresignURL: presignURL, }) if err != nil { @@ -296,16 +331,14 @@ func (s *AWSS3) presignObject(ctx context.Context, bucket, key, ttl string) (str if err != nil { return "", fmt.Errorf("s3 binding error: cannot parse duration %s: %w", ttl, err) } - objReq, _ := s.authProvider.S3().S3.GetObjectRequest(&s3.GetObjectInput{ + presignResult, err := s.presigner.PresignGetObject(ctx, &s3.GetObjectInput{ Bucket: ptr.Of(bucket), Key: ptr.Of(key), - }) - url, err := objReq.Presign(d) + }, func(po *s3.PresignOptions) { po.Expires = d }) if err != nil { return "", fmt.Errorf("s3 binding error: failed to presign URL: %w", err) } - - return url, nil + return presignResult.URL, nil } func (s *AWSS3) get(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { @@ -319,28 +352,30 @@ func (s *AWSS3) get(ctx context.Context, req *bindings.InvokeRequest) (*bindings return nil, fmt.Errorf("s3 binding error: required metadata '%s' missing", metadataKey) } - buff := &aws.WriteAtBuffer{} - _, err = s.authProvider.S3().Downloader.DownloadWithContext(ctx, - buff, - &s3.GetObjectInput{ - Bucket: ptr.Of(s.metadata.Bucket), - Key: ptr.Of(key), - }, - ) + resp, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: ptr.Of(s.metadata.Bucket), + Key: ptr.Of(key), + }) if err != nil { - var awsErr awserr.Error - if errors.As(err, &awsErr) && awsErr.Code() == s3.ErrCodeNoSuchKey { + var apiErr smithy.APIError + if errors.As(err, &apiErr) && apiErr.ErrorCode() == "NoSuchKey" { return nil, errors.New("object not found") } return nil, fmt.Errorf("s3 binding error: error downloading S3 object: %w", err) } + defer resp.Body.Close() + + dataBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("s3 binding error: error reading S3 object body: %w", err) + } var data []byte if metadata.EncodeBase64 { - encoded := b64.StdEncoding.EncodeToString(buff.Bytes()) + encoded := b64.StdEncoding.EncodeToString(dataBytes) data = []byte(encoded) } else { - data = buff.Bytes() + data = dataBytes } return &bindings.InvokeResponse{ @@ -354,16 +389,13 @@ func (s *AWSS3) delete(ctx context.Context, req *bindings.InvokeRequest) (*bindi if key == "" { return nil, fmt.Errorf("s3 binding error: required metadata '%s' missing", metadataKey) } - _, err := s.authProvider.S3().S3.DeleteObjectWithContext( - ctx, - &s3.DeleteObjectInput{ - Bucket: ptr.Of(s.metadata.Bucket), - Key: ptr.Of(key), - }, - ) + _, err := s.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: ptr.Of(s.metadata.Bucket), + Key: ptr.Of(key), + }) if err != nil { - var awsErr awserr.Error - if errors.As(err, &awsErr) && awsErr.Code() == s3.ErrCodeNoSuchKey { + var apiErr smithy.APIError + if errors.As(err, &apiErr) && apiErr.ErrorCode() == "NoSuchKey" { return nil, errors.New("object not found") } return nil, fmt.Errorf("s3 binding error: delete operation failed: %w", err) @@ -383,9 +415,9 @@ func (s *AWSS3) list(ctx context.Context, req *bindings.InvokeRequest) (*binding if payload.MaxResults < 1 { payload.MaxResults = defaultMaxResults } - result, err := s.authProvider.S3().S3.ListObjectsWithContext(ctx, &s3.ListObjectsInput{ + result, err := s.s3Client.ListObjects(ctx, &s3.ListObjectsInput{ Bucket: ptr.Of(s.metadata.Bucket), - MaxKeys: ptr.Of(int64(payload.MaxResults)), + MaxKeys: ptr.Of(payload.MaxResults), Marker: ptr.Of(payload.Marker), Prefix: ptr.Of(payload.Prefix), Delimiter: ptr.Of(payload.Delimiter), @@ -447,7 +479,7 @@ func (s *AWSS3) parseS3Tags(raw string) (*string, error) { return nil, nil } - return aws.String(strings.Join(pairs, "&")), nil + return ptr.Of(strings.Join(pairs, "&")), nil } // Helper to merge config and request metadata. diff --git a/bindings/aws/s3/s3_test.go b/bindings/aws/s3/s3_test.go index 379e27af72..cdde4b8645 100644 --- a/bindings/aws/s3/s3_test.go +++ b/bindings/aws/s3/s3_test.go @@ -16,6 +16,9 @@ package s3 import ( "testing" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -208,3 +211,59 @@ func TestDeleteOption(t *testing.T) { require.Error(t, err) }) } + +func TestInitCreatesV2Clients(t *testing.T) { + s3 := NewAWSS3(logger.NewLogger("s3")).(*AWSS3) + + md := bindings.Metadata{} + md.Properties = map[string]string{ + "bucket": "test-bucket", + "region": "us-west-2", + } + + reqCtx := t.Context() + err := s3.Init(reqCtx, md) + require.NoError(t, err) + require.NotNil(t, s3.s3Client) + require.NotNil(t, s3.tmClient) + require.NotNil(t, s3.presigner) +} + +func TestForcePathStylePresignURL(t *testing.T) { + bucket := "dapr-s3-test" + key := "filename.txt" + region := "us-east-1" + + cfg := aws.Config{ + Region: region, + Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider("AKID", "SECRET", "")), + } + + t.Run("forcePathStyle=false", func(t *testing.T) { + s3Client := s3.NewFromConfig(cfg) + presignClient := s3.NewPresignClient(s3Client) + presigned, err := presignClient.PresignGetObject(t.Context(), &s3.GetObjectInput{ + Bucket: &bucket, + Key: &key, + }) + require.NoError(t, err) + require.Contains(t, presigned.URL, ".s3.") + require.Contains(t, presigned.URL, bucket) + require.Contains(t, presigned.URL, key) + require.Equal(t, "https://"+bucket+".s3."+region+".amazonaws.com/"+key, presigned.URL[:len("https://"+bucket+".s3."+region+".amazonaws.com/"+key)]) + }) + + t.Run("forcePathStyle=true", func(t *testing.T) { + s3Client := s3.NewFromConfig(cfg, func(o *s3.Options) { + o.UsePathStyle = true + }) + presignClient := s3.NewPresignClient(s3Client) + presigned, err := presignClient.PresignGetObject(t.Context(), &s3.GetObjectInput{ + Bucket: &bucket, + Key: &key, + }) + require.NoError(t, err) + require.Contains(t, presigned.URL, "/"+bucket+"/"+key) + require.Equal(t, "https://s3."+region+".amazonaws.com/"+bucket+"/"+key, presigned.URL[:len("https://s3."+region+".amazonaws.com/"+bucket+"/"+key)]) + }) +} diff --git a/bindings/aws/ses/metadata.yaml b/bindings/aws/ses/metadata.yaml new file mode 100644 index 0000000000..fb2f48b074 --- /dev/null +++ b/bindings/aws/ses/metadata.yaml @@ -0,0 +1,60 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: aws.ses +version: v1 +status: stable +title: "AWS Simple Email Service (SES)" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/ses/ +binding: + output: true + input: false + operations: + - name: create + description: "Send email via AWS SES" +authenticationProfiles: + - title: "AWS Access Key Authentication" + description: | + Authenticate using AWS access key credentials. + metadata: + - name: accessKey + required: true + description: "The AWS access key" + example: "AKIAIOSFODNN7EXAMPLE" + - name: secretKey + required: true + sensitive: true + description: "The AWS secret key" + example: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + - name: sessionToken + required: false + sensitive: true + description: "The AWS session token" + example: "TOKEN" + - name: region + required: true + description: "The AWS region" + example: "us-east-1" +metadata: + - name: emailFrom + required: true + description: "The sender email address" + example: "sender@example.com" + - name: emailTo + required: true + description: "The recipient email address" + example: "recipient@example.com" + - name: subject + required: true + description: "The email subject" + example: "Hello from Dapr" + - name: emailCc + required: false + description: "The email CC address" + example: "cc@example.com" + - name: emailBcc + required: false + description: "The email BCC address" + example: "bcc@example.com" diff --git a/bindings/aws/ses/ses.go b/bindings/aws/ses/ses.go index b8d2ff3faa..48cde89808 100644 --- a/bindings/aws/ses/ses.go +++ b/bindings/aws/ses/ses.go @@ -21,11 +21,15 @@ import ( "strconv" "strings" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ses" + "github.com/aws/aws-sdk-go-v2/service/ses/types" + + awsCommon "github.com/dapr/components-contrib/common/aws" + awsCommonAuth "github.com/dapr/components-contrib/common/aws/auth" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ses" "github.com/dapr/components-contrib/bindings" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" contribMetadata "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" kitmd "github.com/dapr/kit/metadata" @@ -38,11 +42,13 @@ const ( // AWSSES is an AWS SNS binding. type AWSSES struct { - authProvider awsAuth.Provider - metadata *sesMetadata - logger logger.Logger + metadata *sesMetadata + logger logger.Logger + + sesClient *ses.Client } +// TODO: the metadata fields need updating to use the builtin aws auth provider fully and reflect in metadata.yaml type sesMetadata struct { Region string `json:"region"` AccessKey string `json:"accessKey"` @@ -70,21 +76,21 @@ func (a *AWSSES) Init(ctx context.Context, metadata bindings.Metadata) error { a.metadata = m - opts := awsAuth.Options{ + configOpts := awsCommonAuth.Options{ Logger: a.logger, Properties: metadata.Properties, Region: m.Region, AccessKey: m.AccessKey, SecretKey: m.SecretKey, - SessionToken: "", + SessionToken: m.SessionToken, } - // extra configs needed per component type - provider, err := awsAuth.NewProvider(ctx, opts, awsAuth.GetConfig(opts)) + + awsConfig, err := awsCommon.NewConfig(ctx, configOpts) if err != nil { - return err + return fmt.Errorf("failed to create AWS config: %w", err) } - a.authProvider = provider + a.sesClient = ses.NewFromConfig(awsConfig) return nil } @@ -117,19 +123,30 @@ func (a *AWSSES) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bind return nil, fmt.Errorf("SES binding error. Can't unquote data field: %w", err) } + // Create destination email addresses. + var destination types.Destination + + if metadata.EmailTo != "" { + destination.ToAddresses = strings.Split(metadata.EmailTo, ";") + } + if metadata.EmailCc != "" { + destination.CcAddresses = strings.Split(metadata.EmailCc, ";") + } + if metadata.EmailBcc != "" { + destination.BccAddresses = strings.Split(metadata.EmailBcc, ";") + } + // Assemble the email. input := &ses.SendEmailInput{ - Destination: &ses.Destination{ - ToAddresses: aws.StringSlice(strings.Split(metadata.EmailTo, ";")), - }, - Message: &ses.Message{ - Body: &ses.Body{ - Html: &ses.Content{ + Destination: &destination, + Message: &types.Message{ + Body: &types.Body{ + Html: &types.Content{ Charset: aws.String(CharSet), Data: aws.String(body), }, }, - Subject: &ses.Content{ + Subject: &types.Content{ Charset: aws.String(CharSet), Data: aws.String(metadata.Subject), }, @@ -139,19 +156,8 @@ func (a *AWSSES) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bind // ConfigurationSetName: aws.String(ConfigurationSet), } - if metadata.EmailCc != "" { - input.SetDestination(&ses.Destination{ - CcAddresses: aws.StringSlice(strings.Split(metadata.EmailCc, ";")), - }) - } - if metadata.EmailBcc != "" { - input.SetDestination(&ses.Destination{ - BccAddresses: aws.StringSlice(strings.Split(metadata.EmailBcc, ";")), - }) - } - // Attempt to send the email. - result, err := a.authProvider.Ses().Ses.SendEmail(input) + result, err := a.sesClient.SendEmail(ctx, input) if err != nil { return nil, fmt.Errorf("SES binding error. Sending email failed: %w", err) } @@ -176,8 +182,6 @@ func (a *AWSSES) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMa } func (a *AWSSES) Close() error { - if a.authProvider != nil { - return a.authProvider.Close() - } + // Removed authprovider close method return nil } diff --git a/bindings/aws/sns/sns.go b/bindings/aws/sns/sns.go index 370cabdf25..281e6dbc92 100644 --- a/bindings/aws/sns/sns.go +++ b/bindings/aws/sns/sns.go @@ -19,10 +19,12 @@ import ( "fmt" "reflect" - "github.com/aws/aws-sdk-go/service/sns" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/dapr/components-contrib/bindings" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsAuth "github.com/dapr/components-contrib/common/aws/auth" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" kitmd "github.com/dapr/kit/metadata" @@ -30,10 +32,9 @@ import ( // AWSSNS is an AWS SNS binding. type AWSSNS struct { - authProvider awsAuth.Provider - topicARN string - - logger logger.Logger + snsClient *sns.Client + topicARN string + logger logger.Logger } type snsMetadata struct { @@ -65,7 +66,7 @@ func (a *AWSSNS) Init(ctx context.Context, metadata bindings.Metadata) error { return err } - opts := awsAuth.Options{ + authOptions := awsAuth.Options{ Logger: a.logger, Properties: metadata.Properties, Region: m.Region, @@ -74,12 +75,13 @@ func (a *AWSSNS) Init(ctx context.Context, metadata bindings.Metadata) error { SecretKey: m.SecretKey, SessionToken: m.SessionToken, } - // extra configs needed per component type - provider, err := awsAuth.NewProvider(ctx, opts, awsAuth.GetConfig(opts)) + + awsConfig, err := awsCommon.NewConfig(ctx, authOptions) if err != nil { return err } - a.authProvider = provider + + a.snsClient = sns.NewFromConfig(awsConfig) a.topicARN = m.TopicArn return nil @@ -109,10 +111,10 @@ func (a *AWSSNS) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bind msg := fmt.Sprintf("%v", payload.Message) subject := fmt.Sprintf("%v", payload.Subject) - _, err = a.authProvider.Sns().Sns.PublishWithContext(ctx, &sns.PublishInput{ - Message: &msg, - Subject: &subject, - TopicArn: &a.topicARN, + _, err = a.snsClient.Publish(ctx, &sns.PublishInput{ + Message: aws.String(msg), + Subject: aws.String(subject), + TopicArn: aws.String(a.topicARN), }) if err != nil { return nil, err @@ -124,13 +126,13 @@ func (a *AWSSNS) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bind // GetComponentMetadata returns the metadata of the component. func (a *AWSSNS) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := snsMetadata{} - metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) + err := metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) + if err != nil { + a.logger.Errorf("failed to get component metadata: %v", err) + } return } func (a *AWSSNS) Close() error { - if a.authProvider != nil { - return a.authProvider.Close() - } return nil } diff --git a/bindings/aws/sqs/metadata.yaml b/bindings/aws/sqs/metadata.yaml new file mode 100644 index 0000000000..3bb0d13cf7 --- /dev/null +++ b/bindings/aws/sqs/metadata.yaml @@ -0,0 +1,50 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: aws.sqs +version: v1 +status: alpha +title: "AWS SQS" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/sqs/ +binding: + output: true + input: true + operations: + - name: create + description: "Send message to SQS queue" + - name: read + description: "Receive messages from SQS queue" +authenticationProfiles: + - title: "AWS Access Key Authentication" + description: | + Authenticate using AWS access key credentials. + metadata: + - name: accessKey + required: true + description: "The AWS access key" + example: "AKIAIOSFODNN7EXAMPLE" + - name: secretKey + required: true + sensitive: true + description: "The AWS secret key" + example: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + - name: sessionToken + required: false + sensitive: true + description: "The AWS session token" + example: '"TOKEN"' + - name: region + required: true + description: "The AWS region" + example: "us-east-1" +metadata: + - name: queueName + required: true + description: "The SQS queue name" + example: "my-queue" + - name: endpoint + required: false + description: "The SQS endpoint URL" + example: "http://localhost:4566" diff --git a/bindings/aws/sqs/sqs.go b/bindings/aws/sqs/sqs.go index b09fde61f6..a97bb4383e 100644 --- a/bindings/aws/sqs/sqs.go +++ b/bindings/aws/sqs/sqs.go @@ -21,11 +21,12 @@ import ( "sync/atomic" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go-v2/service/sqs" + sqsTypes "github.com/aws/aws-sdk-go-v2/service/sqs/types" "github.com/dapr/components-contrib/bindings" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsCommonAuth "github.com/dapr/components-contrib/common/aws/auth" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" kitmd "github.com/dapr/kit/metadata" @@ -33,14 +34,15 @@ import ( // AWSSQS allows receiving and sending data to/from AWS SQS. type AWSSQS struct { - authProvider awsAuth.Provider - queueName string - logger logger.Logger - wg sync.WaitGroup - closeCh chan struct{} - closed atomic.Bool + sqsClient *sqs.Client + queueName string + logger logger.Logger + wg sync.WaitGroup + closeCh chan struct{} + closed atomic.Bool } +// TODO: the metadata fields need updating to use the builtin aws auth provider fully and reflect in metadata.yaml type sqsMetadata struct { QueueName string `json:"queueName"` Region string `json:"region"` @@ -65,7 +67,7 @@ func (a *AWSSQS) Init(ctx context.Context, metadata bindings.Metadata) error { return err } - opts := awsAuth.Options{ + configOpts := awsCommonAuth.Options{ Logger: a.logger, Properties: metadata.Properties, Region: m.Region, @@ -74,14 +76,12 @@ func (a *AWSSQS) Init(ctx context.Context, metadata bindings.Metadata) error { SecretKey: m.SecretKey, SessionToken: m.SessionToken, } - // extra configs needed per component type - provider, err := awsAuth.NewProvider(ctx, opts, awsAuth.GetConfig(opts)) + awsConfig, err := awsCommon.NewConfig(ctx, configOpts) if err != nil { return err } - a.authProvider = provider + a.sqsClient = sqs.NewFromConfig(awsConfig) a.queueName = m.QueueName - return nil } @@ -91,16 +91,15 @@ func (a *AWSSQS) Operations() []bindings.OperationKind { func (a *AWSSQS) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { msgBody := string(req.Data) - url, err := a.authProvider.Sqs().QueueURL(ctx, a.queueName) + url, err := a.getQueueURL(ctx, a.queueName) if err != nil { a.logger.Errorf("failed to get queue url: %v", err) + return nil, err } - - _, err = a.authProvider.Sqs().Sqs.SendMessageWithContext(ctx, &sqs.SendMessageInput{ + _, err = a.sqsClient.SendMessage(ctx, &sqs.SendMessageInput{ MessageBody: &msgBody, QueueUrl: url, }) - return nil, err } @@ -108,36 +107,29 @@ func (a *AWSSQS) Read(ctx context.Context, handler bindings.Handler) error { if a.closed.Load() { return errors.New("binding is closed") } - a.wg.Add(1) go func() { defer a.wg.Done() - - // Repeat until the context is canceled or component is closed for { if ctx.Err() != nil || a.closed.Load() { return } - url, err := a.authProvider.Sqs().QueueURL(ctx, a.queueName) + url, err := a.getQueueURL(ctx, a.queueName) if err != nil { a.logger.Errorf("failed to get queue url: %v", err) + continue } - - result, err := a.authProvider.Sqs().Sqs.ReceiveMessageWithContext(ctx, &sqs.ReceiveMessageInput{ - QueueUrl: url, - AttributeNames: aws.StringSlice([]string{ - "SentTimestamp", - }), - MaxNumberOfMessages: aws.Int64(1), - MessageAttributeNames: aws.StringSlice([]string{ - "All", - }), - WaitTimeSeconds: aws.Int64(20), + result, err := a.sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{ + QueueUrl: url, + AttributeNames: []sqsTypes.QueueAttributeName{"SentTimestamp"}, // Use string literal for attribute name + MaxNumberOfMessages: 1, + MessageAttributeNames: []string{"All"}, + WaitTimeSeconds: 20, }) if err != nil { a.logger.Errorf("Unable to receive message from queue %q, %v.", url, err) + continue } - if len(result.Messages) > 0 { for _, m := range result.Messages { body := m.Body @@ -147,16 +139,18 @@ func (a *AWSSQS) Read(ctx context.Context, handler bindings.Handler) error { _, err := handler(ctx, &res) if err == nil { msgHandle := m.ReceiptHandle - - // Use a background context here because ctx may be canceled already - a.authProvider.Sqs().Sqs.DeleteMessageWithContext(context.Background(), &sqs.DeleteMessageInput{ - QueueUrl: url, - ReceiptHandle: msgHandle, - }) + if msgHandle != nil { + _, deleteError := a.sqsClient.DeleteMessage(context.Background(), &sqs.DeleteMessageInput{ + QueueUrl: url, + ReceiptHandle: msgHandle, + }) + if deleteError != nil { + a.logger.Errorf("failed to delete message from queue %q: %v", url, deleteError) + } + } } } } - select { case <-ctx.Done(): case <-a.closeCh: @@ -164,18 +158,24 @@ func (a *AWSSQS) Read(ctx context.Context, handler bindings.Handler) error { } } }() - return nil } +func (a *AWSSQS) getQueueURL(ctx context.Context, queueName string) (*string, error) { + out, err := a.sqsClient.GetQueueUrl(ctx, &sqs.GetQueueUrlInput{ + QueueName: &queueName, + }) + if err != nil { + return nil, err + } + return out.QueueUrl, nil +} + func (a *AWSSQS) Close() error { if a.closed.CompareAndSwap(false, true) { close(a.closeCh) } a.wg.Wait() - if a.authProvider != nil { - return a.authProvider.Close() - } return nil } @@ -192,6 +192,10 @@ func (a *AWSSQS) parseSQSMetadata(meta bindings.Metadata) (*sqsMetadata, error) // GetComponentMetadata returns the metadata of the component. func (a *AWSSQS) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := sqsMetadata{} - metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) + if err := metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType); err != nil { + if a != nil && a.logger != nil { + a.logger.Errorf("failed to get component metadata: %v", err) + } + } return } diff --git a/bindings/azure/cosmosdbgremlinapi/cosmosdbgremlinapi.go b/bindings/azure/cosmosdb/gremlinapi/cosmosdbgremlinapi.go similarity index 100% rename from bindings/azure/cosmosdbgremlinapi/cosmosdbgremlinapi.go rename to bindings/azure/cosmosdb/gremlinapi/cosmosdbgremlinapi.go diff --git a/bindings/azure/cosmosdbgremlinapi/cosmosdbgremlinapi_test.go b/bindings/azure/cosmosdb/gremlinapi/cosmosdbgremlinapi_test.go similarity index 100% rename from bindings/azure/cosmosdbgremlinapi/cosmosdbgremlinapi_test.go rename to bindings/azure/cosmosdb/gremlinapi/cosmosdbgremlinapi_test.go diff --git a/bindings/azure/cosmosdbgremlinapi/metadata.yaml b/bindings/azure/cosmosdb/gremlinapi/metadata.yaml similarity index 86% rename from bindings/azure/cosmosdbgremlinapi/metadata.yaml rename to bindings/azure/cosmosdb/gremlinapi/metadata.yaml index 609bd1aea9..765a8391da 100644 --- a/bindings/azure/cosmosdbgremlinapi/metadata.yaml +++ b/bindings/azure/cosmosdb/gremlinapi/metadata.yaml @@ -24,17 +24,17 @@ authenticationProfiles: sensitive: true description: | The key to authenticate to the Cosmos DB account. - example: '"my-secret-key"' + example: "my-secret-key" - name: username required: true sensitive: false description: | The username of the Cosmos DB database. - example: '"/dbs//colls/"' + example: "/dbs//colls/" metadata: - name: url type: string required: true description: | The Cosmos DB URL for Gremlin APIs - example: '"wss://******.gremlin.cosmos.azure.com:443/"' \ No newline at end of file + example: "wss://******.gremlin.cosmos.azure.com:443/" diff --git a/bindings/cloudflare/queues/metadata.yaml b/bindings/cloudflare/queues/metadata.yaml new file mode 100644 index 0000000000..fb7a4c7bce --- /dev/null +++ b/bindings/cloudflare/queues/metadata.yaml @@ -0,0 +1,68 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: cloudflare.queues +version: v1 +status: alpha +title: "Cloudflare Queues" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/cloudflare-queues/ +binding: + output: true + input: false + operations: + - name: create + description: "Send message to Cloudflare Queue" + - name: read + description: "Receive messages from Cloudflare Queue" +authenticationProfiles: + - title: "API Token Authentication" + description: | + Authenticate using Cloudflare API token and account ID. Dapr will create/manage the worker. + metadata: + - name: cfAPIToken + required: true + sensitive: true + description: "The Cloudflare API token" + example: "api-token" + - name: cfAccountID + required: true + description: "The Cloudflare account ID" + example: "account-id" + - name: key + required: true + sensitive: true + description: "The Ed25519 private key in PKCS#8 PEM format for JWT signing" + example: "-----BEGIN PRIVATE KEY-----\nXXX..." + - name: workerName + required: true + description: "The worker name for JWT token audience" + example: "worker" + - title: "Connect to Pre-deployed Worker" + description: | + Connect to a worker that has been pre-deployed and is ready to use. No API tokens needed. + metadata: + - name: workerUrl + required: true + description: "The Cloudflare worker URL" + example: "https://your-worker.your-subdomain.workers.dev" + - name: key + required: true + sensitive: true + description: "The Ed25519 private key in PKCS#8 PEM format for JWT signing" + example: "-----BEGIN PRIVATE KEY-----\nXXX..." + - name: workerName + required: true + description: "The worker name for JWT token audience" + example: "my-worker" +metadata: + - name: queueName + required: true + description: "The Cloudflare queue name" + example: "my-queue" + - name: timeoutInSeconds + required: false + description: "Timeout for network requests in seconds" + example: '20' + default: '20' diff --git a/bindings/commercetools/commercetools.go b/bindings/commercetools/commercetools.go index d48acba52b..fe36540f5a 100644 --- a/bindings/commercetools/commercetools.go +++ b/bindings/commercetools/commercetools.go @@ -40,12 +40,12 @@ type Data struct { } type commercetoolsMetadata struct { - Region string - Provider string - ProjectKey string - ClientID string - ClientSecret string - Scopes string + Region string `json:"region"` + Provider string `json:"provider"` + ProjectKey string `json:"projectKey"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + Scopes string `json:"scopes"` } func NewCommercetools(logger logger.Logger) bindings.OutputBinding { diff --git a/bindings/commercetools/metadata.yaml b/bindings/commercetools/metadata.yaml new file mode 100644 index 0000000000..e86bfdeffa --- /dev/null +++ b/bindings/commercetools/metadata.yaml @@ -0,0 +1,50 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: commercetools +version: v1 +status: alpha +title: "Commercetools" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/commercetools/ +binding: + output: true + input: false + operations: + - name: create + description: "Create resource in Commercetools" +authenticationProfiles: + - title: "OAuth Client Authentication" + description: | + Authenticate using OAuth client credentials. + metadata: + - name: clientID + required: true + description: "The Commercetools client ID" + example: "client-id" + - name: clientSecret + required: true + sensitive: true + description: "The Commercetools client secret" + example: "client-secret" +metadata: + - name: projectKey + required: true + description: "The Commercetools project key" + example: "my-project" + - name: region + required: true + description: "The Commercetools region" + example: "gcp-europe-west1" + default: "gcp-europe-west1" + - name: provider + required: true + description: "The Commercetools provider" + example: "gcp" + default: "gcp" + - name: scopes + required: true + description: "The OAuth scopes" + example: "manage_project:my-project" + diff --git a/bindings/dubbo/metadata.yaml b/bindings/dubbo/metadata.yaml new file mode 100644 index 0000000000..198d3bb8cf --- /dev/null +++ b/bindings/dubbo/metadata.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: dubbo +version: v1 +status: alpha +title: "Apache Dubbo" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/ +binding: + output: true + input: false + operations: + - name: create + description: "Invoke Dubbo service" +metadata: + - name: interfaceName + required: true + description: "The Dubbo interface name" + example: "com.example.UserService" + - name: methodName + required: true + description: "The method name to invoke" + example: "getUser" + - name: version + required: false + description: "The service version" + example: "1.0.0" + - name: group + required: false + description: "The service group" + example: "mygroup" + - name: providerHostname + required: false + description: "The provider hostname" + example: "localhost" + - name: providerPort + required: false + description: "The provider port" + example: '8080' diff --git a/bindings/gcp/bucket/bucket.go b/bindings/gcp/bucket/bucket.go index 027702f572..9dd53eb2ca 100644 --- a/bindings/gcp/bucket/bucket.go +++ b/bindings/gcp/bucket/bucket.go @@ -47,8 +47,9 @@ const ( metadataEncodeBase64 = "encodeBase64" metadataSignTTL = "signTTL" - metadataKey = "key" - maxResults = 1000 + metadataContentType = "contentType" + metadataKey = "key" + maxResults = 1000 metadataKeyBC = "name" signOperation = "sign" @@ -77,6 +78,7 @@ type gcpMetadata struct { TokenURI string `json:"token_uri" mapstructure:"tokenURI" mdignore:"true" mapstructurealiases:"token_uri"` AuthProviderCertURL string `json:"auth_provider_x509_cert_url" mapstructure:"authProviderX509CertURL" mdignore:"true" mapstructurealiases:"auth_provider_x509_cert_url"` ClientCertURL string `json:"client_x509_cert_url" mapstructure:"clientX509CertURL" mdignore:"true" mapstructurealiases:"client_x509_cert_url"` + ContentType string `json:"contentType,omitempty" mapstructure:"contentType"` Bucket string `json:"bucket" mapstructure:"bucket"` DecodeBase64 bool `json:"decodeBase64,string" mapstructure:"decodeBase64"` @@ -233,6 +235,12 @@ func (g *GCPStorage) create(ctx context.Context, req *bindings.InvokeRequest) (* } h := g.client.Bucket(g.metadata.Bucket).Object(name).NewWriter(ctx) + + // Set content type if provided + if metadata.ContentType != "" { + h.ContentType = metadata.ContentType + } + // Cannot do `defer h.Close()` as Close() will flush the bytes and need to have error handling. if _, err = io.Copy(h, r); err != nil { cerr := h.Close() @@ -378,9 +386,15 @@ func (metadata gcpMetadata) mergeWithRequestMetadata(req *bindings.InvokeRequest if val, ok := req.Metadata[metadataEncodeBase64]; ok && val != "" { merged.EncodeBase64 = strings.IsTruthy(val) } + if val, ok := req.Metadata[metadataSignTTL]; ok && val != "" { merged.SignTTL = val } + + if val, ok := req.Metadata[metadataContentType]; ok && val != "" { + merged.ContentType = val + } + return merged, nil } diff --git a/bindings/gcp/bucket/bucket_test.go b/bindings/gcp/bucket/bucket_test.go index 09392f044e..1fee85bdc1 100644 --- a/bindings/gcp/bucket/bucket_test.go +++ b/bindings/gcp/bucket/bucket_test.go @@ -233,6 +233,42 @@ func TestMergeWithRequestMetadata(t *testing.T) { assert.NotNil(t, mergedMeta) assert.False(t, mergedMeta.EncodeBase64) }) + + t.Run("Has merged contentType metadata", func(t *testing.T) { + m := bindings.Metadata{} + m.Properties = map[string]string{ + "bucket": "my_bucket", + "projectID": "my_project_id", + "contentType": "text/plain", + } + gs := GCPStorage{logger: logger.NewLogger("test")} + meta, err := gs.parseMetadata(m) + require.NoError(t, err) + assert.Equal(t, "text/plain", meta.ContentType) + + // Empty request doesn't override + request := bindings.InvokeRequest{} + request.Metadata = map[string]string{} + mergedMeta, err := meta.mergeWithRequestMetadata(&request) + require.NoError(t, err) + assert.Equal(t, "text/plain", mergedMeta.ContentType) + + // Request overrides component + request.Metadata = map[string]string{ + "contentType": "text/csv", + } + mergedMeta, err = meta.mergeWithRequestMetadata(&request) + require.NoError(t, err) + assert.Equal(t, "text/csv", mergedMeta.ContentType) + + // Empty string doesn't override + request.Metadata = map[string]string{ + "contentType": "", + } + mergedMeta, err = meta.mergeWithRequestMetadata(&request) + require.NoError(t, err) + assert.Equal(t, "text/plain", mergedMeta.ContentType) + }) } func TestInit(t *testing.T) { diff --git a/bindings/gcp/bucket/metadata.yaml b/bindings/gcp/bucket/metadata.yaml index 6e3448744e..08e1bd0d02 100644 --- a/bindings/gcp/bucket/metadata.yaml +++ b/bindings/gcp/bucket/metadata.yaml @@ -44,4 +44,12 @@ metadata: description: | Configuration to encode base64 file content before return the content. (In case of saving a file with binary content). - example: '"true, false"' \ No newline at end of file + example: '"true, false"' + - name: contentType + type: string + required: false + description: | + The MIME type of the object being stored. If not specified, GCS will attempt to + auto-detect the type, which may default to application/octet-stream or text/plain. + Common values include text/csv, application/json, image/png, etc. + example: '"text/csv", "application/json"' \ No newline at end of file diff --git a/bindings/gcp/pubsub/metadata.yaml b/bindings/gcp/pubsub/metadata.yaml new file mode 100644 index 0000000000..cb8e847682 --- /dev/null +++ b/bindings/gcp/pubsub/metadata.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: gcp.pubsub +version: v1 +status: alpha +title: "Google Cloud Pub/Sub" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/pubsub/ +binding: + output: true + input: true + operations: + - name: create + description: "Publish message to Pub/Sub topic" + - name: read + description: "Receive messages from Pub/Sub subscription" +builtinAuthenticationProfiles: + - name: "gcp" +metadata: + - name: topic + required: true + description: "The Pub/Sub topic name" + example: "my-topic" + - name: subscription + required: false + description: "The Pub/Sub subscription name" + example: "my-subscription" diff --git a/bindings/gcp/pubsub/pubsub.go b/bindings/gcp/pubsub/pubsub.go index dfc8a69787..bf89d91aef 100644 --- a/bindings/gcp/pubsub/pubsub.go +++ b/bindings/gcp/pubsub/pubsub.go @@ -28,6 +28,7 @@ import ( "github.com/dapr/components-contrib/bindings" contribMetadata "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" ) const ( @@ -46,10 +47,27 @@ type GCPPubSub struct { wg sync.WaitGroup } +// These JSON tags directly match the builtin auth provider metadata fields for GCP. type pubSubMetadata struct { - Topic string `json:"topic"` - Subscription string `json:"subscription"` - Type string `json:"type"` + Topic string `json:"topic" mapstructure:"topic"` + Subscription string `json:"subscription" mapstructure:"subscription"` + + // Note: the mdignore is to ignore these fields on the metadata analyzer, + // as these fields are parsed and used by the builtin auth provider, + // so they are still captured in the metadata.yaml file and in parsing. + Type string `json:"type" mapstructure:"type" mdignore:"true"` + ProjectID string `json:"projectID" mapstructure:"project_id" mapstructurealiases:"projectID" mdignore:"true"` + PrivateKeyID string `json:"privateKeyID" mapstructure:"private_key_id" mapstructurealiases:"privateKeyID" mdignore:"true"` + PrivateKey string `json:"privateKey" mapstructure:"private_key" mapstructurealiases:"privateKey" mdignore:"true"` + ClientEmail string `json:"clientEmail" mapstructure:"client_email" mapstructurealiases:"clientEmail" mdignore:"true"` + ClientID string `json:"clientID" mapstructure:"client_id" mapstructurealiases:"clientID" mdignore:"true"` + AuthURI string `json:"authURI" mapstructure:"auth_uri" mapstructurealiases:"authURI" mdignore:"true"` + TokenURI string `json:"tokenURI" mapstructure:"token_uri" mapstructurealiases:"tokenURI" mdignore:"true"` + AuthProviderX509CertURL string `json:"authProviderX509CertURL" mapstructure:"auth_provider_x509_cert_url" mapstructurealiases:"authProviderX509CertURL" mdignore:"true"` + ClientX509CertURL string `json:"clientX509CertURL" mapstructure:"client_x509_cert_url" mapstructurealiases:"clientX509CertURL" mdignore:"true"` +} + +type GCPAuthJSON struct { ProjectID string `json:"project_id"` PrivateKeyID string `json:"private_key_id"` PrivateKey string `json:"private_key"` @@ -59,6 +77,7 @@ type pubSubMetadata struct { TokenURI string `json:"token_uri"` AuthProviderCertURL string `json:"auth_provider_x509_cert_url"` ClientCertURL string `json:"client_x509_cert_url"` + Type string `json:"type"` } // NewGCPPubSub returns a new GCPPubSub instance. @@ -71,20 +90,39 @@ func NewGCPPubSub(logger logger.Logger) bindings.InputOutputBinding { // Init parses metadata and creates a new Pub Sub client. func (g *GCPPubSub) Init(ctx context.Context, metadata bindings.Metadata) error { - b, err := g.parseMetadata(metadata) + var ( + pubsubMeta pubSubMetadata + pubsubClient *pubsub.Client + ) + err := kitmd.DecodeMetadata(metadata.Properties, &pubsubMeta) if err != nil { return err } - var pubsubMeta pubSubMetadata - err = json.Unmarshal(b, &pubsubMeta) - if err != nil { - return err - } - clientOptions := option.WithCredentialsJSON(b) - pubsubClient, err := pubsub.NewClient(ctx, pubsubMeta.ProjectID, clientOptions) - if err != nil { - return fmt.Errorf("error creating pubsub client: %s", err) + if pubsubMeta.PrivateKeyID != "" { + authJSON := &GCPAuthJSON{ + ProjectID: pubsubMeta.ProjectID, + PrivateKeyID: pubsubMeta.PrivateKeyID, + PrivateKey: pubsubMeta.PrivateKey, + ClientEmail: pubsubMeta.ClientEmail, + ClientID: pubsubMeta.ClientID, + AuthURI: pubsubMeta.AuthURI, + TokenURI: pubsubMeta.TokenURI, + AuthProviderCertURL: pubsubMeta.AuthProviderX509CertURL, + ClientCertURL: pubsubMeta.ClientX509CertURL, + Type: pubsubMeta.Type, + } + gcpCompatibleJSON, _ := json.Marshal(authJSON) + clientOptions := option.WithCredentialsJSON(gcpCompatibleJSON) + pubsubClient, err = pubsub.NewClient(ctx, pubsubMeta.ProjectID, clientOptions) + if err != nil { + return fmt.Errorf("error creating pubsub client: %s", err) + } + } else { + pubsubClient, err = pubsub.NewClient(ctx, pubsubMeta.ProjectID) + if err != nil { + return fmt.Errorf("error creating pubsub client: %s", err) + } } g.client = pubsubClient @@ -93,10 +131,6 @@ func (g *GCPPubSub) Init(ctx context.Context, metadata bindings.Metadata) error return nil } -func (g *GCPPubSub) parseMetadata(metadata bindings.Metadata) ([]byte, error) { - return json.Marshal(metadata.Properties) -} - func (g *GCPPubSub) Read(ctx context.Context, handler bindings.Handler) error { if g.closed.Load() { return errors.New("binding is closed") diff --git a/bindings/gcp/pubsub/pubsub_test.go b/bindings/gcp/pubsub/pubsub_test.go index cd4d3f8171..8da98de3e1 100644 --- a/bindings/gcp/pubsub/pubsub_test.go +++ b/bindings/gcp/pubsub/pubsub_test.go @@ -14,14 +14,13 @@ limitations under the License. package pubsub import ( - "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/dapr/components-contrib/bindings" - "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" ) func TestInit(t *testing.T) { @@ -30,19 +29,15 @@ func TestInit(t *testing.T) { "auth_provider_x509_cert_url": "https://auth", "auth_uri": "https://auth", "client_x509_cert_url": "https://cert", "client_email": "test@test.com", "client_id": "id", "private_key": "****", "private_key_id": "key_id", "project_id": "project1", "token_uri": "https://token", "type": "serviceaccount", "topic": "t1", "subscription": "s1", } - ps := GCPPubSub{logger: logger.NewLogger("test")} - b, err := ps.parseMetadata(m) - require.NoError(t, err) - var pubsubMeta pubSubMetadata - err = json.Unmarshal(b, &pubsubMeta) + err := kitmd.DecodeMetadata(m.Properties, &pubsubMeta) require.NoError(t, err) assert.Equal(t, "s1", pubsubMeta.Subscription) assert.Equal(t, "t1", pubsubMeta.Topic) - assert.Equal(t, "https://auth", pubsubMeta.AuthProviderCertURL) + assert.Equal(t, "https://auth", pubsubMeta.AuthProviderX509CertURL) assert.Equal(t, "https://auth", pubsubMeta.AuthURI) - assert.Equal(t, "https://cert", pubsubMeta.ClientCertURL) + assert.Equal(t, "https://cert", pubsubMeta.ClientX509CertURL) assert.Equal(t, "test@test.com", pubsubMeta.ClientEmail) assert.Equal(t, "id", pubsubMeta.ClientID) assert.Equal(t, "****", pubsubMeta.PrivateKey) @@ -51,3 +46,43 @@ func TestInit(t *testing.T) { assert.Equal(t, "https://token", pubsubMeta.TokenURI) assert.Equal(t, "serviceaccount", pubsubMeta.Type) } + +func TestInit_MetadataCaseInsensitive(t *testing.T) { + t.Run("snake_case metadata", func(t *testing.T) { + m := bindings.Metadata{} + m.Properties = map[string]string{ + "project_id": "snake-project", + "private_key_id": "snake-key", + "client_email": "snake@test.com", + "topic": "snake-topic", + } + + var pubsubMeta pubSubMetadata + err := kitmd.DecodeMetadata(m.Properties, &pubsubMeta) + require.NoError(t, err) + + assert.Equal(t, "snake-project", pubsubMeta.ProjectID) + assert.Equal(t, "snake-key", pubsubMeta.PrivateKeyID) + assert.Equal(t, "snake@test.com", pubsubMeta.ClientEmail) + assert.Equal(t, "snake-topic", pubsubMeta.Topic) + }) + + t.Run("camelCase metadata", func(t *testing.T) { + m := bindings.Metadata{} + m.Properties = map[string]string{ + "projectID": "camel-project", + "privateKeyID": "camel-key", + "clientEmail": "camel@test.com", + "topic": "camel-topic", + } + + var pubsubMeta pubSubMetadata + err := kitmd.DecodeMetadata(m.Properties, &pubsubMeta) + require.NoError(t, err) + + assert.Equal(t, "camel-project", pubsubMeta.ProjectID) + assert.Equal(t, "camel-key", pubsubMeta.PrivateKeyID) + assert.Equal(t, "camel@test.com", pubsubMeta.ClientEmail) + assert.Equal(t, "camel-topic", pubsubMeta.Topic) + }) +} diff --git a/bindings/graphql/metadata.yaml b/bindings/graphql/metadata.yaml new file mode 100644 index 0000000000..e7c0fb075b --- /dev/null +++ b/bindings/graphql/metadata.yaml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: graphql +version: v1 +status: alpha +title: "GraphQL" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/graphql/ +binding: + output: true + input: false + operations: + - name: create + description: "Execute GraphQL query or mutation" +metadata: + - name: endpoint + required: true + description: "The GraphQL endpoint URL" + example: "https://api.example.com/graphql" diff --git a/bindings/huawei/obs/metadata.yaml b/bindings/huawei/obs/metadata.yaml new file mode 100644 index 0000000000..09d7e5c629 --- /dev/null +++ b/bindings/huawei/obs/metadata.yaml @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: huawei.obs +version: v1 +status: alpha +title: "Huawei Object Storage Service (OBS)" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/huawei-obs +binding: + output: true + input: false + operations: + - name: create + description: "Upload file to OBS" +authenticationProfiles: + - title: "Access Key Authentication" + description: | + Authenticate using Huawei Cloud access key credentials. + metadata: + - name: accessKey + required: true + description: "The Huawei Cloud access key ID" + example: "access-key-id" + - name: secretKey + required: true + sensitive: true + description: "The Huawei Cloud secret access key" + example: "secret-access-key" + - name: region + required: true + description: "The Huawei Cloud region" + example: "cn-north-4" +metadata: + - name: endpoint + required: true + description: "The OBS endpoint" + example: "https://obs.cn-north-4.myhuaweicloud.com" + - name: bucket + required: true + description: "The OBS bucket name" + example: "bucket-name" diff --git a/bindings/influx/metadata.yaml b/bindings/influx/metadata.yaml new file mode 100644 index 0000000000..a33810eaed --- /dev/null +++ b/bindings/influx/metadata.yaml @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: influx +version: v1 +status: alpha +title: "InfluxDB" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/influxdb/ +binding: + output: true + input: false + operations: + - name: create + description: "Write data points to InfluxDB" +authenticationProfiles: + - title: "Token Authentication" + description: | + Authenticate using InfluxDB token. + metadata: + - name: token + required: true + sensitive: true + description: "The InfluxDB authentication token" + example: "your-influxdb-token" +metadata: + - name: url + required: true + description: "The InfluxDB server URL" + example: "http://localhost:8086" + - name: org + required: true + description: "The InfluxDB organization name" + example: "your-org" + - name: bucket + required: true + description: "The InfluxDB bucket name" + example: "your-bucket" + - name: measurement + required: true + description: "The measurement name" + example: "cpu_usage" diff --git a/bindings/kafka/metadata.yaml b/bindings/kafka/metadata.yaml index c37ed9f9de..79ddc7bd8c 100644 --- a/bindings/kafka/metadata.yaml +++ b/bindings/kafka/metadata.yaml @@ -33,7 +33,7 @@ builtinAuthenticationProfiles: type: string required: false description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'accessKey' instead. If both fields are set, then 'accessKey' value will be used. AWS access key associated with an IAM account. @@ -43,7 +43,7 @@ builtinAuthenticationProfiles: required: false sensitive: true description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'secretKey' instead. If both fields are set, then 'secretKey' value will be used. The secret key associated with the access key. @@ -52,7 +52,7 @@ builtinAuthenticationProfiles: type: string sensitive: true description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'sessionToken' instead. If both fields are set, then 'sessionToken' value will be used. AWS session token to use. A session token is only required if you are using temporary security credentials. @@ -61,7 +61,7 @@ builtinAuthenticationProfiles: type: string required: false description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'assumeRoleArn' instead. If both fields are set, then 'assumeRoleArn' value will be used. IAM role that has access to MSK. This is another option to authenticate with MSK aside from the AWS Credentials. @@ -69,7 +69,7 @@ builtinAuthenticationProfiles: - name: awsStsSessionName type: string description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'sessionName' instead. If both fields are set, then 'sessionName' value will be used. Represents the session name for assuming a role. @@ -78,7 +78,7 @@ builtinAuthenticationProfiles: authenticationProfiles: - title: "OIDC Authentication" description: | - Authenticate using OpenID Connect. + Authenticate using OpenID Connect providing a client secret. metadata: - name: authType type: string @@ -121,6 +121,78 @@ authenticationProfiles: example: | {"cluster":"kafka","poolid":"kafkapool"} type: string + - title: "OIDC Private Key JWT Authentication" + description: | + Authenticate using OpenID Connect providing a client certificate and private key. + metadata: + - name: authType + type: string + required: true + description: | + Authentication type. + This must be set to "oidc_private_key_jwt" for this authentication profile. + example: '"oidc_private_key_jwt"' + allowedValues: + - "oidc_private_key_jwt" + - name: oidcTokenEndpoint + type: string + required: true + description: | + URL of the OAuth2 identity provider access token endpoint. + example: '"https://identity.example.com/v1/token"' + - name: oidcClientID + description: | + The OAuth2 client ID that has been provisioned in the identity provider. + example: '"my-client-id"' + type: string + required: true + - name: oidcClientAssertionCert + type: string + required: true + description: | + PEM-encoded X.509 certificate used to advertise the client certificate in the x5c header. + example: | + -----BEGIN CERTIFICATE-----\n... + - name: oidcClientAssertionKey + type: string + required: true + sensitive: true + description: | + PEM-encoded private key used to sign the client certificate. + example: | + -----BEGIN PRIVATE KEY-----\n... + - name: oidcResource + type: string + required: false + description: | + Optional OAuth2 resource (audience) parameter to include in the token request when required by the identity provider. + example: '"api://kafka"' + - name: oidcAudience + type: string + required: false + description: | + Overrides the JWT client assertion audience (aud). If not set, the component uses the + issuer derived from the token endpoint URL when available; otherwise, it falls back to the token URL. + example: '"http:///realms/local"' + - name: oidcScopes + type: string + description: | + Comma-delimited list of OAuth2/OIDC scopes to request with the access token. + Although not required, this field is recommended. + example: '"openid,kafka-prod"' + default: '"openid"' + - name: oidcExtensions + description: | + String containing a JSON-encoded dictionary of OAuth2/OIDC extensions to request with the access token. + example: | + {"cluster":"kafka","poolid":"kafkapool"} + type: string + - name: oidcKid + type: string + required: false + description: | + The JWT key ID (kid) to use for the client assertion. + example: '"1234567890"' - title: "SASL Authentication" description: | Authenticate using SASL. @@ -348,10 +420,17 @@ metadata: type: bool required: false description: | - Enables URL escaping of the message header values. + Enables URL escaping of the message header values. It allows sending headers with special characters that are usually not allowed in HTTP headers. example: "true" default: "false" + - name: useAvroJSON + type: bool + required: false + description: | + Enables Avro JSON schema for serialization. Only applicable when the subscription uses valueSchemaType=Avro + example: "true" + default: "false" - name: compression type: string required: false diff --git a/bindings/kitex/kitex_output_test.go b/bindings/kitex/kitex_output_test.go index 8b7927da16..bb8bb8f2bc 100644 --- a/bindings/kitex/kitex_output_test.go +++ b/bindings/kitex/kitex_output_test.go @@ -17,10 +17,9 @@ import ( "testing" "time" - "github.com/apache/thrift/lib/go/thrift" + "github.com/cloudwego/gopkg/protocol/thrift" "github.com/cloudwego/kitex" "github.com/cloudwego/kitex-examples/kitex_gen/api" - "github.com/cloudwego/kitex/pkg/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -47,9 +46,8 @@ func TestInvoke(t *testing.T) { output := NewKitexOutput(logger.NewLogger("test")) // 2 create req bytes - codec := utils.NewThriftMessageCodec() req := &api.EchoEchoArgs{Req: &api.Request{Message: "hello dapr"}} - reqData, err := codec.Encode(MethodName, thrift.CALL, 0, req) + reqData, err := thrift.MarshalFastMsg(MethodName, thrift.CALL, 0, req) require.NoError(t, err) // 3. Invoke dapr kitex output binding, get rsp bytes @@ -69,7 +67,7 @@ func TestInvoke(t *testing.T) { // 4. get resp value result := &api.EchoEchoResult{} - _, _, err = codec.Decode(resp.Data, result) + _, _, err = thrift.UnmarshalFastMsg(resp.Data, result) require.NoError(t, err) assert.Equal(t, "hello dapr,hi Kitex", result.Success.Message) } diff --git a/bindings/kitex/metadata.yaml b/bindings/kitex/metadata.yaml new file mode 100644 index 0000000000..64d161d661 --- /dev/null +++ b/bindings/kitex/metadata.yaml @@ -0,0 +1,33 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: kitex +version: v1 +status: alpha +title: "Kitex" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/kitex/ +binding: + output: true + input: false + operations: + - name: create + description: "Invoke Kitex service" +metadata: + - name: serviceName + required: true + description: "The Kitex service name" + example: "my-service" + - name: methodName + required: true + description: "The method name to invoke" + example: "getUser" + - name: destService + required: true + description: "The destination service name" + example: "my-service" + - name: hostPorts + required: true + description: "The service address" + example: "localhost:8080" diff --git a/bindings/kubemq/metadata.yaml b/bindings/kubemq/metadata.yaml new file mode 100644 index 0000000000..fcb8fbb0bf --- /dev/null +++ b/bindings/kubemq/metadata.yaml @@ -0,0 +1,56 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: kubemq +version: v1 +status: beta +title: "KubeMQ" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/kubemq/ +binding: + output: true + input: true + operations: + - name: create + description: "Send message to KubeMQ" + - name: read + description: "Receive messages from KubeMQ" +authenticationProfiles: + - title: "Token Authentication" + description: "Connect to KubeMQ using an authentication token." + metadata: + - name: authToken + type: string + required: true + description: The authentication token for KubeMQ. + example: "auth-token" +metadata: + - name: address + type: string + required: true + description: The KubeMQ server address in format host:port. + example: "localhost:50000" + - name: channel + type: string + required: true + description: The KubeMQ channel name. + example: "my-channel" + - name: pollMaxItems + type: number + required: false + description: The maximum number of items to poll. + example: "10" + default: "1" + - name: pollTimeoutSeconds + type: number + required: false + description: The timeout in seconds for polling. + example: "3600" + default: "3600" + - name: autoAcknowledged + type: bool + required: false + description: Whether to automatically acknowledge messages. + example: "true" + default: "false" diff --git a/bindings/kubernetes/kubernetes.go b/bindings/kubernetes/kubernetes.go index f41bc88cba..8f53e000ee 100644 --- a/bindings/kubernetes/kubernetes.go +++ b/bindings/kubernetes/kubernetes.go @@ -52,9 +52,12 @@ type EventResponse struct { } type kubernetesMetadata struct { - Namespace string `mapstructure:"namespace"` - KubeconfigPath string `mapstructure:"kubeconfigPath"` - ResyncPeriod time.Duration `mapstructure:"resyncPeriod" mapstructurealiases:"resyncPeriodInSec"` + Namespace string `mapstructure:"namespace"` + KubeconfigPath string `mapstructure:"kubeconfigPath"` + // Note: we add mdignore to this so the metadata parser doesn't throw an error if resyncPeriodInSec on the metadata.yaml file. + // It has the ResyncPeriodInSec as a field, but we don't need users to see both resyncPeriod and resyncPeriodInSec, + // so the mdignore is just to make CI happy since we support both representations. + ResyncPeriod time.Duration `mapstructure:"resyncPeriod" mapstructurealiases:"resyncPeriodInSec" mdignore:"true"` } // NewKubernetes returns a new Kubernetes event input binding. @@ -116,12 +119,15 @@ func (k *kubernetesInput) Read(ctx context.Context, handler bindings.Handler) er fields.Everything(), ) resultChan := make(chan EventResponse) + // TODO: + // cache.NewInformer is deprecated: Use NewInformerWithOptions instead. + //nolint:staticcheck _, controller := cache.NewInformer( watchlist, &corev1.Event{}, k.metadata.ResyncPeriod, cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { + AddFunc: func(obj any) { if obj != nil { resultChan <- EventResponse{ Event: "add", @@ -132,7 +138,7 @@ func (k *kubernetesInput) Read(ctx context.Context, handler bindings.Handler) er k.logger.Warnf("Nil Object in Add handle %v", obj) } }, - DeleteFunc: func(obj interface{}) { + DeleteFunc: func(obj any) { if obj != nil { resultChan <- EventResponse{ Event: "delete", @@ -143,7 +149,7 @@ func (k *kubernetesInput) Read(ctx context.Context, handler bindings.Handler) er k.logger.Warnf("Nil Object in Delete handle %v", obj) } }, - UpdateFunc: func(oldObj, newObj interface{}) { + UpdateFunc: func(oldObj, newObj any) { if oldObj != nil && newObj != nil { resultChan <- EventResponse{ Event: "update", diff --git a/bindings/kubernetes/metadata.yaml b/bindings/kubernetes/metadata.yaml new file mode 100644 index 0000000000..237e4b3a4c --- /dev/null +++ b/bindings/kubernetes/metadata.yaml @@ -0,0 +1,33 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: kubernetes +version: v1 +status: alpha +title: "Kubernetes Events" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/kubernetes-binding/ +binding: + output: false + input: true + operations: + - name: read + description: "Read Kubernetes events" +metadata: + - name: namespace + required: false + description: "The Kubernetes namespace" + example: "default" + default: "default" + - name: kubeconfigPath + required: false + description: "The path to the kubeconfig file" + example: "~/.kube/config" + default: "~/.kube/config" + - name: resyncPeriodInSec + required: false + type: duration + description: "The resync period in seconds" + example: '30s' + default: '10s' diff --git a/bindings/localstorage/metadata.yaml b/bindings/localstorage/metadata.yaml new file mode 100644 index 0000000000..58107c88d9 --- /dev/null +++ b/bindings/localstorage/metadata.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: localstorage +version: v1 +status: stable +title: "Local Storage" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/localstorage/ +binding: + output: true + input: false + operations: + - name: create + description: "Write file to local storage" +metadata: + - name: rootPath + required: true + description: "The root directory path" + example: "/tmp/dapr" + - name: fileName + required: true + description: "The file name to write" + example: "data.txt" diff --git a/bindings/mqtt3/metadata.go b/bindings/mqtt3/metadata.go index b9fd05abe2..5bc38066f1 100644 --- a/bindings/mqtt3/metadata.go +++ b/bindings/mqtt3/metadata.go @@ -25,16 +25,14 @@ import ( const ( // Keys. - mqttURL = "url" - mqttTopic = "topic" - mqttQOS = "qos" // This key is deprecated - mqttRetain = "retain" - mqttClientID = "consumerID" - mqttCleanSession = "cleanSession" - mqttCACert = "caCert" - mqttClientCert = "clientCert" - mqttClientKey = "clientKey" - mqttBackOffMaxRetries = "backOffMaxRetries" + mqttURL = "url" + mqttTopic = "topic" + mqttQOS = "qos" // This key is deprecated + mqttRetain = "retain" + mqttCleanSession = "cleanSession" + mqttCACert = "caCert" + mqttClientCert = "clientCert" + mqttClientKey = "clientKey" // Defaults. defaultQOS = 1 diff --git a/bindings/mqtt3/metadata.yaml b/bindings/mqtt3/metadata.yaml new file mode 100644 index 0000000000..6edcb38145 --- /dev/null +++ b/bindings/mqtt3/metadata.yaml @@ -0,0 +1,64 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: mqtt3 +version: v1 +status: beta +title: "MQTT v3" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/mqtt3/ +binding: + output: true + input: true + operations: + - name: create + description: "Publish message to MQTT topic" + - name: read + description: "Subscribe to MQTT topic" +authenticationProfiles: + - title: "TLS Authentication" + description: | + Authenticate using TLS certificates. + metadata: + - name: caCert + required: true + description: "CA certificate for TLS" + example: "-----BEGIN CERTIFICATE-----\n..." + - name: clientCert + required: true + description: "Client certificate for TLS" + example: "-----BEGIN CERTIFICATE-----\n..." + - name: clientKey + required: true + sensitive: true + description: "Client private key for TLS" + example: "-----BEGIN PRIVATE KEY-----\n..." +metadata: + - name: url + required: true + description: "The MQTT broker URL" + example: "tcp://localhost:1883" + - name: topic + required: true + description: "The MQTT topic" + example: "my-topic" + - name: consumerID + required: false + description: "The MQTT client ID" + example: "my-client" + - name: retain + required: false + description: "Whether to retain messages" + example: "false" + default: "false" + - name: cleanSession + required: false + description: "Whether to use clean session" + example: "true" + default: "true" + - name: backOffMaxRetries + required: false + description: "Maximum retries for backoff" + example: '3' + default: '3' diff --git a/bindings/mysql/mysql.go b/bindings/mysql/mysql.go index 4bcd99a009..a7d5c5256f 100644 --- a/bindings/mysql/mysql.go +++ b/bindings/mysql/mysql.go @@ -64,8 +64,12 @@ const ( respRowsAffectedKey = "rows-affected" respEndTimeKey = "end-time" respDurationKey = "duration" + + jsonType = "JSON" ) +var rawByteType = reflect.TypeOf(&sql.RawBytes{}) + // Mysql represents MySQL output bindings. type Mysql struct { db *sql.DB @@ -330,6 +334,10 @@ func (m *Mysql) jsonify(rows *sql.Rows) ([]byte, error) { func prepareValues(columnTypes []*sql.ColumnType) []any { types := make([]reflect.Type, len(columnTypes)) for i, tp := range columnTypes { + if tp.DatabaseTypeName() == jsonType { + types[i] = rawByteType + continue + } types[i] = tp.ScanType() } diff --git a/bindings/mysql/mysql_integration_test.go b/bindings/mysql/mysql_integration_test.go index f9d1502757..cba07e97c4 100644 --- a/bindings/mysql/mysql_integration_test.go +++ b/bindings/mysql/mysql_integration_test.go @@ -14,6 +14,7 @@ limitations under the License. package mysql import ( + "encoding/base64" "encoding/json" "fmt" "os" @@ -44,11 +45,15 @@ func TestOperations(t *testing.T) { } // SETUP TESTS -// 1. `CREATE DATABASE daprtest;` -// 2. `CREATE USER daprtest;` -// 3. `GRANT ALL PRIVILEGES ON daprtest.* to daprtest;` -// 4. `export MYSQL_TEST_CONN_URL=daprtest@tcp(localhost:3306)/daprtest` -// 5. `go test -v -count=1 ./bindings/mysql -run ^TestMysqlIntegrationWithURL` +// 1. docker run --name mysql -p 3306:3306 -e MYSQL_DATABASE=daprtest -e MYSQL_USER=daprtest -e MYSQL_PASSWORD=daprtest -e MYSQL_ALLOW_EMPTY_PASSWORD=true -d mysql:latest +// 2. `export MYSQL_TEST_CONN_URL=daprtest:daprtest:@tcp(localhost:3306)/daprtest` +// 3. `go test -v -count=1 ./bindings/mysql -run ^TestMysqlIntegrationWithURL` + +type TestJSONData struct { + ID int `json:"id"` + Name string `json:"name"` + Color string `json:"color"` +} func TestMysqlIntegration(t *testing.T) { url := os.Getenv("MYSQL_TEST_CONN_URL") @@ -70,6 +75,7 @@ func TestMysqlIntegration(t *testing.T) { Metadata: map[string]string{ commandSQLKey: `CREATE TABLE IF NOT EXISTS foo ( id bigint NOT NULL, + j JSON NOT NULL, v1 character varying(50) NOT NULL, b BOOLEAN, ts TIMESTAMP, @@ -91,12 +97,18 @@ func TestMysqlIntegration(t *testing.T) { t.Run("Invoke insert", func(t *testing.T) { for i := range 10 { + jData := &TestJSONData{ID: i, Name: "test", Color: "red"} + jsonBytes, _ := json.Marshal(jData) + v := base64.StdEncoding.EncodeToString(jsonBytes) + encB, _ := json.Marshal(v) + enc := string(encB) + res, err := b.Invoke(t.Context(), &bindings.InvokeRequest{ Operation: execOperation, Metadata: map[string]string{ commandSQLKey: fmt.Sprintf( - "INSERT INTO foo (id, v1, b, ts, data) VALUES (%d, 'test-%d', %t, '%v', '%s')", - i, i, true, time.Now().Format(mySQLDateTimeFormat), `{"key":"val"}`), + "INSERT INTO foo (id, j, v1, b, ts, data) VALUES (%d, '%s', 'test-%d', %t, '%v', '%s')", + i, enc, i, true, time.Now().Format(mySQLDateTimeFormat), `{"key":"val"}`), }, }) assertResponse(t, res, err) @@ -153,7 +165,7 @@ func TestMysqlIntegration(t *testing.T) { result := make([]any, 0) err = json.Unmarshal(res.Data, &result) require.NoError(t, err) - assert.Len(t, len(result), 3) + assert.Len(t, result, 3) // verify timestamp ts, ok := result[0].(map[string]any)["ts"].(string) @@ -185,7 +197,47 @@ func TestMysqlIntegration(t *testing.T) { result := make([]any, 0) err = json.Unmarshal(res.Data, &result) require.NoError(t, err) - assert.Len(t, len(result), 1) + assert.Len(t, result, 1) + }) + + t.Run("Invoke select binary json", func(t *testing.T) { + res, err := b.Invoke(t.Context(), &bindings.InvokeRequest{ + Operation: queryOperation, + Metadata: map[string]string{ + commandSQLKey: "SELECT j FROM foo WHERE id = 1", + }, + }) + assertResponse(t, res, err) + t.Logf("received result: %s", res.Data) + + result := make([]any, 0) + err = json.Unmarshal(res.Data, &result) + require.NoError(t, err) + assert.Len(t, result, 1) + + jsonBytes := result[0].(map[string]any)["j"].(string) + require.NotNil(t, jsonBytes) + + require.NoError(t, err) + + var decodedBytes []byte + decodedBytes, err = base64.StdEncoding.DecodeString(jsonBytes) + require.NoError(t, err) + t.Logf("bytes decoded received: %s", decodedBytes) + + var strBytes string + err = json.Unmarshal(decodedBytes, &strBytes) + require.NoError(t, err) + jBytes, _ := base64.StdEncoding.DecodeString(strBytes) + require.NotNil(t, jBytes) + t.Logf("Json received: %s", string(jBytes)) + + var tData TestJSONData + err = json.Unmarshal(jBytes, &tData) + require.NoError(t, err) + assert.Equal(t, 1, tData.ID) + assert.Equal(t, "test", tData.Name) + assert.Equal(t, "red", tData.Color) }) t.Run("Invoke drop", func(t *testing.T) { diff --git a/bindings/postgres/metadata.go b/bindings/postgres/metadata.go index 44e55a37f3..8c362f68d1 100644 --- a/bindings/postgres/metadata.go +++ b/bindings/postgres/metadata.go @@ -17,8 +17,8 @@ import ( "errors" "time" - "github.com/dapr/components-contrib/common/authentication/aws" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" + "github.com/dapr/components-contrib/common/aws" kitmd "github.com/dapr/kit/metadata" ) diff --git a/bindings/postgres/postgres.go b/bindings/postgres/postgres.go index c0729cab06..1effbb7d20 100644 --- a/bindings/postgres/postgres.go +++ b/bindings/postgres/postgres.go @@ -26,8 +26,9 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/dapr/components-contrib/bindings" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsAuth "github.com/dapr/components-contrib/common/aws/auth" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" ) @@ -50,8 +51,6 @@ type Postgres struct { enableAzureAD bool enableAWSIAM bool - - awsAuthProvider awsAuth.Provider } // NewPostgres returns a new PostgreSQL output binding. @@ -87,13 +86,20 @@ func (p *Postgres) Init(ctx context.Context, meta bindings.Metadata) error { return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr) } - var provider awsAuth.Provider - provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts)) - if err != nil { + authOpts := awsAuth.Options{ + Logger: p.logger, + Properties: meta.Properties, + Region: opts.Region, + AccessKey: opts.AccessKey, + SecretKey: opts.SecretKey, + SessionToken: opts.SessionToken, + AssumeRoleArn: opts.AssumeRoleArn, + AssumeRoleSessionName: opts.AssumeRoleSessionName, + Endpoint: opts.Endpoint, + } + if err = awsCommon.ConfigurePostgresIAM(ctx, poolConfig, authOpts); err != nil { return err } - p.awsAuthProvider = provider - p.awsAuthProvider.UpdatePostgres(ctx, poolConfig) } // This context doesn't control the lifetime of the connection pool, and is @@ -211,11 +217,7 @@ func (p *Postgres) Close() error { } p.db = nil - errs := make([]error, 1) - if p.awsAuthProvider != nil { - errs[0] = p.awsAuthProvider.Close() - } - return errors.Join(errs...) + return nil } func (p *Postgres) query(ctx context.Context, sql string, args ...any) (result []byte, err error) { diff --git a/bindings/postmark/metadata.yaml b/bindings/postmark/metadata.yaml new file mode 100644 index 0000000000..a634279f5f --- /dev/null +++ b/bindings/postmark/metadata.yaml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: postmark +version: v1 +status: alpha +title: "Postmark" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/postmark/ +binding: + output: true + input: false + operations: + - name: create + description: "Send an email using Postmark" +metadata: + - name: serverToken + required: true + description: "The Postmark server token" + example: "your-server-token" + - name: accountToken + required: true + description: "The Postmark account token" + example: "your-account-token" + - name: emailFrom + required: false + description: "The sender email address" + example: "sender@example.com" + - name: emailTo + required: false + description: "The recipient email address" + example: "recipient@example.com" + - name: subject + required: false + description: "The email subject" + example: "Hello from Dapr" + - name: emailCc + required: false + description: "The CC email address" + example: "cc@example.com" + - name: emailBcc + required: false + description: "The BCC email address" + example: "bcc@example.com" diff --git a/bindings/redis/metadata.yaml b/bindings/redis/metadata.yaml index ad2bd7c62c..9d2c69a85e 100644 --- a/bindings/redis/metadata.yaml +++ b/bindings/redis/metadata.yaml @@ -69,9 +69,23 @@ authenticationProfiles: metadata: - name: redisHost required: true - description: Connection-string for the Redis host + description: | + Connection-string for the Redis host. If "redisType" is "cluster" it + can be multiple hosts separated by commas or just a single host. + The port can be included in the host string (e.g. "host:6379") or + provided separately via "redisPort". example: "redis-master.default.svc.cluster.local:6379" type: string + - name: redisPort + required: false + description: | + The Redis port. Optional: if "redisHost" already contains a port, this + field must either match or be omitted. When "redisHost" does not include + a port and this field is not set, the default Redis port 6379 is used. + In cluster mode, this port is applied to every host in the + comma-separated list. + example: "6379" + type: string - name: enableTLS type: bool required: false diff --git a/bindings/redis/redis_test.go b/bindings/redis/redis_test.go index 9d57b76f2d..578eebc259 100644 --- a/bindings/redis/redis_test.go +++ b/bindings/redis/redis_test.go @@ -59,6 +59,26 @@ func TestInvokeCreate(t *testing.T) { assert.JSONEq(t, testData, getRes.(string)) } +// TestInvokeGetMissingKeyReturnsEmptyNotError ensures that when a key does not exist +// we don't propagate 'redis: nil' as an error to users +func TestInvokeGetMissingKeyReturnsEmptyNotError(t *testing.T) { + s, c := setupMiniredis() + defer s.Close() + + bind := &Redis{ + client: c, + logger: logger.NewLogger("test"), + } + + res, err := bind.Invoke(t.Context(), &bindings.InvokeRequest{ + Metadata: map[string]string{"key": "nonexistent-key"}, + Operation: bindings.GetOperation, + }) + require.NoError(t, err) + require.NotNil(t, res) + assert.Empty(t, res.Data) +} + func TestInvokeGetWithoutDeleteFlag(t *testing.T) { s, c := setupMiniredis() defer s.Close() diff --git a/bindings/rethinkdb/statechange/metadata.yaml b/bindings/rethinkdb/statechange/metadata.yaml new file mode 100644 index 0000000000..58c415f76c --- /dev/null +++ b/bindings/rethinkdb/statechange/metadata.yaml @@ -0,0 +1,158 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: rethinkdb.statechange +version: v1 +status: beta +title: "RethinkDB State Change" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/rethinkdb/ +binding: + output: false + input: true + operations: + - name: read + description: "Listen for state changes in RethinkDB" +authenticationProfiles: + - title: "Basic Authentication" + description: "Authenticate using username and password." + metadata: + - name: address + type: string + required: false + description: The RethinkDB server address. + example: "localhost:28015" + - name: addresses + type: string + required: false + description: Comma-separated list of RethinkDB server addresses. + example: "localhost:28015,localhost:28016" + - name: database + type: string + required: true + description: The RethinkDB database name. + example: "dapr" + default: "" + - name: username + type: string + required: false + description: The username for authentication. If not provided, the admin user is used for v1 handshake protocol. + example: "admin" + - name: password + type: string + required: false + description: The password for authentication. This is only used for v1 handshake protocol. + example: "password" + - title: "TLS Authentication" + description: "Authenticate using client certificate and key." + metadata: + - name: enableTLS + type: bool + required: false + description: Whether to enable TLS encryption. + example: "false" + default: "false" + - name: clientCert + type: string + required: true + description: The client certificate for TLS authentication. + example: "-----BEGIN CERTIFICATE-----\nXXX..." + - name: clientKey + type: string + required: true + description: The client key for TLS authentication. + example: "-----BEGIN PRIVATE KEY-----\nXXX..." + sensitive: true +metadata: + - name: table + type: string + required: false + description: The table name to store state data. + example: "daprstate" + default: "daprstate" + - name: archive + type: bool + required: false + description: Whether to archive changes to a separate table. + example: "false" + default: "false" + - name: timeout + type: string + required: false + description: Connection timeout duration. + example: "10s" + - name: useJSONNumber + type: bool + required: false + description: Whether to use json.Number instead of float64. + example: "false" + default: "false" + - name: numRetries + type: number + required: false + description: Number of times to retry queries on connection errors. + example: "3" + - name: hostDecayDuration + type: string + required: false + description: Host decay duration for weighted host selection. + example: "5m" + default: "5m" + - name: useOpentracing + type: bool + required: false + description: Whether to enable opentracing for queries. + example: "false" + default: "false" + - name: writeTimeout + type: string + required: false + description: Write timeout duration." + example: "10s" + - name: readTimeout + type: string + required: false + description: Read timeout duration." + example: "10s" + - name: handshakeVersion + type: number + required: false + description: Handshake version for RethinkDB." + example: "1" + - name: keepAlivePeriod + type: string + required: false + description: Keep alive period duration." + example: "30s" + - name: maxIdle + type: number + required: false + description: Maximum number of idle connections." + example: "5" + - name: authKey + type: string + required: false + description: The authentication key for RethinkDB. This field is now deprecated." + example: "auth-key" + sensitive: true + - name: initialCap + type: number + required: false + description: Initial connection pool capacity." + example: "5" + - name: maxOpen + type: number + required: false + description: Maximum number of open connections." + example: "10" + - name: discoverHosts + type: bool + required: false + description: Whether to discover hosts." + example: "false" + - name: nodeRefreshInterval + type: string + required: false + description: Node refresh interval duration." + example: "5m" diff --git a/bindings/rethinkdb/statechange/statechange.go b/bindings/rethinkdb/statechange/statechange.go index 8f53c9cc52..74e7bbba29 100644 --- a/bindings/rethinkdb/statechange/statechange.go +++ b/bindings/rethinkdb/statechange/statechange.go @@ -15,6 +15,7 @@ package statechange import ( "context" + "crypto/tls" "encoding/json" "errors" "fmt" @@ -22,6 +23,7 @@ import ( "strings" "sync" "sync/atomic" + "time" r "github.com/dancannon/gorethink" @@ -44,8 +46,37 @@ type Binding struct { // StateConfig is the binding config. type StateConfig struct { - r.ConnectOpts `mapstructure:",squash"` - Table string `mapstructure:"table"` + ConnectOptsWrapper `mapstructure:",squash"` + Table string `mapstructure:"table"` +} + +// ConnectOptsWrapper wraps r.ConnectOpts but excludes TLSConfig +// This is needed because the metadata decoder does not support nested structs with tags as inputs in the metadata.yaml file +type ConnectOptsWrapper struct { + Address string `gorethink:"address,omitempty"` + Addresses []string `gorethink:"addresses,omitempty"` + Database string `gorethink:"database,omitempty"` + Username string `gorethink:"username,omitempty"` + Password string `gorethink:"password,omitempty"` + AuthKey string `gorethink:"authkey,omitempty"` + Timeout time.Duration `gorethink:"timeout,omitempty"` + WriteTimeout time.Duration `gorethink:"write_timeout,omitempty"` + ReadTimeout time.Duration `gorethink:"read_timeout,omitempty"` + KeepAlivePeriod time.Duration `gorethink:"keep_alive_timeout,omitempty"` + HandshakeVersion int `gorethink:"handshake_version,omitempty"` + MaxIdle int `gorethink:"max_idle,omitempty"` + InitialCap int `gorethink:"initial_cap,omitempty"` + MaxOpen int `gorethink:"max_open,omitempty"` + DiscoverHosts bool `gorethink:"discover_hosts,omitempty"` + NodeRefreshInterval time.Duration `gorethink:"node_refresh_interval,omitempty"` + UseJSONNumber bool `gorethink:"use_json_number,omitempty"` + NumRetries int `gorethink:"num_retries,omitempty"` + HostDecayDuration time.Duration `gorethink:"host_decay_duration,omitempty"` + UseOpentracing bool `gorethink:"use_opentracing,omitempty"` + // TLS fields must be brought in as separate fields as they will not be processed by the metadata decoder properly without this + EnableTLS bool `gorethink:"enable_tls,omitempty"` + ClientCert string `gorethink:"client_cert,omitempty"` + ClientKey string `gorethink:"client_key,omitempty"` } // NewRethinkDBStateChangeBinding returns a new RethinkDB actor event input binding. @@ -64,7 +95,40 @@ func (b *Binding) Init(ctx context.Context, metadata bindings.Metadata) error { } b.config = cfg - ses, err := r.Connect(b.config.ConnectOpts) + // Convert wrapper to r.ConnectOpts + connectOpts := r.ConnectOpts{ + Address: cfg.Address, + Addresses: cfg.Addresses, + Database: cfg.Database, + Username: cfg.Username, + Password: cfg.Password, + AuthKey: cfg.AuthKey, + Timeout: cfg.Timeout, + WriteTimeout: cfg.WriteTimeout, + ReadTimeout: cfg.ReadTimeout, + KeepAlivePeriod: cfg.KeepAlivePeriod, + HandshakeVersion: r.HandshakeVersion(cfg.HandshakeVersion), + MaxIdle: cfg.MaxIdle, + InitialCap: cfg.InitialCap, + MaxOpen: cfg.MaxOpen, + DiscoverHosts: cfg.DiscoverHosts, + NodeRefreshInterval: cfg.NodeRefreshInterval, + UseJSONNumber: cfg.UseJSONNumber, + NumRetries: cfg.NumRetries, + HostDecayDuration: cfg.HostDecayDuration, + UseOpentracing: cfg.UseOpentracing, + } + + // Configure TLS if enabled + if cfg.EnableTLS { + tlsConfig, tlsErr := createTLSConfig(cfg.ClientCert, cfg.ClientKey) + if tlsErr != nil { + return fmt.Errorf("error creating TLS config: %w", tlsErr) + } + connectOpts.TLSConfig = tlsConfig + } + + ses, err := r.Connect(connectOpts) if err != nil { return fmt.Errorf("error connecting to the database: %w", err) } @@ -73,6 +137,23 @@ func (b *Binding) Init(ctx context.Context, metadata bindings.Metadata) error { return nil } +// createTLSConfig creates a tls.Config from client certificate and key +func createTLSConfig(clientCert, clientKey string) (*tls.Config, error) { + if clientCert == "" || clientKey == "" { + return nil, errors.New("both client certificate and key are required for TLS") + } + + cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) + if err != nil { + return nil, fmt.Errorf("error parsing client certificate and key: %w", err) + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + }, nil +} + // Read triggers the RethinkDB scheduler. func (b *Binding) Read(ctx context.Context, handler bindings.Handler) error { if b.closed.Load() { @@ -107,7 +188,7 @@ func (b *Binding) Read(ctx context.Context, handler bindings.Handler) error { go func() { defer b.wg.Done() for readCtx.Err() == nil { - var change interface{} + var change any ok := cursor.Next(&change) if !ok { b.logger.Errorf("error detecting change: %v", cursor.Err()) @@ -149,7 +230,7 @@ func (b *Binding) Close() error { return b.session.Close() } -func metadataToConfig(cfg map[string]string, logger logger.Logger) (StateConfig, error) { +func metadataToConfig(cfg map[string]string, _ logger.Logger) (StateConfig, error) { c := StateConfig{} // prepare metadata keys for decoding diff --git a/bindings/rocketmq/metadata.yaml b/bindings/rocketmq/metadata.yaml new file mode 100644 index 0000000000..3d87c0dcad --- /dev/null +++ b/bindings/rocketmq/metadata.yaml @@ -0,0 +1,77 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: rocketmq +version: v1 +status: alpha +title: "Apache RocketMQ" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/ +binding: + output: true + input: true + operations: + - name: create + description: "Send message to RocketMQ topic" + - name: read + description: "Receive messages from RocketMQ topic" +authenticationProfiles: + - title: "Access Key Authentication" + description: | + Authenticate with RocketMQ using access key and secret. + metadata: + - name: accessKey + required: true + description: "The access key for authentication" + example: "access-key" + - name: secretKey + required: true + sensitive: true + description: "The secret key for authentication" + example: "secret-key" +metadata: + - name: accessProto + required: false + description: "SDK protocol" + example: "tcp" + allowedValues: + - "tcp" + - "tcp-cgo" + - "http" + - name: nameServer + required: false + description: "The RocketMQ name server address" + example: "localhost:9876" + - name: endpoint + required: false + description: "The RocketMQ endpoint (for http proto)" + example: "http://localhost:8080" + - name: consumerGroup + required: false + description: "Consumer group for RocketMQ subscribers" + example: "my-consumer-group" + - name: consumerBatchSize + required: false + description: "Consumer batch size" + example: '10' + - name: consumerThreadNums + required: false + description: "Consumer thread numbers (for tcp-cgo proto)" + example: '4' + - name: instanceId + required: false + description: "RocketMQ namespace" + example: "my-instance" + - name: nameServerDomain + required: false + description: "RocketMQ name server domain" + example: "rocketmq.example.com" + - name: retries + required: false + description: "Retry times to connect to RocketMQ broker" + example: '3' + - name: topics + required: true + description: "Topics to subscribe (comma-separated for multiple topics)" + example: "topic1,topic2,topic3" diff --git a/bindings/rocketmq/settings.go b/bindings/rocketmq/settings.go index de3f7932eb..a2588da2ae 100644 --- a/bindings/rocketmq/settings.go +++ b/bindings/rocketmq/settings.go @@ -62,7 +62,7 @@ type Settings struct { Topics TopicsDelimited `mapstructure:"topics"` } -func (s *Settings) Decode(in interface{}) error { +func (s *Settings) Decode(in any) error { if err := metadata.DecodeMetadata(in, s); err != nil { return fmt.Errorf("decode error: %w", err) } diff --git a/bindings/sftp/client.go b/bindings/sftp/client.go new file mode 100644 index 0000000000..c187c4046a --- /dev/null +++ b/bindings/sftp/client.go @@ -0,0 +1,318 @@ +/* +Copyright 2025 The Dapr Authors +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 sftp + +import ( + "errors" + "fmt" + "io" + "os" + sysPath "path" + "strings" + "sync" + "sync/atomic" + + sftpClient "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" + + "github.com/dapr/kit/logger" +) + +type Client struct { + sshClient *ssh.Client + sftpClient *sftpClient.Client + address string + config *ssh.ClientConfig + lock sync.RWMutex + needsReconnect atomic.Bool + log logger.Logger + sequentialMode bool +} + +func newClient(address string, config *ssh.ClientConfig, sequentialMode bool, log logger.Logger) (*Client, error) { + if address == "" || config == nil { + return nil, errors.New("sftp binding error: client not initialized") + } + + sshClient, err := newSSHClient(address, config) + if err != nil { + return nil, err + } + + newSftpClient, err := sftpClient.NewClient(sshClient) + if err != nil { + _ = sshClient.Close() + return nil, fmt.Errorf("sftp binding error: error create sftp client: %w", err) + } + + return &Client{ + sshClient: sshClient, + sftpClient: newSftpClient, + address: address, + config: config, + log: log, + sequentialMode: sequentialMode, + }, nil +} + +func (c *Client) Close() error { + c.lock.Lock() + defer c.lock.Unlock() + + // Close SFTP first, then SSH + var sftpErr, sshErr error + if c.sftpClient != nil { + sftpErr = c.sftpClient.Close() + } + if c.sshClient != nil { + sshErr = c.sshClient.Close() + } + + // Return the first error encountered + if sftpErr != nil { + return sftpErr + } + return sshErr +} + +func (c *Client) list(path string) ([]os.FileInfo, error) { + var fi []os.FileInfo + + fn := func() error { + var err error + fi, err = c.sftpClient.ReadDir(path) + return err + } + + err := c.withReconnection(fn) + if err != nil { + return nil, err + } + + return fi, nil +} + +func (c *Client) create(data []byte, path string) (string, error) { + dir, fileName := sftpClient.Split(path) + + createFn := func() error { + // Only create directory if it doesn't already exist + // This prevents "not a directory" errors on strict SFTP servers like Axway MFT + dir = sysPath.Clean(dir) + if dir != "." { + if _, statErr := c.sftpClient.Stat(dir); statErr != nil { + // Directory doesn't exist, create it + if isDirNotExistError(statErr) { + if mkdirErr := c.sftpClient.MkdirAll(dir); mkdirErr != nil { + return fmt.Errorf("error create dir %s: %w", dir, mkdirErr) + } + } else { + return fmt.Errorf("error checking dir %s: %w", dir, statErr) + } + } + } + + file, cErr := c.sftpClient.Create(path) + if cErr != nil { + return fmt.Errorf("error create file %s: %w", path, cErr) + } + defer file.Close() + + _, wErr := file.Write(data) + if wErr != nil { + return fmt.Errorf("error write file %s: %w", path, wErr) + } + + return nil + } + + rErr := c.withReconnection(createFn) + if rErr != nil { + return "", rErr + } + + return fileName, nil +} + +func (c *Client) get(path string) ([]byte, error) { + var data []byte + + fn := func() error { + f, err := c.sftpClient.Open(path) + if err != nil { + return err + } + defer f.Close() + + data, err = io.ReadAll(f) + return err + } + + err := c.withReconnection(fn) + if err != nil { + return nil, err + } + + return data, nil +} + +func (c *Client) delete(path string) error { + fn := func() error { + return c.sftpClient.Remove(path) + } + + err := c.withReconnection(fn) + if err != nil { + return err + } + + return nil +} + +func (c *Client) ping() error { + _, err := c.sftpClient.Getwd() + if err != nil { + return err + } + return nil +} + +func (c *Client) withReconnection(fn func() error) error { + err := c.do(fn) + if !c.shouldReconnect(err) { + return err + } + + c.log.Debugf("sftp binding error: %s", err) + c.needsReconnect.Store(true) + + rErr := c.doReconnect() + if rErr != nil { + c.log.Debugf("sftp binding error: reconnect failed: %s", rErr) + return errors.Join(err, rErr) + } + c.log.Debugf("sftp binding: reconnected to %s", c.address) + + c.log.Debugf("sftp binding: retrying operation") + return c.do(fn) +} + +func (c *Client) do(fn func() error) error { + if c.sequentialMode { + c.lock.Lock() + defer c.lock.Unlock() + } else { + c.lock.RLock() + defer c.lock.RUnlock() + } + + return fn() +} + +func (c *Client) doReconnect() error { + c.lock.Lock() + defer c.lock.Unlock() + + c.log.Debugf("sftp binding: reconnecting to %s", c.address) + + if !c.needsReconnect.Load() { + return nil + } + + pErr := c.ping() + if pErr == nil { + c.needsReconnect.Store(false) + return nil + } + + sshClient, err := newSSHClient(c.address, c.config) + if err != nil { + return err + } + + newSftpClient, err := sftpClient.NewClient(sshClient) + if err != nil { + _ = sshClient.Close() + return fmt.Errorf("sftp binding error: error create sftp client: %w", err) + } + + if c.sftpClient != nil { + _ = c.sftpClient.Close() + } + if c.sshClient != nil { + _ = c.sshClient.Close() + } + + c.sftpClient = newSftpClient + c.sshClient = sshClient + + c.needsReconnect.Store(false) + return nil +} + +func newSSHClient(address string, config *ssh.ClientConfig) (*ssh.Client, error) { + sshClient, err := ssh.Dial("tcp", address, config) + if err != nil { + return nil, fmt.Errorf("sftp binding error: error dialing ssh server: %w", err) + } + return sshClient, nil +} + +// shouldReconnect returns true if the error looks like a transport-level failure +func (c *Client) shouldReconnect(err error) bool { + if err == nil { + return false + } + + // Check for StatusError using errors.As - if it's a StatusError, + // it's an SFTP protocol error, not a connection issue + var statusErr *sftpClient.StatusError + if errors.As(err, &statusErr) { + // Any StatusError is a protocol-level response from the server, + // meaning the connection is working fine + return false + } + + // Check sentinel errors for SFTP logical errors + // These errors indicate application-level issues, not connection problems + if errors.Is(err, sftpClient.ErrSSHFxPermissionDenied) || + errors.Is(err, sftpClient.ErrSSHFxNoSuchFile) || + errors.Is(err, sftpClient.ErrSSHFxOpUnsupported) || + errors.Is(err, sftpClient.ErrSSHFxFailure) || + errors.Is(err, sftpClient.ErrSSHFxBadMessage) || + errors.Is(err, sftpClient.ErrSSHFxEOF) { + return false + } + + // Fallback: string matching for wrapped errors that may not implement Is/As + errStr := strings.ToLower(err.Error()) + if strings.Contains(errStr, "permission denied") || + strings.Contains(errStr, "no such file") || + strings.Contains(errStr, "not a directory") || + strings.Contains(errStr, "file exists") || + strings.Contains(errStr, "bad message") { + return false + } + + return true +} + +func isDirNotExistError(err error) bool { + if err == nil { + return false + } + + return errors.Is(err, sftpClient.ErrSSHFxNoSuchFile) || + strings.Contains(strings.ToLower(err.Error()), "file does not exist") +} diff --git a/bindings/sftp/client_test.go b/bindings/sftp/client_test.go new file mode 100644 index 0000000000..436bb8cd40 --- /dev/null +++ b/bindings/sftp/client_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2025 The Dapr Authors +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 sftp + +import ( + "errors" + "fmt" + "testing" + + sftpClient "github.com/pkg/sftp" + "github.com/stretchr/testify/assert" +) + +func TestShouldReconnect(t *testing.T) { + c := &Client{} + + t.Run("nil error should not reconnect", func(t *testing.T) { + assert.False(t, c.shouldReconnect(nil)) + }) + + t.Run("StatusError should not reconnect", func(t *testing.T) { + // StatusError with permission denied code (3) + statusErr := &sftpClient.StatusError{ + Code: 3, // SSH_FX_PERMISSION_DENIED + } + assert.False(t, c.shouldReconnect(statusErr)) + }) + + t.Run("wrapped StatusError should not reconnect", func(t *testing.T) { + // StatusError with no such file code (2) + statusErr := &sftpClient.StatusError{ + Code: 2, // SSH_FX_NO_SUCH_FILE + } + wrappedErr := fmt.Errorf("operation failed: %w", statusErr) + assert.False(t, c.shouldReconnect(wrappedErr)) + }) + + t.Run("ErrSSHFxPermissionDenied should not reconnect", func(t *testing.T) { + assert.False(t, c.shouldReconnect(sftpClient.ErrSSHFxPermissionDenied)) + }) + + t.Run("ErrSSHFxNoSuchFile should not reconnect", func(t *testing.T) { + assert.False(t, c.shouldReconnect(sftpClient.ErrSSHFxNoSuchFile)) + }) + + t.Run("ErrSSHFxOpUnsupported should not reconnect", func(t *testing.T) { + assert.False(t, c.shouldReconnect(sftpClient.ErrSSHFxOpUnsupported)) + }) + + t.Run("ErrSSHFxFailure should not reconnect", func(t *testing.T) { + assert.False(t, c.shouldReconnect(sftpClient.ErrSSHFxFailure)) + }) + + t.Run("ErrSSHFxBadMessage should not reconnect", func(t *testing.T) { + assert.False(t, c.shouldReconnect(sftpClient.ErrSSHFxBadMessage)) + }) + + t.Run("ErrSSHFxEOF should not reconnect", func(t *testing.T) { + assert.False(t, c.shouldReconnect(sftpClient.ErrSSHFxEOF)) + }) + + t.Run("permission denied string should not reconnect", func(t *testing.T) { + err := errors.New("sftp: permission denied") + assert.False(t, c.shouldReconnect(err)) + }) + + t.Run("no such file string should not reconnect", func(t *testing.T) { + err := errors.New("sftp: no such file or directory") + assert.False(t, c.shouldReconnect(err)) + }) + + t.Run("not a directory string should not reconnect", func(t *testing.T) { + err := errors.New("mkdir upload/: not a directory") + assert.False(t, c.shouldReconnect(err)) + }) + + t.Run("file exists string should not reconnect", func(t *testing.T) { + err := errors.New("sftp: file exists error") + assert.False(t, c.shouldReconnect(err)) + }) + + t.Run("bad message string should not reconnect", func(t *testing.T) { + err := errors.New("sftp: bad message") + assert.False(t, c.shouldReconnect(err)) + }) + + t.Run("connection reset should reconnect", func(t *testing.T) { + err := errors.New("connection reset by peer") + assert.True(t, c.shouldReconnect(err)) + }) + + t.Run("EOF should reconnect", func(t *testing.T) { + err := errors.New("EOF") + assert.True(t, c.shouldReconnect(err)) + }) + + t.Run("broken pipe should reconnect", func(t *testing.T) { + err := errors.New("write: broken pipe") + assert.True(t, c.shouldReconnect(err)) + }) + + t.Run("connection refused should reconnect", func(t *testing.T) { + err := errors.New("connection refused") + assert.True(t, c.shouldReconnect(err)) + }) + + t.Run("timeout should reconnect", func(t *testing.T) { + err := errors.New("i/o timeout") + assert.True(t, c.shouldReconnect(err)) + }) + + t.Run("unknown error should reconnect", func(t *testing.T) { + err := errors.New("unknown transport error") + assert.True(t, c.shouldReconnect(err)) + }) +} diff --git a/bindings/sftp/docker-compose.yaml b/bindings/sftp/docker-compose.yaml new file mode 100644 index 0000000000..188dc6c952 --- /dev/null +++ b/bindings/sftp/docker-compose.yaml @@ -0,0 +1,11 @@ +services: + sftp: + image: + atmoz/sftp + environment: + - SFTP_USERS=foo:pass:1001:1001:upload + volumes: + - ./upload:/home/foo/upload + ports: + - "2222:22" + diff --git a/bindings/sftp/metadata.yaml b/bindings/sftp/metadata.yaml new file mode 100644 index 0000000000..49dadf4e3e --- /dev/null +++ b/bindings/sftp/metadata.yaml @@ -0,0 +1,84 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: sftp +version: v1 +status: alpha +title: "Secure File Transfer Protocol (SFTP)" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/sftp/ +binding: + output: true + input: false + operations: + - name: create + description: "Upload file via SFTP" + - name: get + description: "Download file from SFTP" + - name: delete + description: "Delete file from SFTP" + - name: list + description: "List files in SFTP directory" +authenticationProfiles: + - title: "Password Authentication" + description: | + Authenticate using username and password. + metadata: + - name: username + required: true + description: "The SFTP username" + example: "sftpuser" + - name: password + required: true + sensitive: true + description: "The SFTP password" + example: "password" + - title: "Private Key Authentication" + description: | + Authenticate using username and private key. + metadata: + - name: username + required: true + description: "The SFTP username" + example: "sftpuser" + - name: privateKey + required: true + sensitive: true + description: "The private key for authentication" + example: "-----BEGIN OPENSSH PRIVATE KEY-----\n..." + - name: privateKeyPassphrase + required: false + sensitive: true + description: "The passphrase for the private key" + example: "your-passphrase" +metadata: + - name: address + required: true + description: "The SFTP server address (host:port)" + example: "sftp.example.com:22" + - name: rootPath + required: true + description: "The root directory path on the SFTP server" + example: "/home/sftpuser" + - name: fileName + required: false + description: "The file name (can be overridden in request metadata)" + example: "data.txt" + - name: hostPublicKey + required: false + description: "The host public key for verification" + - name: knownHostsFile + required: false + description: "Path to the known_hosts file" + example: "/path/to/known_hosts" + - name: insecureIgnoreHostKey + required: false + description: "Skip host key verification (insecure)" + example: "false" + default: "false" + - name: sequentialMode + required: false + description: "Used to specify if concurrent operations are allowed within a single connection" + example: "false" + default: "false" diff --git a/bindings/sftp/sftp.go b/bindings/sftp/sftp.go index 960294a44a..47d67c6dba 100644 --- a/bindings/sftp/sftp.go +++ b/bindings/sftp/sftp.go @@ -1,3 +1,16 @@ +/* +Copyright 2025 The Dapr Authors +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 sftp import ( @@ -5,7 +18,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "reflect" sftpClient "github.com/pkg/sftp" @@ -25,9 +37,9 @@ const ( // Sftp is a binding for file operations on sftp server. type Sftp struct { - metadata *sftpMetadata - logger logger.Logger - sftpClient *sftpClient.Client + metadata *sftpMetadata + logger logger.Logger + c *Client } // sftpMetadata defines the sftp metadata. @@ -41,6 +53,7 @@ type sftpMetadata struct { HostPublicKey []byte `json:"hostPublicKey"` KnownHostsFile string `json:"knownHostsFile"` InsecureIgnoreHostKey bool `json:"insecureIgnoreHostKey"` + SequentialMode bool `json:"sequentialMode"` } type createResponse struct { @@ -115,19 +128,12 @@ func (sftp *Sftp) Init(_ context.Context, metadata bindings.Metadata) error { HostKeyCallback: hostKeyCallback, } - sshClient, err := ssh.Dial("tcp", m.Address, config) - if err != nil { - return fmt.Errorf("sftp binding error: error create ssh client: %w", err) - } - - newSftpClient, err := sftpClient.NewClient(sshClient) + sftp.metadata = m + sftp.c, err = newClient(m.Address, config, m.SequentialMode, sftp.logger) if err != nil { - return fmt.Errorf("sftp binding error: error create sftp client: %w", err) + return fmt.Errorf("sftp binding error: create sftp client error: %w", err) } - sftp.metadata = m - sftp.sftpClient = newSftpClient - return nil } @@ -161,21 +167,9 @@ func (sftp *Sftp) create(_ context.Context, req *bindings.InvokeRequest) (*bindi return nil, fmt.Errorf("sftp binding error: %w", err) } - dir, fileName := sftpClient.Split(path) - - err = sftp.sftpClient.MkdirAll(dir) - if err != nil { - return nil, fmt.Errorf("sftp binding error: error create dir %s: %w", dir, err) - } - - file, err := sftp.sftpClient.Create(path) + fileName, err := sftp.c.create(req.Data, path) if err != nil { - return nil, fmt.Errorf("sftp binding error: error create file %s: %w", path, err) - } - - _, err = file.Write(req.Data) - if err != nil { - return nil, fmt.Errorf("sftp binding error: error write file: %w", err) + return nil, fmt.Errorf("sftp binding error: %w", err) } jsonResponse, err := json.Marshal(createResponse{ @@ -204,7 +198,9 @@ func (sftp *Sftp) list(_ context.Context, req *bindings.InvokeRequest) (*binding return nil, fmt.Errorf("sftp binding error: %w", err) } - files, err := sftp.sftpClient.ReadDir(path) + c := sftp.c + + files, err := c.list(path) if err != nil { return nil, fmt.Errorf("sftp binding error: error read dir %s: %w", path, err) } @@ -239,18 +235,13 @@ func (sftp *Sftp) get(_ context.Context, req *bindings.InvokeRequest) (*bindings return nil, fmt.Errorf("sftp binding error: %w", err) } - file, err := sftp.sftpClient.Open(path) + data, err := sftp.c.get(path) if err != nil { - return nil, fmt.Errorf("sftp binding error: error open file %s: %w", path, err) - } - - b, err := io.ReadAll(file) - if err != nil { - return nil, fmt.Errorf("sftp binding error: error read file %s: %w", path, err) + return nil, fmt.Errorf("sftp binding error: error reading file %s: %w", path, err) } return &bindings.InvokeResponse{ - Data: b, + Data: data, }, nil } @@ -265,7 +256,9 @@ func (sftp *Sftp) delete(_ context.Context, req *bindings.InvokeRequest) (*bindi return nil, fmt.Errorf("sftp binding error: %w", err) } - err = sftp.sftpClient.Remove(path) + c := sftp.c + + err = c.delete(path) if err != nil { return nil, fmt.Errorf("sftp binding error: error remove file %s: %w", path, err) } @@ -289,7 +282,7 @@ func (sftp *Sftp) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bin } func (sftp *Sftp) Close() error { - return sftp.sftpClient.Close() + return sftp.c.Close() } func (metadata sftpMetadata) getPath(requestMetadata map[string]string) (path string, err error) { diff --git a/bindings/sftp/sftp_integration_test.go b/bindings/sftp/sftp_integration_test.go new file mode 100644 index 0000000000..fea07f12d9 --- /dev/null +++ b/bindings/sftp/sftp_integration_test.go @@ -0,0 +1,303 @@ +//go:build integration_test + +/* +Copyright 2025 The Dapr Authors +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 sftp + +import ( + "context" + "encoding/json" + "math/rand" + "os" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/bindings" + "github.com/dapr/components-contrib/tests/certification/flow" + "github.com/dapr/components-contrib/tests/certification/flow/dockercompose" + sftp "github.com/dapr/components-contrib/tests/utils/sftpproxy" + "github.com/dapr/kit/logger" +) + +const ( + ProxySftp = "0.0.0.0:2223" + ConnectionString = "0.0.0.0:2222" +) + +func TestIntegrationCases(t *testing.T) { + cleanUp := setupSftp(t) + defer cleanUp() + time.Sleep(1 * time.Second) + t.Run("List operation", testListOperation) + t.Run("Create operation", testCreateOperation) + t.Run("Reconnections", testReconnect) +} + +func setupSftp(t *testing.T) func() { + dc := dockercompose.New("sftp", "docker-compose.yaml") + ctx := flow.Context{ + T: t, + Context: t.Context(), + Flow: nil, + } + err := dc.Up(ctx) + + if err != nil { + t.Fatal(err) + } + + return func() { dc.Down(ctx) } +} + +func testListOperation(t *testing.T) { + proxy := &sftp.Proxy{ + ListenAddr: ProxySftp, + UpstreamAddr: ConnectionString, + } + + defer proxy.Close() + go proxy.ListenAndServe() + + c := Sftp{ + logger: logger.NewLogger("sftp"), + } + + m := bindings.Metadata{} + + m.Properties = map[string]string{ + "rootPath": "/upload", + "address": ProxySftp, + "username": "foo", + "password": "pass", + "insecureIgnoreHostKey": "true", + } + + err := c.Init(t.Context(), m) + require.NoError(t, err) + + r, err := c.Invoke(t.Context(), &bindings.InvokeRequest{Operation: bindings.ListOperation}) + require.NoError(t, err) + assert.NotNil(t, r.Data) + + var d []listResponse + err = json.Unmarshal(r.Data, &d) + require.NoError(t, err) + + assert.EqualValues(t, 1, proxy.ReconnectionCount.Load()) +} + +func testCreateOperation(t *testing.T) { + proxy := &sftp.Proxy{ + ListenAddr: ProxySftp, + UpstreamAddr: ConnectionString, + } + defer proxy.Close() + go proxy.ListenAndServe() + c := Sftp{ + logger: logger.NewLogger("sftp"), + } + m := bindings.Metadata{} + m.Properties = map[string]string{ + "rootPath": "/upload", + "address": ProxySftp, + "username": "foo", + "password": "pass", + "insecureIgnoreHostKey": "true", + } + + err := os.Remove("./upload/test.txt") + if err != nil && !os.IsNotExist(err) { + require.NoError(t, err) + } + + err = c.Init(t.Context(), m) + require.NoError(t, err) + + r, err := c.Invoke(t.Context(), &bindings.InvokeRequest{ + Operation: bindings.CreateOperation, + Data: []byte("test data 1"), + Metadata: map[string]string{ + "fileName": "test.txt", + }, + }) + require.NoError(t, err) + assert.NotNil(t, r.Data) + + file, err := os.Stat("./upload/test.txt") + require.NoError(t, err) + assert.Equal(t, "test.txt", file.Name()) + assert.EqualValues(t, 1, proxy.ReconnectionCount.Load()) +} + +func testReconnect(t *testing.T) { + proxy := &sftp.Proxy{ + ListenAddr: ProxySftp, + UpstreamAddr: ConnectionString, + } + defer proxy.Close() + go proxy.ListenAndServe() + + c := Sftp{ + logger: logger.NewLogger("sftp"), + } + + m := bindings.Metadata{} + + m.Properties = map[string]string{ + "rootPath": "/upload", + "address": ProxySftp, + "username": "foo", + "password": "pass", + "insecureIgnoreHostKey": "true", + } + + err := c.Init(t.Context(), m) + require.NoError(t, err) + + t.Run("List operation", func(t *testing.T) { + r, err := c.Invoke(t.Context(), &bindings.InvokeRequest{Operation: bindings.ListOperation}) + require.NoError(t, err) + assert.NotNil(t, r.Data) + + _ = proxy.KillServerConn() + + r, err = c.Invoke(t.Context(), &bindings.InvokeRequest{Operation: bindings.ListOperation}) + require.NoError(t, err) + assert.NotNil(t, r.Data) + + var d []listResponse + err = json.Unmarshal(r.Data, &d) + require.NoError(t, err) + + assert.EqualValues(t, 2, proxy.ReconnectionCount.Load()) + }) + + t.Run("List delete - no reconnection", func(t *testing.T) { + numReconnects := proxy.ReconnectionCount.Load() + _, err := c.Invoke(t.Context(), &bindings.InvokeRequest{ + Operation: bindings.DeleteOperation, + Metadata: map[string]string{ + "fileName": "file_does_not_exist.txt", + }, + }) + + require.Error(t, err) + + assert.EqualValues(t, numReconnects, proxy.ReconnectionCount.Load()) + }) + + t.Run("List delete - reconnection", func(t *testing.T) { + numReconnects := proxy.ReconnectionCount.Load() + _ = proxy.KillServerConn() + _, err := c.Invoke(t.Context(), &bindings.InvokeRequest{ + Operation: bindings.DeleteOperation, + Metadata: map[string]string{ + "fileName": "file_does_not_exist.txt", + }, + }) + + require.Error(t, err) + + assert.EqualValues(t, numReconnects+1, proxy.ReconnectionCount.Load()) + }) + + t.Run("List get - no reconnection", func(t *testing.T) { + numReconnects := proxy.ReconnectionCount.Load() + _, err := c.Invoke(t.Context(), &bindings.InvokeRequest{ + Operation: bindings.GetOperation, + Metadata: map[string]string{ + "fileName": "file_does_not_exist.txt", + }, + }) + + require.Error(t, err) + + assert.EqualValues(t, numReconnects, proxy.ReconnectionCount.Load()) + }) + + t.Run("List get - reconnection", func(t *testing.T) { + numReconnects := proxy.ReconnectionCount.Load() + _ = proxy.KillServerConn() + _, err := c.Invoke(t.Context(), &bindings.InvokeRequest{ + Operation: bindings.GetOperation, + Metadata: map[string]string{ + "fileName": "file_does_not_exist.txt", + }, + }) + + require.Error(t, err) + + assert.EqualValues(t, numReconnects+1, proxy.ReconnectionCount.Load()) + }) + + t.Run("Parallel ops - reconnection", func(t *testing.T) { + numReconnects := proxy.ReconnectionCount.Load() + ctx, cancelFn := context.WithCancel(t.Context()) + opCount := atomic.Int32{} + opFailed := atomic.Int32{} + for range 10 { + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(100*rand.Float32()) * time.Millisecond): + opCount.Add(1) + r, err := c.Invoke(t.Context(), &bindings.InvokeRequest{Operation: bindings.ListOperation}) + if err != nil { + opFailed.Add(1) + break + } + + assert.NotNil(t, r.Data) + } + } + }(ctx) + } + + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(1 * time.Second): + _ = proxy.KillServerConn() + } + } + + }(ctx) + + time.Sleep(time.Second * 5) + cancelFn() + + totalOps := opCount.Load() + failedOps := opFailed.Load() + + // Calculate 5% tolerance + tolerance := float64(totalOps) * 0.05 + + // Assert that failed operations are within 1% of total operations + assert.InDelta(t, 0, failedOps, tolerance, + "Expected less than 1%% of operations to fail. Total: %d, Failed: %d (%.2f%%)", + totalOps, failedOps, (float64(failedOps)/float64(totalOps))*100) + + expectedReconnects := numReconnects + 5 + currentReconnects := proxy.ReconnectionCount.Load() + assert.InDelta(t, expectedReconnects, currentReconnects, 2.0, "Expected %d reconnections, got %d", expectedReconnects, currentReconnects) + }) +} diff --git a/bindings/sftp/upload/test.txt b/bindings/sftp/upload/test.txt new file mode 100644 index 0000000000..6d78b42a41 --- /dev/null +++ b/bindings/sftp/upload/test.txt @@ -0,0 +1 @@ +test data 1 \ No newline at end of file diff --git a/bindings/smtp/metadata.yaml b/bindings/smtp/metadata.yaml new file mode 100644 index 0000000000..187915302f --- /dev/null +++ b/bindings/smtp/metadata.yaml @@ -0,0 +1,70 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: smtp +version: v1 +status: alpha +title: "SMTP" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/smtp/ +binding: + output: true + input: false + operations: + - name: create + description: "Send email via SMTP" +authenticationProfiles: + - title: "User/Password Authentication" + description: | + Authenticate with SMTP server using username and password. + metadata: + - name: user + required: true + description: "The SMTP username" + example: "user@gmail.com" + - name: password + required: true + sensitive: true + description: "The SMTP password" + example: "password" +metadata: + - name: host + required: true + description: "The SMTP server host" + example: "smtp.gmail.com" + - name: port + required: false + description: "The SMTP server port" + example: '587' + default: '587' + - name: emailFrom + required: true + description: "The sender email address" + example: "sender@example.com" + - name: emailTo + required: true + description: "The recipient email address" + example: "recipient@example.com" + - name: emailCC + required: false + description: "The email CC address" + example: "cc@example.com" + - name: emailBCC + required: false + description: "The email BCC address" + example: "bcc@example.com" + - name: priority + required: false + description: "The email priority" + example: '3' + default: '3' + - name: subject + required: true + description: "The email subject" + example: "Hello from Dapr" + - name: skipTLSVerify + required: false + description: "Skip TLS verification" + example: "false" + default: "false" diff --git a/bindings/twilio/sendgrid/metadata.yaml b/bindings/twilio/sendgrid/metadata.yaml new file mode 100644 index 0000000000..f645420b1b --- /dev/null +++ b/bindings/twilio/sendgrid/metadata.yaml @@ -0,0 +1,63 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: twilio.sendgrid +version: v1 +status: alpha +title: "Twilio SendGrid" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/sendgrid/ +binding: + output: true + input: false + operations: + - name: create + description: "Send email via SendGrid" +authenticationProfiles: + - title: "API Key Authentication" + description: | + Authenticate using SendGrid API key. + metadata: + - name: apiKey + required: true + sensitive: true + description: "The SendGrid API key" + example: "SG.api-key" +metadata: + - name: emailFrom + required: true + description: "The sender email address" + example: "sender@example.com" + - name: emailFromName + required: false + description: "The sender name" + example: "John Doe" + - name: emailTo + required: true + description: "The recipient email address" + example: "recipient@example.com" + - name: emailToName + required: false + description: "The recipient name" + example: "Jane Smith" + - name: subject + required: true + description: "The email subject" + example: "Hello from Dapr" + - name: emailCc + required: false + description: "The CC email address" + example: "cc@example.com" + - name: emailBcc + required: false + description: "The BCC email address" + example: "bcc@example.com" + - name: dynamicTemplateData + required: false + description: "The dynamic template data" + example: '{"name":"John","age":30}' + - name: dynamicTemplateId + required: false + description: "The dynamic template ID" + example: "your-template-id" diff --git a/bindings/twilio/sms/metadata.yaml b/bindings/twilio/sms/metadata.yaml new file mode 100644 index 0000000000..e0ba290454 --- /dev/null +++ b/bindings/twilio/sms/metadata.yaml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: twilio.sms +version: v1 +status: stable +title: "Twilio SMS" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/twilio/ +binding: + output: true + input: false + operations: + - name: create + description: "Send SMS via Twilio" +authenticationProfiles: + - title: "Twilio Authentication" + description: | + Authenticate using Twilio account credentials. + metadata: + - name: accountSid + required: true + description: "The Twilio account SID" + example: "AC1234567890abcdef" + - name: authToken + required: true + sensitive: true + description: "The Twilio auth token" + example: "auth-token" +metadata: + - name: fromNumber + required: true + description: "The sender phone number" + example: "+1234567890" + - name: toNumber + required: true + description: "The recipient phone number" + example: "+0987654321" + - name: timeout + required: false + type: duration + description: "The timeout for the SMS request" + example: '30s' + default: '30s' diff --git a/bindings/twilio/sms/sms.go b/bindings/twilio/sms/sms.go index 2bd4a59609..42503d4ce3 100644 --- a/bindings/twilio/sms/sms.go +++ b/bindings/twilio/sms/sms.go @@ -33,10 +33,6 @@ import ( const ( toNumber = "toNumber" - fromNumber = "fromNumber" - accountSid = "accountSid" - authToken = "authToken" - timeout = "timeout" twilioURLBase = "https://api.twilio.com/2010-04-01/Accounts/" ) diff --git a/bindings/wasm/metadata.yaml b/bindings/wasm/metadata.yaml new file mode 100644 index 0000000000..f83516f212 --- /dev/null +++ b/bindings/wasm/metadata.yaml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: wasm +version: v1 +status: alpha +title: "WebAssembly (WASM)" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/wasm/ +binding: + output: true + input: false + operations: + - name: create + description: "Execute WASM function" +metadata: + - name: strictSandbox + required: false + description: "Strict sandbox mode. When true, uses fake sources to avoid vulnerabilities such as timing attacks." + example: "true" + default: "false" + - name: url + required: true + description: "The URL of the WASM file" + example: "https://example.com/function.wasm" diff --git a/common/authentication/aws/aws.go b/common/authentication/aws/aws.go deleted file mode 100644 index ef39402663..0000000000 --- a/common/authentication/aws/aws.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -Copyright 2021 The Dapr Authors -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 aws - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/dapr/kit/logger" -) - -type EnvironmentSettings struct { - Metadata map[string]string -} - -type Options struct { - Logger logger.Logger - Properties map[string]string - - PoolConfig *pgxpool.Config `json:"poolConfig" mapstructure:"poolConfig"` - ConnectionString string `json:"connectionString" mapstructure:"connectionString"` - - // TODO: in Dapr 1.17 rm the alias on regions as we rm the aws prefixed one. - // Docs have it just as region, but most metadata fields show the aws prefix... - Region string `json:"region" mapstructure:"region" mapstructurealiases:"awsRegion"` - AccessKey string `json:"accessKey" mapstructure:"accessKey"` - SecretKey string `json:"secretKey" mapstructure:"secretKey"` - SessionName string `json:"sessionName" mapstructure:"sessionName"` - AssumeRoleARN string `json:"assumeRoleArn" mapstructure:"assumeRoleArn"` - SessionToken string `json:"sessionToken" mapstructure:"sessionToken"` - - Endpoint string -} - -// TODO: Delete in Dapr 1.17 so we can move all IAM fields to use the defaults of: -// accessKey and secretKey and region as noted in the docs, and Options struct above. -type DeprecatedPostgresIAM struct { - // Access key to use for accessing PostgreSQL. - AccessKey string `json:"awsAccessKey" mapstructure:"awsAccessKey"` - // Secret key to use for accessing PostgreSQL. - SecretKey string `json:"awsSecretKey" mapstructure:"awsSecretKey"` -} - -func GetConfig(opts Options) *aws.Config { - cfg := aws.NewConfig() - - switch { - case opts.Region != "": - cfg.WithRegion(opts.Region) - case opts.Endpoint != "": - cfg.WithEndpoint(opts.Endpoint) - } - - return cfg -} - -//nolint:interfacebloat -type Provider interface { - S3() *S3Clients - DynamoDB() *DynamoDBClients - Sqs() *SqsClients - Sns() *SnsClients - SnsSqs() *SnsSqsClients - SecretManager() *SecretManagerClients - ParameterStore() *ParameterStoreClients - Kinesis() *KinesisClients - Ses() *SesClients - - // Postgres is an outlier to the others in the sense that we can update only it's config, - // as we use a max connection time of 8 minutes. - // This means that we can just update the config session credentials, - // and then in 8 minutes it will update to a new session automatically for us. - UpdatePostgres(context.Context, *pgxpool.Config) - - Close() error -} - -func NewProvider(ctx context.Context, opts Options, cfg *aws.Config) (Provider, error) { - if isX509Auth(opts.Properties) { - return newX509(ctx, opts, cfg) - } - return newStaticIAM(ctx, opts, cfg) -} - -// NewEnvironmentSettings returns a new EnvironmentSettings configured for a given AWS resource. -func NewEnvironmentSettings(md map[string]string) (EnvironmentSettings, error) { - es := EnvironmentSettings{ - Metadata: md, - } - - return es, nil -} diff --git a/common/authentication/aws/aws_test.go b/common/authentication/aws/aws_test.go deleted file mode 100644 index 15aac78ad7..0000000000 --- a/common/authentication/aws/aws_test.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2024 The Dapr Authors -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 aws - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewEnvironmentSettings(t *testing.T) { - tests := []struct { - name string - metadata map[string]string - }{ - { - name: "valid metadata", - metadata: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := NewEnvironmentSettings(tt.metadata) - require.NoError(t, err) - assert.NotNil(t, result) - }) - } -} diff --git a/common/authentication/aws/client.go b/common/authentication/aws/client.go deleted file mode 100644 index b210e32944..0000000000 --- a/common/authentication/aws/client.go +++ /dev/null @@ -1,210 +0,0 @@ -/* -Copyright 2024 The Dapr Authors -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 aws - -import ( - "context" - "errors" - "sync" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" - "github.com/aws/aws-sdk-go/service/kinesis" - "github.com/aws/aws-sdk-go/service/kinesis/kinesisiface" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" - "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" - "github.com/aws/aws-sdk-go/service/ses" - "github.com/aws/aws-sdk-go/service/sns" - "github.com/aws/aws-sdk-go/service/sqs" - "github.com/aws/aws-sdk-go/service/sqs/sqsiface" - "github.com/aws/aws-sdk-go/service/ssm" - "github.com/aws/aws-sdk-go/service/ssm/ssmiface" - "github.com/aws/aws-sdk-go/service/sts" - "github.com/vmware/vmware-go-kcl/clientlibrary/config" -) - -type Clients struct { - mu sync.RWMutex - - s3 *S3Clients - Dynamo *DynamoDBClients - sns *SnsClients - sqs *SqsClients - snssqs *SnsSqsClients - Secret *SecretManagerClients - ParameterStore *ParameterStoreClients - kinesis *KinesisClients - ses *SesClients -} - -func newClients() *Clients { - return new(Clients) -} - -func (c *Clients) refresh(session *session.Session) error { - c.mu.Lock() - defer c.mu.Unlock() - switch { - case c.s3 != nil: - c.s3.New(session) - case c.Dynamo != nil: - c.Dynamo.New(session) - case c.sns != nil: - c.sns.New(session) - case c.sqs != nil: - c.sqs.New(session) - case c.snssqs != nil: - c.snssqs.New(session) - case c.Secret != nil: - c.Secret.New(session) - case c.ParameterStore != nil: - c.ParameterStore.New(session) - case c.kinesis != nil: - c.kinesis.New(session) - case c.ses != nil: - c.ses.New(session) - } - return nil -} - -type S3Clients struct { - S3 *s3.S3 - Uploader *s3manager.Uploader - Downloader *s3manager.Downloader -} - -type DynamoDBClients struct { - DynamoDB dynamodbiface.DynamoDBAPI -} - -type SnsSqsClients struct { - Sns *sns.SNS - Sqs *sqs.SQS - Sts *sts.STS -} - -type SnsClients struct { - Sns *sns.SNS -} - -type SqsClients struct { - Sqs sqsiface.SQSAPI -} - -type SecretManagerClients struct { - Manager secretsmanageriface.SecretsManagerAPI -} - -type ParameterStoreClients struct { - Store ssmiface.SSMAPI -} - -type KinesisClients struct { - Kinesis kinesisiface.KinesisAPI - Region string - Credentials *credentials.Credentials -} - -type SesClients struct { - Ses *ses.SES -} - -func (c *S3Clients) New(session *session.Session) { - refreshedS3 := s3.New(session, session.Config) - c.S3 = refreshedS3 - c.Uploader = s3manager.NewUploaderWithClient(refreshedS3) - c.Downloader = s3manager.NewDownloaderWithClient(refreshedS3) -} - -func (c *DynamoDBClients) New(session *session.Session) { - c.DynamoDB = dynamodb.New(session, session.Config) -} - -func (c *SnsClients) New(session *session.Session) { - c.Sns = sns.New(session, session.Config) -} - -func (c *SnsSqsClients) New(session *session.Session) { - c.Sns = sns.New(session, session.Config) - c.Sqs = sqs.New(session, session.Config) - c.Sts = sts.New(session, session.Config) -} - -func (c *SqsClients) New(session *session.Session) { - c.Sqs = sqs.New(session, session.Config) -} - -func (c *SqsClients) QueueURL(ctx context.Context, queueName string) (*string, error) { - if c.Sqs != nil { - resultURL, err := c.Sqs.GetQueueUrlWithContext(ctx, &sqs.GetQueueUrlInput{ - QueueName: aws.String(queueName), - }) - if resultURL != nil { - return resultURL.QueueUrl, err - } - } - return nil, errors.New("unable to get queue url due to empty client") -} - -func (c *SecretManagerClients) New(session *session.Session) { - c.Manager = secretsmanager.New(session, session.Config) -} - -func (c *ParameterStoreClients) New(session *session.Session) { - c.Store = ssm.New(session, session.Config) -} - -func (c *KinesisClients) New(session *session.Session) { - c.Kinesis = kinesis.New(session, session.Config) - c.Region = *session.Config.Region - c.Credentials = session.Config.Credentials -} - -func (c *KinesisClients) Stream(ctx context.Context, streamName string) (*string, error) { - if c.Kinesis != nil { - stream, err := c.Kinesis.DescribeStreamWithContext(ctx, &kinesis.DescribeStreamInput{ - StreamName: aws.String(streamName), - }) - if stream != nil { - return stream.StreamDescription.StreamARN, err - } - } - - return nil, errors.New("unable to get stream arn due to empty client") -} - -func (c *KinesisClients) WorkerCfg(ctx context.Context, stream, consumer, mode string) *config.KinesisClientLibConfiguration { - const sharedMode = "shared" - if c.Kinesis != nil { - if mode == sharedMode { - if c.Credentials != nil { - kclConfig := config.NewKinesisClientLibConfigWithCredential(consumer, - stream, c.Region, consumer, - c.Credentials) - return kclConfig - } - } - } - - return nil -} - -func (c *SesClients) New(session *session.Session) { - c.Ses = ses.New(session, session.Config) -} diff --git a/common/authentication/aws/client_fake.go b/common/authentication/aws/client_fake.go deleted file mode 100644 index c9e23641ba..0000000000 --- a/common/authentication/aws/client_fake.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2024 The Dapr Authors -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 aws - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" - "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" - "github.com/aws/aws-sdk-go/service/ssm" - "github.com/aws/aws-sdk-go/service/ssm/ssmiface" -) - -type MockParameterStore struct { - GetParameterFn func(context.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error) - DescribeParametersFn func(context.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error) - ssmiface.SSMAPI -} - -func (m *MockParameterStore) GetParameterWithContext(ctx context.Context, input *ssm.GetParameterInput, option ...request.Option) (*ssm.GetParameterOutput, error) { - return m.GetParameterFn(ctx, input, option...) -} - -func (m *MockParameterStore) DescribeParametersWithContext(ctx context.Context, input *ssm.DescribeParametersInput, option ...request.Option) (*ssm.DescribeParametersOutput, error) { - return m.DescribeParametersFn(ctx, input, option...) -} - -type MockSecretManager struct { - GetSecretValueFn func(context.Context, *secretsmanager.GetSecretValueInput, ...request.Option) (*secretsmanager.GetSecretValueOutput, error) - secretsmanageriface.SecretsManagerAPI -} - -func (m *MockSecretManager) GetSecretValueWithContext(ctx context.Context, input *secretsmanager.GetSecretValueInput, option ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - return m.GetSecretValueFn(ctx, input, option...) -} - -type MockDynamoDB struct { - GetItemWithContextFn func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (*dynamodb.GetItemOutput, error) - PutItemWithContextFn func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (*dynamodb.PutItemOutput, error) - DeleteItemWithContextFn func(ctx context.Context, input *dynamodb.DeleteItemInput, op ...request.Option) (*dynamodb.DeleteItemOutput, error) - BatchWriteItemWithContextFn func(ctx context.Context, input *dynamodb.BatchWriteItemInput, op ...request.Option) (*dynamodb.BatchWriteItemOutput, error) - TransactWriteItemsWithContextFn func(aws.Context, *dynamodb.TransactWriteItemsInput, ...request.Option) (*dynamodb.TransactWriteItemsOutput, error) - dynamodbiface.DynamoDBAPI -} - -func (m *MockDynamoDB) GetItemWithContext(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (*dynamodb.GetItemOutput, error) { - return m.GetItemWithContextFn(ctx, input, op...) -} - -func (m *MockDynamoDB) PutItemWithContext(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (*dynamodb.PutItemOutput, error) { - return m.PutItemWithContextFn(ctx, input, op...) -} - -func (m *MockDynamoDB) DeleteItemWithContext(ctx context.Context, input *dynamodb.DeleteItemInput, op ...request.Option) (*dynamodb.DeleteItemOutput, error) { - return m.DeleteItemWithContextFn(ctx, input, op...) -} - -func (m *MockDynamoDB) BatchWriteItemWithContext(ctx context.Context, input *dynamodb.BatchWriteItemInput, op ...request.Option) (*dynamodb.BatchWriteItemOutput, error) { - return m.BatchWriteItemWithContextFn(ctx, input, op...) -} - -func (m *MockDynamoDB) TransactWriteItemsWithContext(ctx context.Context, input *dynamodb.TransactWriteItemsInput, op ...request.Option) (*dynamodb.TransactWriteItemsOutput, error) { - return m.TransactWriteItemsWithContextFn(ctx, input, op...) -} diff --git a/common/authentication/aws/client_test.go b/common/authentication/aws/client_test.go deleted file mode 100644 index 85e0392aae..0000000000 --- a/common/authentication/aws/client_test.go +++ /dev/null @@ -1,265 +0,0 @@ -/* -Copyright 2024 The Dapr Authors -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 aws - -import ( - "context" - "errors" - "testing" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/kinesis" - "github.com/aws/aws-sdk-go/service/kinesis/kinesisiface" - "github.com/aws/aws-sdk-go/service/sqs" - "github.com/aws/aws-sdk-go/service/sqs/sqsiface" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/vmware/vmware-go-kcl/clientlibrary/config" -) - -type mockedSQS struct { - sqsiface.SQSAPI - GetQueueURLFn func(ctx context.Context, input *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error) -} - -func (m *mockedSQS) GetQueueUrlWithContext(ctx context.Context, input *sqs.GetQueueUrlInput, opts ...request.Option) (*sqs.GetQueueUrlOutput, error) { //nolint:stylecheck - return m.GetQueueURLFn(ctx, input) -} - -type mockedKinesis struct { - kinesisiface.KinesisAPI - DescribeStreamFn func(ctx context.Context, input *kinesis.DescribeStreamInput) (*kinesis.DescribeStreamOutput, error) -} - -func (m *mockedKinesis) DescribeStreamWithContext(ctx context.Context, input *kinesis.DescribeStreamInput, opts ...request.Option) (*kinesis.DescribeStreamOutput, error) { - return m.DescribeStreamFn(ctx, input) -} - -func TestS3Clients_New(t *testing.T) { - tests := []struct { - name string - s3Client *S3Clients - session *session.Session - }{ - {"initializes S3 client", &S3Clients{}, session.Must(session.NewSession())}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.s3Client.New(tt.session) - require.NotNil(t, tt.s3Client.S3) - require.NotNil(t, tt.s3Client.Uploader) - require.NotNil(t, tt.s3Client.Downloader) - }) - } -} - -func TestSqsClients_QueueURL(t *testing.T) { - tests := []struct { - name string - mockFn func() *mockedSQS - queueName string - expectedURL *string - expectError bool - }{ - { - name: "returns queue URL successfully", - mockFn: func() *mockedSQS { - return &mockedSQS{ - GetQueueURLFn: func(ctx context.Context, input *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error) { - return &sqs.GetQueueUrlOutput{ - QueueUrl: aws.String("https://sqs.aws.com/123456789012/queue"), - }, nil - }, - } - }, - queueName: "valid-queue", - expectedURL: aws.String("https://sqs.aws.com/123456789012/queue"), - expectError: false, - }, - { - name: "returns error when queue URL not found", - mockFn: func() *mockedSQS { - return &mockedSQS{ - GetQueueURLFn: func(ctx context.Context, input *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error) { - return nil, errors.New("unable to get stream arn due to empty client") - }, - } - }, - queueName: "missing-queue", - expectedURL: nil, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockSQS := tt.mockFn() - - // Initialize SqsClients with the mocked SQS client - client := &SqsClients{ - Sqs: mockSQS, - } - - url, err := client.QueueURL(t.Context(), tt.queueName) - - if tt.expectError { - require.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.expectedURL, url) - } - }) - } -} - -func TestKinesisClients_Stream(t *testing.T) { - tests := []struct { - name string - kinesisClient *KinesisClients - streamName string - mockStreamARN *string - mockError error - expectedStream *string - expectedErr error - }{ - { - name: "successfully retrieves stream ARN", - kinesisClient: &KinesisClients{ - Kinesis: &mockedKinesis{DescribeStreamFn: func(ctx context.Context, input *kinesis.DescribeStreamInput) (*kinesis.DescribeStreamOutput, error) { - return &kinesis.DescribeStreamOutput{ - StreamDescription: &kinesis.StreamDescription{ - StreamARN: aws.String("arn:aws:kinesis:some-region:123456789012:stream/some-stream"), - }, - }, nil - }}, - Region: "us-west-1", - Credentials: credentials.NewStaticCredentials("accessKey", "secretKey", ""), - }, - streamName: "some-stream", - expectedStream: aws.String("arn:aws:kinesis:some-region:123456789012:stream/some-stream"), - expectedErr: nil, - }, - { - name: "returns error when stream not found", - kinesisClient: &KinesisClients{ - Kinesis: &mockedKinesis{DescribeStreamFn: func(ctx context.Context, input *kinesis.DescribeStreamInput) (*kinesis.DescribeStreamOutput, error) { - return nil, errors.New("stream not found") - }}, - Region: "us-west-1", - Credentials: credentials.NewStaticCredentials("accessKey", "secretKey", ""), - }, - streamName: "nonexistent-stream", - expectedStream: nil, - expectedErr: errors.New("unable to get stream arn due to empty client"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tt.kinesisClient.Stream(t.Context(), tt.streamName) - if tt.expectedErr != nil { - require.Error(t, err) - assert.Equal(t, tt.expectedErr.Error(), err.Error()) - } else { - require.NoError(t, err) - assert.Equal(t, tt.expectedStream, got) - } - }) - } -} - -func TestKinesisClients_WorkerCfg(t *testing.T) { - testCreds := credentials.NewStaticCredentials("accessKey", "secretKey", "") - tests := []struct { - name string - kinesisClient *KinesisClients - streamName string - consumer string - mode string - expectedConfig *config.KinesisClientLibConfiguration - }{ - { - name: "successfully creates shared mode worker config", - kinesisClient: &KinesisClients{ - Kinesis: &mockedKinesis{ - DescribeStreamFn: func(ctx context.Context, input *kinesis.DescribeStreamInput) (*kinesis.DescribeStreamOutput, error) { - return &kinesis.DescribeStreamOutput{ - StreamDescription: &kinesis.StreamDescription{ - StreamARN: aws.String("arn:aws:kinesis:us-east-1:123456789012:stream/existing-stream"), - }, - }, nil - }, - }, - Region: "us-west-1", - Credentials: testCreds, - }, - streamName: "existing-stream", - consumer: "consumer1", - mode: "shared", - expectedConfig: config.NewKinesisClientLibConfigWithCredential( - "consumer1", "existing-stream", "us-west-1", "consumer1", testCreds, - ), - }, - { - name: "returns nil when mode is not shared", - kinesisClient: &KinesisClients{ - Kinesis: &mockedKinesis{ - DescribeStreamFn: func(ctx context.Context, input *kinesis.DescribeStreamInput) (*kinesis.DescribeStreamOutput, error) { - return &kinesis.DescribeStreamOutput{ - StreamDescription: &kinesis.StreamDescription{ - StreamARN: aws.String("arn:aws:kinesis:us-east-1:123456789012:stream/existing-stream"), - }, - }, nil - }, - }, - Region: "us-west-1", - Credentials: testCreds, - }, - streamName: "existing-stream", - consumer: "consumer1", - mode: "exclusive", - expectedConfig: nil, - }, - { - name: "returns nil when client is nil", - kinesisClient: &KinesisClients{ - Kinesis: nil, - Region: "us-west-1", - Credentials: credentials.NewStaticCredentials("accessKey", "secretKey", ""), - }, - streamName: "existing-stream", - consumer: "consumer1", - mode: "shared", - expectedConfig: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := tt.kinesisClient.WorkerCfg(t.Context(), tt.streamName, tt.consumer, tt.mode) - if tt.expectedConfig == nil { - assert.Equal(t, tt.expectedConfig, cfg) - return - } - assert.Equal(t, tt.expectedConfig.StreamName, cfg.StreamName) - assert.Equal(t, tt.expectedConfig.EnhancedFanOutConsumerName, cfg.EnhancedFanOutConsumerName) - assert.Equal(t, tt.expectedConfig.EnableEnhancedFanOutConsumer, cfg.EnableEnhancedFanOutConsumer) - assert.Equal(t, tt.expectedConfig.RegionName, cfg.RegionName) - }) - } -} diff --git a/common/authentication/aws/static.go b/common/authentication/aws/static.go deleted file mode 100644 index 3c3bf7a389..0000000000 --- a/common/authentication/aws/static.go +++ /dev/null @@ -1,386 +0,0 @@ -/* -Copyright 2021 The Dapr Authors -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 aws - -import ( - "context" - "fmt" - "strconv" - "sync" - "time" - - awsv2 "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - v2creds "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/credentials/stscreds" - "github.com/aws/aws-sdk-go-v2/feature/rds/auth" - "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/dapr/kit/logger" -) - -type StaticAuth struct { - mu sync.RWMutex - logger logger.Logger - - region *string - endpoint *string - accessKey *string - secretKey *string - sessionToken string - - assumeRoleARN *string - sessionName string - - session *session.Session - cfg *aws.Config - clients *Clients -} - -func newStaticIAM(_ context.Context, opts Options, cfg *aws.Config) (*StaticAuth, error) { - auth := &StaticAuth{ - logger: opts.Logger, - cfg: func() *aws.Config { - // if nil is passed or it's just a default cfg, - // then we use the options to build the aws cfg. - if cfg != nil && cfg != aws.NewConfig() { - return cfg - } - return GetConfig(opts) - }(), - clients: newClients(), - } - - if opts.Region != "" { - auth.region = &opts.Region - } - if opts.Endpoint != "" { - auth.endpoint = &opts.Endpoint - } - if opts.AccessKey != "" { - auth.accessKey = &opts.AccessKey - } - if opts.SecretKey != "" { - auth.secretKey = &opts.SecretKey - } - if opts.SessionToken != "" { - auth.sessionToken = opts.SessionToken - } - if opts.AssumeRoleARN != "" { - auth.assumeRoleARN = &opts.AssumeRoleARN - } - if opts.SessionName != "" { - auth.sessionName = opts.SessionName - } - - initialSession, err := auth.createSession() - if err != nil { - return nil, fmt.Errorf("failed to get token client: %v", err) - } - - auth.session = initialSession - - return auth, nil -} - -// This is to be used only for test purposes to inject mocked clients -func (a *StaticAuth) WithMockClients(clients *Clients) { - a.clients = clients -} - -func (a *StaticAuth) S3() *S3Clients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.s3 != nil { - return a.clients.s3 - } - - s3Clients := S3Clients{} - a.clients.s3 = &s3Clients - a.clients.s3.New(a.session) - return a.clients.s3 -} - -func (a *StaticAuth) DynamoDB() *DynamoDBClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.Dynamo != nil { - return a.clients.Dynamo - } - - clients := DynamoDBClients{} - a.clients.Dynamo = &clients - a.clients.Dynamo.New(a.session) - - return a.clients.Dynamo -} - -func (a *StaticAuth) Sqs() *SqsClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.sqs != nil { - return a.clients.sqs - } - - clients := SqsClients{} - a.clients.sqs = &clients - a.clients.sqs.New(a.session) - - return a.clients.sqs -} - -func (a *StaticAuth) Sns() *SnsClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.sns != nil { - return a.clients.sns - } - - clients := SnsClients{} - a.clients.sns = &clients - a.clients.sns.New(a.session) - return a.clients.sns -} - -func (a *StaticAuth) SnsSqs() *SnsSqsClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.snssqs != nil { - return a.clients.snssqs - } - - clients := SnsSqsClients{} - a.clients.snssqs = &clients - a.clients.snssqs.New(a.session) - return a.clients.snssqs -} - -func (a *StaticAuth) SecretManager() *SecretManagerClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.Secret != nil { - return a.clients.Secret - } - - clients := SecretManagerClients{} - a.clients.Secret = &clients - a.clients.Secret.New(a.session) - return a.clients.Secret -} - -func (a *StaticAuth) ParameterStore() *ParameterStoreClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.ParameterStore != nil { - return a.clients.ParameterStore - } - - clients := ParameterStoreClients{} - a.clients.ParameterStore = &clients - a.clients.ParameterStore.New(a.session) - return a.clients.ParameterStore -} - -func (a *StaticAuth) Kinesis() *KinesisClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.kinesis != nil { - return a.clients.kinesis - } - - clients := KinesisClients{} - a.clients.kinesis = &clients - a.clients.kinesis.New(a.session) - return a.clients.kinesis -} - -func (a *StaticAuth) Ses() *SesClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.ses != nil { - return a.clients.ses - } - - clients := SesClients{} - a.clients.ses = &clients - a.clients.ses.New(a.session) - return a.clients.ses -} - -func (a *StaticAuth) UpdatePostgres(ctx context.Context, poolConfig *pgxpool.Config) { - a.mu.Lock() - defer a.mu.Unlock() - - // Set max connection lifetime to 8 minutes in postgres connection pool configuration. - // Note: this will refresh connections before the 15 min expiration on the IAM AWS auth token, - // while leveraging the BeforeConnect hook to recreate the token in time dynamically. - poolConfig.MaxConnLifetime = time.Minute * 8 - - // Setup connection pool config needed for AWS IAM authentication - poolConfig.BeforeConnect = func(ctx context.Context, pgConfig *pgx.ConnConfig) error { - // Manually reset auth token with aws and reset the config password using the new iam token - pwd, err := a.getDatabaseToken(ctx, poolConfig) - if err != nil { - return fmt.Errorf("failed to get database token: %w", err) - } - pgConfig.Password = pwd - poolConfig.ConnConfig.Password = pwd - - return nil - } -} - -// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.Go.html -func (a *StaticAuth) getDatabaseToken(ctx context.Context, poolConfig *pgxpool.Config) (string, error) { - dbEndpoint := poolConfig.ConnConfig.Host + ":" + strconv.Itoa(int(poolConfig.ConnConfig.Port)) - - // First, check if there are credentials set explicitly with accesskey and secretkey - if a.accessKey != nil && a.secretKey != nil { - awsCfg := v2creds.NewStaticCredentialsProvider(*a.accessKey, *a.secretKey, a.sessionToken) - authenticationToken, err := auth.BuildAuthToken( - ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, awsCfg) - if err != nil { - return "", fmt.Errorf("failed to create AWS authentication token: %w", err) - } - - return authenticationToken, nil - } - - // Second, check if we are assuming a role instead - if a.assumeRoleARN != nil { - awsCfg, err := config.LoadDefaultConfig(ctx) - if err != nil { - return "", fmt.Errorf("failed to load default AWS authentication configuration %w", err) - } - stsClient := sts.NewFromConfig(awsCfg) - - assumeRoleCfg, err := config.LoadDefaultConfig(ctx, - config.WithRegion(*a.region), - config.WithCredentialsProvider( - awsv2.NewCredentialsCache( - stscreds.NewAssumeRoleProvider(stsClient, *a.assumeRoleARN, func(aro *stscreds.AssumeRoleOptions) { - if a.sessionName != "" { - aro.RoleSessionName = a.sessionName - } - }), - ), - ), - ) - if err != nil { - return "", fmt.Errorf("failed to assume aws role %w", err) - } - - authenticationToken, err := auth.BuildAuthToken( - ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, assumeRoleCfg.Credentials) - if err != nil { - return "", fmt.Errorf("failed to create AWS authentication token: %w", err) - } - return authenticationToken, nil - } - - // Lastly, and by default, just use the default aws configuration - awsCfg, err := config.LoadDefaultConfig(ctx) - if err != nil { - return "", fmt.Errorf("failed to load default AWS authentication configuration %w", err) - } - - authenticationToken, err := auth.BuildAuthToken(ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, awsCfg.Credentials) - if err != nil { - return "", fmt.Errorf("failed to create AWS authentication token: %w", err) - } - - return authenticationToken, nil -} - -func (a *StaticAuth) createSession() (*session.Session, error) { - var awsConfig *aws.Config - if a.cfg == nil { - awsConfig = aws.NewConfig() - } else { - awsConfig = a.cfg - } - - if a.region != nil { - awsConfig = awsConfig.WithRegion(*a.region) - } - - if a.accessKey != nil && a.secretKey != nil { - // session token is an option field - awsConfig = awsConfig.WithCredentials(credentials.NewStaticCredentials(*a.accessKey, *a.secretKey, a.sessionToken)) - } - - if a.endpoint != nil { - awsConfig = awsConfig.WithEndpoint(*a.endpoint) - } - - // TODO support assume role for all aws components - - awsSession, err := session.NewSessionWithOptions(session.Options{ - Config: *awsConfig, - SharedConfigState: session.SharedConfigEnable, - }) - if err != nil { - return nil, err - } - - userAgentHandler := request.NamedHandler{ - Name: "UserAgentHandler", - Fn: request.MakeAddToUserAgentHandler("dapr", logger.DaprVersion), - } - awsSession.Handlers.Build.PushBackNamed(userAgentHandler) - - return awsSession, nil -} - -func (a *StaticAuth) Close() error { - return nil -} - -func GetConfigV2(accessKey string, secretKey string, sessionToken string, region string, endpoint string) (awsv2.Config, error) { - optFns := []func(*config.LoadOptions) error{} - if region != "" { - optFns = append(optFns, config.WithRegion(region)) - } - - if accessKey != "" && secretKey != "" { - provider := v2creds.NewStaticCredentialsProvider(accessKey, secretKey, sessionToken) - optFns = append(optFns, config.WithCredentialsProvider(provider)) - } - - awsCfg, err := config.LoadDefaultConfig(context.Background(), optFns...) - if err != nil { - return awsv2.Config{}, err - } - - if endpoint != "" { - awsCfg.BaseEndpoint = &endpoint - } - - return awsCfg, nil -} diff --git a/common/authentication/aws/static_test.go b/common/authentication/aws/static_test.go deleted file mode 100644 index 8ceb5639e4..0000000000 --- a/common/authentication/aws/static_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package aws - -import ( - "testing" - - "github.com/aws/aws-sdk-go/aws" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetConfigV2(t *testing.T) { - tests := []struct { - name string - accessKey string - secretKey string - sessionToken string - region string - endpoint string - }{ - { - name: "valid config", - accessKey: "testAccessKey", - secretKey: "testSecretKey", - sessionToken: "testSessionToken", - region: "us-west-2", - endpoint: "https://test.endpoint.com", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - awsCfg, err := GetConfigV2(tt.accessKey, tt.secretKey, tt.sessionToken, tt.region, tt.endpoint) - require.NoError(t, err) - assert.NotNil(t, awsCfg) - assert.Equal(t, tt.region, awsCfg.Region) - assert.Equal(t, tt.endpoint, *awsCfg.BaseEndpoint) - }) - } -} - -func TestGetTokenClient(t *testing.T) { - tests := []struct { - name string - awsInstance *StaticAuth - }{ - { - name: "valid token client", - awsInstance: &StaticAuth{ - accessKey: aws.String("testAccessKey"), - secretKey: aws.String("testSecretKey"), - sessionToken: "testSessionToken", - region: aws.String("us-west-2"), - endpoint: aws.String("https://test.endpoint.com"), - }, - }, - { - name: "creds from environment", - awsInstance: &StaticAuth{ - region: aws.String("us-west-2"), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - session, err := tt.awsInstance.createSession() - require.NotNil(t, session) - require.NoError(t, err) - assert.Equal(t, tt.awsInstance.region, session.Config.Region) - }) - } -} diff --git a/common/authentication/aws/x509.go b/common/authentication/aws/x509.go deleted file mode 100644 index 5a42ff4b41..0000000000 --- a/common/authentication/aws/x509.go +++ /dev/null @@ -1,571 +0,0 @@ -/* -Copyright 2024 The Dapr Authors -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 aws - -import ( - "context" - "crypto/ecdsa" - "crypto/tls" - cryptoX509 "crypto/x509" - "errors" - "fmt" - "net/http" - "runtime" - "strconv" - "sync" - "time" - - awsv2 "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - v2creds "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/feature/rds/auth" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/arn" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws/session" - awssh "github.com/aws/rolesanywhere-credential-helper/aws_signing_helper" - "github.com/aws/rolesanywhere-credential-helper/rolesanywhere" - "github.com/aws/rolesanywhere-credential-helper/rolesanywhere/rolesanywhereiface" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/aws/aws-sdk-go-v2/credentials/stscreds" - "github.com/aws/aws-sdk-go-v2/service/sts" - - cryptopem "github.com/dapr/kit/crypto/pem" - spiffecontext "github.com/dapr/kit/crypto/spiffe/context" - "github.com/dapr/kit/logger" - kitmd "github.com/dapr/kit/metadata" - "github.com/dapr/kit/ptr" -) - -func isX509Auth(m map[string]string) bool { - tp := m["trustProfileArn"] - ta := m["trustAnchorArn"] - ar := m["assumeRoleArn"] - return tp != "" && ta != "" && ar != "" -} - -type x509Options struct { - TrustProfileArn *string `json:"trustProfileArn" mapstructure:"trustProfileArn"` - TrustAnchorArn *string `json:"trustAnchorArn" mapstructure:"trustAnchorArn"` - AssumeRoleArn *string `json:"assumeRoleArn" mapstructure:"assumeRoleArn"` -} - -type x509 struct { - mu sync.RWMutex - wg sync.WaitGroup - closeCh chan struct{} - - logger logger.Logger - clients *Clients - rolesAnywhereClient rolesanywhereiface.RolesAnywhereAPI // this is so we can mock it in tests - session *session.Session - cfg *aws.Config - - chainPEM []byte - keyPEM []byte - - region *string - trustProfileArn *string - trustAnchorArn *string - assumeRoleArn *string - sessionName string -} - -func newX509(ctx context.Context, opts Options, cfg *aws.Config) (*x509, error) { - var x509Auth x509Options - if err := kitmd.DecodeMetadata(opts.Properties, &x509Auth); err != nil { - return nil, err - } - - switch { - case x509Auth.TrustProfileArn == nil: - return nil, errors.New("trustProfileArn is required") - case x509Auth.TrustAnchorArn == nil: - return nil, errors.New("trustAnchorArn is required") - case x509Auth.AssumeRoleArn == nil: - return nil, errors.New("assumeRoleArn is required") - } - - auth := &x509{ - logger: opts.Logger, - trustProfileArn: x509Auth.TrustProfileArn, - trustAnchorArn: x509Auth.TrustAnchorArn, - assumeRoleArn: x509Auth.AssumeRoleArn, - cfg: func() *aws.Config { - // if nil is passed or it's just a default cfg, - // then we use the options to build the aws cfg. - if cfg != nil && cfg != aws.NewConfig() { - return cfg - } - return GetConfig(opts) - }(), - clients: newClients(), - closeCh: make(chan struct{}), - } - - if err := auth.getCertPEM(ctx); err != nil { - return nil, fmt.Errorf("failed to get x.509 credentials: %v", err) - } - - // Parse trust anchor and profile ARNs - if err := auth.initializeTrustAnchors(); err != nil { - return nil, err - } - - initialSession, err := auth.createOrRefreshSession(ctx) - if err != nil { - return nil, fmt.Errorf("failed to create the initial session: %v", err) - } - auth.session = initialSession - auth.startSessionRefresher() - - return auth, nil -} - -func (a *x509) Close() error { - return nil -} - -func (a *x509) getCertPEM(ctx context.Context) error { - // retrieve svid from spiffe context - svid, ok := spiffecontext.From(ctx) - if !ok { - return errors.New("no SVID found in context") - } - // get x.509 svid - svidx, err := svid.GetX509SVID() - if err != nil { - return err - } - - // marshal x.509 svid to pem format - chainPEM, keyPEM, err := svidx.Marshal() - if err != nil { - return fmt.Errorf("failed to marshal SVID: %w", err) - } - - a.chainPEM = chainPEM - a.keyPEM = keyPEM - return nil -} - -func (a *x509) S3() *S3Clients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.s3 != nil { - return a.clients.s3 - } - - s3Clients := S3Clients{} - a.clients.s3 = &s3Clients - a.clients.s3.New(a.session) - return a.clients.s3 -} - -func (a *x509) DynamoDB() *DynamoDBClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.Dynamo != nil { - return a.clients.Dynamo - } - - clients := DynamoDBClients{} - a.clients.Dynamo = &clients - a.clients.Dynamo.New(a.session) - - return a.clients.Dynamo -} - -func (a *x509) Sqs() *SqsClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.sqs != nil { - return a.clients.sqs - } - - clients := SqsClients{} - a.clients.sqs = &clients - a.clients.sqs.New(a.session) - - return a.clients.sqs -} - -func (a *x509) Sns() *SnsClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.sns != nil { - return a.clients.sns - } - - clients := SnsClients{} - a.clients.sns = &clients - a.clients.sns.New(a.session) - return a.clients.sns -} - -func (a *x509) SnsSqs() *SnsSqsClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.snssqs != nil { - return a.clients.snssqs - } - - clients := SnsSqsClients{} - a.clients.snssqs = &clients - a.clients.snssqs.New(a.session) - return a.clients.snssqs -} - -func (a *x509) SecretManager() *SecretManagerClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.Secret != nil { - return a.clients.Secret - } - - clients := SecretManagerClients{} - a.clients.Secret = &clients - a.clients.Secret.New(a.session) - return a.clients.Secret -} - -func (a *x509) ParameterStore() *ParameterStoreClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.ParameterStore != nil { - return a.clients.ParameterStore - } - - clients := ParameterStoreClients{} - a.clients.ParameterStore = &clients - a.clients.ParameterStore.New(a.session) - return a.clients.ParameterStore -} - -func (a *x509) Kinesis() *KinesisClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.kinesis != nil { - return a.clients.kinesis - } - - clients := KinesisClients{} - a.clients.kinesis = &clients - a.clients.kinesis.New(a.session) - return a.clients.kinesis -} - -func (a *x509) Ses() *SesClients { - a.mu.Lock() - defer a.mu.Unlock() - - if a.clients.ses != nil { - return a.clients.ses - } - - clients := SesClients{} - a.clients.ses = &clients - a.clients.ses.New(a.session) - return a.clients.ses -} - -// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.Go.html -func (a *x509) getDatabaseToken(ctx context.Context, poolConfig *pgxpool.Config) (string, error) { - dbEndpoint := poolConfig.ConnConfig.Host + ":" + strconv.Itoa(int(poolConfig.ConnConfig.Port)) - - // First, check session credentials. - // This should always be what we use to generate the x509 auth credentials for postgres. - // However, we can leave the Second and Lastly checks as backup for now. - var creds credentials.Value - if a.session != nil { - var err error - creds, err = a.session.Config.Credentials.Get() - if err != nil { - a.logger.Infof("failed to get access key and secret key, will fallback to reading the default AWS credentials file: %w", err) - } - } - - if creds.AccessKeyID != "" && creds.SecretAccessKey != "" { - creds, err := a.session.Config.Credentials.Get() - if err != nil { - return "", fmt.Errorf("failed to retrieve session credentials: %w", err) - } - awsCfg := v2creds.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken) - authenticationToken, err := auth.BuildAuthToken( - ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, awsCfg) - if err != nil { - return "", fmt.Errorf("failed to create AWS authentication token: %w", err) - } - - return authenticationToken, nil - } - - // Second, check if we are assuming a role instead - if a.assumeRoleArn != nil { - awsCfg, err := config.LoadDefaultConfig(ctx) - if err != nil { - return "", fmt.Errorf("failed to load default AWS authentication configuration %w", err) - } - stsClient := sts.NewFromConfig(awsCfg) - - assumeRoleCfg, err := config.LoadDefaultConfig(ctx, - config.WithRegion(*a.region), - config.WithCredentialsProvider( - awsv2.NewCredentialsCache( - stscreds.NewAssumeRoleProvider(stsClient, *a.assumeRoleArn, func(aro *stscreds.AssumeRoleOptions) { - if a.sessionName != "" { - aro.RoleSessionName = a.sessionName - } - }), - ), - ), - ) - if err != nil { - return "", fmt.Errorf("failed to assume aws role %w", err) - } - - authenticationToken, err := auth.BuildAuthToken( - ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, assumeRoleCfg.Credentials) - if err != nil { - return "", fmt.Errorf("failed to create AWS authentication token: %w", err) - } - return authenticationToken, nil - } - - // Lastly, and by default, just use the default aws configuration - awsCfg, err := config.LoadDefaultConfig(ctx) - if err != nil { - return "", fmt.Errorf("failed to load default AWS authentication configuration %w", err) - } - - authenticationToken, err := auth.BuildAuthToken( - ctx, dbEndpoint, *a.region, poolConfig.ConnConfig.User, awsCfg.Credentials) - if err != nil { - return "", fmt.Errorf("failed to create AWS authentication token: %w", err) - } - - return authenticationToken, nil -} - -func (a *x509) UpdatePostgres(ctx context.Context, poolConfig *pgxpool.Config) { - a.mu.Lock() - defer a.mu.Unlock() - - // Set max connection lifetime to 8 minutes in postgres connection pool configuration. - // Note: this will refresh connections before the 15 min expiration on the IAM AWS auth token, - // while leveraging the BeforeConnect hook to recreate the token in time dynamically. - poolConfig.MaxConnLifetime = time.Minute * 8 - - // Setup connection pool config needed for AWS IAM authentication - poolConfig.BeforeConnect = func(ctx context.Context, pgConfig *pgx.ConnConfig) error { - // Manually reset auth token with aws and reset the config password using the new iam token - pwd, err := a.getDatabaseToken(ctx, poolConfig) - if err != nil { - return fmt.Errorf("failed to get database token: %w", err) - } - pgConfig.Password = pwd - poolConfig.ConnConfig.Password = pwd - - return nil - } -} - -func (a *x509) initializeTrustAnchors() error { - var ( - trustAnchor arn.ARN - profile arn.ARN - err error - ) - if a.trustAnchorArn != nil { - trustAnchor, err = arn.Parse(*a.trustAnchorArn) - if err != nil { - return err - } - a.region = &trustAnchor.Region - } - - if a.trustProfileArn != nil { - profile, err = arn.Parse(*a.trustProfileArn) - if err != nil { - return err - } - - if profile.Region != "" && trustAnchor.Region != profile.Region { - return fmt.Errorf("trust anchor and profile must be in the same region: trustAnchor=%s, profile=%s", - trustAnchor.Region, profile.Region) - } - } - return nil -} - -func (a *x509) setSigningFunction(rolesAnywhereClient *rolesanywhere.RolesAnywhere) error { - certs, err := cryptopem.DecodePEMCertificatesChain(a.chainPEM) - if err != nil { - return err - } - - ints := make([]cryptoX509.Certificate, 0, len(certs)-1) - for i := range certs[1:] { - ints = append(ints, *certs[i+1]) - } - - key, err := cryptopem.DecodePEMPrivateKey(a.keyPEM) - if err != nil { - return err - } - - keyECDSA := key.(*ecdsa.PrivateKey) - signFunc := awssh.CreateSignFunction(*keyECDSA, *certs[0], ints) - - agentHandlerFunc := request.MakeAddToUserAgentHandler("dapr", logger.DaprVersion, runtime.Version(), runtime.GOOS, runtime.GOARCH) - rolesAnywhereClient.Handlers.Build.RemoveByName("core.SDKVersionUserAgentHandler") - rolesAnywhereClient.Handlers.Build.PushBackNamed(request.NamedHandler{Name: "v4x509.CredHelperUserAgentHandler", Fn: agentHandlerFunc}) - rolesAnywhereClient.Handlers.Sign.Clear() - rolesAnywhereClient.Handlers.Sign.PushBackNamed(request.NamedHandler{Name: "v4x509.SignRequestHandler", Fn: signFunc}) - - return nil -} - -func (a *x509) createOrRefreshSession(ctx context.Context) (*session.Session, error) { - a.mu.Lock() - defer a.mu.Unlock() - - client := &http.Client{Transport: &http.Transport{ - TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, - }} - var mySession *session.Session - - var awsConfig *aws.Config - if a.cfg == nil { - awsConfig = aws.NewConfig().WithHTTPClient(client).WithLogLevel(aws.LogOff) - } else { - awsConfig = a.cfg.WithHTTPClient(client).WithLogLevel(aws.LogOff) - } - if a.region != nil { - awsConfig.WithRegion(*a.region) - } - // this is needed for testing purposes to mock the client, - // so code never sets the client, but tests do. - var rolesClient *rolesanywhere.RolesAnywhere - if a.rolesAnywhereClient == nil { - mySession = session.Must(session.NewSession(awsConfig)) - rolesAnywhereClient := rolesanywhere.New(mySession, awsConfig) - // Set up signing function and handlers - if err := a.setSigningFunction(rolesAnywhereClient); err != nil { - return nil, err - } - rolesClient = rolesAnywhereClient - } - - createSessionRequest := rolesanywhere.CreateSessionInput{ - Cert: ptr.Of(string(a.chainPEM)), - ProfileArn: a.trustProfileArn, - TrustAnchorArn: a.trustAnchorArn, - RoleArn: a.assumeRoleArn, - // https://aws.amazon.com/about-aws/whats-new/2024/03/iam-roles-anywhere-credentials-valid-12-hours/#:~:text=The%20duration%20can%20range%20from,and%20applications%2C%20to%20use%20X. - DurationSeconds: aws.Int64(int64(time.Hour.Seconds())), // AWS default is 1hr timeout - InstanceProperties: nil, - SessionName: nil, - } - - var output *rolesanywhere.CreateSessionOutput - if a.rolesAnywhereClient != nil { - var err error - output, err = a.rolesAnywhereClient.CreateSessionWithContext(ctx, &createSessionRequest) - if err != nil { - return nil, fmt.Errorf("failed to create session using dapr app identity: %w", err) - } - } else { - var err error - output, err = rolesClient.CreateSessionWithContext(ctx, &createSessionRequest) - if err != nil { - return nil, fmt.Errorf("failed to create session using dapr app identity: %w", err) - } - } - - if output == nil || len(output.CredentialSet) != 1 { - return nil, fmt.Errorf("expected 1 credential set from X.509 rolesanyway response, got %d", len(output.CredentialSet)) - } - - accessKey := output.CredentialSet[0].Credentials.AccessKeyId - secretKey := output.CredentialSet[0].Credentials.SecretAccessKey - sessionToken := output.CredentialSet[0].Credentials.SessionToken - awsCreds := credentials.NewStaticCredentials(*accessKey, *secretKey, *sessionToken) - sess := session.Must(session.NewSession(&aws.Config{ - Credentials: awsCreds, - }, awsConfig)) - if sess == nil { - return nil, errors.New("session is nil") - } - - return sess, nil -} - -func (a *x509) startSessionRefresher() { - a.logger.Infof("starting session refresher for x509 auth") - - a.wg.Add(1) - go func() { - defer a.wg.Done() - for { - // renew at ~half the lifespan - expiration, err := a.session.Config.Credentials.ExpiresAt() - if err != nil { - a.logger.Errorf("Failed to retrieve session expiration time, using 30 minute interval: %w", err) - expiration = time.Now().Add(time.Hour) - } - timeUntilExpiration := time.Until(expiration) - refreshInterval := timeUntilExpiration / 2 - select { - case <-time.After(refreshInterval): - a.refreshClient() - case <-a.closeCh: - a.logger.Debugf("Session refresher is stopped") - return - } - } - }() -} - -func (a *x509) refreshClient() { - for { - newSession, err := a.createOrRefreshSession(context.Background()) - if err == nil { - err = a.clients.refresh(newSession) - if err != nil { - a.logger.Errorf("Failed to refresh client, retrying in 5 seconds: %w", err) - } - a.logger.Debugf("AWS IAM Roles Anywhere session credentials refreshed successfully") - return - } - a.logger.Errorf("Failed to refresh session, retrying in 5 seconds: %w", err) - select { - case <-time.After(time.Second * 5): - case <-a.closeCh: - return - } - } -} diff --git a/common/authentication/aws/x509_test.go b/common/authentication/aws/x509_test.go deleted file mode 100644 index 1c1985f364..0000000000 --- a/common/authentication/aws/x509_test.go +++ /dev/null @@ -1,127 +0,0 @@ -/* -Copyright 2024 The Dapr Authors -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 aws - -import ( - "context" - cryptoX509 "crypto/x509" - "sync/atomic" - "testing" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/rolesanywhere-credential-helper/rolesanywhere" - "github.com/aws/rolesanywhere-credential-helper/rolesanywhere/rolesanywhereiface" - "github.com/spiffe/go-spiffe/v2/spiffeid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/dapr/kit/crypto/spiffe" - spiffecontext "github.com/dapr/kit/crypto/spiffe/context" - "github.com/dapr/kit/crypto/test" - "github.com/dapr/kit/logger" -) - -type mockRolesAnywhereClient struct { - rolesanywhereiface.RolesAnywhereAPI - - CreateSessionOutput *rolesanywhere.CreateSessionOutput - CreateSessionError error -} - -func (m *mockRolesAnywhereClient) CreateSessionWithContext(ctx context.Context, input *rolesanywhere.CreateSessionInput, opts ...request.Option) (*rolesanywhere.CreateSessionOutput, error) { - return m.CreateSessionOutput, m.CreateSessionError -} - -func TestGetX509Client(t *testing.T) { - tests := []struct { - name string - mockOutput *rolesanywhere.CreateSessionOutput - mockError error - }{ - { - name: "valid x509 client", - mockOutput: &rolesanywhere.CreateSessionOutput{ - CredentialSet: []*rolesanywhere.CredentialResponse{ - { - Credentials: &rolesanywhere.Credentials{ - AccessKeyId: aws.String("mockAccessKeyId"), - SecretAccessKey: aws.String("mockSecretAccessKey"), - SessionToken: aws.String("mockSessionToken"), - Expiration: aws.String(time.Now().Add(15 * time.Minute).Format(time.RFC3339)), - }, - }, - }, - }, - mockError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockSvc := &mockRolesAnywhereClient{ - CreateSessionOutput: tt.mockOutput, - CreateSessionError: tt.mockError, - } - mockAWS := x509{ - logger: logger.NewLogger("testLogger"), - assumeRoleArn: aws.String("arn:aws:iam:012345678910:role/exampleIAMRoleName"), - trustAnchorArn: aws.String("arn:aws:rolesanywhere:us-west-1:012345678910:trust-anchor/01234568-0123-0123-0123-012345678901"), - trustProfileArn: aws.String("arn:aws:rolesanywhere:us-west-1:012345678910:profile/01234568-0123-0123-0123-012345678901"), - rolesAnywhereClient: mockSvc, - } - pki := test.GenPKI(t, test.PKIOptions{ - LeafID: spiffeid.RequireFromString("spiffe://example.com/foo/bar"), - }) - - respCert := []*cryptoX509.Certificate{pki.LeafCert} - var respErr error - - var fetches atomic.Int32 - s := spiffe.New(spiffe.Options{ - Log: logger.NewLogger("test"), - RequestSVIDFn: func(context.Context, []byte) (*spiffe.SVIDResponse, error) { - fetches.Add(1) - return &spiffe.SVIDResponse{ - X509Certificates: respCert, - }, respErr - }, - }) - - ctx, cancel := context.WithCancel(t.Context()) - defer cancel() - errCh := make(chan error) - go func() { - errCh <- s.Run(ctx) - }() - - select { - case err := <-errCh: - require.NoError(t, err) - default: - } - - err := s.Ready(ctx) - require.NoError(t, err) - - // inject the SVID source into the context - ctx = spiffecontext.With(ctx, s) - session, err := mockAWS.createOrRefreshSession(ctx) - - require.NoError(t, err) - assert.NotNil(t, session) - }) - } -} diff --git a/common/authentication/azure/auth.go b/common/authentication/azure/auth.go index cf588a7e84..5ab80430ad 100644 --- a/common/authentication/azure/auth.go +++ b/common/authentication/azure/auth.go @@ -139,6 +139,17 @@ func (s EnvironmentSettings) addWorkloadIdentityProvider(creds *[]azcore.TokenCr } } +func (s EnvironmentSettings) addSpiffeWorkloadIdentityProvider(creds *[]azcore.TokenCredential, errs *[]error) { + if c, e := s.GetSpiffeWorkloadIdentity(); e == nil { + cred, err := c.GetTokenCredential() + if err == nil { + *creds = append(*creds, cred) + } else { + *errs = append(*errs, err) + } + } +} + func (s EnvironmentSettings) addManagedIdentityProvider(timeout time.Duration, creds *[]azcore.TokenCredential, errs *[]error) { c := s.GetMSI() msiCred, err := c.GetTokenCredential() @@ -172,6 +183,8 @@ func (s EnvironmentSettings) addProviderByAuthMethodName(authMethod string, cred s.addClientCertificateProvider(creds, errs) case "workloadidentity", "wi": s.addWorkloadIdentityProvider(creds, errs) + case "spiffeworkloadidentity", "spiffe": + s.addSpiffeWorkloadIdentityProvider(creds, errs) case "managedidentity", "mi": s.addManagedIdentityProvider(1*time.Second, creds, errs) case "commandlineinterface", "cli": @@ -180,13 +193,13 @@ func (s EnvironmentSettings) addProviderByAuthMethodName(authMethod string, cred } func getAzureAuthMethods() []string { - return []string{"clientcredentials", "creds", "clientcertificate", "cert", "workloadidentity", "wi", "managedidentity", "mi", "commandlineinterface", "cli", "none"} + return []string{"clientcredentials", "creds", "clientcertificate", "cert", "workloadidentity", "wi", "spiffeworkloadidentity", "spiffe", "managedidentity", "mi", "commandlineinterface", "cli", "none"} } // GetTokenCredential returns an azcore.TokenCredential retrieved from the order specified via // the azureAuthMethods component metadata property which denotes a comma-separated list of auth methods to try in order. // The possible values contained are (case-insensitive): -// ServicePrincipal, Certificate, WorkloadIdentity, ManagedIdentity, CLI +// ServicePrincipal, Certificate, WorkloadIdentity, SPIFFEWorkloadIdentity, ManagedIdentity, CLI // The string "None" can be used to disable Azure authentication. // // If the azureAuthMethods property is not present, the following order is used (which with the exception of step 5 @@ -194,8 +207,9 @@ func getAzureAuthMethods() []string { // 1. Client credentials // 2. Client certificate // 3. Workload identity -// 4. MSI (we use a timeout of 1 second when no compatible managed identity implementation is available) -// 5. Azure CLI +// 4. SPIFFE workload identity +// 5. MSI (we use a timeout of 1 second when no compatible managed identity implementation is available) +// 6. Azure CLI func (s EnvironmentSettings) GetTokenCredential() (azcore.TokenCredential, error) { // Create a chain var creds []azcore.TokenCredential @@ -212,10 +226,13 @@ func (s EnvironmentSettings) GetTokenCredential() (azcore.TokenCredential, error // 3. Workload identity s.addWorkloadIdentityProvider(&creds, &errs) - // 4. MSI with timeout of 1 second (same as DefaultAzureCredential) + // 4. SPIFFE workload identity + s.addSpiffeWorkloadIdentityProvider(&creds, &errs) + + // 5. MSI with timeout of 1 second (same as DefaultAzureCredential) s.addManagedIdentityProvider(1*time.Second, &creds, &errs) - // 5. AzureCLICredential + // 6. AzureCLICredential // We omit this if running in a cloud environment if !isCloudServiceWithManagedIdentity() { s.addCLIProvider(30*time.Second, &creds, &errs) diff --git a/common/authentication/azure/spiffe.go b/common/authentication/azure/spiffe.go new file mode 100644 index 0000000000..61947a8339 --- /dev/null +++ b/common/authentication/azure/spiffe.go @@ -0,0 +1,92 @@ +/* +Copyright 2025 The Dapr Authors +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 azure + +import ( + "context" + "errors" + "fmt" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + + spiffecontext "github.com/dapr/kit/crypto/spiffe/context" +) + +const ( + //nolint:gosec + AzureADTokenExchangeAudience = "api://AzureADTokenExchange" +) + +// SpiffeWorkloadIdentityConfig provides the options to get a bearer authorizer using SPIFFE-based workload identity. +type SpiffeWorkloadIdentityConfig struct { + TenantID string + ClientID string + AzureCloud *cloud.Configuration +} + +// GetTokenCredential returns the azcore.TokenCredential object using a SPIFFE JWT token. +func (c SpiffeWorkloadIdentityConfig) GetTokenCredential() (azcore.TokenCredential, error) { + if c.TenantID == "" || c.ClientID == "" { + return nil, errors.New("parameters clientId and tenantId must be present for SPIFFE workload identity") + } + + var opts *azidentity.ClientAssertionCredentialOptions + if c.AzureCloud != nil { + opts = &azidentity.ClientAssertionCredentialOptions{ + ClientOptions: azcore.ClientOptions{ + Cloud: *c.AzureCloud, + }, + } + } + + // Create a token provider function that retrieves the JWT from SPIFFE context + tokenProvider := func(ctx context.Context) (string, error) { + tknSource, ok := spiffecontext.JWTFrom(ctx) + if !ok { + return "", errors.New("failed to get JWT SVID source from context") + } + jwt, err := tknSource.FetchJWTSVID(ctx, jwtsvid.Params{ + Audience: AzureADTokenExchangeAudience, + }) + if err != nil { + return "", fmt.Errorf("failed to get JWT SVID: %w", err) + } + return jwt.Marshal(), nil + } + + return azidentity.NewClientAssertionCredential(c.TenantID, c.ClientID, tokenProvider, opts) +} + +// GetSpiffeWorkloadIdentity creates a config object from the available SPIFFE workload identity credentials. +// An error is returned if required credentials are not available. +func (s EnvironmentSettings) GetSpiffeWorkloadIdentity() (config SpiffeWorkloadIdentityConfig, err error) { + azureCloud, err := s.GetAzureEnvironment() + if err != nil { + return config, err + } + + config.ClientID, _ = s.GetEnvironment("ClientID") + config.TenantID, _ = s.GetEnvironment("TenantID") + + if config.ClientID == "" || config.TenantID == "" { + return config, errors.New("parameters clientId and tenantId must be present for SPIFFE workload identity") + } + + config.AzureCloud = azureCloud + + return config, nil +} diff --git a/common/authentication/oauth2/clientcredentials.go b/common/authentication/oauth2/clientcredentials.go index 8122984839..0843ab7351 100644 --- a/common/authentication/oauth2/clientcredentials.go +++ b/common/authentication/oauth2/clientcredentials.go @@ -17,10 +17,13 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/json" "errors" "fmt" "net/http" "net/url" + "os" + "strings" "sync" "time" @@ -33,12 +36,93 @@ import ( // ClientCredentialsMetadata is the metadata fields which can be used by a // component to configure an OIDC client_credentials token source. type ClientCredentialsMetadata struct { - TokenCAPEM string `mapstructure:"oauth2TokenCAPEM"` - TokenURL string `mapstructure:"oauth2TokenURL"` - ClientID string `mapstructure:"oauth2ClientID"` - ClientSecret string `mapstructure:"oauth2ClientSecret"` - Audiences []string `mapstructure:"oauth2Audiences"` - Scopes []string `mapstructure:"oauth2Scopes"` + TokenCAPEM string `mapstructure:"oauth2TokenCAPEM"` + TokenURL string `mapstructure:"oauth2TokenURL"` + ClientID string `mapstructure:"oauth2ClientID"` + ClientSecret string `mapstructure:"oauth2ClientSecret"` + ClientSecretPath string `mapstructure:"oauth2ClientSecretPath"` + CredentialsFilePath string `mapstructure:"oauth2CredentialsFile"` + Audiences []string `mapstructure:"oauth2Audiences"` + Scopes []string `mapstructure:"oauth2Scopes"` +} + +// ResolveCredentials loads client_id and client_secret from files if configured. +func (m *ClientCredentialsMetadata) ResolveCredentials() error { + if m.CredentialsFilePath != "" && m.ClientSecretPath != "" { + return errors.New("'oauth2CredentialsFile' and 'oauth2ClientSecretPath' fields are mutually exclusive") + } + + if m.CredentialsFilePath != "" { + fileClientID, fileClientSecret, fileIssuerURL, err := LoadCredentialsFromJSONFile(m.CredentialsFilePath) + if err != nil { + return fmt.Errorf("failed to load credentials from JSON file: %w", err) + } + + // Metadata overrides file values + if m.ClientID == "" { + m.ClientID = fileClientID + } + if m.ClientSecret == "" { + m.ClientSecret = fileClientSecret + } + if m.TokenURL == "" { + m.TokenURL = fileIssuerURL + } + return nil + } + + if m.ClientSecretPath != "" { + // Metadata overrides file value + if m.ClientSecret == "" { + secretBytes, err := os.ReadFile(m.ClientSecretPath) + if err != nil { + return fmt.Errorf("could not read oauth2 client secret from file %q: %w", m.ClientSecretPath, err) + } + m.ClientSecret = strings.TrimSpace(string(secretBytes)) + } + return nil + } + + return nil +} + +// ToOptions converts ClientCredentialsMetadata to ClientCredentialsOptions. +func (m *ClientCredentialsMetadata) ToOptions(logger logger.Logger) ClientCredentialsOptions { + return ClientCredentialsOptions{ + Logger: logger, + TokenURL: m.TokenURL, + CAPEM: []byte(m.TokenCAPEM), + ClientID: m.ClientID, + ClientSecret: m.ClientSecret, + Scopes: m.Scopes, + Audiences: m.Audiences, + } +} + +// CredentialsFile represents a JSON credentials file. +type CredentialsFile struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + IssuerURL string `json:"issuer_url"` +} + +// LoadCredentialsFromJSONFile reads client_id, client_secret, and issuer_url from a JSON file. +func LoadCredentialsFromJSONFile(filePath string) (clientID, clientSecret, issuerURL string, err error) { + secretBytes, err := os.ReadFile(filePath) + if err != nil { + return "", "", "", fmt.Errorf("could not read oauth2 credentials from file %q: %w", filePath, err) + } + + var creds CredentialsFile + if err := json.Unmarshal(secretBytes, &creds); err != nil { + return "", "", "", fmt.Errorf("failed to parse JSON credentials file: %w", err) + } + + if creds.ClientID == "" || creds.ClientSecret == "" { + return "", "", "", errors.New("credentials file must contain client_id and client_secret") + } + + return creds.ClientID, creds.ClientSecret, creds.IssuerURL, nil } type ClientCredentialsOptions struct { @@ -59,7 +143,7 @@ type ClientCredentials struct { httpClient *http.Client fetchTokenFn func(context.Context) (*oauth2.Token, error) - lock sync.RWMutex + lock sync.Mutex } func NewClientCredentials(ctx context.Context, opts ClientCredentialsOptions) (*ClientCredentials, error) { @@ -126,40 +210,33 @@ func (c *ClientCredentialsOptions) toConfig() (*ccreds.Config, *http.Client, err } func (c *ClientCredentials) Token() (string, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - if !c.currentToken.Valid() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - if err := c.renewToken(ctx); err != nil { - return "", err - } - } - - return c.currentToken.AccessToken, nil -} - -func (c *ClientCredentials) renewToken(ctx context.Context) error { c.lock.Lock() defer c.lock.Unlock() - // We need to check if the current token is valid because we might have lost - // the mutex lock race from the caller and we don't want to double-fetch a - // token unnecessarily! if c.currentToken.Valid() { - return nil + return c.currentToken.AccessToken, nil } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + if err := c.renewToken(ctx); err != nil { + return "", err + } + return c.currentToken.AccessToken, nil +} + +func (c *ClientCredentials) renewToken(ctx context.Context) error { + c.log.Debug("renewing token: fetching new token from OAuth server...") token, err := c.fetchTokenFn(context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)) if err != nil { return err } - if !c.currentToken.Valid() { + if !token.Valid() { return errors.New("oauth2 client_credentials token source returned an invalid token") } c.currentToken = token + c.log.Debugf("OAuth token renewed successfully, new expiry: %s", token.Expiry) return nil } diff --git a/common/authentication/oauth2/clientcredentials_test.go b/common/authentication/oauth2/clientcredentials_test.go index ba643b2129..a7b3fe66ec 100644 --- a/common/authentication/oauth2/clientcredentials_test.go +++ b/common/authentication/oauth2/clientcredentials_test.go @@ -14,11 +14,18 @@ limitations under the License. package oauth2 import ( + "context" "net/url" + "os" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" ccreds "golang.org/x/oauth2/clientcredentials" + + "github.com/dapr/kit/logger" ) func Test_toConfig(t *testing.T) { @@ -93,3 +100,198 @@ func Test_toConfig(t *testing.T) { }) } } + +func Test_TokenRenewal(t *testing.T) { + expired := &oauth2.Token{AccessToken: "old-token", Expiry: time.Now().Add(-1 * time.Minute)} + renewed := &oauth2.Token{AccessToken: "new-token", Expiry: time.Now().Add(1 * time.Hour)} + + c := &ClientCredentials{ + log: logger.NewLogger("test"), + currentToken: expired, + fetchTokenFn: func(ctx context.Context) (*oauth2.Token, error) { + return renewed, nil + }, + } + + tok, err := c.Token() + require.NoError(t, err) + assert.Equal(t, "new-token", tok) +} + +func TestLoadCredentialsFromJSONFile(t *testing.T) { + t.Run("valid JSON", func(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "credentials-*.json") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + content := `{"client_id": "test-id", "client_secret": "test-secret", "issuer_url": "https://oauth.example.com/token"}` + _, err = tmpFile.WriteString(content) + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + + clientID, clientSecret, issuerURL, err := LoadCredentialsFromJSONFile(tmpFile.Name()) + require.NoError(t, err) + assert.Equal(t, "test-id", clientID) + assert.Equal(t, "test-secret", clientSecret) + assert.Equal(t, "https://oauth.example.com/token", issuerURL) + }) + + t.Run("missing required fields", func(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "credentials-*.json") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(`{"client_id": "test-id"}`) + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + + _, _, _, err = LoadCredentialsFromJSONFile(tmpFile.Name()) + require.Error(t, err) + assert.Contains(t, err.Error(), "must contain client_id and client_secret") + }) + + t.Run("invalid JSON", func(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "credentials-*.json") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString("{ invalid json }") + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + + _, _, _, err = LoadCredentialsFromJSONFile(tmpFile.Name()) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse JSON") + }) + + t.Run("file not found", func(t *testing.T) { + _, _, _, err := LoadCredentialsFromJSONFile("/nonexistent/file/path") + require.Error(t, err) + assert.Contains(t, err.Error(), "could not read oauth2 credentials from file") + }) +} + +func TestClientCredentialsMetadata_ResolveCredentials(t *testing.T) { + t.Run("oauth2CredentialsFile with metadata override", func(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "credentials-*.json") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(`{"client_id": "file-id", "client_secret": "file-secret", "issuer_url": "https://file.com/token"}`) + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + + m := ClientCredentialsMetadata{ + ClientID: "meta-id", + ClientSecret: "meta-secret", + TokenURL: "https://meta.com/token", + CredentialsFilePath: tmpFile.Name(), + } + + err = m.ResolveCredentials() + require.NoError(t, err) + assert.Equal(t, "meta-id", m.ClientID) // metadata overrides + assert.Equal(t, "meta-secret", m.ClientSecret) // metadata overrides + assert.Equal(t, "https://meta.com/token", m.TokenURL) // metadata overrides + }) + + t.Run("oauth2CredentialsFile without metadata", func(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "credentials-*.json") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(`{"client_id": "file-id", "client_secret": "file-secret", "issuer_url": "https://file.com/token"}`) + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + + m := ClientCredentialsMetadata{CredentialsFilePath: tmpFile.Name()} + err = m.ResolveCredentials() + require.NoError(t, err) + assert.Equal(t, "file-id", m.ClientID) + assert.Equal(t, "file-secret", m.ClientSecret) + assert.Equal(t, "https://file.com/token", m.TokenURL) + }) + + t.Run("oauth2ClientSecretPath with metadata override", func(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "secret-*.txt") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString("file-secret") + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + + m := ClientCredentialsMetadata{ + ClientID: "meta-id", + ClientSecret: "meta-secret", + ClientSecretPath: tmpFile.Name(), + } + + err = m.ResolveCredentials() + require.NoError(t, err) + assert.Equal(t, "meta-id", m.ClientID) + assert.Equal(t, "meta-secret", m.ClientSecret) // metadata overrides + }) + + t.Run("oauth2ClientSecretPath without metadata", func(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "secret-*.txt") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString("file-secret") + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + + m := ClientCredentialsMetadata{ClientSecretPath: tmpFile.Name()} + err = m.ResolveCredentials() + require.NoError(t, err) + assert.Equal(t, "file-secret", m.ClientSecret) + }) + + t.Run("error both fields set", func(t *testing.T) { + jsonFile, err := os.CreateTemp(t.TempDir(), "credentials-*.json") + require.NoError(t, err) + defer os.Remove(jsonFile.Name()) + _, err = jsonFile.WriteString(`{"client_id": "id", "client_secret": "secret", "issuer_url": "https://example.com"}`) + require.NoError(t, err) + require.NoError(t, jsonFile.Close()) + + txtFile, err := os.CreateTemp(t.TempDir(), "secret-*.txt") + require.NoError(t, err) + defer os.Remove(txtFile.Name()) + _, err = txtFile.WriteString("secret") + require.NoError(t, err) + require.NoError(t, txtFile.Close()) + + m := ClientCredentialsMetadata{ + CredentialsFilePath: jsonFile.Name(), + ClientSecretPath: txtFile.Name(), + } + + err = m.ResolveCredentials() + require.Error(t, err) + assert.Contains(t, err.Error(), "mutually exclusive") + }) +} + +func TestClientCredentialsMetadata_ToOptions(t *testing.T) { + logger := logger.NewLogger("test") + metadata := ClientCredentialsMetadata{ + TokenURL: "https://token.example.com", + TokenCAPEM: "cert-pem-content", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + Scopes: []string{"scope1", "scope2"}, + Audiences: []string{"audience1"}, + } + + opts := metadata.ToOptions(logger) + + assert.Equal(t, logger, opts.Logger) + assert.Equal(t, "https://token.example.com", opts.TokenURL) + assert.Equal(t, []byte("cert-pem-content"), opts.CAPEM) + assert.Equal(t, "test-client-id", opts.ClientID) + assert.Equal(t, "test-client-secret", opts.ClientSecret) + assert.Equal(t, []string{"scope1", "scope2"}, opts.Scopes) + assert.Equal(t, []string{"audience1"}, opts.Audiences) +} diff --git a/common/authentication/postgresql/metadata.go b/common/authentication/postgresql/metadata.go index c9c24432bd..b7c8b42ffc 100644 --- a/common/authentication/postgresql/metadata.go +++ b/common/authentication/postgresql/metadata.go @@ -25,8 +25,8 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" - "github.com/dapr/components-contrib/common/authentication/aws" "github.com/dapr/components-contrib/common/authentication/azure" + awsAuth "github.com/dapr/components-contrib/common/aws/auth" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" ) @@ -48,7 +48,7 @@ type PostgresAuthMetadata struct { QueryExecMode string `mapstructure:"queryExecMode"` azureEnv azure.EnvironmentSettings - awsEnv aws.EnvironmentSettings + awsEnv awsAuth.EnvironmentSettings } // Reset the object. @@ -91,7 +91,7 @@ func (m *PostgresAuthMetadata) InitWithMetadata(meta map[string]string, opts Ini } case opts.AWSIAMEnabled && m.UseAWSIAM: // Populate the AWS environment if using AWS IAM - m.awsEnv, err = aws.NewEnvironmentSettings(meta) + m.awsEnv, err = awsAuth.NewEnvironmentSettings(meta) if err != nil { return err } @@ -216,7 +216,7 @@ func (m *PostgresAuthMetadata) buildURLConnectionString(metadata map[string]stri return u.String(), nil } -func (m *PostgresAuthMetadata) BuildAwsIamOptions(logger logger.Logger, properties map[string]string) (*aws.Options, error) { +func (m *PostgresAuthMetadata) BuildAwsIamOptions(logger logger.Logger, properties map[string]string) (*awsAuth.Options, error) { awsRegion, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "AWSRegion") region, _ := metadata.GetMetadataProperty(m.awsEnv.Metadata, "region") if region == "" { @@ -246,13 +246,13 @@ func (m *PostgresAuthMetadata) BuildAwsIamOptions(logger logger.Logger, properti if sessionName == "" { sessionName = "DaprDefaultSession" } - return &aws.Options{ - Region: region, - AccessKey: awsAccessKey, - SecretKey: awsSecretKey, - SessionToken: sessionToken, - AssumeRoleARN: assumeRoleArn, - SessionName: sessionName, + return &awsAuth.Options{ + Region: region, + AccessKey: awsAccessKey, + SecretKey: awsSecretKey, + SessionToken: sessionToken, + AssumeRoleArn: assumeRoleArn, + AssumeRoleSessionName: sessionName, Logger: logger, Properties: properties, diff --git a/common/aws/auth/auth.go b/common/aws/auth/auth.go index 7923f9eaf3..b359df1b73 100644 --- a/common/aws/auth/auth.go +++ b/common/aws/auth/auth.go @@ -18,6 +18,20 @@ const ( ProviderTypeUnknown // Or default ) +// EnvironmentSettings stores generic metadata. +type EnvironmentSettings struct { + Metadata map[string]string +} + +// NewEnvironmentSettings constructs a new EnvironmentSettings struct from +// raw metadata. +func NewEnvironmentSettings(md map[string]string) (EnvironmentSettings, error) { + es := EnvironmentSettings{ + Metadata: md, + } + return es, nil +} + type Options struct { Logger logger.Logger Properties map[string]string diff --git a/common/aws/deprecated.go b/common/aws/deprecated.go new file mode 100644 index 0000000000..ef85c25d80 --- /dev/null +++ b/common/aws/deprecated.go @@ -0,0 +1,13 @@ +package aws + +// DeprecatedPostgresIAM contains legacy aws IAM fields used by PostgreSQL +// components. This is retained here to avoid breaking existing metadata parsing logic. +// +// NOTE: this type is kept for backwards compatibility and may be removed in a +// future release at any time from 1.17 onwards once all consumers have migrated. +type DeprecatedPostgresIAM struct { + // Access key to use for accessing PostgreSQL. + AccessKey string `json:"awsAccessKey" mapstructure:"awsAccessKey"` + // Secret key to use for accessing PostgreSQL. + SecretKey string `json:"awsSecretKey" mapstructure:"awsSecretKey"` +} diff --git a/common/aws/dynamodb_client.go b/common/aws/dynamodb_client.go new file mode 100644 index 0000000000..d6845f3bb1 --- /dev/null +++ b/common/aws/dynamodb_client.go @@ -0,0 +1,29 @@ +/* +Copyright 2025 The Dapr Authors +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 aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" +) + +type DynamoDBClient interface { + GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) + PutItem(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) + DeleteItem(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) + TransactWriteItems(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) +} diff --git a/common/aws/helpers_common_test.go b/common/aws/helpers_common_test.go new file mode 100644 index 0000000000..b7630866f5 --- /dev/null +++ b/common/aws/helpers_common_test.go @@ -0,0 +1,20 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +// fakeCreds is a simple credentials provider used across multiple tests. +type fakeCreds struct{} + +func (f *fakeCreds) Retrieve(ctx context.Context) (aws.Credentials, error) { + return aws.Credentials{ + AccessKeyID: "AKIDFAKE", + SecretAccessKey: "SECRET", + SessionToken: "TOKEN", + }, nil +} + +func (f *fakeCreds) IsExpired() bool { return true } diff --git a/common/aws/kinesis.go b/common/aws/kinesis.go new file mode 100644 index 0000000000..c1d401a468 --- /dev/null +++ b/common/aws/kinesis.go @@ -0,0 +1,48 @@ +package aws + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/kinesis" + "github.com/vmware/vmware-go-kcl-v2/clientlibrary/config" +) + +// StreamARN fetches the ARN of a Kinesis stream using a v2 client. +func StreamARN(ctx context.Context, client *kinesis.Client, streamName string) (*string, error) { + if client == nil { + return nil, errors.New("kinesis client is nil") + } + resp, err := client.DescribeStream(ctx, &kinesis.DescribeStreamInput{StreamName: aws.String(streamName)}) + if err != nil { + return nil, err + } + if resp.StreamDescription == nil { + return nil, errors.New("empty stream description") + } + return resp.StreamDescription.StreamARN, nil +} + +// NewKinesisWorkerConfig builds a KCL configuration object configured for the +// supplied stream, consumer and region. It converts the credentials contained +// in the aws.Config into a form that the KCL library can understand. The +// function returns an error if the consumer mode is unsupported or the +// provided AWS configuration is missing required information such as region. +func NewKinesisWorkerConfig(cfg aws.Config, stream, consumer, mode string) (*config.KinesisClientLibConfiguration, error) { + const sharedMode = "shared" + if mode != sharedMode { + return nil, fmt.Errorf("unsupported consumer mode %q", mode) + } + + region := cfg.Region + if region == "" { + return nil, errors.New("region is required for Kinesis worker config") + } + + if cfg.Credentials != nil { + return config.NewKinesisClientLibConfigWithCredentials(consumer, stream, region, consumer, cfg.Credentials, cfg.Credentials), nil + } + return config.NewKinesisClientLibConfig(consumer, stream, region, consumer), nil +} diff --git a/common/aws/kinesis_worker_test.go b/common/aws/kinesis_worker_test.go new file mode 100644 index 0000000000..706833d5cf --- /dev/null +++ b/common/aws/kinesis_worker_test.go @@ -0,0 +1,34 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewKinesisWorkerConfig(t *testing.T) { + cfg := aws.Config{} + // missing region should yield an error + _, err := NewKinesisWorkerConfig(cfg, "s", "c", "shared") + require.Error(t, err) + + cfg.Region = "us-east-1" + // credentials nil should still produce a configuration + kcl, err := NewKinesisWorkerConfig(cfg, "stream", "consumer", "shared") + require.NoError(t, err) + assert.NotNil(t, kcl) + assert.Equal(t, "stream", kcl.StreamName) + assert.Equal(t, "consumer", kcl.ApplicationName) + + cfg.Credentials = aws.NewCredentialsCache(&fakeCreds{}) + kcl, err = NewKinesisWorkerConfig(cfg, "stream", "consumer", "shared") + require.NoError(t, err) + assert.NotNil(t, kcl) + assert.Equal(t, cfg.Credentials, kcl.KinesisCredentials) + + // unsupported mode should return an error + _, err = NewKinesisWorkerConfig(cfg, "s", "c", "extended") + require.Error(t, err) +} diff --git a/common/aws/mock/dynamodb_client.go b/common/aws/mock/dynamodb_client.go new file mode 100644 index 0000000000..6873379556 --- /dev/null +++ b/common/aws/mock/dynamodb_client.go @@ -0,0 +1,45 @@ +/* +Copyright 2025 The Dapr Authors +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 awsmock + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" +) + +type DynamoDBClient struct { + GetItemFn func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) + PutItemFn func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) + DeleteItemFn func(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) + TransactWriteItemsFn func(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) +} + +func (m DynamoDBClient) GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + return m.GetItemFn(ctx, params, optFns...) +} + +func (m DynamoDBClient) PutItem(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + return m.PutItemFn(ctx, params, optFns...) +} + +func (m DynamoDBClient) DeleteItem(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) { + return m.DeleteItemFn(ctx, params, optFns...) +} + +func (m DynamoDBClient) TransactWriteItems(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) { + return m.TransactWriteItemsFn(ctx, params, optFns...) +} diff --git a/common/aws/mock/secretsmanager_client.go b/common/aws/mock/secretsmanager_client.go new file mode 100644 index 0000000000..7306ebe805 --- /dev/null +++ b/common/aws/mock/secretsmanager_client.go @@ -0,0 +1,34 @@ +/* +Copyright 2025 The Dapr Authors +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 awsmock + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +type SecretsManagerClient struct { + GetSecretValueFn func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) + ListSecretsFn func(ctx context.Context, params *secretsmanager.ListSecretsInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) +} + +func (m SecretsManagerClient) GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + return m.GetSecretValueFn(ctx, params, optFns...) +} + +func (m SecretsManagerClient) ListSecrets(ctx context.Context, params *secretsmanager.ListSecretsInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) { + return m.ListSecretsFn(ctx, params, optFns...) +} diff --git a/common/aws/mock/ssm_client.go b/common/aws/mock/ssm_client.go new file mode 100644 index 0000000000..b3f4a4dcc0 --- /dev/null +++ b/common/aws/mock/ssm_client.go @@ -0,0 +1,35 @@ +/* +Copyright 2025 The Dapr Authors +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 awsmock + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ssm" +) + +type ParameterStoreClient struct { + GetParameterFn func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) + DescribeParametersFn func(ctx context.Context, params *ssm.DescribeParametersInput, optFns ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) +} + +func (m ParameterStoreClient) GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + return m.GetParameterFn(ctx, params, optFns...) +} + +func (m ParameterStoreClient) DescribeParameters(ctx context.Context, params *ssm.DescribeParametersInput, optFns ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) { + return m.DescribeParametersFn(ctx, params, optFns...) +} diff --git a/common/aws/postgres.go b/common/aws/postgres.go new file mode 100644 index 0000000000..6ec0833afc --- /dev/null +++ b/common/aws/postgres.go @@ -0,0 +1,38 @@ +package aws + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/feature/rds/auth" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + + awsAuth "github.com/dapr/components-contrib/common/aws/auth" +) + +// ConfigurePostgresIAM mutates a pgxpool.Config so that it will obtain +// temporary credentials from AWS IAM each time a new connection is opened. +func ConfigurePostgresIAM(ctx context.Context, poolConfig *pgxpool.Config, opts awsAuth.Options) error { + provider, err := awsAuth.NewCredentialProvider(ctx, opts, nil) + if err != nil { + return err + } + + poolConfig.MaxConnLifetime = 8 * time.Minute + + region := opts.Region + + poolConfig.BeforeConnect = func(ctx context.Context, pgConfig *pgx.ConnConfig) error { + dbEndpoint := fmt.Sprintf("%s:%d", pgConfig.Host, pgConfig.Port) + + pwd, err := auth.BuildAuthToken(ctx, dbEndpoint, region, pgConfig.User, provider) + if err != nil { + return fmt.Errorf("failed to create AWS authentication token: %w", err) + } + pgConfig.Password = pwd + return nil + } + return nil +} diff --git a/common/aws/postgres_iam_test.go b/common/aws/postgres_iam_test.go new file mode 100644 index 0000000000..eb8463adcb --- /dev/null +++ b/common/aws/postgres_iam_test.go @@ -0,0 +1,38 @@ +package aws + +import ( + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + awsAuth "github.com/dapr/components-contrib/common/aws/auth" + "github.com/dapr/kit/logger" +) + +func TestConfigurePostgresIAM(t *testing.T) { + ctx := t.Context() + pgCfg, err := pgx.ParseConfig("host=localhost port=5432 user=user") + require.NoError(t, err) + poolCfg := &pgxpool.Config{ConnConfig: pgCfg} + authOpts := awsAuth.Options{ + Logger: logger.NewLogger("test"), + Region: "us-west-2", + AccessKey: "k", + SecretKey: "s", + } + + err = ConfigurePostgresIAM(ctx, poolCfg, authOpts) + require.NoError(t, err) + assert.Equal(t, 8*time.Minute, poolCfg.MaxConnLifetime) + assert.NotNil(t, poolCfg.BeforeConnect) + + pgCfg, err = pgx.ParseConfig("host=localhost port=5432 user=user") + require.NoError(t, err) + err = poolCfg.BeforeConnect(ctx, pgCfg) + require.NoError(t, err) + assert.NotEmpty(t, pgCfg.Password) +} diff --git a/common/aws/secretsmanager_client.go b/common/aws/secretsmanager_client.go new file mode 100644 index 0000000000..c1d41b88f9 --- /dev/null +++ b/common/aws/secretsmanager_client.go @@ -0,0 +1,27 @@ +/* +Copyright 2025 The Dapr Authors +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 aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +type SecretsManagerClient interface { + GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) + ListSecrets(ctx context.Context, params *secretsmanager.ListSecretsInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) +} diff --git a/common/aws/ssm_client.go b/common/aws/ssm_client.go new file mode 100644 index 0000000000..66b424bc85 --- /dev/null +++ b/common/aws/ssm_client.go @@ -0,0 +1,27 @@ +/* +Copyright 2025 The Dapr Authors +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 aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ssm" +) + +type ParameterStoreClient interface { + GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) + DescribeParameters(ctx context.Context, params *ssm.DescribeParametersInput, optFns ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) +} diff --git a/common/aws/stream_arn_test.go b/common/aws/stream_arn_test.go new file mode 100644 index 0000000000..ac2fe4a159 --- /dev/null +++ b/common/aws/stream_arn_test.go @@ -0,0 +1,15 @@ +package aws + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStreamARN_NilClient(t *testing.T) { + arn, err := StreamARN(t.Context(), nil, "foo") + assert.Nil(t, arn) + + require.ErrorContains(t, err, "kinesis client is nil") +} diff --git a/common/component/azure/blobstorage/client.go b/common/component/azure/blobstorage/client.go index ca054a4f37..68cb0ea7c2 100644 --- a/common/component/azure/blobstorage/client.go +++ b/common/component/azure/blobstorage/client.go @@ -161,7 +161,6 @@ func (opts *ContainerClientOpts) InitContainerClient(azEnvSettings azauth.Enviro if err != nil { return nil, fmt.Errorf("cannot init blob storage container client with shared key: %w", err) } - // Use Azure AD as fallback default: credential, tokenErr := azEnvSettings.GetTokenCredential() diff --git a/common/component/azure/servicebus/subscription.go b/common/component/azure/servicebus/subscription.go index ecdf7c00bb..a08d4b0530 100644 --- a/common/component/azure/servicebus/subscription.go +++ b/common/component/azure/servicebus/subscription.go @@ -273,8 +273,17 @@ func (s *Subscription) ReceiveBlocking(parentCtx context.Context, handler Handle continue } - // Handle the messages in background - go s.handleAsync(ctx, msgs, handler, receiver) + // If we require sessions then we must process the message + // synchronously to ensure the FIFO order is maintained. + // This is considered safe as even when using bulk receives, + // the messages are merged into a single request to the app + // containing multiple messages and thus it becomes an app + // concern to process them in order. + if s.requireSessions { + s.handleMessages(ctx, msgs, handler, receiver) + } else { + go s.handleMessages(ctx, msgs, handler, receiver) + } } } @@ -393,8 +402,8 @@ func (s *Subscription) doRenewLocksSession(ctx context.Context, sessionReceiver } } -// handleAsync handles messages from azure service bus and is meant to be called in a goroutine (go s.handleAsync). -func (s *Subscription) handleAsync(ctx context.Context, msgs []*azservicebus.ReceivedMessage, handler HandlerFn, receiver Receiver) { +// handleMessages handles messages from azure service bus and can be called synchronously or asynchronously depending on order requirements. +func (s *Subscription) handleMessages(ctx context.Context, msgs []*azservicebus.ReceivedMessage, handler HandlerFn, receiver Receiver) { var ( consumeToken bool takenConcurrentHandler bool diff --git a/common/component/cloudflare/worker-src/package-lock.json b/common/component/cloudflare/worker-src/package-lock.json index 3f141aa638..94f2fc7d42 100644 --- a/common/component/cloudflare/worker-src/package-lock.json +++ b/common/component/cloudflare/worker-src/package-lock.json @@ -10,104 +10,1088 @@ "license": "Apache2", "dependencies": { "itty-router": "3.0.12", - "jose": "4.14.4" + "jose": "4.15.5" }, "devDependencies": { "@cloudflare/workers-types": "^4.20230511.0", - "esbuild": "^0.17.19", + "esbuild": "^0.27.2", "prettier": "^2.8.8", "typescript": "^5.0.4", - "wrangler": "^3.19.0" + "wrangler": "^4.61.0" } }, "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.1.tgz", - "integrity": "sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.11.0.tgz", + "integrity": "sha512-z3hxFajL765VniNPGV0JRStZolNz63gU3B3AktwoGdDlnQvz5nP+Ah4RL04PONlZQjwmDdGHowEStJ94+RsaJg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "^1.20260115.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260124.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260124.0.tgz", + "integrity": "sha512-VuqscLhiiVIf7t/dcfkjtT0LKJH+a06KUFwFTHgdTcqyLbFZ44u1SLpOONu5fyva4A9MdaKh9a+Z/tBC1d76nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260124.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260124.0.tgz", + "integrity": "sha512-PfnjoFooPgRKFUIZcEP9irnn5Y7OgXinjM+IMlKTdEyLWjMblLsbsqAgydf75+ii0715xAeUlWQjZrWdyOZjMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260124.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260124.0.tgz", + "integrity": "sha512-KSkZl4kwcWeFXI7qsaLlMnKwjgdZwI0OEARjyZpiHCxJCqAqla9XxQKNDscL2Z3qUflIo30i+uteGbFrhzuVGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260124.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260124.0.tgz", + "integrity": "sha512-61xjSUNk745EVV4vXZP0KGyLCatcmamfBB+dcdQ8kDr6PrNU4IJ1kuQFSJdjybyDhJRm4TpGVywq+9hREuF7xA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260124.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260124.0.tgz", + "integrity": "sha512-j9O11pwQQV6Vi3peNrJoyIas3SrZHlPj0Ah+z1hDW9o1v35euVBQJw/PuzjPOXxTFUlGQoMJdfzPsO9xP86g7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260124.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260124.0.tgz", + "integrity": "sha512-h6TJlew6AtGuEXFc+k5ifalk+tg3fkg0lla6XbMAb2AKKfJGwlFNTwW2xyT/Ha92KY631CIJ+Ace08DPdFohdA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", "dependencies": { - "mime": "^3.0.0" + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" } }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20240404.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240404.0.tgz", - "integrity": "sha512-V9oPjeC2PYBCoAYnjbO2bsjT7PtzxfUHnh780FUi1r59Hwxd7FNlojwsIidA0nS/1WV5UKeJusIdrYlQbsketA==", + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=16" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20240405.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240405.0.tgz", - "integrity": "sha512-sEVOhyOgXUwfLkgHqbLZa/sfkSYrh7/zLmI6EZNibPaVPvAnAcItbNNl3SAlLyLKuwf8m4wAIAgu9meKWCvXjg==", - "dev": true + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, - "node_modules/@esbuild-plugins/node-globals-polyfill": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", - "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], "dev": true, - "peerDependencies": { - "esbuild": "*" + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, - "node_modules/@esbuild-plugins/node-modules-polyfill": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", - "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "escape-string-regexp": "^4.0.0", - "rollup-plugin-node-polyfills": "^0.2.1" + "@emnapi/runtime": "^1.7.0" }, - "peerDependencies": { - "esbuild": "*" + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": ">=12" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@jridgewell/resolve-uri": { @@ -115,753 +1099,907 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", "dev": true, + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" } }, - "node_modules/as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", "dev": true, - "dependencies": { - "printable-characters": "^1.0.42" - } + "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@speed-highlight/core": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", "dev": true }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "dev": true, - "dependencies": { - "fill-range": "^7.0.1" + "license": "MIT", + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=8" } }, - "node_modules/capnp-ts": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/capnp-ts/-/capnp-ts-0.7.0.tgz", - "integrity": "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==", + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", "dev": true, - "dependencies": { - "debug": "^4.3.1", - "tslib": "^2.2.0" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">= 8.10.0" + "node": ">=18" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.6" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true + "node_modules/itty-router": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-3.0.12.tgz", + "integrity": "sha512-s98XTPhle6GGbaFf0kYrOD3Q8gyhnqvOqkwYijC3AmkceNKqWUp13YHg6dWmqmVv4pP7l7c94XI92I0EXVGO0w==" }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/jose": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true, - "dependencies": { - "ms": "2.1.2" - }, + "license": "MIT", "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=6" } }, - "node_modules/esbuild": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", - "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "node_modules/miniflare": { + "version": "4.20260124.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260124.0.tgz", + "integrity": "sha512-Co8onUh+POwOuLty4myQg+Nzg9/xZ5eAJc1oqYBzRovHd/XIpb5WAnRVaubcfAQJ85awWtF3yXUHCDx6cIaN3w==", "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.18.2", + "workerd": "1.20260124.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" }, - "engines": { - "node": ">=12" + "bin": { + "miniflare": "bootstrap.js" }, - "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18.0.0" } }, - "node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" }, - "node_modules/exit-hook": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", - "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, "engines": { - "node": ">=6" + "node": ">=10.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "dev": true, "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/get-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "dependencies": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - } + "license": "0BSD", + "optional": true }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true, - "dependencies": { - "is-glob": "^4.0.1" + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">= 6" + "node": ">=12.20" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/undici": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=20.18.1" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, + "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" + "pathe": "^2.0.3" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "node_modules/workerd": { + "version": "1.20260124.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260124.0.tgz", + "integrity": "sha512-JN6voV/fUQK342a39Rl+20YVmtIXZVbpxc7V/m809lUnlTGPy4aa5MI7PMoc+9qExgAEOw9cojvN5zOfqmMWLg==", "dev": true, - "dependencies": { - "hasown": "^2.0.0" + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260124.0", + "@cloudflare/workerd-darwin-arm64": "1.20260124.0", + "@cloudflare/workerd-linux-64": "1.20260124.0", + "@cloudflare/workerd-linux-arm64": "1.20260124.0", + "@cloudflare/workerd-windows-64": "1.20260124.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/wrangler": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.61.0.tgz", + "integrity": "sha512-Kb8NMe1B/HM7/ds3hU+fcV1U7T996vRKJ0UU/qqgNUMwdemTRA+sSaH3mQvQslIBbprHHU81s0huA6fDIcwiaQ==", "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.11.0", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.0", + "miniflare": "4.20260124.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260124.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260124.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/itty-router": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-3.0.12.tgz", - "integrity": "sha512-s98XTPhle6GGbaFf0kYrOD3Q8gyhnqvOqkwYijC3AmkceNKqWUp13YHg6dWmqmVv4pP7l7c94XI92I0EXVGO0w==" - }, - "node_modules/jose": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", - "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", - "funding": { - "url": "https://github.com/sponsors/panva" + "node": ">=18" } }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "sourcemap-codec": "^1.4.8" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], "dev": true, - "bin": { - "mime": "cli.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10.0.0" + "node": ">=18" } }, - "node_modules/miniflare": { - "version": "3.20240404.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240404.0.tgz", - "integrity": "sha512-+FOTcztPMW3akmucX4vE0PWMNvP4JBwl4s9ieA84fcOaDtTbtfU1rHXpcacj16klpUpvSnD6xd8Sjsn6SJXPfg==", + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "acorn": "^8.8.0", - "acorn-walk": "^8.2.0", - "capnp-ts": "^0.7.0", - "exit-hook": "^2.2.1", - "glob-to-regexp": "^0.4.1", - "stoppable": "^1.1.0", - "undici": "^5.28.2", - "workerd": "1.20240404.0", - "ws": "^8.11.0", - "youch": "^3.2.2", - "zod": "^3.20.6" - }, - "bin": { - "miniflare": "bootstrap.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=16.13" + "node": ">=18" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], "dev": true, - "bin": { - "mustache": "bin/mustache" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "license": "MIT", + "optional": true, + "os": [ + "freebsd" ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=18" } }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 6.13.0" + "node": ">=18" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "node": ">=18" } }, - "node_modules/printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.10.0" + "node": ">=18" } }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/rollup-plugin-inject": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", - "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "estree-walker": "^0.6.1", - "magic-string": "^0.25.3", - "rollup-pluginutils": "^2.8.1" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/rollup-plugin-node-polyfills": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", - "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], "dev": true, - "dependencies": { - "rollup-plugin-inject": "^3.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "estree-walker": "^0.6.1" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "node-forge": "^1" - }, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true - }, - "node_modules/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=4", - "npm": ">=6" + "node": ">=18" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.20" + "node": ">=18" } }, - "node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14.0" + "node": ">=18" } }, - "node_modules/workerd": { - "version": "1.20240404.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240404.0.tgz", - "integrity": "sha512-U4tfnvBcPMsv7pmRGuF0J5UnoZi6tbc64tXNfyijI74r6w6Vlb2+a6eibdQL8g0g46+4vjuTKME9G5RvSvdc8g==", + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "bin": { - "workerd": "bin/workerd" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20240404.0", - "@cloudflare/workerd-darwin-arm64": "1.20240404.0", - "@cloudflare/workerd-linux-64": "1.20240404.0", - "@cloudflare/workerd-linux-arm64": "1.20240404.0", - "@cloudflare/workerd-windows-64": "1.20240404.0" + "node": ">=18" } }, - "node_modules/wrangler": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.48.0.tgz", - "integrity": "sha512-Wv7JS6FyX1j9HkaM6WL3fmTzBMAYc4hPSyZCuxuH55hkJDX/7ts+YAgsaN1U8rKoDrV3FVSgBfI9TyqP9iuM8Q==", + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "dev": true, - "dependencies": { - "@cloudflare/kv-asset-handler": "0.3.1", - "@esbuild-plugins/node-globals-polyfill": "^0.2.3", - "@esbuild-plugins/node-modules-polyfill": "^0.2.2", - "blake3-wasm": "^2.1.5", - "chokidar": "^3.5.3", - "esbuild": "0.17.19", - "miniflare": "3.20240404.0", - "nanoid": "^3.3.3", - "path-to-regexp": "^6.2.0", - "resolve": "^1.22.8", - "resolve.exports": "^2.0.2", - "selfsigned": "^2.0.1", - "source-map": "0.6.1", - "xxhash-wasm": "^1.0.1" - }, + "hasInstallScript": true, + "license": "MIT", "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=16.17.0" + "node": ">=18" }, "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20240404.0" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -878,30 +2016,29 @@ } } }, - "node_modules/xxhash-wasm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz", - "integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==", - "dev": true - }, "node_modules/youch": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.3.tgz", - "integrity": "sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==", + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", "dev": true, + "license": "MIT", "dependencies": { - "cookie": "^0.5.0", - "mustache": "^4.2.0", - "stacktracey": "^2.1.8" + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" } }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" } } } diff --git a/common/component/cloudflare/worker-src/package.json b/common/component/cloudflare/worker-src/package.json index 9de66a7940..3eb385166b 100644 --- a/common/component/cloudflare/worker-src/package.json +++ b/common/component/cloudflare/worker-src/package.json @@ -13,13 +13,13 @@ "license": "Apache2", "devDependencies": { "@cloudflare/workers-types": "^4.20230511.0", - "esbuild": "^0.17.19", + "esbuild": "^0.27.2", "prettier": "^2.8.8", "typescript": "^5.0.4", - "wrangler": "^3.19.0" + "wrangler": "^4.61.0" }, "dependencies": { "itty-router": "3.0.12", - "jose": "4.14.4" + "jose": "4.15.5" } } diff --git a/common/component/cloudflare/workers/code/worker.js b/common/component/cloudflare/workers/code/worker.js index b551740020..db9352d2b9 100644 --- a/common/component/cloudflare/workers/code/worker.js +++ b/common/component/cloudflare/workers/code/worker.js @@ -1,2 +1,2 @@ -var se=({base:e="",routes:t=[]}={})=>({__proto__:new Proxy({},{get:(r,n,o)=>(i,...a)=>t.push([n.toUpperCase(),RegExp(`^${(e+i).replace(/(\/?)\*/g,"($1.*)?").replace(/(\/$)|((?<=\/)\/)/,"").replace(/(:(\w+)\+)/,"(?<$2>.*)").replace(/:(\w+)(\?)?(\.)?/g,"$2(?<$1>[^/]+)$2$3").replace(/\.(?=[\w(])/,"\\.").replace(/\)\.\?\(([^\[]+)\[\^/g,"?)\\.?($1(?<=\\.)[^\\.")}/*$`),a])&&o}),routes:t,async handle(r,...n){let o,i,a=new URL(r.url),c=r.query={};for(let[s,d]of a.searchParams)c[s]=c[s]===void 0?d:[c[s],d].flat();for(let[s,d,m]of t)if((s===r.method||s==="ALL")&&(i=a.pathname.match(d))){r.params=i.groups||{};for(let I of m)if((o=await I(r.proxy||r,...n))!==void 0)return o}}});var f=crypto,g=e=>e instanceof CryptoKey;var E=new TextEncoder,y=new TextDecoder,yt=2**32;function H(...e){let t=e.reduce((o,{length:i})=>o+i,0),r=new Uint8Array(t),n=0;return e.forEach(o=>{r.set(o,n),n+=o.length}),r}var ce=e=>{let t=atob(e),r=new Uint8Array(t.length);for(let n=0;n{let t=e;t instanceof Uint8Array&&(t=y.decode(t)),t=t.replace(/-/g,"+").replace(/_/g,"/").replace(/\s/g,"");try{return ce(t)}catch{throw new TypeError("The input to be decoded is not correctly encoded.")}};var b=class extends Error{static get code(){return"ERR_JOSE_GENERIC"}constructor(t){var r;super(t),this.code="ERR_JOSE_GENERIC",this.name=this.constructor.name,(r=Error.captureStackTrace)===null||r===void 0||r.call(Error,this,this.constructor)}},w=class extends b{static get code(){return"ERR_JWT_CLAIM_VALIDATION_FAILED"}constructor(t,r="unspecified",n="unspecified"){super(t),this.code="ERR_JWT_CLAIM_VALIDATION_FAILED",this.claim=r,this.reason=n}},U=class extends b{static get code(){return"ERR_JWT_EXPIRED"}constructor(t,r="unspecified",n="unspecified"){super(t),this.code="ERR_JWT_EXPIRED",this.claim=r,this.reason=n}},D=class extends b{constructor(){super(...arguments),this.code="ERR_JOSE_ALG_NOT_ALLOWED"}static get code(){return"ERR_JOSE_ALG_NOT_ALLOWED"}},p=class extends b{constructor(){super(...arguments),this.code="ERR_JOSE_NOT_SUPPORTED"}static get code(){return"ERR_JOSE_NOT_SUPPORTED"}};var u=class extends b{constructor(){super(...arguments),this.code="ERR_JWS_INVALID"}static get code(){return"ERR_JWS_INVALID"}},_=class extends b{constructor(){super(...arguments),this.code="ERR_JWT_INVALID"}static get code(){return"ERR_JWT_INVALID"}};var O=class extends b{constructor(){super(...arguments),this.code="ERR_JWS_SIGNATURE_VERIFICATION_FAILED",this.message="signature verification failed"}static get code(){return"ERR_JWS_SIGNATURE_VERIFICATION_FAILED"}};var $=f.getRandomValues.bind(f);function v(e,t="algorithm.name"){return new TypeError(`CryptoKey does not support this operation, its ${t} must be ${e}`)}function B(e,t){return e.name===t}function Y(e){return parseInt(e.name.slice(4),10)}function Ke(e){switch(e){case"ES256":return"P-256";case"ES384":return"P-384";case"ES512":return"P-521";default:throw new Error("unreachable")}}function xe(e,t){if(t.length&&!t.some(r=>e.usages.includes(r))){let r="CryptoKey does not support this operation, its usages must include ";if(t.length>2){let n=t.pop();r+=`one of ${t.join(", ")}, or ${n}.`}else t.length===2?r+=`one of ${t[0]} or ${t[1]}.`:r+=`${t[0]}.`;throw new TypeError(r)}}function pe(e,t,...r){switch(t){case"HS256":case"HS384":case"HS512":{if(!B(e.algorithm,"HMAC"))throw v("HMAC");let n=parseInt(t.slice(2),10);if(Y(e.algorithm.hash)!==n)throw v(`SHA-${n}`,"algorithm.hash");break}case"RS256":case"RS384":case"RS512":{if(!B(e.algorithm,"RSASSA-PKCS1-v1_5"))throw v("RSASSA-PKCS1-v1_5");let n=parseInt(t.slice(2),10);if(Y(e.algorithm.hash)!==n)throw v(`SHA-${n}`,"algorithm.hash");break}case"PS256":case"PS384":case"PS512":{if(!B(e.algorithm,"RSA-PSS"))throw v("RSA-PSS");let n=parseInt(t.slice(2),10);if(Y(e.algorithm.hash)!==n)throw v(`SHA-${n}`,"algorithm.hash");break}case"EdDSA":{if(e.algorithm.name!=="Ed25519"&&e.algorithm.name!=="Ed448")throw v("Ed25519 or Ed448");break}case"ES256":case"ES384":case"ES512":{if(!B(e.algorithm,"ECDSA"))throw v("ECDSA");let n=Ke(t);if(e.algorithm.namedCurve!==n)throw v(n,"algorithm.namedCurve");break}default:throw new TypeError("CryptoKey does not support this operation")}xe(e,r)}function ue(e,t,...r){if(r.length>2){let n=r.pop();e+=`one of type ${r.join(", ")}, or ${n}.`}else r.length===2?e+=`one of type ${r[0]} or ${r[1]}.`:e+=`of type ${r[0]}.`;return t==null?e+=` Received ${t}`:typeof t=="function"&&t.name?e+=` Received function ${t.name}`:typeof t=="object"&&t!=null&&t.constructor&&t.constructor.name&&(e+=` Received an instance of ${t.constructor.name}`),e}var S=(e,...t)=>ue("Key must be ",e,...t);function Q(e,t,...r){return ue(`Key for the ${e} algorithm must be `,t,...r)}var Z=e=>g(e),l=["CryptoKey"];var Je=(...e)=>{let t=e.filter(Boolean);if(t.length===0||t.length===1)return!0;let r;for(let n of t){let o=Object.keys(n);if(!r||r.size===0){r=new Set(o);continue}for(let i of o){if(r.has(i))return!1;r.add(i)}}return!0},J=Je;function Re(e){return typeof e=="object"&&e!==null}function h(e){if(!Re(e)||Object.prototype.toString.call(e)!=="[object Object]")return!1;if(Object.getPrototypeOf(e)===null)return!0;let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}var L=(e,t)=>{if(e.startsWith("RS")||e.startsWith("PS")){let{modulusLength:r}=t.algorithm;if(typeof r!="number"||r<2048)throw new TypeError(`${e} requires key modulusLength to be 2048 bits or larger`)}};var P=(e,t,r=0)=>{r===0&&(t.unshift(t.length),t.unshift(6));let n=e.indexOf(t[0],r);if(n===-1)return!1;let o=e.subarray(n,n+t.length);return o.length!==t.length?!1:o.every((i,a)=>i===t[a])||P(e,t,n+1)},le=e=>{switch(!0){case P(e,[42,134,72,206,61,3,1,7]):return"P-256";case P(e,[43,129,4,0,34]):return"P-384";case P(e,[43,129,4,0,35]):return"P-521";case P(e,[43,101,110]):return"X25519";case P(e,[43,101,111]):return"X448";case P(e,[43,101,112]):return"Ed25519";case P(e,[43,101,113]):return"Ed448";default:throw new p("Invalid or unsupported EC Key Curve or OKP Key Sub Type")}},De=async(e,t,r,n,o)=>{var i;let a,c,s=new Uint8Array(atob(r.replace(e,"")).split("").map(m=>m.charCodeAt(0))),d=t==="spki";switch(n){case"PS256":case"PS384":case"PS512":a={name:"RSA-PSS",hash:`SHA-${n.slice(-3)}`},c=d?["verify"]:["sign"];break;case"RS256":case"RS384":case"RS512":a={name:"RSASSA-PKCS1-v1_5",hash:`SHA-${n.slice(-3)}`},c=d?["verify"]:["sign"];break;case"RSA-OAEP":case"RSA-OAEP-256":case"RSA-OAEP-384":case"RSA-OAEP-512":a={name:"RSA-OAEP",hash:`SHA-${parseInt(n.slice(-3),10)||1}`},c=d?["encrypt","wrapKey"]:["decrypt","unwrapKey"];break;case"ES256":a={name:"ECDSA",namedCurve:"P-256"},c=d?["verify"]:["sign"];break;case"ES384":a={name:"ECDSA",namedCurve:"P-384"},c=d?["verify"]:["sign"];break;case"ES512":a={name:"ECDSA",namedCurve:"P-521"},c=d?["verify"]:["sign"];break;case"ECDH-ES":case"ECDH-ES+A128KW":case"ECDH-ES+A192KW":case"ECDH-ES+A256KW":{let m=le(s);a=m.startsWith("P-")?{name:"ECDH",namedCurve:m}:{name:m},c=d?[]:["deriveBits"];break}case"EdDSA":a={name:le(s)},c=d?["verify"]:["sign"];break;default:throw new p('Invalid or unsupported "alg" (Algorithm) value')}return f.subtle.importKey(t,s,a,(i=o?.extractable)!==null&&i!==void 0?i:!1,c)};var me=(e,t,r)=>De(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g,"spki",e,t,r);async function j(e,t,r){if(typeof e!="string"||e.indexOf("-----BEGIN PUBLIC KEY-----")!==0)throw new TypeError('"spki" must be SPKI formatted string');return me(e,t,r)}var Oe=(e,t)=>{if(!(t instanceof Uint8Array)){if(!Z(t))throw new TypeError(Q(e,t,...l,"Uint8Array"));if(t.type!=="secret")throw new TypeError(`${l.join(" or ")} instances for symmetric algorithms must be of type "secret"`)}},Me=(e,t,r)=>{if(!Z(t))throw new TypeError(Q(e,t,...l));if(t.type==="secret")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithms must not be of type "secret"`);if(r==="sign"&&t.type==="public")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithm signing must be of type "private"`);if(r==="decrypt"&&t.type==="public")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithm decryption must be of type "private"`);if(t.algorithm&&r==="verify"&&t.type==="private")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithm verifying must be of type "public"`);if(t.algorithm&&r==="encrypt"&&t.type==="private")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithm encryption must be of type "public"`)},ke=(e,t,r)=>{e.startsWith("HS")||e==="dir"||e.startsWith("PBES2")||/^A\d{3}(?:GCM)?KW$/.test(e)?Oe(e,t):Me(e,t,r)},k=ke;function Fe(e,t,r,n,o){if(o.crit!==void 0&&n.crit===void 0)throw new e('"crit" (Critical) Header Parameter MUST be integrity protected');if(!n||n.crit===void 0)return new Set;if(!Array.isArray(n.crit)||n.crit.length===0||n.crit.some(a=>typeof a!="string"||a.length===0))throw new e('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');let i;r!==void 0?i=new Map([...Object.entries(r),...t.entries()]):i=t;for(let a of n.crit){if(!i.has(a))throw new p(`Extension Header Parameter "${a}" is not recognized`);if(o[a]===void 0)throw new e(`Extension Header Parameter "${a}" is missing`);if(i.get(a)&&n[a]===void 0)throw new e(`Extension Header Parameter "${a}" MUST be integrity protected`)}return new Set(n.crit)}var R=Fe;var ze=(e,t)=>{if(t!==void 0&&(!Array.isArray(t)||t.some(r=>typeof r!="string")))throw new TypeError(`"${e}" option must be an array of strings`);if(t)return new Set(t)},te=ze;var Ye=Symbol();function G(e,t){let r=`SHA-${e.slice(-3)}`;switch(e){case"HS256":case"HS384":case"HS512":return{hash:r,name:"HMAC"};case"PS256":case"PS384":case"PS512":return{hash:r,name:"RSA-PSS",saltLength:e.slice(-3)>>3};case"RS256":case"RS384":case"RS512":return{hash:r,name:"RSASSA-PKCS1-v1_5"};case"ES256":case"ES384":case"ES512":return{hash:r,name:"ECDSA",namedCurve:t.namedCurve};case"EdDSA":return{name:t.name};default:throw new p(`alg ${e} is not supported either by JOSE or your javascript runtime`)}}function V(e,t,r){if(g(t))return pe(t,e,r),t;if(t instanceof Uint8Array){if(!e.startsWith("HS"))throw new TypeError(S(t,...l));return f.subtle.importKey("raw",t,{hash:`SHA-${e.slice(-3)}`,name:"HMAC"},!1,[r])}throw new TypeError(S(t,...l,"Uint8Array"))}var Qe=async(e,t,r,n)=>{let o=await V(e,t,"verify");L(e,o);let i=G(e,o.algorithm);try{return await f.subtle.verify(i,o,r,n)}catch{return!1}},Ee=Qe;async function F(e,t,r){var n;if(!h(e))throw new u("Flattened JWS must be an object");if(e.protected===void 0&&e.header===void 0)throw new u('Flattened JWS must have either of the "protected" or "header" members');if(e.protected!==void 0&&typeof e.protected!="string")throw new u("JWS Protected Header incorrect type");if(e.payload===void 0)throw new u("JWS Payload missing");if(typeof e.signature!="string")throw new u("JWS Signature missing or incorrect type");if(e.header!==void 0&&!h(e.header))throw new u("JWS Unprotected Header incorrect type");let o={};if(e.protected)try{let ie=A(e.protected);o=JSON.parse(y.decode(ie))}catch{throw new u("JWS Protected Header is invalid")}if(!J(o,e.header))throw new u("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");let i={...o,...e.header},a=R(u,new Map([["b64",!0]]),r?.crit,o,i),c=!0;if(a.has("b64")&&(c=o.b64,typeof c!="boolean"))throw new u('The "b64" (base64url-encode payload) Header Parameter must be a boolean');let{alg:s}=i;if(typeof s!="string"||!s)throw new u('JWS "alg" (Algorithm) Header Parameter missing or invalid');let d=r&&te("algorithms",r.algorithms);if(d&&!d.has(s))throw new D('"alg" (Algorithm) Header Parameter not allowed');if(c){if(typeof e.payload!="string")throw new u("JWS Payload must be a string")}else if(typeof e.payload!="string"&&!(e.payload instanceof Uint8Array))throw new u("JWS Payload must be a string or an Uint8Array instance");let m=!1;typeof t=="function"&&(t=await t(o,e),m=!0),k(s,t,"verify");let I=H(E.encode((n=e.protected)!==null&&n!==void 0?n:""),E.encode("."),typeof e.payload=="string"?E.encode(e.payload):e.payload),T=A(e.signature);if(!await Ee(s,t,T,I))throw new O;let W;c?W=A(e.payload):typeof e.payload=="string"?W=E.encode(e.payload):W=e.payload;let N={payload:W};return e.protected!==void 0&&(N.protectedHeader=o),e.header!==void 0&&(N.unprotectedHeader=e.header),m?{...N,key:t}:N}async function re(e,t,r){if(e instanceof Uint8Array&&(e=y.decode(e)),typeof e!="string")throw new u("Compact JWS must be a string or Uint8Array");let{0:n,1:o,2:i,length:a}=e.split(".");if(a!==3)throw new u("Invalid Compact JWS");let c=await F({payload:o,protected:n,signature:i},t,r),s={payload:c.payload,protectedHeader:c.protectedHeader};return typeof t=="function"?{...s,key:c.key}:s}var ne=e=>Math.floor(e.getTime()/1e3);var Ze=/^(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i,z=e=>{let t=Ze.exec(e);if(!t)throw new TypeError("Invalid time period format");let r=parseFloat(t[1]);switch(t[2].toLowerCase()){case"sec":case"secs":case"second":case"seconds":case"s":return Math.round(r);case"minute":case"minutes":case"min":case"mins":case"m":return Math.round(r*60);case"hour":case"hours":case"hr":case"hrs":case"h":return Math.round(r*3600);case"day":case"days":case"d":return Math.round(r*86400);case"week":case"weeks":case"w":return Math.round(r*604800);default:return Math.round(r*31557600)}};var ge=e=>e.toLowerCase().replace(/^application\//,""),je=(e,t)=>typeof e=="string"?t.includes(e):Array.isArray(e)?t.some(Set.prototype.has.bind(new Set(e))):!1,X=(e,t,r={})=>{let{typ:n}=r;if(n&&(typeof e.typ!="string"||ge(e.typ)!==ge(n)))throw new w('unexpected "typ" JWT header value',"typ","check_failed");let o;try{o=JSON.parse(y.decode(t))}catch{}if(!h(o))throw new _("JWT Claims Set must be a top-level JSON object");let{requiredClaims:i=[],issuer:a,subject:c,audience:s,maxTokenAge:d}=r;d!==void 0&&i.push("iat"),s!==void 0&&i.push("aud"),c!==void 0&&i.push("sub"),a!==void 0&&i.push("iss");for(let x of new Set(i.reverse()))if(!(x in o))throw new w(`missing required "${x}" claim`,x,"missing");if(a&&!(Array.isArray(a)?a:[a]).includes(o.iss))throw new w('unexpected "iss" claim value',"iss","check_failed");if(c&&o.sub!==c)throw new w('unexpected "sub" claim value',"sub","check_failed");if(s&&!je(o.aud,typeof s=="string"?[s]:s))throw new w('unexpected "aud" claim value',"aud","check_failed");let m;switch(typeof r.clockTolerance){case"string":m=z(r.clockTolerance);break;case"number":m=r.clockTolerance;break;case"undefined":m=0;break;default:throw new TypeError("Invalid clockTolerance option type")}let{currentDate:I}=r,T=ne(I||new Date);if((o.iat!==void 0||d)&&typeof o.iat!="number")throw new w('"iat" claim must be a number',"iat","invalid");if(o.nbf!==void 0){if(typeof o.nbf!="number")throw new w('"nbf" claim must be a number',"nbf","invalid");if(o.nbf>T+m)throw new w('"nbf" claim timestamp check failed',"nbf","check_failed")}if(o.exp!==void 0){if(typeof o.exp!="number")throw new w('"exp" claim must be a number',"exp","invalid");if(o.exp<=T-m)throw new U('"exp" claim timestamp check failed',"exp","check_failed")}if(d){let x=T-o.iat,W=typeof d=="number"?d:z(d);if(x-m>W)throw new U('"iat" claim timestamp check failed (too far in the past)',"iat","check_failed");if(x<0-m)throw new w('"iat" claim timestamp check failed (it should be in the past)',"iat","check_failed")}return o};async function oe(e,t,r){var n;let o=await re(e,t,r);if(!((n=o.protectedHeader.crit)===null||n===void 0)&&n.includes("b64")&&o.protectedHeader.b64===!1)throw new _("JWTs MUST NOT use unencoded payload");let a={payload:X(o.protectedHeader,o.payload,r),protectedHeader:o.protectedHeader};return typeof t=="function"?{...a,key:o.key}:a}var st=/^(?:Bearer )?([A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+)/i;async function q(e,t){if(t.SKIP_AUTH==="true")return!0;let r=st.exec(e.headers.get("authorization")||"");if(!r||!r[1])return!1;let n=await j(t.PUBLIC_KEY,"EdDSA");try{await oe(r[1],n,{issuer:"dapr.io/cloudflare",audience:t.TOKEN_AUDIENCE,algorithms:["EdDSA"],clockTolerance:300})}catch(o){return console.error("Failed to validate JWT: "+o),!1}return!0}var be="20230517";var dt=se().get("/.well-known/dapr/info",async(e,t)=>{if(!await q(e,t))return new Response("Unauthorized",{status:401});let n=[],o=[],i=[],a=Object.keys(t);for(let s=0;s{let{namespace:r,key:n,errorRes:o}=await ae(e,t);if(o)return o;let i=await r.get(n,"stream");return i?new Response(i,{status:200}):new Response("",{status:404})}).post("/kv/:namespace/:key",async(e,t)=>{let{namespace:r,key:n,errorRes:o}=await ae(e,t);if(o)return o;let i,a=new URL(e.url),c=parseInt(a.searchParams.get("ttl")||"",10);return c>0&&(i=c),await r.put(n,e.body,{expirationTtl:i}),new Response("",{status:201})}).delete("/kv/:namespace/:key",async(e,t)=>{let{namespace:r,key:n,errorRes:o}=await ae(e,t);return o||(await r.delete(n),new Response("",{status:204}))}).post("/queues/:queue",async(e,t)=>{let{queue:r,errorRes:n}=await pt(e,t);if(n)return n;let o=await e.text();return await r.send(o),new Response("",{status:201})}).all("*",()=>new Response("Not found",{status:404}));async function ae(e,t){if(!e?.text||!e.params?.namespace||!e.params?.key)return{errorRes:new Response("Bad request",{status:400})};let r=t[e.params.namespace];return typeof r!="object"||!["KVNamespace","KvNamespace"].includes(r?.constructor?.name)?{errorRes:new Response(`Worker is not bound to KV '${e.params.kv}'`,{status:412})}:await q(e,t)?{namespace:r,key:e.params.key}:{errorRes:new Response("Unauthorized",{status:401})}}async function pt(e,t){if(!e?.text||!e.params?.queue)return{errorRes:new Response("Bad request",{status:400})};let r=t[e.params.queue];return typeof r!="object"||!["WorkerQueue","Queue"].includes(r?.constructor?.name)?{errorRes:new Response(`Worker is not bound to queue '${e.params.queue}'`,{status:412})}:await q(e,t)?{queue:r}:{errorRes:new Response("Unauthorized",{status:401})}}var Cc={fetch:dt.handle};export{Cc as default}; +var se=({base:e="",routes:t=[]}={})=>({__proto__:new Proxy({},{get:(r,n,o)=>(i,...a)=>t.push([n.toUpperCase(),RegExp(`^${(e+i).replace(/(\/?)\*/g,"($1.*)?").replace(/(\/$)|((?<=\/)\/)/,"").replace(/(:(\w+)\+)/,"(?<$2>.*)").replace(/:(\w+)(\?)?(\.)?/g,"$2(?<$1>[^/]+)$2$3").replace(/\.(?=[\w(])/,"\\.").replace(/\)\.\?\(([^\[]+)\[\^/g,"?)\\.?($1(?<=\\.)[^\\.")}/*$`),a])&&o}),routes:t,async handle(r,...n){let o,i,a=new URL(r.url),c=r.query={};for(let[s,d]of a.searchParams)c[s]=c[s]===void 0?d:[c[s],d].flat();for(let[s,d,m]of t)if((s===r.method||s==="ALL")&&(i=a.pathname.match(d))){r.params=i.groups||{};for(let T of m)if((o=await T(r.proxy||r,...n))!==void 0)return o}}});var f=crypto,g=e=>e instanceof CryptoKey;var E=new TextEncoder,y=new TextDecoder,ht=2**32;function H(...e){let t=e.reduce((o,{length:i})=>o+i,0),r=new Uint8Array(t),n=0;return e.forEach(o=>{r.set(o,n),n+=o.length}),r}var ce=e=>{let t=atob(e),r=new Uint8Array(t.length);for(let n=0;n{let t=e;t instanceof Uint8Array&&(t=y.decode(t)),t=t.replace(/-/g,"+").replace(/_/g,"/").replace(/\s/g,"");try{return ce(t)}catch{throw new TypeError("The input to be decoded is not correctly encoded.")}};var b=class extends Error{static get code(){return"ERR_JOSE_GENERIC"}constructor(t){var r;super(t),this.code="ERR_JOSE_GENERIC",this.name=this.constructor.name,(r=Error.captureStackTrace)===null||r===void 0||r.call(Error,this,this.constructor)}},w=class extends b{static get code(){return"ERR_JWT_CLAIM_VALIDATION_FAILED"}constructor(t,r="unspecified",n="unspecified"){super(t),this.code="ERR_JWT_CLAIM_VALIDATION_FAILED",this.claim=r,this.reason=n}},U=class extends b{static get code(){return"ERR_JWT_EXPIRED"}constructor(t,r="unspecified",n="unspecified"){super(t),this.code="ERR_JWT_EXPIRED",this.claim=r,this.reason=n}},D=class extends b{constructor(){super(...arguments),this.code="ERR_JOSE_ALG_NOT_ALLOWED"}static get code(){return"ERR_JOSE_ALG_NOT_ALLOWED"}},u=class extends b{constructor(){super(...arguments),this.code="ERR_JOSE_NOT_SUPPORTED"}static get code(){return"ERR_JOSE_NOT_SUPPORTED"}};var p=class extends b{constructor(){super(...arguments),this.code="ERR_JWS_INVALID"}static get code(){return"ERR_JWS_INVALID"}},_=class extends b{constructor(){super(...arguments),this.code="ERR_JWT_INVALID"}static get code(){return"ERR_JWT_INVALID"}};var O=class extends b{constructor(){super(...arguments),this.code="ERR_JWS_SIGNATURE_VERIFICATION_FAILED",this.message="signature verification failed"}static get code(){return"ERR_JWS_SIGNATURE_VERIFICATION_FAILED"}};var $=f.getRandomValues.bind(f);function v(e,t="algorithm.name"){return new TypeError(`CryptoKey does not support this operation, its ${t} must be ${e}`)}function B(e,t){return e.name===t}function Q(e){return parseInt(e.name.slice(4),10)}function xe(e){switch(e){case"ES256":return"P-256";case"ES384":return"P-384";case"ES512":return"P-521";default:throw new Error("unreachable")}}function Ke(e,t){if(t.length&&!t.some(r=>e.usages.includes(r))){let r="CryptoKey does not support this operation, its usages must include ";if(t.length>2){let n=t.pop();r+=`one of ${t.join(", ")}, or ${n}.`}else t.length===2?r+=`one of ${t[0]} or ${t[1]}.`:r+=`${t[0]}.`;throw new TypeError(r)}}function pe(e,t,...r){switch(t){case"HS256":case"HS384":case"HS512":{if(!B(e.algorithm,"HMAC"))throw v("HMAC");let n=parseInt(t.slice(2),10);if(Q(e.algorithm.hash)!==n)throw v(`SHA-${n}`,"algorithm.hash");break}case"RS256":case"RS384":case"RS512":{if(!B(e.algorithm,"RSASSA-PKCS1-v1_5"))throw v("RSASSA-PKCS1-v1_5");let n=parseInt(t.slice(2),10);if(Q(e.algorithm.hash)!==n)throw v(`SHA-${n}`,"algorithm.hash");break}case"PS256":case"PS384":case"PS512":{if(!B(e.algorithm,"RSA-PSS"))throw v("RSA-PSS");let n=parseInt(t.slice(2),10);if(Q(e.algorithm.hash)!==n)throw v(`SHA-${n}`,"algorithm.hash");break}case"EdDSA":{if(e.algorithm.name!=="Ed25519"&&e.algorithm.name!=="Ed448")throw v("Ed25519 or Ed448");break}case"ES256":case"ES384":case"ES512":{if(!B(e.algorithm,"ECDSA"))throw v("ECDSA");let n=xe(t);if(e.algorithm.namedCurve!==n)throw v(n,"algorithm.namedCurve");break}default:throw new TypeError("CryptoKey does not support this operation")}Ke(e,r)}function ue(e,t,...r){if(r.length>2){let n=r.pop();e+=`one of type ${r.join(", ")}, or ${n}.`}else r.length===2?e+=`one of type ${r[0]} or ${r[1]}.`:e+=`of type ${r[0]}.`;return t==null?e+=` Received ${t}`:typeof t=="function"&&t.name?e+=` Received function ${t.name}`:typeof t=="object"&&t!=null&&t.constructor&&t.constructor.name&&(e+=` Received an instance of ${t.constructor.name}`),e}var S=(e,...t)=>ue("Key must be ",e,...t);function Z(e,t,...r){return ue(`Key for the ${e} algorithm must be `,t,...r)}var j=e=>g(e),l=["CryptoKey"];var Je=(...e)=>{let t=e.filter(Boolean);if(t.length===0||t.length===1)return!0;let r;for(let n of t){let o=Object.keys(n);if(!r||r.size===0){r=new Set(o);continue}for(let i of o){if(r.has(i))return!1;r.add(i)}}return!0},R=Je;function Re(e){return typeof e=="object"&&e!==null}function h(e){if(!Re(e)||Object.prototype.toString.call(e)!=="[object Object]")return!1;if(Object.getPrototypeOf(e)===null)return!0;let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}var G=(e,t)=>{if(e.startsWith("RS")||e.startsWith("PS")){let{modulusLength:r}=t.algorithm;if(typeof r!="number"||r<2048)throw new TypeError(`${e} requires key modulusLength to be 2048 bits or larger`)}};var P=(e,t,r=0)=>{r===0&&(t.unshift(t.length),t.unshift(6));let n=e.indexOf(t[0],r);if(n===-1)return!1;let o=e.subarray(n,n+t.length);return o.length!==t.length?!1:o.every((i,a)=>i===t[a])||P(e,t,n+1)},le=e=>{switch(!0){case P(e,[42,134,72,206,61,3,1,7]):return"P-256";case P(e,[43,129,4,0,34]):return"P-384";case P(e,[43,129,4,0,35]):return"P-521";case P(e,[43,101,110]):return"X25519";case P(e,[43,101,111]):return"X448";case P(e,[43,101,112]):return"Ed25519";case P(e,[43,101,113]):return"Ed448";default:throw new u("Invalid or unsupported EC Key Curve or OKP Key Sub Type")}},De=async(e,t,r,n,o)=>{var i;let a,c,s=new Uint8Array(atob(r.replace(e,"")).split("").map(m=>m.charCodeAt(0))),d=t==="spki";switch(n){case"PS256":case"PS384":case"PS512":a={name:"RSA-PSS",hash:`SHA-${n.slice(-3)}`},c=d?["verify"]:["sign"];break;case"RS256":case"RS384":case"RS512":a={name:"RSASSA-PKCS1-v1_5",hash:`SHA-${n.slice(-3)}`},c=d?["verify"]:["sign"];break;case"RSA-OAEP":case"RSA-OAEP-256":case"RSA-OAEP-384":case"RSA-OAEP-512":a={name:"RSA-OAEP",hash:`SHA-${parseInt(n.slice(-3),10)||1}`},c=d?["encrypt","wrapKey"]:["decrypt","unwrapKey"];break;case"ES256":a={name:"ECDSA",namedCurve:"P-256"},c=d?["verify"]:["sign"];break;case"ES384":a={name:"ECDSA",namedCurve:"P-384"},c=d?["verify"]:["sign"];break;case"ES512":a={name:"ECDSA",namedCurve:"P-521"},c=d?["verify"]:["sign"];break;case"ECDH-ES":case"ECDH-ES+A128KW":case"ECDH-ES+A192KW":case"ECDH-ES+A256KW":{let m=le(s);a=m.startsWith("P-")?{name:"ECDH",namedCurve:m}:{name:m},c=d?[]:["deriveBits"];break}case"EdDSA":a={name:le(s)},c=d?["verify"]:["sign"];break;default:throw new u('Invalid or unsupported "alg" (Algorithm) value')}return f.subtle.importKey(t,s,a,(i=o?.extractable)!==null&&i!==void 0?i:!1,c)};var me=(e,t,r)=>De(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g,"spki",e,t,r);async function ee(e,t,r){if(typeof e!="string"||e.indexOf("-----BEGIN PUBLIC KEY-----")!==0)throw new TypeError('"spki" must be SPKI formatted string');return me(e,t,r)}var Oe=(e,t)=>{if(!(t instanceof Uint8Array)){if(!j(t))throw new TypeError(Z(e,t,...l,"Uint8Array"));if(t.type!=="secret")throw new TypeError(`${l.join(" or ")} instances for symmetric algorithms must be of type "secret"`)}},Me=(e,t,r)=>{if(!j(t))throw new TypeError(Z(e,t,...l));if(t.type==="secret")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithms must not be of type "secret"`);if(r==="sign"&&t.type==="public")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithm signing must be of type "private"`);if(r==="decrypt"&&t.type==="public")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithm decryption must be of type "private"`);if(t.algorithm&&r==="verify"&&t.type==="private")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithm verifying must be of type "public"`);if(t.algorithm&&r==="encrypt"&&t.type==="private")throw new TypeError(`${l.join(" or ")} instances for asymmetric algorithm encryption must be of type "public"`)},ke=(e,t,r)=>{e.startsWith("HS")||e==="dir"||e.startsWith("PBES2")||/^A\d{3}(?:GCM)?KW$/.test(e)?Oe(e,t):Me(e,t,r)},k=ke;function Ve(e,t,r,n,o){if(o.crit!==void 0&&n.crit===void 0)throw new e('"crit" (Critical) Header Parameter MUST be integrity protected');if(!n||n.crit===void 0)return new Set;if(!Array.isArray(n.crit)||n.crit.length===0||n.crit.some(a=>typeof a!="string"||a.length===0))throw new e('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');let i;r!==void 0?i=new Map([...Object.entries(r),...t.entries()]):i=t;for(let a of n.crit){if(!i.has(a))throw new u(`Extension Header Parameter "${a}" is not recognized`);if(o[a]===void 0)throw new e(`Extension Header Parameter "${a}" is missing`);if(i.get(a)&&n[a]===void 0)throw new e(`Extension Header Parameter "${a}" MUST be integrity protected`)}return new Set(n.crit)}var I=Ve;var ze=(e,t)=>{if(t!==void 0&&(!Array.isArray(t)||t.some(r=>typeof r!="string")))throw new TypeError(`"${e}" option must be an array of strings`);if(t)return new Set(t)},re=ze;function F(e,t){let r=`SHA-${e.slice(-3)}`;switch(e){case"HS256":case"HS384":case"HS512":return{hash:r,name:"HMAC"};case"PS256":case"PS384":case"PS512":return{hash:r,name:"RSA-PSS",saltLength:e.slice(-3)>>3};case"RS256":case"RS384":case"RS512":return{hash:r,name:"RSASSA-PKCS1-v1_5"};case"ES256":case"ES384":case"ES512":return{hash:r,name:"ECDSA",namedCurve:t.namedCurve};case"EdDSA":return{name:t.name};default:throw new u(`alg ${e} is not supported either by JOSE or your javascript runtime`)}}function V(e,t,r){if(g(t))return pe(t,e,r),t;if(t instanceof Uint8Array){if(!e.startsWith("HS"))throw new TypeError(S(t,...l));return f.subtle.importKey("raw",t,{hash:`SHA-${e.slice(-3)}`,name:"HMAC"},!1,[r])}throw new TypeError(S(t,...l,"Uint8Array"))}var Ye=async(e,t,r,n)=>{let o=await V(e,t,"verify");G(e,o);let i=F(e,o.algorithm);try{return await f.subtle.verify(i,o,r,n)}catch{return!1}},Ee=Ye;async function z(e,t,r){var n;if(!h(e))throw new p("Flattened JWS must be an object");if(e.protected===void 0&&e.header===void 0)throw new p('Flattened JWS must have either of the "protected" or "header" members');if(e.protected!==void 0&&typeof e.protected!="string")throw new p("JWS Protected Header incorrect type");if(e.payload===void 0)throw new p("JWS Payload missing");if(typeof e.signature!="string")throw new p("JWS Signature missing or incorrect type");if(e.header!==void 0&&!h(e.header))throw new p("JWS Unprotected Header incorrect type");let o={};if(e.protected)try{let L=A(e.protected);o=JSON.parse(y.decode(L))}catch{throw new p("JWS Protected Header is invalid")}if(!R(o,e.header))throw new p("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");let i={...o,...e.header},a=I(p,new Map([["b64",!0]]),r?.crit,o,i),c=!0;if(a.has("b64")&&(c=o.b64,typeof c!="boolean"))throw new p('The "b64" (base64url-encode payload) Header Parameter must be a boolean');let{alg:s}=i;if(typeof s!="string"||!s)throw new p('JWS "alg" (Algorithm) Header Parameter missing or invalid');let d=r&&re("algorithms",r.algorithms);if(d&&!d.has(s))throw new D('"alg" (Algorithm) Header Parameter not allowed');if(c){if(typeof e.payload!="string")throw new p("JWS Payload must be a string")}else if(typeof e.payload!="string"&&!(e.payload instanceof Uint8Array))throw new p("JWS Payload must be a string or an Uint8Array instance");let m=!1;typeof t=="function"&&(t=await t(o,e),m=!0),k(s,t,"verify");let T=H(E.encode((n=e.protected)!==null&&n!==void 0?n:""),E.encode("."),typeof e.payload=="string"?E.encode(e.payload):e.payload),W;try{W=A(e.signature)}catch{throw new p("Failed to base64url decode the signature")}if(!await Ee(s,t,W,T))throw new O;let J;if(c)try{J=A(e.payload)}catch{throw new p("Failed to base64url decode the payload")}else typeof e.payload=="string"?J=E.encode(e.payload):J=e.payload;let N={payload:J};return e.protected!==void 0&&(N.protectedHeader=o),e.header!==void 0&&(N.unprotectedHeader=e.header),m?{...N,key:t}:N}async function ne(e,t,r){if(e instanceof Uint8Array&&(e=y.decode(e)),typeof e!="string")throw new p("Compact JWS must be a string or Uint8Array");let{0:n,1:o,2:i,length:a}=e.split(".");if(a!==3)throw new p("Invalid Compact JWS");let c=await z({payload:o,protected:n,signature:i},t,r),s={payload:c.payload,protectedHeader:c.protectedHeader};return typeof t=="function"?{...s,key:c.key}:s}var oe=e=>Math.floor(e.getTime()/1e3);var Qe=/^(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i,X=e=>{let t=Qe.exec(e);if(!t)throw new TypeError("Invalid time period format");let r=parseFloat(t[1]);switch(t[2].toLowerCase()){case"sec":case"secs":case"second":case"seconds":case"s":return Math.round(r);case"minute":case"minutes":case"min":case"mins":case"m":return Math.round(r*60);case"hour":case"hours":case"hr":case"hrs":case"h":return Math.round(r*3600);case"day":case"days":case"d":return Math.round(r*86400);case"week":case"weeks":case"w":return Math.round(r*604800);default:return Math.round(r*31557600)}};var ge=e=>e.toLowerCase().replace(/^application\//,""),Ze=(e,t)=>typeof e=="string"?t.includes(e):Array.isArray(e)?t.some(Set.prototype.has.bind(new Set(e))):!1,q=(e,t,r={})=>{let{typ:n}=r;if(n&&(typeof e.typ!="string"||ge(e.typ)!==ge(n)))throw new w('unexpected "typ" JWT header value',"typ","check_failed");let o;try{o=JSON.parse(y.decode(t))}catch{}if(!h(o))throw new _("JWT Claims Set must be a top-level JSON object");let{requiredClaims:i=[],issuer:a,subject:c,audience:s,maxTokenAge:d}=r;d!==void 0&&i.push("iat"),s!==void 0&&i.push("aud"),c!==void 0&&i.push("sub"),a!==void 0&&i.push("iss");for(let K of new Set(i.reverse()))if(!(K in o))throw new w(`missing required "${K}" claim`,K,"missing");if(a&&!(Array.isArray(a)?a:[a]).includes(o.iss))throw new w('unexpected "iss" claim value',"iss","check_failed");if(c&&o.sub!==c)throw new w('unexpected "sub" claim value',"sub","check_failed");if(s&&!Ze(o.aud,typeof s=="string"?[s]:s))throw new w('unexpected "aud" claim value',"aud","check_failed");let m;switch(typeof r.clockTolerance){case"string":m=X(r.clockTolerance);break;case"number":m=r.clockTolerance;break;case"undefined":m=0;break;default:throw new TypeError("Invalid clockTolerance option type")}let{currentDate:T}=r,W=oe(T||new Date);if((o.iat!==void 0||d)&&typeof o.iat!="number")throw new w('"iat" claim must be a number',"iat","invalid");if(o.nbf!==void 0){if(typeof o.nbf!="number")throw new w('"nbf" claim must be a number',"nbf","invalid");if(o.nbf>W+m)throw new w('"nbf" claim timestamp check failed',"nbf","check_failed")}if(o.exp!==void 0){if(typeof o.exp!="number")throw new w('"exp" claim must be a number',"exp","invalid");if(o.exp<=W-m)throw new U('"exp" claim timestamp check failed',"exp","check_failed")}if(d){let K=W-o.iat,J=typeof d=="number"?d:X(d);if(K-m>J)throw new U('"iat" claim timestamp check failed (too far in the past)',"iat","check_failed");if(K<0-m)throw new w('"iat" claim timestamp check failed (it should be in the past)',"iat","check_failed")}return o};async function ae(e,t,r){var n;let o=await ne(e,t,r);if(!((n=o.protectedHeader.crit)===null||n===void 0)&&n.includes("b64")&&o.protectedHeader.b64===!1)throw new _("JWTs MUST NOT use unencoded payload");let a={payload:q(o.protectedHeader,o.payload,r),protectedHeader:o.protectedHeader};return typeof t=="function"?{...a,key:o.key}:a}var it=/^(?:Bearer )?([A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+)/i;async function Y(e,t){if(t.SKIP_AUTH==="true")return!0;let r=it.exec(e.headers.get("authorization")||"");if(!r||!r[1])return!1;let n=await ee(t.PUBLIC_KEY,"EdDSA");try{await ae(r[1],n,{issuer:"dapr.io/cloudflare",audience:t.TOKEN_AUDIENCE,algorithms:["EdDSA"],clockTolerance:300})}catch(o){return console.error("Failed to validate JWT: "+o),!1}return!0}var be="20230517";var ct=se().get("/.well-known/dapr/info",async(e,t)=>{if(!await Y(e,t))return new Response("Unauthorized",{status:401});let n=[],o=[],i=[],a=Object.keys(t);for(let s=0;s{let{namespace:r,key:n,errorRes:o}=await ie(e,t);if(o)return o;let i=await r.get(n,"stream");return i?new Response(i,{status:200}):new Response("",{status:404})}).post("/kv/:namespace/:key",async(e,t)=>{let{namespace:r,key:n,errorRes:o}=await ie(e,t);if(o)return o;let i,a=new URL(e.url),c=parseInt(a.searchParams.get("ttl")||"",10);return c>0&&(i=c),await r.put(n,e.body,{expirationTtl:i}),new Response("",{status:201})}).delete("/kv/:namespace/:key",async(e,t)=>{let{namespace:r,key:n,errorRes:o}=await ie(e,t);return o||(await r.delete(n),new Response("",{status:204}))}).post("/queues/:queue",async(e,t)=>{let{queue:r,errorRes:n}=await dt(e,t);if(n)return n;let o=await e.text();return await r.send(o),new Response("",{status:201})}).all("*",()=>new Response("Not found",{status:404}));async function ie(e,t){if(!e?.text||!e.params?.namespace||!e.params?.key)return{errorRes:new Response("Bad request",{status:400})};let r=t[e.params.namespace];return typeof r!="object"||!["KVNamespace","KvNamespace"].includes(r?.constructor?.name)?{errorRes:new Response(`Worker is not bound to KV '${e.params.kv}'`,{status:412})}:await Y(e,t)?{namespace:r,key:e.params.key}:{errorRes:new Response("Unauthorized",{status:401})}}async function dt(e,t){if(!e?.text||!e.params?.queue)return{errorRes:new Response("Bad request",{status:400})};let r=t[e.params.queue];return typeof r!="object"||!["WorkerQueue","Queue"].includes(r?.constructor?.name)?{errorRes:new Response(`Worker is not bound to queue '${e.params.queue}'`,{status:412})}:await Y(e,t)?{queue:r}:{errorRes:new Response("Unauthorized",{status:401})}}var Tc={fetch:ct.handle};export{Tc as default}; //# sourceMappingURL=worker.js.map diff --git a/common/component/cloudflare/workers/code/worker.js.map b/common/component/cloudflare/workers/code/worker.js.map index e517e7314a..d5c9ca00eb 100644 --- a/common/component/cloudflare/workers/code/worker.js.map +++ b/common/component/cloudflare/workers/code/worker.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../../worker-src/node_modules/itty-router/dist/itty-router.mjs", "../../worker-src/node_modules/jose/dist/browser/runtime/webcrypto.js", "../../worker-src/node_modules/jose/dist/browser/lib/buffer_utils.js", "../../worker-src/node_modules/jose/dist/browser/runtime/base64url.js", "../../worker-src/node_modules/jose/dist/browser/util/errors.js", "../../worker-src/node_modules/jose/dist/browser/runtime/random.js", "../../worker-src/node_modules/jose/dist/browser/lib/crypto_key.js", "../../worker-src/node_modules/jose/dist/browser/lib/invalid_key_input.js", "../../worker-src/node_modules/jose/dist/browser/runtime/is_key_like.js", "../../worker-src/node_modules/jose/dist/browser/lib/is_disjoint.js", "../../worker-src/node_modules/jose/dist/browser/lib/is_object.js", "../../worker-src/node_modules/jose/dist/browser/runtime/check_key_length.js", "../../worker-src/node_modules/jose/dist/browser/runtime/asn1.js", "../../worker-src/node_modules/jose/dist/browser/key/import.js", "../../worker-src/node_modules/jose/dist/browser/lib/check_key_type.js", "../../worker-src/node_modules/jose/dist/browser/lib/validate_crit.js", "../../worker-src/node_modules/jose/dist/browser/lib/validate_algorithms.js", "../../worker-src/node_modules/jose/dist/browser/jwe/flattened/encrypt.js", "../../worker-src/node_modules/jose/dist/browser/runtime/subtle_dsa.js", "../../worker-src/node_modules/jose/dist/browser/runtime/get_sign_verify_key.js", "../../worker-src/node_modules/jose/dist/browser/runtime/verify.js", "../../worker-src/node_modules/jose/dist/browser/jws/flattened/verify.js", "../../worker-src/node_modules/jose/dist/browser/jws/compact/verify.js", "../../worker-src/node_modules/jose/dist/browser/lib/epoch.js", "../../worker-src/node_modules/jose/dist/browser/lib/secs.js", "../../worker-src/node_modules/jose/dist/browser/lib/jwt_claims_set.js", "../../worker-src/node_modules/jose/dist/browser/jwt/verify.js", "../../worker-src/lib/jwt-auth.ts", "../../worker-src/package.json", "../../worker-src/worker.ts"], - "sourcesContent": ["const e=({base:e=\"\",routes:r=[]}={})=>({__proto__:new Proxy({},{get:(a,o,t)=>(a,...p)=>r.push([o.toUpperCase(),RegExp(`^${(e+a).replace(/(\\/?)\\*/g,\"($1.*)?\").replace(/(\\/$)|((?<=\\/)\\/)/,\"\").replace(/(:(\\w+)\\+)/,\"(?<$2>.*)\").replace(/:(\\w+)(\\?)?(\\.)?/g,\"$2(?<$1>[^/]+)$2$3\").replace(/\\.(?=[\\w(])/,\"\\\\.\").replace(/\\)\\.\\?\\(([^\\[]+)\\[\\^/g,\"?)\\\\.?($1(?<=\\\\.)[^\\\\.\")}/*$`),p])&&t}),routes:r,async handle(e,...a){let o,t,p=new URL(e.url),l=e.query={};for(let[e,r]of p.searchParams)l[e]=void 0===l[e]?r:[l[e],r].flat();for(let[l,s,c]of r)if((l===e.method||\"ALL\"===l)&&(t=p.pathname.match(s))){e.params=t.groups||{};for(let r of c)if(void 0!==(o=await r(e.proxy||e,...a)))return o}}});export{e as Router};\n", "export default crypto;\nexport const isCryptoKey = (key) => key instanceof CryptoKey;\n", "import digest from '../runtime/digest.js';\nexport const encoder = new TextEncoder();\nexport const decoder = new TextDecoder();\nconst MAX_INT32 = 2 ** 32;\nexport function concat(...buffers) {\n const size = buffers.reduce((acc, { length }) => acc + length, 0);\n const buf = new Uint8Array(size);\n let i = 0;\n buffers.forEach((buffer) => {\n buf.set(buffer, i);\n i += buffer.length;\n });\n return buf;\n}\nexport function p2s(alg, p2sInput) {\n return concat(encoder.encode(alg), new Uint8Array([0]), p2sInput);\n}\nfunction writeUInt32BE(buf, value, offset) {\n if (value < 0 || value >= MAX_INT32) {\n throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);\n }\n buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);\n}\nexport function uint64be(value) {\n const high = Math.floor(value / MAX_INT32);\n const low = value % MAX_INT32;\n const buf = new Uint8Array(8);\n writeUInt32BE(buf, high, 0);\n writeUInt32BE(buf, low, 4);\n return buf;\n}\nexport function uint32be(value) {\n const buf = new Uint8Array(4);\n writeUInt32BE(buf, value);\n return buf;\n}\nexport function lengthAndInput(input) {\n return concat(uint32be(input.length), input);\n}\nexport async function concatKdf(secret, bits, value) {\n const iterations = Math.ceil((bits >> 3) / 32);\n const res = new Uint8Array(iterations * 32);\n for (let iter = 0; iter < iterations; iter++) {\n const buf = new Uint8Array(4 + secret.length + value.length);\n buf.set(uint32be(iter + 1));\n buf.set(secret, 4);\n buf.set(value, 4 + secret.length);\n res.set(await digest('sha256', buf), iter * 32);\n }\n return res.slice(0, bits >> 3);\n}\n", "import { encoder, decoder } from '../lib/buffer_utils.js';\nexport const encodeBase64 = (input) => {\n let unencoded = input;\n if (typeof unencoded === 'string') {\n unencoded = encoder.encode(unencoded);\n }\n const CHUNK_SIZE = 0x8000;\n const arr = [];\n for (let i = 0; i < unencoded.length; i += CHUNK_SIZE) {\n arr.push(String.fromCharCode.apply(null, unencoded.subarray(i, i + CHUNK_SIZE)));\n }\n return btoa(arr.join(''));\n};\nexport const encode = (input) => {\n return encodeBase64(input).replace(/=/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');\n};\nexport const decodeBase64 = (encoded) => {\n const binary = atob(encoded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n};\nexport const decode = (input) => {\n let encoded = input;\n if (encoded instanceof Uint8Array) {\n encoded = decoder.decode(encoded);\n }\n encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/\\s/g, '');\n try {\n return decodeBase64(encoded);\n }\n catch (_a) {\n throw new TypeError('The input to be decoded is not correctly encoded.');\n }\n};\n", "export class JOSEError extends Error {\n static get code() {\n return 'ERR_JOSE_GENERIC';\n }\n constructor(message) {\n var _a;\n super(message);\n this.code = 'ERR_JOSE_GENERIC';\n this.name = this.constructor.name;\n (_a = Error.captureStackTrace) === null || _a === void 0 ? void 0 : _a.call(Error, this, this.constructor);\n }\n}\nexport class JWTClaimValidationFailed extends JOSEError {\n static get code() {\n return 'ERR_JWT_CLAIM_VALIDATION_FAILED';\n }\n constructor(message, claim = 'unspecified', reason = 'unspecified') {\n super(message);\n this.code = 'ERR_JWT_CLAIM_VALIDATION_FAILED';\n this.claim = claim;\n this.reason = reason;\n }\n}\nexport class JWTExpired extends JOSEError {\n static get code() {\n return 'ERR_JWT_EXPIRED';\n }\n constructor(message, claim = 'unspecified', reason = 'unspecified') {\n super(message);\n this.code = 'ERR_JWT_EXPIRED';\n this.claim = claim;\n this.reason = reason;\n }\n}\nexport class JOSEAlgNotAllowed extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JOSE_ALG_NOT_ALLOWED';\n }\n static get code() {\n return 'ERR_JOSE_ALG_NOT_ALLOWED';\n }\n}\nexport class JOSENotSupported extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JOSE_NOT_SUPPORTED';\n }\n static get code() {\n return 'ERR_JOSE_NOT_SUPPORTED';\n }\n}\nexport class JWEDecryptionFailed extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWE_DECRYPTION_FAILED';\n this.message = 'decryption operation failed';\n }\n static get code() {\n return 'ERR_JWE_DECRYPTION_FAILED';\n }\n}\nexport class JWEInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWE_INVALID';\n }\n static get code() {\n return 'ERR_JWE_INVALID';\n }\n}\nexport class JWSInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWS_INVALID';\n }\n static get code() {\n return 'ERR_JWS_INVALID';\n }\n}\nexport class JWTInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWT_INVALID';\n }\n static get code() {\n return 'ERR_JWT_INVALID';\n }\n}\nexport class JWKInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWK_INVALID';\n }\n static get code() {\n return 'ERR_JWK_INVALID';\n }\n}\nexport class JWKSInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWKS_INVALID';\n }\n static get code() {\n return 'ERR_JWKS_INVALID';\n }\n}\nexport class JWKSNoMatchingKey extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWKS_NO_MATCHING_KEY';\n this.message = 'no applicable key found in the JSON Web Key Set';\n }\n static get code() {\n return 'ERR_JWKS_NO_MATCHING_KEY';\n }\n}\nexport class JWKSMultipleMatchingKeys extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';\n this.message = 'multiple matching keys found in the JSON Web Key Set';\n }\n static get code() {\n return 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';\n }\n}\nSymbol.asyncIterator;\nexport class JWKSTimeout extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWKS_TIMEOUT';\n this.message = 'request timed out';\n }\n static get code() {\n return 'ERR_JWKS_TIMEOUT';\n }\n}\nexport class JWSSignatureVerificationFailed extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';\n this.message = 'signature verification failed';\n }\n static get code() {\n return 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';\n }\n}\n", "import crypto from './webcrypto.js';\nexport default crypto.getRandomValues.bind(crypto);\n", "function unusable(name, prop = 'algorithm.name') {\n return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);\n}\nfunction isAlgorithm(algorithm, name) {\n return algorithm.name === name;\n}\nfunction getHashLength(hash) {\n return parseInt(hash.name.slice(4), 10);\n}\nfunction getNamedCurve(alg) {\n switch (alg) {\n case 'ES256':\n return 'P-256';\n case 'ES384':\n return 'P-384';\n case 'ES512':\n return 'P-521';\n default:\n throw new Error('unreachable');\n }\n}\nfunction checkUsage(key, usages) {\n if (usages.length && !usages.some((expected) => key.usages.includes(expected))) {\n let msg = 'CryptoKey does not support this operation, its usages must include ';\n if (usages.length > 2) {\n const last = usages.pop();\n msg += `one of ${usages.join(', ')}, or ${last}.`;\n }\n else if (usages.length === 2) {\n msg += `one of ${usages[0]} or ${usages[1]}.`;\n }\n else {\n msg += `${usages[0]}.`;\n }\n throw new TypeError(msg);\n }\n}\nexport function checkSigCryptoKey(key, alg, ...usages) {\n switch (alg) {\n case 'HS256':\n case 'HS384':\n case 'HS512': {\n if (!isAlgorithm(key.algorithm, 'HMAC'))\n throw unusable('HMAC');\n const expected = parseInt(alg.slice(2), 10);\n const actual = getHashLength(key.algorithm.hash);\n if (actual !== expected)\n throw unusable(`SHA-${expected}`, 'algorithm.hash');\n break;\n }\n case 'RS256':\n case 'RS384':\n case 'RS512': {\n if (!isAlgorithm(key.algorithm, 'RSASSA-PKCS1-v1_5'))\n throw unusable('RSASSA-PKCS1-v1_5');\n const expected = parseInt(alg.slice(2), 10);\n const actual = getHashLength(key.algorithm.hash);\n if (actual !== expected)\n throw unusable(`SHA-${expected}`, 'algorithm.hash');\n break;\n }\n case 'PS256':\n case 'PS384':\n case 'PS512': {\n if (!isAlgorithm(key.algorithm, 'RSA-PSS'))\n throw unusable('RSA-PSS');\n const expected = parseInt(alg.slice(2), 10);\n const actual = getHashLength(key.algorithm.hash);\n if (actual !== expected)\n throw unusable(`SHA-${expected}`, 'algorithm.hash');\n break;\n }\n case 'EdDSA': {\n if (key.algorithm.name !== 'Ed25519' && key.algorithm.name !== 'Ed448') {\n throw unusable('Ed25519 or Ed448');\n }\n break;\n }\n case 'ES256':\n case 'ES384':\n case 'ES512': {\n if (!isAlgorithm(key.algorithm, 'ECDSA'))\n throw unusable('ECDSA');\n const expected = getNamedCurve(alg);\n const actual = key.algorithm.namedCurve;\n if (actual !== expected)\n throw unusable(expected, 'algorithm.namedCurve');\n break;\n }\n default:\n throw new TypeError('CryptoKey does not support this operation');\n }\n checkUsage(key, usages);\n}\nexport function checkEncCryptoKey(key, alg, ...usages) {\n switch (alg) {\n case 'A128GCM':\n case 'A192GCM':\n case 'A256GCM': {\n if (!isAlgorithm(key.algorithm, 'AES-GCM'))\n throw unusable('AES-GCM');\n const expected = parseInt(alg.slice(1, 4), 10);\n const actual = key.algorithm.length;\n if (actual !== expected)\n throw unusable(expected, 'algorithm.length');\n break;\n }\n case 'A128KW':\n case 'A192KW':\n case 'A256KW': {\n if (!isAlgorithm(key.algorithm, 'AES-KW'))\n throw unusable('AES-KW');\n const expected = parseInt(alg.slice(1, 4), 10);\n const actual = key.algorithm.length;\n if (actual !== expected)\n throw unusable(expected, 'algorithm.length');\n break;\n }\n case 'ECDH': {\n switch (key.algorithm.name) {\n case 'ECDH':\n case 'X25519':\n case 'X448':\n break;\n default:\n throw unusable('ECDH, X25519, or X448');\n }\n break;\n }\n case 'PBES2-HS256+A128KW':\n case 'PBES2-HS384+A192KW':\n case 'PBES2-HS512+A256KW':\n if (!isAlgorithm(key.algorithm, 'PBKDF2'))\n throw unusable('PBKDF2');\n break;\n case 'RSA-OAEP':\n case 'RSA-OAEP-256':\n case 'RSA-OAEP-384':\n case 'RSA-OAEP-512': {\n if (!isAlgorithm(key.algorithm, 'RSA-OAEP'))\n throw unusable('RSA-OAEP');\n const expected = parseInt(alg.slice(9), 10) || 1;\n const actual = getHashLength(key.algorithm.hash);\n if (actual !== expected)\n throw unusable(`SHA-${expected}`, 'algorithm.hash');\n break;\n }\n default:\n throw new TypeError('CryptoKey does not support this operation');\n }\n checkUsage(key, usages);\n}\n", "function message(msg, actual, ...types) {\n if (types.length > 2) {\n const last = types.pop();\n msg += `one of type ${types.join(', ')}, or ${last}.`;\n }\n else if (types.length === 2) {\n msg += `one of type ${types[0]} or ${types[1]}.`;\n }\n else {\n msg += `of type ${types[0]}.`;\n }\n if (actual == null) {\n msg += ` Received ${actual}`;\n }\n else if (typeof actual === 'function' && actual.name) {\n msg += ` Received function ${actual.name}`;\n }\n else if (typeof actual === 'object' && actual != null) {\n if (actual.constructor && actual.constructor.name) {\n msg += ` Received an instance of ${actual.constructor.name}`;\n }\n }\n return msg;\n}\nexport default (actual, ...types) => {\n return message('Key must be ', actual, ...types);\n};\nexport function withAlg(alg, actual, ...types) {\n return message(`Key for the ${alg} algorithm must be `, actual, ...types);\n}\n", "import { isCryptoKey } from './webcrypto.js';\nexport default (key) => {\n return isCryptoKey(key);\n};\nexport const types = ['CryptoKey'];\n", "const isDisjoint = (...headers) => {\n const sources = headers.filter(Boolean);\n if (sources.length === 0 || sources.length === 1) {\n return true;\n }\n let acc;\n for (const header of sources) {\n const parameters = Object.keys(header);\n if (!acc || acc.size === 0) {\n acc = new Set(parameters);\n continue;\n }\n for (const parameter of parameters) {\n if (acc.has(parameter)) {\n return false;\n }\n acc.add(parameter);\n }\n }\n return true;\n};\nexport default isDisjoint;\n", "function isObjectLike(value) {\n return typeof value === 'object' && value !== null;\n}\nexport default function isObject(input) {\n if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') {\n return false;\n }\n if (Object.getPrototypeOf(input) === null) {\n return true;\n }\n let proto = input;\n while (Object.getPrototypeOf(proto) !== null) {\n proto = Object.getPrototypeOf(proto);\n }\n return Object.getPrototypeOf(input) === proto;\n}\n", "export default (alg, key) => {\n if (alg.startsWith('RS') || alg.startsWith('PS')) {\n const { modulusLength } = key.algorithm;\n if (typeof modulusLength !== 'number' || modulusLength < 2048) {\n throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);\n }\n }\n};\n", "import crypto, { isCryptoKey } from './webcrypto.js';\nimport invalidKeyInput from '../lib/invalid_key_input.js';\nimport { encodeBase64, decodeBase64 } from './base64url.js';\nimport formatPEM from '../lib/format_pem.js';\nimport { JOSENotSupported } from '../util/errors.js';\nimport { types } from './is_key_like.js';\nconst genericExport = async (keyType, keyFormat, key) => {\n if (!isCryptoKey(key)) {\n throw new TypeError(invalidKeyInput(key, ...types));\n }\n if (!key.extractable) {\n throw new TypeError('CryptoKey is not extractable');\n }\n if (key.type !== keyType) {\n throw new TypeError(`key is not a ${keyType} key`);\n }\n return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`);\n};\nexport const toSPKI = (key) => {\n return genericExport('public', 'spki', key);\n};\nexport const toPKCS8 = (key) => {\n return genericExport('private', 'pkcs8', key);\n};\nconst findOid = (keyData, oid, from = 0) => {\n if (from === 0) {\n oid.unshift(oid.length);\n oid.unshift(0x06);\n }\n let i = keyData.indexOf(oid[0], from);\n if (i === -1)\n return false;\n const sub = keyData.subarray(i, i + oid.length);\n if (sub.length !== oid.length)\n return false;\n return sub.every((value, index) => value === oid[index]) || findOid(keyData, oid, i + 1);\n};\nconst getNamedCurve = (keyData) => {\n switch (true) {\n case findOid(keyData, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]):\n return 'P-256';\n case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x22]):\n return 'P-384';\n case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]):\n return 'P-521';\n case findOid(keyData, [0x2b, 0x65, 0x6e]):\n return 'X25519';\n case findOid(keyData, [0x2b, 0x65, 0x6f]):\n return 'X448';\n case findOid(keyData, [0x2b, 0x65, 0x70]):\n return 'Ed25519';\n case findOid(keyData, [0x2b, 0x65, 0x71]):\n return 'Ed448';\n default:\n throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type');\n }\n};\nconst genericImport = async (replace, keyFormat, pem, alg, options) => {\n var _a;\n let algorithm;\n let keyUsages;\n const keyData = new Uint8Array(atob(pem.replace(replace, ''))\n .split('')\n .map((c) => c.charCodeAt(0)));\n const isPublic = keyFormat === 'spki';\n switch (alg) {\n case 'PS256':\n case 'PS384':\n case 'PS512':\n algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'RS256':\n case 'RS384':\n case 'RS512':\n algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'RSA-OAEP':\n case 'RSA-OAEP-256':\n case 'RSA-OAEP-384':\n case 'RSA-OAEP-512':\n algorithm = {\n name: 'RSA-OAEP',\n hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`,\n };\n keyUsages = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];\n break;\n case 'ES256':\n algorithm = { name: 'ECDSA', namedCurve: 'P-256' };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'ES384':\n algorithm = { name: 'ECDSA', namedCurve: 'P-384' };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'ES512':\n algorithm = { name: 'ECDSA', namedCurve: 'P-521' };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'ECDH-ES':\n case 'ECDH-ES+A128KW':\n case 'ECDH-ES+A192KW':\n case 'ECDH-ES+A256KW': {\n const namedCurve = getNamedCurve(keyData);\n algorithm = namedCurve.startsWith('P-') ? { name: 'ECDH', namedCurve } : { name: namedCurve };\n keyUsages = isPublic ? [] : ['deriveBits'];\n break;\n }\n case 'EdDSA':\n algorithm = { name: getNamedCurve(keyData) };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n default:\n throw new JOSENotSupported('Invalid or unsupported \"alg\" (Algorithm) value');\n }\n return crypto.subtle.importKey(keyFormat, keyData, algorithm, (_a = options === null || options === void 0 ? void 0 : options.extractable) !== null && _a !== void 0 ? _a : false, keyUsages);\n};\nexport const fromPKCS8 = (pem, alg, options) => {\n return genericImport(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\\s)/g, 'pkcs8', pem, alg, options);\n};\nexport const fromSPKI = (pem, alg, options) => {\n return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\\s)/g, 'spki', pem, alg, options);\n};\nfunction getElement(seq) {\n let result = [];\n let next = 0;\n while (next < seq.length) {\n let nextPart = parseElement(seq.subarray(next));\n result.push(nextPart);\n next += nextPart.byteLength;\n }\n return result;\n}\nfunction parseElement(bytes) {\n let position = 0;\n let tag = bytes[0] & 0x1f;\n position++;\n if (tag === 0x1f) {\n tag = 0;\n while (bytes[position] >= 0x80) {\n tag = tag * 128 + bytes[position] - 0x80;\n position++;\n }\n tag = tag * 128 + bytes[position] - 0x80;\n position++;\n }\n let length = 0;\n if (bytes[position] < 0x80) {\n length = bytes[position];\n position++;\n }\n else if (length === 0x80) {\n length = 0;\n while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) {\n if (length > bytes.byteLength) {\n throw new TypeError('invalid indefinite form length');\n }\n length++;\n }\n const byteLength = position + length + 2;\n return {\n byteLength,\n contents: bytes.subarray(position, position + length),\n raw: bytes.subarray(0, byteLength),\n };\n }\n else {\n let numberOfDigits = bytes[position] & 0x7f;\n position++;\n length = 0;\n for (let i = 0; i < numberOfDigits; i++) {\n length = length * 256 + bytes[position];\n position++;\n }\n }\n const byteLength = position + length;\n return {\n byteLength,\n contents: bytes.subarray(position, byteLength),\n raw: bytes.subarray(0, byteLength),\n };\n}\nfunction spkiFromX509(buf) {\n const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents);\n return encodeBase64(tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw);\n}\nfunction getSPKI(x509) {\n const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\\s)/g, '');\n const raw = decodeBase64(pem);\n return formatPEM(spkiFromX509(raw), 'PUBLIC KEY');\n}\nexport const fromX509 = (pem, alg, options) => {\n let spki;\n try {\n spki = getSPKI(pem);\n }\n catch (cause) {\n throw new TypeError('failed to parse the X.509 certificate', { cause });\n }\n return fromSPKI(spki, alg, options);\n};\n", "import { decode as decodeBase64URL } from '../runtime/base64url.js';\nimport { fromSPKI, fromPKCS8, fromX509 } from '../runtime/asn1.js';\nimport asKeyObject from '../runtime/jwk_to_key.js';\nimport { JOSENotSupported } from '../util/errors.js';\nimport isObject from '../lib/is_object.js';\nexport async function importSPKI(spki, alg, options) {\n if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) {\n throw new TypeError('\"spki\" must be SPKI formatted string');\n }\n return fromSPKI(spki, alg, options);\n}\nexport async function importX509(x509, alg, options) {\n if (typeof x509 !== 'string' || x509.indexOf('-----BEGIN CERTIFICATE-----') !== 0) {\n throw new TypeError('\"x509\" must be X.509 formatted string');\n }\n return fromX509(x509, alg, options);\n}\nexport async function importPKCS8(pkcs8, alg, options) {\n if (typeof pkcs8 !== 'string' || pkcs8.indexOf('-----BEGIN PRIVATE KEY-----') !== 0) {\n throw new TypeError('\"pkcs8\" must be PKCS#8 formatted string');\n }\n return fromPKCS8(pkcs8, alg, options);\n}\nexport async function importJWK(jwk, alg, octAsKeyObject) {\n var _a;\n if (!isObject(jwk)) {\n throw new TypeError('JWK must be an object');\n }\n alg || (alg = jwk.alg);\n switch (jwk.kty) {\n case 'oct':\n if (typeof jwk.k !== 'string' || !jwk.k) {\n throw new TypeError('missing \"k\" (Key Value) Parameter value');\n }\n octAsKeyObject !== null && octAsKeyObject !== void 0 ? octAsKeyObject : (octAsKeyObject = jwk.ext !== true);\n if (octAsKeyObject) {\n return asKeyObject({ ...jwk, alg, ext: (_a = jwk.ext) !== null && _a !== void 0 ? _a : false });\n }\n return decodeBase64URL(jwk.k);\n case 'RSA':\n if (jwk.oth !== undefined) {\n throw new JOSENotSupported('RSA JWK \"oth\" (Other Primes Info) Parameter value is not supported');\n }\n case 'EC':\n case 'OKP':\n return asKeyObject({ ...jwk, alg });\n default:\n throw new JOSENotSupported('Unsupported \"kty\" (Key Type) Parameter value');\n }\n}\n", "import { withAlg as invalidKeyInput } from './invalid_key_input.js';\nimport isKeyLike, { types } from '../runtime/is_key_like.js';\nconst symmetricTypeCheck = (alg, key) => {\n if (key instanceof Uint8Array)\n return;\n if (!isKeyLike(key)) {\n throw new TypeError(invalidKeyInput(alg, key, ...types, 'Uint8Array'));\n }\n if (key.type !== 'secret') {\n throw new TypeError(`${types.join(' or ')} instances for symmetric algorithms must be of type \"secret\"`);\n }\n};\nconst asymmetricTypeCheck = (alg, key, usage) => {\n if (!isKeyLike(key)) {\n throw new TypeError(invalidKeyInput(alg, key, ...types));\n }\n if (key.type === 'secret') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithms must not be of type \"secret\"`);\n }\n if (usage === 'sign' && key.type === 'public') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm signing must be of type \"private\"`);\n }\n if (usage === 'decrypt' && key.type === 'public') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm decryption must be of type \"private\"`);\n }\n if (key.algorithm && usage === 'verify' && key.type === 'private') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm verifying must be of type \"public\"`);\n }\n if (key.algorithm && usage === 'encrypt' && key.type === 'private') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm encryption must be of type \"public\"`);\n }\n};\nconst checkKeyType = (alg, key, usage) => {\n const symmetric = alg.startsWith('HS') ||\n alg === 'dir' ||\n alg.startsWith('PBES2') ||\n /^A\\d{3}(?:GCM)?KW$/.test(alg);\n if (symmetric) {\n symmetricTypeCheck(alg, key);\n }\n else {\n asymmetricTypeCheck(alg, key, usage);\n }\n};\nexport default checkKeyType;\n", "import { JOSENotSupported } from '../util/errors.js';\nfunction validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {\n if (joseHeader.crit !== undefined && protectedHeader.crit === undefined) {\n throw new Err('\"crit\" (Critical) Header Parameter MUST be integrity protected');\n }\n if (!protectedHeader || protectedHeader.crit === undefined) {\n return new Set();\n }\n if (!Array.isArray(protectedHeader.crit) ||\n protectedHeader.crit.length === 0 ||\n protectedHeader.crit.some((input) => typeof input !== 'string' || input.length === 0)) {\n throw new Err('\"crit\" (Critical) Header Parameter MUST be an array of non-empty strings when present');\n }\n let recognized;\n if (recognizedOption !== undefined) {\n recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]);\n }\n else {\n recognized = recognizedDefault;\n }\n for (const parameter of protectedHeader.crit) {\n if (!recognized.has(parameter)) {\n throw new JOSENotSupported(`Extension Header Parameter \"${parameter}\" is not recognized`);\n }\n if (joseHeader[parameter] === undefined) {\n throw new Err(`Extension Header Parameter \"${parameter}\" is missing`);\n }\n else if (recognized.get(parameter) && protectedHeader[parameter] === undefined) {\n throw new Err(`Extension Header Parameter \"${parameter}\" MUST be integrity protected`);\n }\n }\n return new Set(protectedHeader.crit);\n}\nexport default validateCrit;\n", "const validateAlgorithms = (option, algorithms) => {\n if (algorithms !== undefined &&\n (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== 'string'))) {\n throw new TypeError(`\"${option}\" option must be an array of strings`);\n }\n if (!algorithms) {\n return undefined;\n }\n return new Set(algorithms);\n};\nexport default validateAlgorithms;\n", "import { encode as base64url } from '../../runtime/base64url.js';\nimport encrypt from '../../runtime/encrypt.js';\nimport { deflate } from '../../runtime/zlib.js';\nimport generateIv from '../../lib/iv.js';\nimport encryptKeyManagement from '../../lib/encrypt_key_management.js';\nimport { JOSENotSupported, JWEInvalid } from '../../util/errors.js';\nimport isDisjoint from '../../lib/is_disjoint.js';\nimport { encoder, decoder, concat } from '../../lib/buffer_utils.js';\nimport validateCrit from '../../lib/validate_crit.js';\nexport const unprotected = Symbol();\nexport class FlattenedEncrypt {\n constructor(plaintext) {\n if (!(plaintext instanceof Uint8Array)) {\n throw new TypeError('plaintext must be an instance of Uint8Array');\n }\n this._plaintext = plaintext;\n }\n setKeyManagementParameters(parameters) {\n if (this._keyManagementParameters) {\n throw new TypeError('setKeyManagementParameters can only be called once');\n }\n this._keyManagementParameters = parameters;\n return this;\n }\n setProtectedHeader(protectedHeader) {\n if (this._protectedHeader) {\n throw new TypeError('setProtectedHeader can only be called once');\n }\n this._protectedHeader = protectedHeader;\n return this;\n }\n setSharedUnprotectedHeader(sharedUnprotectedHeader) {\n if (this._sharedUnprotectedHeader) {\n throw new TypeError('setSharedUnprotectedHeader can only be called once');\n }\n this._sharedUnprotectedHeader = sharedUnprotectedHeader;\n return this;\n }\n setUnprotectedHeader(unprotectedHeader) {\n if (this._unprotectedHeader) {\n throw new TypeError('setUnprotectedHeader can only be called once');\n }\n this._unprotectedHeader = unprotectedHeader;\n return this;\n }\n setAdditionalAuthenticatedData(aad) {\n this._aad = aad;\n return this;\n }\n setContentEncryptionKey(cek) {\n if (this._cek) {\n throw new TypeError('setContentEncryptionKey can only be called once');\n }\n this._cek = cek;\n return this;\n }\n setInitializationVector(iv) {\n if (this._iv) {\n throw new TypeError('setInitializationVector can only be called once');\n }\n this._iv = iv;\n return this;\n }\n async encrypt(key, options) {\n if (!this._protectedHeader && !this._unprotectedHeader && !this._sharedUnprotectedHeader) {\n throw new JWEInvalid('either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()');\n }\n if (!isDisjoint(this._protectedHeader, this._unprotectedHeader, this._sharedUnprotectedHeader)) {\n throw new JWEInvalid('JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint');\n }\n const joseHeader = {\n ...this._protectedHeader,\n ...this._unprotectedHeader,\n ...this._sharedUnprotectedHeader,\n };\n validateCrit(JWEInvalid, new Map(), options === null || options === void 0 ? void 0 : options.crit, this._protectedHeader, joseHeader);\n if (joseHeader.zip !== undefined) {\n if (!this._protectedHeader || !this._protectedHeader.zip) {\n throw new JWEInvalid('JWE \"zip\" (Compression Algorithm) Header MUST be integrity protected');\n }\n if (joseHeader.zip !== 'DEF') {\n throw new JOSENotSupported('Unsupported JWE \"zip\" (Compression Algorithm) Header Parameter value');\n }\n }\n const { alg, enc } = joseHeader;\n if (typeof alg !== 'string' || !alg) {\n throw new JWEInvalid('JWE \"alg\" (Algorithm) Header Parameter missing or invalid');\n }\n if (typeof enc !== 'string' || !enc) {\n throw new JWEInvalid('JWE \"enc\" (Encryption Algorithm) Header Parameter missing or invalid');\n }\n let encryptedKey;\n if (alg === 'dir') {\n if (this._cek) {\n throw new TypeError('setContentEncryptionKey cannot be called when using Direct Encryption');\n }\n }\n else if (alg === 'ECDH-ES') {\n if (this._cek) {\n throw new TypeError('setContentEncryptionKey cannot be called when using Direct Key Agreement');\n }\n }\n let cek;\n {\n let parameters;\n ({ cek, encryptedKey, parameters } = await encryptKeyManagement(alg, enc, key, this._cek, this._keyManagementParameters));\n if (parameters) {\n if (options && unprotected in options) {\n if (!this._unprotectedHeader) {\n this.setUnprotectedHeader(parameters);\n }\n else {\n this._unprotectedHeader = { ...this._unprotectedHeader, ...parameters };\n }\n }\n else {\n if (!this._protectedHeader) {\n this.setProtectedHeader(parameters);\n }\n else {\n this._protectedHeader = { ...this._protectedHeader, ...parameters };\n }\n }\n }\n }\n this._iv || (this._iv = generateIv(enc));\n let additionalData;\n let protectedHeader;\n let aadMember;\n if (this._protectedHeader) {\n protectedHeader = encoder.encode(base64url(JSON.stringify(this._protectedHeader)));\n }\n else {\n protectedHeader = encoder.encode('');\n }\n if (this._aad) {\n aadMember = base64url(this._aad);\n additionalData = concat(protectedHeader, encoder.encode('.'), encoder.encode(aadMember));\n }\n else {\n additionalData = protectedHeader;\n }\n let ciphertext;\n let tag;\n if (joseHeader.zip === 'DEF') {\n const deflated = await ((options === null || options === void 0 ? void 0 : options.deflateRaw) || deflate)(this._plaintext);\n ({ ciphertext, tag } = await encrypt(enc, deflated, cek, this._iv, additionalData));\n }\n else {\n ;\n ({ ciphertext, tag } = await encrypt(enc, this._plaintext, cek, this._iv, additionalData));\n }\n const jwe = {\n ciphertext: base64url(ciphertext),\n iv: base64url(this._iv),\n tag: base64url(tag),\n };\n if (encryptedKey) {\n jwe.encrypted_key = base64url(encryptedKey);\n }\n if (aadMember) {\n jwe.aad = aadMember;\n }\n if (this._protectedHeader) {\n jwe.protected = decoder.decode(protectedHeader);\n }\n if (this._sharedUnprotectedHeader) {\n jwe.unprotected = this._sharedUnprotectedHeader;\n }\n if (this._unprotectedHeader) {\n jwe.header = this._unprotectedHeader;\n }\n return jwe;\n }\n}\n", "import { JOSENotSupported } from '../util/errors.js';\nexport default function subtleDsa(alg, algorithm) {\n const hash = `SHA-${alg.slice(-3)}`;\n switch (alg) {\n case 'HS256':\n case 'HS384':\n case 'HS512':\n return { hash, name: 'HMAC' };\n case 'PS256':\n case 'PS384':\n case 'PS512':\n return { hash, name: 'RSA-PSS', saltLength: alg.slice(-3) >> 3 };\n case 'RS256':\n case 'RS384':\n case 'RS512':\n return { hash, name: 'RSASSA-PKCS1-v1_5' };\n case 'ES256':\n case 'ES384':\n case 'ES512':\n return { hash, name: 'ECDSA', namedCurve: algorithm.namedCurve };\n case 'EdDSA':\n return { name: algorithm.name };\n default:\n throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);\n }\n}\n", "import crypto, { isCryptoKey } from './webcrypto.js';\nimport { checkSigCryptoKey } from '../lib/crypto_key.js';\nimport invalidKeyInput from '../lib/invalid_key_input.js';\nimport { types } from './is_key_like.js';\nexport default function getCryptoKey(alg, key, usage) {\n if (isCryptoKey(key)) {\n checkSigCryptoKey(key, alg, usage);\n return key;\n }\n if (key instanceof Uint8Array) {\n if (!alg.startsWith('HS')) {\n throw new TypeError(invalidKeyInput(key, ...types));\n }\n return crypto.subtle.importKey('raw', key, { hash: `SHA-${alg.slice(-3)}`, name: 'HMAC' }, false, [usage]);\n }\n throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array'));\n}\n", "import subtleAlgorithm from './subtle_dsa.js';\nimport crypto from './webcrypto.js';\nimport checkKeyLength from './check_key_length.js';\nimport getVerifyKey from './get_sign_verify_key.js';\nconst verify = async (alg, key, signature, data) => {\n const cryptoKey = await getVerifyKey(alg, key, 'verify');\n checkKeyLength(alg, cryptoKey);\n const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);\n try {\n return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);\n }\n catch (_a) {\n return false;\n }\n};\nexport default verify;\n", "import { decode as base64url } from '../../runtime/base64url.js';\nimport verify from '../../runtime/verify.js';\nimport { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from '../../util/errors.js';\nimport { concat, encoder, decoder } from '../../lib/buffer_utils.js';\nimport isDisjoint from '../../lib/is_disjoint.js';\nimport isObject from '../../lib/is_object.js';\nimport checkKeyType from '../../lib/check_key_type.js';\nimport validateCrit from '../../lib/validate_crit.js';\nimport validateAlgorithms from '../../lib/validate_algorithms.js';\nexport async function flattenedVerify(jws, key, options) {\n var _a;\n if (!isObject(jws)) {\n throw new JWSInvalid('Flattened JWS must be an object');\n }\n if (jws.protected === undefined && jws.header === undefined) {\n throw new JWSInvalid('Flattened JWS must have either of the \"protected\" or \"header\" members');\n }\n if (jws.protected !== undefined && typeof jws.protected !== 'string') {\n throw new JWSInvalid('JWS Protected Header incorrect type');\n }\n if (jws.payload === undefined) {\n throw new JWSInvalid('JWS Payload missing');\n }\n if (typeof jws.signature !== 'string') {\n throw new JWSInvalid('JWS Signature missing or incorrect type');\n }\n if (jws.header !== undefined && !isObject(jws.header)) {\n throw new JWSInvalid('JWS Unprotected Header incorrect type');\n }\n let parsedProt = {};\n if (jws.protected) {\n try {\n const protectedHeader = base64url(jws.protected);\n parsedProt = JSON.parse(decoder.decode(protectedHeader));\n }\n catch (_b) {\n throw new JWSInvalid('JWS Protected Header is invalid');\n }\n }\n if (!isDisjoint(parsedProt, jws.header)) {\n throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');\n }\n const joseHeader = {\n ...parsedProt,\n ...jws.header,\n };\n const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options === null || options === void 0 ? void 0 : options.crit, parsedProt, joseHeader);\n let b64 = true;\n if (extensions.has('b64')) {\n b64 = parsedProt.b64;\n if (typeof b64 !== 'boolean') {\n throw new JWSInvalid('The \"b64\" (base64url-encode payload) Header Parameter must be a boolean');\n }\n }\n const { alg } = joseHeader;\n if (typeof alg !== 'string' || !alg) {\n throw new JWSInvalid('JWS \"alg\" (Algorithm) Header Parameter missing or invalid');\n }\n const algorithms = options && validateAlgorithms('algorithms', options.algorithms);\n if (algorithms && !algorithms.has(alg)) {\n throw new JOSEAlgNotAllowed('\"alg\" (Algorithm) Header Parameter not allowed');\n }\n if (b64) {\n if (typeof jws.payload !== 'string') {\n throw new JWSInvalid('JWS Payload must be a string');\n }\n }\n else if (typeof jws.payload !== 'string' && !(jws.payload instanceof Uint8Array)) {\n throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance');\n }\n let resolvedKey = false;\n if (typeof key === 'function') {\n key = await key(parsedProt, jws);\n resolvedKey = true;\n }\n checkKeyType(alg, key, 'verify');\n const data = concat(encoder.encode((_a = jws.protected) !== null && _a !== void 0 ? _a : ''), encoder.encode('.'), typeof jws.payload === 'string' ? encoder.encode(jws.payload) : jws.payload);\n const signature = base64url(jws.signature);\n const verified = await verify(alg, key, signature, data);\n if (!verified) {\n throw new JWSSignatureVerificationFailed();\n }\n let payload;\n if (b64) {\n payload = base64url(jws.payload);\n }\n else if (typeof jws.payload === 'string') {\n payload = encoder.encode(jws.payload);\n }\n else {\n payload = jws.payload;\n }\n const result = { payload };\n if (jws.protected !== undefined) {\n result.protectedHeader = parsedProt;\n }\n if (jws.header !== undefined) {\n result.unprotectedHeader = jws.header;\n }\n if (resolvedKey) {\n return { ...result, key };\n }\n return result;\n}\n", "import { flattenedVerify } from '../flattened/verify.js';\nimport { JWSInvalid } from '../../util/errors.js';\nimport { decoder } from '../../lib/buffer_utils.js';\nexport async function compactVerify(jws, key, options) {\n if (jws instanceof Uint8Array) {\n jws = decoder.decode(jws);\n }\n if (typeof jws !== 'string') {\n throw new JWSInvalid('Compact JWS must be a string or Uint8Array');\n }\n const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.');\n if (length !== 3) {\n throw new JWSInvalid('Invalid Compact JWS');\n }\n const verified = await flattenedVerify({ payload, protected: protectedHeader, signature }, key, options);\n const result = { payload: verified.payload, protectedHeader: verified.protectedHeader };\n if (typeof key === 'function') {\n return { ...result, key: verified.key };\n }\n return result;\n}\n", "export default (date) => Math.floor(date.getTime() / 1000);\n", "const minute = 60;\nconst hour = minute * 60;\nconst day = hour * 24;\nconst week = day * 7;\nconst year = day * 365.25;\nconst REGEX = /^(\\d+|\\d+\\.\\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i;\nexport default (str) => {\n const matched = REGEX.exec(str);\n if (!matched) {\n throw new TypeError('Invalid time period format');\n }\n const value = parseFloat(matched[1]);\n const unit = matched[2].toLowerCase();\n switch (unit) {\n case 'sec':\n case 'secs':\n case 'second':\n case 'seconds':\n case 's':\n return Math.round(value);\n case 'minute':\n case 'minutes':\n case 'min':\n case 'mins':\n case 'm':\n return Math.round(value * minute);\n case 'hour':\n case 'hours':\n case 'hr':\n case 'hrs':\n case 'h':\n return Math.round(value * hour);\n case 'day':\n case 'days':\n case 'd':\n return Math.round(value * day);\n case 'week':\n case 'weeks':\n case 'w':\n return Math.round(value * week);\n default:\n return Math.round(value * year);\n }\n};\n", "import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from '../util/errors.js';\nimport { decoder } from './buffer_utils.js';\nimport epoch from './epoch.js';\nimport secs from './secs.js';\nimport isObject from './is_object.js';\nconst normalizeTyp = (value) => value.toLowerCase().replace(/^application\\//, '');\nconst checkAudiencePresence = (audPayload, audOption) => {\n if (typeof audPayload === 'string') {\n return audOption.includes(audPayload);\n }\n if (Array.isArray(audPayload)) {\n return audOption.some(Set.prototype.has.bind(new Set(audPayload)));\n }\n return false;\n};\nexport default (protectedHeader, encodedPayload, options = {}) => {\n const { typ } = options;\n if (typ &&\n (typeof protectedHeader.typ !== 'string' ||\n normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) {\n throw new JWTClaimValidationFailed('unexpected \"typ\" JWT header value', 'typ', 'check_failed');\n }\n let payload;\n try {\n payload = JSON.parse(decoder.decode(encodedPayload));\n }\n catch (_a) {\n }\n if (!isObject(payload)) {\n throw new JWTInvalid('JWT Claims Set must be a top-level JSON object');\n }\n const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;\n if (maxTokenAge !== undefined)\n requiredClaims.push('iat');\n if (audience !== undefined)\n requiredClaims.push('aud');\n if (subject !== undefined)\n requiredClaims.push('sub');\n if (issuer !== undefined)\n requiredClaims.push('iss');\n for (const claim of new Set(requiredClaims.reverse())) {\n if (!(claim in payload)) {\n throw new JWTClaimValidationFailed(`missing required \"${claim}\" claim`, claim, 'missing');\n }\n }\n if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {\n throw new JWTClaimValidationFailed('unexpected \"iss\" claim value', 'iss', 'check_failed');\n }\n if (subject && payload.sub !== subject) {\n throw new JWTClaimValidationFailed('unexpected \"sub\" claim value', 'sub', 'check_failed');\n }\n if (audience &&\n !checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) {\n throw new JWTClaimValidationFailed('unexpected \"aud\" claim value', 'aud', 'check_failed');\n }\n let tolerance;\n switch (typeof options.clockTolerance) {\n case 'string':\n tolerance = secs(options.clockTolerance);\n break;\n case 'number':\n tolerance = options.clockTolerance;\n break;\n case 'undefined':\n tolerance = 0;\n break;\n default:\n throw new TypeError('Invalid clockTolerance option type');\n }\n const { currentDate } = options;\n const now = epoch(currentDate || new Date());\n if ((payload.iat !== undefined || maxTokenAge) && typeof payload.iat !== 'number') {\n throw new JWTClaimValidationFailed('\"iat\" claim must be a number', 'iat', 'invalid');\n }\n if (payload.nbf !== undefined) {\n if (typeof payload.nbf !== 'number') {\n throw new JWTClaimValidationFailed('\"nbf\" claim must be a number', 'nbf', 'invalid');\n }\n if (payload.nbf > now + tolerance) {\n throw new JWTClaimValidationFailed('\"nbf\" claim timestamp check failed', 'nbf', 'check_failed');\n }\n }\n if (payload.exp !== undefined) {\n if (typeof payload.exp !== 'number') {\n throw new JWTClaimValidationFailed('\"exp\" claim must be a number', 'exp', 'invalid');\n }\n if (payload.exp <= now - tolerance) {\n throw new JWTExpired('\"exp\" claim timestamp check failed', 'exp', 'check_failed');\n }\n }\n if (maxTokenAge) {\n const age = now - payload.iat;\n const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge);\n if (age - tolerance > max) {\n throw new JWTExpired('\"iat\" claim timestamp check failed (too far in the past)', 'iat', 'check_failed');\n }\n if (age < 0 - tolerance) {\n throw new JWTClaimValidationFailed('\"iat\" claim timestamp check failed (it should be in the past)', 'iat', 'check_failed');\n }\n }\n return payload;\n};\n", "import { compactVerify } from '../jws/compact/verify.js';\nimport jwtPayload from '../lib/jwt_claims_set.js';\nimport { JWTInvalid } from '../util/errors.js';\nexport async function jwtVerify(jwt, key, options) {\n var _a;\n const verified = await compactVerify(jwt, key, options);\n if (((_a = verified.protectedHeader.crit) === null || _a === void 0 ? void 0 : _a.includes('b64')) && verified.protectedHeader.b64 === false) {\n throw new JWTInvalid('JWTs MUST NOT use unencoded payload');\n }\n const payload = jwtPayload(verified.protectedHeader, verified.payload, options);\n const result = { payload, protectedHeader: verified.protectedHeader };\n if (typeof key === 'function') {\n return { ...result, key: verified.key };\n }\n return result;\n}\n", "/*\nCopyright 2022 The Dapr Authors\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { importSPKI, jwtVerify } from 'jose'\nimport { IRequest } from 'itty-router'\n\nimport { Environment } from '$lib/environment'\n\nconst tokenHeaderMatch =\n /^(?:Bearer )?([A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]+)/i\n\nexport async function AuthorizeRequest(\n req: IRequest,\n env: Environment\n): Promise {\n // If \"SKIP_AUTH\" is set, we can allow skipping authorization\n if (env.SKIP_AUTH === 'true') {\n return true\n }\n\n // Ensure we have an Authorization header with a bearer JWT token\n const match = tokenHeaderMatch.exec(req.headers.get('authorization') || '')\n if (!match || !match[1]) {\n return false\n }\n\n // Validate the JWT\n const pk = await importSPKI(env.PUBLIC_KEY, 'EdDSA')\n try {\n await jwtVerify(match[1], pk, {\n issuer: 'dapr.io/cloudflare',\n audience: env.TOKEN_AUDIENCE,\n algorithms: ['EdDSA'],\n // Allow 5 mins of clock skew\n clockTolerance: 300,\n })\n } catch (err) {\n console.error('Failed to validate JWT: ' + err)\n return false\n }\n\n return true\n}\n", "{\n \"private\": true,\n \"name\": \"dapr-cfworkers-client\",\n \"description\": \"Client code for Dapr to interact with Cloudflare Workers\",\n \"version\": \"20230517\",\n \"main\": \"worker.ts\",\n \"scripts\": {\n \"build\": \"esbuild --bundle --minify --outfile=../workers/code/worker.js --format=esm --platform=browser --sourcemap worker.ts\",\n \"start\": \"wrangler dev\",\n \"format\": \"prettier --write .\"\n },\n \"author\": \"Dapr authors\",\n \"license\": \"Apache2\",\n \"devDependencies\": {\n \"@cloudflare/workers-types\": \"^4.20230511.0\",\n \"esbuild\": \"^0.17.19\",\n \"prettier\": \"^2.8.8\",\n \"typescript\": \"^5.0.4\",\n \"wrangler\": \"^3.19.0\"\n },\n \"dependencies\": {\n \"itty-router\": \"3.0.12\",\n \"jose\": \"4.14.4\"\n }\n}\n", "/*\nCopyright 2022 The Dapr Authors\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { Router, IRequest } from 'itty-router'\n\nimport { Environment } from '$lib/environment'\nimport { AuthorizeRequest } from '$lib/jwt-auth'\n\nimport { version } from './package.json'\n\nconst router = Router()\n // Handle the info endpoint\n .get(\n '/.well-known/dapr/info',\n async (req: IRequest, env: Environment): Promise => {\n const auth = await AuthorizeRequest(req, env)\n if (!auth) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Filter all bindings by type\n const queues: string[] = []\n const kv: string[] = []\n const r2: string[] = []\n const all = Object.keys(env)\n for (let i = 0; i < all.length; i++) {\n if (!all[i]) {\n continue\n }\n const obj = env[all[i]]\n if (!obj || typeof obj != 'object' || !obj.constructor) {\n continue\n }\n switch (obj.constructor.name) {\n case 'KvNamespace':\n case 'KVNamespace':\n kv.push(all[i])\n break\n case 'WorkerQueue':\n case 'Queue':\n queues.push(all[i])\n break\n case 'R2Bucket':\n // Note that we currently don't support R2 yet\n r2.push(all[i])\n break\n }\n }\n\n const res = JSON.stringify({\n version,\n queues: queues && queues.length ? queues : undefined,\n kv: kv && kv.length ? kv : undefined,\n r2: r2 && r2.length ? r2 : undefined,\n })\n return new Response(res, {\n headers: {\n 'content-type': 'application/json',\n },\n })\n }\n )\n\n // Retrieve a value from KV\n .get(\n '/kv/:namespace/:key',\n async (req: IRequest, env: Environment): Promise => {\n const { namespace, key, errorRes } = await setupKVRequest(req, env)\n if (errorRes) {\n return errorRes\n }\n\n const val = await namespace!.get(key!, 'stream')\n if (!val) {\n return new Response('', { status: 404 })\n }\n\n return new Response(val, { status: 200 })\n }\n )\n\n // Store a value in KV\n .post(\n '/kv/:namespace/:key',\n async (req: IRequest, env: Environment): Promise => {\n const { namespace, key, errorRes } = await setupKVRequest(req, env)\n if (errorRes) {\n return errorRes\n }\n\n let expirationTtl: number | undefined = undefined\n const reqUrl = new URL(req.url)\n const ttlParam = parseInt(reqUrl.searchParams.get('ttl') || '', 10)\n if (ttlParam > 0) {\n expirationTtl = ttlParam\n }\n await namespace!.put(key!, req.body!, { expirationTtl })\n\n return new Response('', { status: 201 })\n }\n )\n\n // Delete a value from KV\n .delete(\n '/kv/:namespace/:key',\n async (req: IRequest, env: Environment): Promise => {\n const { namespace, key, errorRes } = await setupKVRequest(req, env)\n if (errorRes) {\n return errorRes\n }\n\n await namespace!.delete(key!)\n\n return new Response('', { status: 204 })\n }\n )\n\n // Publish a message in a queue\n .post(\n '/queues/:queue',\n async (req: IRequest, env: Environment): Promise => {\n const { queue, errorRes } = await setupQueueRequest(req, env)\n if (errorRes) {\n return errorRes\n }\n\n let message = await req.text()\n await queue!.send(message)\n return new Response('', { status: 201 })\n }\n )\n\n // Catch-all route to handle 404s\n .all('*', (): Response => {\n return new Response('Not found', { status: 404 })\n })\n\n// Performs the init setps for a KV request. Returns a Response object in case of error.\nasync function setupKVRequest(\n req: IRequest,\n env: Environment\n): Promise<{\n namespace?: KVNamespace\n key?: string\n errorRes?: Response\n}> {\n if (!req?.text || !req.params?.namespace || !req.params?.key) {\n return { errorRes: new Response('Bad request', { status: 400 }) }\n }\n const namespace = env[req.params.namespace] as KVNamespace\n if (\n typeof namespace != 'object' ||\n !['KVNamespace', 'KvNamespace'].includes(namespace?.constructor?.name)\n ) {\n return {\n errorRes: new Response(\n `Worker is not bound to KV '${req.params.kv}'`,\n { status: 412 }\n ),\n }\n }\n\n const auth = await AuthorizeRequest(req, env)\n if (!auth) {\n return { errorRes: new Response('Unauthorized', { status: 401 }) }\n }\n\n return { namespace, key: req.params.key }\n}\n\n// Performs the init setps for a Queue request. Returns a Response object in case of error.\nasync function setupQueueRequest(\n req: IRequest,\n env: Environment\n): Promise<{ queue?: Queue; errorRes?: Response }> {\n if (!req?.text || !req.params?.queue) {\n return { errorRes: new Response('Bad request', { status: 400 }) }\n }\n const queue = env[req.params.queue] as Queue\n if (\n typeof queue != 'object' ||\n !['WorkerQueue', 'Queue'].includes(queue?.constructor?.name)\n ) {\n return {\n errorRes: new Response(\n `Worker is not bound to queue '${req.params.queue}'`,\n { status: 412 }\n ),\n }\n }\n\n const auth = await AuthorizeRequest(req, env)\n if (!auth) {\n return { errorRes: new Response('Unauthorized', { status: 401 }) }\n }\n\n return { queue }\n}\n\nexport default {\n fetch: router.handle,\n}\n"], - "mappings": "AAAA,IAAMA,GAAE,CAAC,CAAC,KAAK,EAAE,GAAG,OAAOC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,CAACC,EAAEC,EAAEC,IAAI,CAACF,KAAKG,IAAIJ,EAAE,KAAK,CAACE,EAAE,YAAY,EAAE,OAAO,KAAK,EAAED,GAAG,QAAQ,WAAW,SAAS,EAAE,QAAQ,oBAAoB,EAAE,EAAE,QAAQ,aAAa,WAAW,EAAE,QAAQ,oBAAoB,oBAAoB,EAAE,QAAQ,cAAc,KAAK,EAAE,QAAQ,wBAAwB,wBAAwB,MAAM,EAAEG,CAAC,CAAC,GAAGD,CAAC,CAAC,EAAE,OAAOH,EAAE,MAAM,OAAOD,KAAKE,EAAE,CAAC,IAAI,EAAEE,EAAEC,EAAE,IAAI,IAAIL,EAAE,GAAG,EAAEM,EAAEN,EAAE,MAAM,CAAC,EAAE,OAAO,CAACA,EAAEC,CAAC,IAAII,EAAE,aAAaC,EAAEN,CAAC,EAAWM,EAAEN,CAAC,IAAZ,OAAcC,EAAE,CAACK,EAAEN,CAAC,EAAEC,CAAC,EAAE,KAAK,EAAE,OAAO,CAACK,EAAEC,EAAEC,CAAC,IAAIP,EAAE,IAAIK,IAAIN,EAAE,QAAgBM,IAAR,SAAaF,EAAEC,EAAE,SAAS,MAAME,CAAC,GAAG,CAACP,EAAE,OAAOI,EAAE,QAAQ,CAAC,EAAE,QAAQH,KAAKO,EAAE,IAAa,EAAE,MAAMP,EAAED,EAAE,OAAOA,EAAE,GAAGE,CAAC,KAAnC,OAAsC,OAAO,EAAE,CAAC,GCAjqB,IAAOO,EAAQ,OACFC,EAAeC,GAAQA,aAAe,UCA5C,IAAMC,EAAU,IAAI,YACdC,EAAU,IAAI,YACrBC,GAAY,GAAK,GAChB,SAASC,KAAUC,EAAS,CAC/B,IAAMC,EAAOD,EAAQ,OAAO,CAACE,EAAK,CAAE,OAAAC,CAAO,IAAMD,EAAMC,EAAQ,CAAC,EAC1DC,EAAM,IAAI,WAAWH,CAAI,EAC3BI,EAAI,EACR,OAAAL,EAAQ,QAASM,GAAW,CACxBF,EAAI,IAAIE,EAAQD,CAAC,EACjBA,GAAKC,EAAO,MAChB,CAAC,EACMF,CACX,CCGO,IAAMG,GAAgBC,GAAY,CACrC,IAAMC,EAAS,KAAKD,CAAO,EACrBE,EAAQ,IAAI,WAAWD,EAAO,MAAM,EAC1C,QAASE,EAAI,EAAGA,EAAIF,EAAO,OAAQE,IAC/BD,EAAMC,CAAC,EAAIF,EAAO,WAAWE,CAAC,EAElC,OAAOD,CACX,EACaE,EAAUC,GAAU,CAC7B,IAAIL,EAAUK,EACVL,aAAmB,aACnBA,EAAUM,EAAQ,OAAON,CAAO,GAEpCA,EAAUA,EAAQ,QAAQ,KAAM,GAAG,EAAE,QAAQ,KAAM,GAAG,EAAE,QAAQ,MAAO,EAAE,EACzE,GAAI,CACA,OAAOD,GAAaC,CAAO,CAC/B,MACA,CACI,MAAM,IAAI,UAAU,mDAAmD,CAC3E,CACJ,ECpCO,IAAMO,EAAN,cAAwB,KAAM,CACjC,WAAW,MAAO,CACd,MAAO,kBACX,CACA,YAAYC,EAAS,CACjB,IAAIC,EACJ,MAAMD,CAAO,EACb,KAAK,KAAO,mBACZ,KAAK,KAAO,KAAK,YAAY,MAC5BC,EAAK,MAAM,qBAAuB,MAAQA,IAAO,QAAkBA,EAAG,KAAK,MAAO,KAAM,KAAK,WAAW,CAC7G,CACJ,EACaC,EAAN,cAAuCH,CAAU,CACpD,WAAW,MAAO,CACd,MAAO,iCACX,CACA,YAAYC,EAASG,EAAQ,cAAeC,EAAS,cAAe,CAChE,MAAMJ,CAAO,EACb,KAAK,KAAO,kCACZ,KAAK,MAAQG,EACb,KAAK,OAASC,CAClB,CACJ,EACaC,EAAN,cAAyBN,CAAU,CACtC,WAAW,MAAO,CACd,MAAO,iBACX,CACA,YAAYC,EAASG,EAAQ,cAAeC,EAAS,cAAe,CAChE,MAAMJ,CAAO,EACb,KAAK,KAAO,kBACZ,KAAK,MAAQG,EACb,KAAK,OAASC,CAClB,CACJ,EACaE,EAAN,cAAgCP,CAAU,CAC7C,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,0BAChB,CACA,WAAW,MAAO,CACd,MAAO,0BACX,CACJ,EACaQ,EAAN,cAA+BR,CAAU,CAC5C,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,wBAChB,CACA,WAAW,MAAO,CACd,MAAO,wBACX,CACJ,EAoBO,IAAMS,EAAN,cAAyBC,CAAU,CACtC,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,iBAChB,CACA,WAAW,MAAO,CACd,MAAO,iBACX,CACJ,EACaC,EAAN,cAAyBD,CAAU,CACtC,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,iBAChB,CACA,WAAW,MAAO,CACd,MAAO,iBACX,CACJ,EAkDO,IAAME,EAAN,cAA6CC,CAAU,CAC1D,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,wCACZ,KAAK,QAAU,+BACnB,CACA,WAAW,MAAO,CACd,MAAO,uCACX,CACJ,EClJA,IAAOC,EAAQC,EAAO,gBAAgB,KAAKA,CAAM,ECDjD,SAASC,EAASC,EAAMC,EAAO,iBAAkB,CAC7C,OAAO,IAAI,UAAU,kDAAkDA,aAAgBD,GAAM,CACjG,CACA,SAASE,EAAYC,EAAWH,EAAM,CAClC,OAAOG,EAAU,OAASH,CAC9B,CACA,SAASI,EAAcC,EAAM,CACzB,OAAO,SAASA,EAAK,KAAK,MAAM,CAAC,EAAG,EAAE,CAC1C,CACA,SAASC,GAAcC,EAAK,CACxB,OAAQA,EAAK,CACT,IAAK,QACD,MAAO,QACX,IAAK,QACD,MAAO,QACX,IAAK,QACD,MAAO,QACX,QACI,MAAM,IAAI,MAAM,aAAa,CACrC,CACJ,CACA,SAASC,GAAWC,EAAKC,EAAQ,CAC7B,GAAIA,EAAO,QAAU,CAACA,EAAO,KAAMC,GAAaF,EAAI,OAAO,SAASE,CAAQ,CAAC,EAAG,CAC5E,IAAIC,EAAM,sEACV,GAAIF,EAAO,OAAS,EAAG,CACnB,IAAMG,EAAOH,EAAO,IAAI,EACxBE,GAAO,UAAUF,EAAO,KAAK,IAAI,SAASG,UAErCH,EAAO,SAAW,EACvBE,GAAO,UAAUF,EAAO,CAAC,QAAQA,EAAO,CAAC,KAGzCE,GAAO,GAAGF,EAAO,CAAC,KAEtB,MAAM,IAAI,UAAUE,CAAG,EAE/B,CACO,SAASE,GAAkBL,EAAKF,KAAQG,EAAQ,CACnD,OAAQH,EAAK,CACT,IAAK,QACL,IAAK,QACL,IAAK,QAAS,CACV,GAAI,CAACL,EAAYO,EAAI,UAAW,MAAM,EAClC,MAAMV,EAAS,MAAM,EACzB,IAAMY,EAAW,SAASJ,EAAI,MAAM,CAAC,EAAG,EAAE,EAE1C,GADeH,EAAcK,EAAI,UAAU,IAAI,IAChCE,EACX,MAAMZ,EAAS,OAAOY,IAAY,gBAAgB,EACtD,KACJ,CACA,IAAK,QACL,IAAK,QACL,IAAK,QAAS,CACV,GAAI,CAACT,EAAYO,EAAI,UAAW,mBAAmB,EAC/C,MAAMV,EAAS,mBAAmB,EACtC,IAAMY,EAAW,SAASJ,EAAI,MAAM,CAAC,EAAG,EAAE,EAE1C,GADeH,EAAcK,EAAI,UAAU,IAAI,IAChCE,EACX,MAAMZ,EAAS,OAAOY,IAAY,gBAAgB,EACtD,KACJ,CACA,IAAK,QACL,IAAK,QACL,IAAK,QAAS,CACV,GAAI,CAACT,EAAYO,EAAI,UAAW,SAAS,EACrC,MAAMV,EAAS,SAAS,EAC5B,IAAMY,EAAW,SAASJ,EAAI,MAAM,CAAC,EAAG,EAAE,EAE1C,GADeH,EAAcK,EAAI,UAAU,IAAI,IAChCE,EACX,MAAMZ,EAAS,OAAOY,IAAY,gBAAgB,EACtD,KACJ,CACA,IAAK,QAAS,CACV,GAAIF,EAAI,UAAU,OAAS,WAAaA,EAAI,UAAU,OAAS,QAC3D,MAAMV,EAAS,kBAAkB,EAErC,KACJ,CACA,IAAK,QACL,IAAK,QACL,IAAK,QAAS,CACV,GAAI,CAACG,EAAYO,EAAI,UAAW,OAAO,EACnC,MAAMV,EAAS,OAAO,EAC1B,IAAMY,EAAWL,GAAcC,CAAG,EAElC,GADeE,EAAI,UAAU,aACdE,EACX,MAAMZ,EAASY,EAAU,sBAAsB,EACnD,KACJ,CACA,QACI,MAAM,IAAI,UAAU,2CAA2C,CACvE,CACAH,GAAWC,EAAKC,CAAM,CAC1B,CC7FA,SAASK,GAAQC,EAAKC,KAAWC,EAAO,CACpC,GAAIA,EAAM,OAAS,EAAG,CAClB,IAAMC,EAAOD,EAAM,IAAI,EACvBF,GAAO,eAAeE,EAAM,KAAK,IAAI,SAASC,UAEzCD,EAAM,SAAW,EACtBF,GAAO,eAAeE,EAAM,CAAC,QAAQA,EAAM,CAAC,KAG5CF,GAAO,WAAWE,EAAM,CAAC,KAE7B,OAAID,GAAU,KACVD,GAAO,aAAaC,IAEf,OAAOA,GAAW,YAAcA,EAAO,KAC5CD,GAAO,sBAAsBC,EAAO,OAE/B,OAAOA,GAAW,UAAYA,GAAU,MACzCA,EAAO,aAAeA,EAAO,YAAY,OACzCD,GAAO,4BAA4BC,EAAO,YAAY,QAGvDD,CACX,CACA,IAAOI,EAAQ,CAACH,KAAWC,IAChBH,GAAQ,eAAgBE,EAAQ,GAAGC,CAAK,EAE5C,SAASG,EAAQC,EAAKL,KAAWC,EAAO,CAC3C,OAAOH,GAAQ,eAAeO,uBAA0BL,EAAQ,GAAGC,CAAK,CAC5E,CC5BA,IAAOK,EAASC,GACLC,EAAYD,CAAG,EAEbE,EAAQ,CAAC,WAAW,ECJjC,IAAMC,GAAa,IAAIC,IAAY,CAC/B,IAAMC,EAAUD,EAAQ,OAAO,OAAO,EACtC,GAAIC,EAAQ,SAAW,GAAKA,EAAQ,SAAW,EAC3C,MAAO,GAEX,IAAIC,EACJ,QAAWC,KAAUF,EAAS,CAC1B,IAAMG,EAAa,OAAO,KAAKD,CAAM,EACrC,GAAI,CAACD,GAAOA,EAAI,OAAS,EAAG,CACxBA,EAAM,IAAI,IAAIE,CAAU,EACxB,SAEJ,QAAWC,KAAaD,EAAY,CAChC,GAAIF,EAAI,IAAIG,CAAS,EACjB,MAAO,GAEXH,EAAI,IAAIG,CAAS,GAGzB,MAAO,EACX,EACOC,EAAQP,GCrBf,SAASQ,GAAaC,EAAO,CACzB,OAAO,OAAOA,GAAU,UAAYA,IAAU,IAClD,CACe,SAARC,EAA0BC,EAAO,CACpC,GAAI,CAACH,GAAaG,CAAK,GAAK,OAAO,UAAU,SAAS,KAAKA,CAAK,IAAM,kBAClE,MAAO,GAEX,GAAI,OAAO,eAAeA,CAAK,IAAM,KACjC,MAAO,GAEX,IAAIC,EAAQD,EACZ,KAAO,OAAO,eAAeC,CAAK,IAAM,MACpCA,EAAQ,OAAO,eAAeA,CAAK,EAEvC,OAAO,OAAO,eAAeD,CAAK,IAAMC,CAC5C,CCfA,IAAOC,EAAQ,CAACC,EAAKC,IAAQ,CACzB,GAAID,EAAI,WAAW,IAAI,GAAKA,EAAI,WAAW,IAAI,EAAG,CAC9C,GAAM,CAAE,cAAAE,CAAc,EAAID,EAAI,UAC9B,GAAI,OAAOC,GAAkB,UAAYA,EAAgB,KACrD,MAAM,IAAI,UAAU,GAAGF,wDAA0D,EAG7F,ECiBA,IAAMG,EAAU,CAACC,EAASC,EAAKC,EAAO,IAAM,CACpCA,IAAS,IACTD,EAAI,QAAQA,EAAI,MAAM,EACtBA,EAAI,QAAQ,CAAI,GAEpB,IAAIE,EAAIH,EAAQ,QAAQC,EAAI,CAAC,EAAGC,CAAI,EACpC,GAAIC,IAAM,GACN,MAAO,GACX,IAAMC,EAAMJ,EAAQ,SAASG,EAAGA,EAAIF,EAAI,MAAM,EAC9C,OAAIG,EAAI,SAAWH,EAAI,OACZ,GACJG,EAAI,MAAM,CAACC,EAAOC,IAAUD,IAAUJ,EAAIK,CAAK,CAAC,GAAKP,EAAQC,EAASC,EAAKE,EAAI,CAAC,CAC3F,EACMI,GAAiBP,GAAY,CAC/B,OAAQ,GAAM,CACV,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAM,IAAM,GAAM,EAAM,EAAM,CAAI,CAAC,EAClE,MAAO,QACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,EAAM,EAAM,EAAI,CAAC,EAChD,MAAO,QACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,EAAM,EAAM,EAAI,CAAC,EAChD,MAAO,QACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAI,CAAC,EACpC,MAAO,SACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAI,CAAC,EACpC,MAAO,OACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAI,CAAC,EACpC,MAAO,UACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAI,CAAC,EACpC,MAAO,QACX,QACI,MAAM,IAAIQ,EAAiB,yDAAyD,CAC5F,CACJ,EACMC,GAAgB,MAAOC,EAASC,EAAWC,EAAKC,EAAKC,IAAY,CACnE,IAAIC,EACJ,IAAIC,EACAC,EACEjB,EAAU,IAAI,WAAW,KAAKY,EAAI,QAAQF,EAAS,EAAE,CAAC,EACvD,MAAM,EAAE,EACR,IAAKQ,GAAMA,EAAE,WAAW,CAAC,CAAC,CAAC,EAC1BC,EAAWR,IAAc,OAC/B,OAAQE,EAAK,CACT,IAAK,QACL,IAAK,QACL,IAAK,QACDG,EAAY,CAAE,KAAM,UAAW,KAAM,OAAOH,EAAI,MAAM,EAAE,GAAI,EAC5DI,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,QACL,IAAK,QACL,IAAK,QACDH,EAAY,CAAE,KAAM,oBAAqB,KAAM,OAAOH,EAAI,MAAM,EAAE,GAAI,EACtEI,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,WACL,IAAK,eACL,IAAK,eACL,IAAK,eACDH,EAAY,CACR,KAAM,WACN,KAAM,OAAO,SAASH,EAAI,MAAM,EAAE,EAAG,EAAE,GAAK,GAChD,EACAI,EAAYE,EAAW,CAAC,UAAW,SAAS,EAAI,CAAC,UAAW,WAAW,EACvE,MACJ,IAAK,QACDH,EAAY,CAAE,KAAM,QAAS,WAAY,OAAQ,EACjDC,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,QACDH,EAAY,CAAE,KAAM,QAAS,WAAY,OAAQ,EACjDC,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,QACDH,EAAY,CAAE,KAAM,QAAS,WAAY,OAAQ,EACjDC,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,UACL,IAAK,iBACL,IAAK,iBACL,IAAK,iBAAkB,CACnB,IAAMC,EAAab,GAAcP,CAAO,EACxCgB,EAAYI,EAAW,WAAW,IAAI,EAAI,CAAE,KAAM,OAAQ,WAAAA,CAAW,EAAI,CAAE,KAAMA,CAAW,EAC5FH,EAAYE,EAAW,CAAC,EAAI,CAAC,YAAY,EACzC,KACJ,CACA,IAAK,QACDH,EAAY,CAAE,KAAMT,GAAcP,CAAO,CAAE,EAC3CiB,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,QACI,MAAM,IAAIX,EAAiB,gDAAgD,CACnF,CACA,OAAOa,EAAO,OAAO,UAAUV,EAAWX,EAASgB,GAAYD,EAAuDD,GAAQ,eAAiB,MAAQC,IAAO,OAASA,EAAK,GAAOE,CAAS,CAChM,EAIO,IAAMK,GAAW,CAACC,EAAKC,EAAKC,IACxBC,GAAc,6CAA8C,OAAQH,EAAKC,EAAKC,CAAO,ECrHhG,eAAsBE,EAAWC,EAAMC,EAAKC,EAAS,CACjD,GAAI,OAAOF,GAAS,UAAYA,EAAK,QAAQ,4BAA4B,IAAM,EAC3E,MAAM,IAAI,UAAU,sCAAsC,EAE9D,OAAOG,GAASH,EAAMC,EAAKC,CAAO,CACtC,CCRA,IAAME,GAAqB,CAACC,EAAKC,IAAQ,CACrC,GAAI,EAAAA,aAAe,YAEnB,IAAI,CAACC,EAAUD,CAAG,EACd,MAAM,IAAI,UAAUE,EAAgBH,EAAKC,EAAK,GAAGG,EAAO,YAAY,CAAC,EAEzE,GAAIH,EAAI,OAAS,SACb,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,+DAA+D,EAE/G,EACMC,GAAsB,CAACL,EAAKC,EAAKK,IAAU,CAC7C,GAAI,CAACJ,EAAUD,CAAG,EACd,MAAM,IAAI,UAAUE,EAAgBH,EAAKC,EAAK,GAAGG,CAAK,CAAC,EAE3D,GAAIH,EAAI,OAAS,SACb,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,oEAAoE,EAEhH,GAAIE,IAAU,QAAUL,EAAI,OAAS,SACjC,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,wEAAwE,EAEpH,GAAIE,IAAU,WAAaL,EAAI,OAAS,SACpC,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,2EAA2E,EAEvH,GAAIH,EAAI,WAAaK,IAAU,UAAYL,EAAI,OAAS,UACpD,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,yEAAyE,EAErH,GAAIH,EAAI,WAAaK,IAAU,WAAaL,EAAI,OAAS,UACrD,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,0EAA0E,CAE1H,EACMG,GAAe,CAACP,EAAKC,EAAKK,IAAU,CACpBN,EAAI,WAAW,IAAI,GACjCA,IAAQ,OACRA,EAAI,WAAW,OAAO,GACtB,qBAAqB,KAAKA,CAAG,EAE7BD,GAAmBC,EAAKC,CAAG,EAG3BI,GAAoBL,EAAKC,EAAKK,CAAK,CAE3C,EACOE,EAAQD,GC3Cf,SAASE,GAAaC,EAAKC,EAAmBC,EAAkBC,EAAiBC,EAAY,CACzF,GAAIA,EAAW,OAAS,QAAaD,EAAgB,OAAS,OAC1D,MAAM,IAAIH,EAAI,gEAAgE,EAElF,GAAI,CAACG,GAAmBA,EAAgB,OAAS,OAC7C,OAAO,IAAI,IAEf,GAAI,CAAC,MAAM,QAAQA,EAAgB,IAAI,GACnCA,EAAgB,KAAK,SAAW,GAChCA,EAAgB,KAAK,KAAME,GAAU,OAAOA,GAAU,UAAYA,EAAM,SAAW,CAAC,EACpF,MAAM,IAAIL,EAAI,uFAAuF,EAEzG,IAAIM,EACAJ,IAAqB,OACrBI,EAAa,IAAI,IAAI,CAAC,GAAG,OAAO,QAAQJ,CAAgB,EAAG,GAAGD,EAAkB,QAAQ,CAAC,CAAC,EAG1FK,EAAaL,EAEjB,QAAWM,KAAaJ,EAAgB,KAAM,CAC1C,GAAI,CAACG,EAAW,IAAIC,CAAS,EACzB,MAAM,IAAIC,EAAiB,+BAA+BD,sBAA8B,EAE5F,GAAIH,EAAWG,CAAS,IAAM,OAC1B,MAAM,IAAIP,EAAI,+BAA+BO,eAAuB,EAEnE,GAAID,EAAW,IAAIC,CAAS,GAAKJ,EAAgBI,CAAS,IAAM,OACjE,MAAM,IAAIP,EAAI,+BAA+BO,gCAAwC,EAG7F,OAAO,IAAI,IAAIJ,EAAgB,IAAI,CACvC,CACA,IAAOM,EAAQV,GCjCf,IAAMW,GAAqB,CAACC,EAAQC,IAAe,CAC/C,GAAIA,IAAe,SACd,CAAC,MAAM,QAAQA,CAAU,GAAKA,EAAW,KAAMC,GAAM,OAAOA,GAAM,QAAQ,GAC3E,MAAM,IAAI,UAAU,IAAIF,uCAA4C,EAExE,GAAKC,EAGL,OAAO,IAAI,IAAIA,CAAU,CAC7B,EACOE,GAAQJ,GCDR,IAAMK,GAAc,OAAO,ECRnB,SAARC,EAA2BC,EAAKC,EAAW,CAC9C,IAAMC,EAAO,OAAOF,EAAI,MAAM,EAAE,IAChC,OAAQA,EAAK,CACT,IAAK,QACL,IAAK,QACL,IAAK,QACD,MAAO,CAAE,KAAAE,EAAM,KAAM,MAAO,EAChC,IAAK,QACL,IAAK,QACL,IAAK,QACD,MAAO,CAAE,KAAAA,EAAM,KAAM,UAAW,WAAYF,EAAI,MAAM,EAAE,GAAK,CAAE,EACnE,IAAK,QACL,IAAK,QACL,IAAK,QACD,MAAO,CAAE,KAAAE,EAAM,KAAM,mBAAoB,EAC7C,IAAK,QACL,IAAK,QACL,IAAK,QACD,MAAO,CAAE,KAAAA,EAAM,KAAM,QAAS,WAAYD,EAAU,UAAW,EACnE,IAAK,QACD,MAAO,CAAE,KAAMA,EAAU,IAAK,EAClC,QACI,MAAM,IAAIE,EAAiB,OAAOH,8DAAgE,CAC1G,CACJ,CCrBe,SAARI,EAA8BC,EAAKC,EAAKC,EAAO,CAClD,GAAIC,EAAYF,CAAG,EACf,OAAAG,GAAkBH,EAAKD,EAAKE,CAAK,EAC1BD,EAEX,GAAIA,aAAe,WAAY,CAC3B,GAAI,CAACD,EAAI,WAAW,IAAI,EACpB,MAAM,IAAI,UAAUK,EAAgBJ,EAAK,GAAGK,CAAK,CAAC,EAEtD,OAAOC,EAAO,OAAO,UAAU,MAAON,EAAK,CAAE,KAAM,OAAOD,EAAI,MAAM,EAAE,IAAK,KAAM,MAAO,EAAG,GAAO,CAACE,CAAK,CAAC,EAE7G,MAAM,IAAI,UAAUG,EAAgBJ,EAAK,GAAGK,EAAO,YAAY,CAAC,CACpE,CCZA,IAAME,GAAS,MAAOC,EAAKC,EAAKC,EAAWC,IAAS,CAChD,IAAMC,EAAY,MAAMC,EAAaL,EAAKC,EAAK,QAAQ,EACvDK,EAAeN,EAAKI,CAAS,EAC7B,IAAMG,EAAYC,EAAgBR,EAAKI,EAAU,SAAS,EAC1D,GAAI,CACA,OAAO,MAAMK,EAAO,OAAO,OAAOF,EAAWH,EAAWF,EAAWC,CAAI,CAC3E,MACA,CACI,MAAO,EACX,CACJ,EACOO,GAAQX,GCNf,eAAsBY,EAAgBC,EAAKC,EAAKC,EAAS,CACrD,IAAIC,EACJ,GAAI,CAACC,EAASJ,CAAG,EACb,MAAM,IAAIK,EAAW,iCAAiC,EAE1D,GAAIL,EAAI,YAAc,QAAaA,EAAI,SAAW,OAC9C,MAAM,IAAIK,EAAW,uEAAuE,EAEhG,GAAIL,EAAI,YAAc,QAAa,OAAOA,EAAI,WAAc,SACxD,MAAM,IAAIK,EAAW,qCAAqC,EAE9D,GAAIL,EAAI,UAAY,OAChB,MAAM,IAAIK,EAAW,qBAAqB,EAE9C,GAAI,OAAOL,EAAI,WAAc,SACzB,MAAM,IAAIK,EAAW,yCAAyC,EAElE,GAAIL,EAAI,SAAW,QAAa,CAACI,EAASJ,EAAI,MAAM,EAChD,MAAM,IAAIK,EAAW,uCAAuC,EAEhE,IAAIC,EAAa,CAAC,EAClB,GAAIN,EAAI,UACJ,GAAI,CACA,IAAMO,GAAkBC,EAAUR,EAAI,SAAS,EAC/CM,EAAa,KAAK,MAAMG,EAAQ,OAAOF,EAAe,CAAC,CAC3D,MACA,CACI,MAAM,IAAIF,EAAW,iCAAiC,CAC1D,CAEJ,GAAI,CAACK,EAAWJ,EAAYN,EAAI,MAAM,EAClC,MAAM,IAAIK,EAAW,2EAA2E,EAEpG,IAAMM,EAAa,CACf,GAAGL,EACH,GAAGN,EAAI,MACX,EACMY,EAAaC,EAAaR,EAAY,IAAI,IAAI,CAAC,CAAC,MAAO,EAAI,CAAC,CAAC,EAAqDH,GAAQ,KAAMI,EAAYK,CAAU,EACxJG,EAAM,GACV,GAAIF,EAAW,IAAI,KAAK,IACpBE,EAAMR,EAAW,IACb,OAAOQ,GAAQ,WACf,MAAM,IAAIT,EAAW,yEAAyE,EAGtG,GAAM,CAAE,IAAAU,CAAI,EAAIJ,EAChB,GAAI,OAAOI,GAAQ,UAAY,CAACA,EAC5B,MAAM,IAAIV,EAAW,2DAA2D,EAEpF,IAAMW,EAAad,GAAWe,GAAmB,aAAcf,EAAQ,UAAU,EACjF,GAAIc,GAAc,CAACA,EAAW,IAAID,CAAG,EACjC,MAAM,IAAIG,EAAkB,gDAAgD,EAEhF,GAAIJ,GACA,GAAI,OAAOd,EAAI,SAAY,SACvB,MAAM,IAAIK,EAAW,8BAA8B,UAGlD,OAAOL,EAAI,SAAY,UAAY,EAAEA,EAAI,mBAAmB,YACjE,MAAM,IAAIK,EAAW,wDAAwD,EAEjF,IAAIc,EAAc,GACd,OAAOlB,GAAQ,aACfA,EAAM,MAAMA,EAAIK,EAAYN,CAAG,EAC/BmB,EAAc,IAElBC,EAAaL,EAAKd,EAAK,QAAQ,EAC/B,IAAMoB,EAAOC,EAAOC,EAAQ,QAAQpB,EAAKH,EAAI,aAAe,MAAQG,IAAO,OAASA,EAAK,EAAE,EAAGoB,EAAQ,OAAO,GAAG,EAAG,OAAOvB,EAAI,SAAY,SAAWuB,EAAQ,OAAOvB,EAAI,OAAO,EAAIA,EAAI,OAAO,EACxLwB,EAAYhB,EAAUR,EAAI,SAAS,EAEzC,GAAI,CADa,MAAMyB,GAAOV,EAAKd,EAAKuB,EAAWH,CAAI,EAEnD,MAAM,IAAIK,EAEd,IAAIC,EACAb,EACAa,EAAUnB,EAAUR,EAAI,OAAO,EAE1B,OAAOA,EAAI,SAAY,SAC5B2B,EAAUJ,EAAQ,OAAOvB,EAAI,OAAO,EAGpC2B,EAAU3B,EAAI,QAElB,IAAM4B,EAAS,CAAE,QAAAD,CAAQ,EAOzB,OANI3B,EAAI,YAAc,SAClB4B,EAAO,gBAAkBtB,GAEzBN,EAAI,SAAW,SACf4B,EAAO,kBAAoB5B,EAAI,QAE/BmB,EACO,CAAE,GAAGS,EAAQ,IAAA3B,CAAI,EAErB2B,CACX,CCpGA,eAAsBC,GAAcC,EAAKC,EAAKC,EAAS,CAInD,GAHIF,aAAe,aACfA,EAAMG,EAAQ,OAAOH,CAAG,GAExB,OAAOA,GAAQ,SACf,MAAM,IAAII,EAAW,4CAA4C,EAErE,GAAM,CAAE,EAAGC,EAAiB,EAAGC,EAAS,EAAGC,EAAW,OAAAC,CAAO,EAAIR,EAAI,MAAM,GAAG,EAC9E,GAAIQ,IAAW,EACX,MAAM,IAAIJ,EAAW,qBAAqB,EAE9C,IAAMK,EAAW,MAAMC,EAAgB,CAAE,QAAAJ,EAAS,UAAWD,EAAiB,UAAAE,CAAU,EAAGN,EAAKC,CAAO,EACjGS,EAAS,CAAE,QAASF,EAAS,QAAS,gBAAiBA,EAAS,eAAgB,EACtF,OAAI,OAAOR,GAAQ,WACR,CAAE,GAAGU,EAAQ,IAAKF,EAAS,GAAI,EAEnCE,CACX,CCpBA,IAAOC,GAASC,GAAS,KAAK,MAAMA,EAAK,QAAQ,EAAI,GAAI,ECKzD,IAAMC,GAAQ,sGACPC,EAASC,GAAQ,CACpB,IAAMC,EAAUH,GAAM,KAAKE,CAAG,EAC9B,GAAI,CAACC,EACD,MAAM,IAAI,UAAU,4BAA4B,EAEpD,IAAMC,EAAQ,WAAWD,EAAQ,CAAC,CAAC,EAEnC,OADaA,EAAQ,CAAC,EAAE,YAAY,EACtB,CACV,IAAK,MACL,IAAK,OACL,IAAK,SACL,IAAK,UACL,IAAK,IACD,OAAO,KAAK,MAAMC,CAAK,EAC3B,IAAK,SACL,IAAK,UACL,IAAK,MACL,IAAK,OACL,IAAK,IACD,OAAO,KAAK,MAAMA,EAAQ,EAAM,EACpC,IAAK,OACL,IAAK,QACL,IAAK,KACL,IAAK,MACL,IAAK,IACD,OAAO,KAAK,MAAMA,EAAQ,IAAI,EAClC,IAAK,MACL,IAAK,OACL,IAAK,IACD,OAAO,KAAK,MAAMA,EAAQ,KAAG,EACjC,IAAK,OACL,IAAK,QACL,IAAK,IACD,OAAO,KAAK,MAAMA,EAAQ,MAAI,EAClC,QACI,OAAO,KAAK,MAAMA,EAAQ,QAAI,CACtC,CACJ,ECtCA,IAAMC,GAAgBC,GAAUA,EAAM,YAAY,EAAE,QAAQ,iBAAkB,EAAE,EAC1EC,GAAwB,CAACC,EAAYC,IACnC,OAAOD,GAAe,SACfC,EAAU,SAASD,CAAU,EAEpC,MAAM,QAAQA,CAAU,EACjBC,EAAU,KAAK,IAAI,UAAU,IAAI,KAAK,IAAI,IAAID,CAAU,CAAC,CAAC,EAE9D,GAEJE,EAAQ,CAACC,EAAiBC,EAAgBC,EAAU,CAAC,IAAM,CAC9D,GAAM,CAAE,IAAAC,CAAI,EAAID,EAChB,GAAIC,IACC,OAAOH,EAAgB,KAAQ,UAC5BN,GAAaM,EAAgB,GAAG,IAAMN,GAAaS,CAAG,GAC1D,MAAM,IAAIC,EAAyB,oCAAqC,MAAO,cAAc,EAEjG,IAAIC,EACJ,GAAI,CACAA,EAAU,KAAK,MAAMC,EAAQ,OAAOL,CAAc,CAAC,CACvD,MACA,CACA,CACA,GAAI,CAACM,EAASF,CAAO,EACjB,MAAM,IAAIG,EAAW,gDAAgD,EAEzE,GAAM,CAAE,eAAAC,EAAiB,CAAC,EAAG,OAAAC,EAAQ,QAAAC,EAAS,SAAAC,EAAU,YAAAC,CAAY,EAAIX,EACpEW,IAAgB,QAChBJ,EAAe,KAAK,KAAK,EACzBG,IAAa,QACbH,EAAe,KAAK,KAAK,EACzBE,IAAY,QACZF,EAAe,KAAK,KAAK,EACzBC,IAAW,QACXD,EAAe,KAAK,KAAK,EAC7B,QAAWK,KAAS,IAAI,IAAIL,EAAe,QAAQ,CAAC,EAChD,GAAI,EAAEK,KAAST,GACX,MAAM,IAAID,EAAyB,qBAAqBU,WAAgBA,EAAO,SAAS,EAGhG,GAAIJ,GAAU,EAAE,MAAM,QAAQA,CAAM,EAAIA,EAAS,CAACA,CAAM,GAAG,SAASL,EAAQ,GAAG,EAC3E,MAAM,IAAID,EAAyB,+BAAgC,MAAO,cAAc,EAE5F,GAAIO,GAAWN,EAAQ,MAAQM,EAC3B,MAAM,IAAIP,EAAyB,+BAAgC,MAAO,cAAc,EAE5F,GAAIQ,GACA,CAAChB,GAAsBS,EAAQ,IAAK,OAAOO,GAAa,SAAW,CAACA,CAAQ,EAAIA,CAAQ,EACxF,MAAM,IAAIR,EAAyB,+BAAgC,MAAO,cAAc,EAE5F,IAAIW,EACJ,OAAQ,OAAOb,EAAQ,eAAgB,CACnC,IAAK,SACDa,EAAYC,EAAKd,EAAQ,cAAc,EACvC,MACJ,IAAK,SACDa,EAAYb,EAAQ,eACpB,MACJ,IAAK,YACDa,EAAY,EACZ,MACJ,QACI,MAAM,IAAI,UAAU,oCAAoC,CAChE,CACA,GAAM,CAAE,YAAAE,CAAY,EAAIf,EAClBgB,EAAMC,GAAMF,GAAe,IAAI,IAAM,EAC3C,IAAKZ,EAAQ,MAAQ,QAAaQ,IAAgB,OAAOR,EAAQ,KAAQ,SACrE,MAAM,IAAID,EAAyB,+BAAgC,MAAO,SAAS,EAEvF,GAAIC,EAAQ,MAAQ,OAAW,CAC3B,GAAI,OAAOA,EAAQ,KAAQ,SACvB,MAAM,IAAID,EAAyB,+BAAgC,MAAO,SAAS,EAEvF,GAAIC,EAAQ,IAAMa,EAAMH,EACpB,MAAM,IAAIX,EAAyB,qCAAsC,MAAO,cAAc,EAGtG,GAAIC,EAAQ,MAAQ,OAAW,CAC3B,GAAI,OAAOA,EAAQ,KAAQ,SACvB,MAAM,IAAID,EAAyB,+BAAgC,MAAO,SAAS,EAEvF,GAAIC,EAAQ,KAAOa,EAAMH,EACrB,MAAM,IAAIK,EAAW,qCAAsC,MAAO,cAAc,EAGxF,GAAIP,EAAa,CACb,IAAMQ,EAAMH,EAAMb,EAAQ,IACpBiB,EAAM,OAAOT,GAAgB,SAAWA,EAAcG,EAAKH,CAAW,EAC5E,GAAIQ,EAAMN,EAAYO,EAClB,MAAM,IAAIF,EAAW,2DAA4D,MAAO,cAAc,EAE1G,GAAIC,EAAM,EAAIN,EACV,MAAM,IAAIX,EAAyB,gEAAiE,MAAO,cAAc,EAGjI,OAAOC,CACX,EClGA,eAAsBkB,GAAUC,EAAKC,EAAKC,EAAS,CAC/C,IAAIC,EACJ,IAAMC,EAAW,MAAMC,GAAcL,EAAKC,EAAKC,CAAO,EACtD,GAAM,GAAAC,EAAKC,EAAS,gBAAgB,QAAU,MAAQD,IAAO,SAAkBA,EAAG,SAAS,KAAK,GAAMC,EAAS,gBAAgB,MAAQ,GACnI,MAAM,IAAIE,EAAW,qCAAqC,EAG9D,IAAMC,EAAS,CAAE,QADDC,EAAWJ,EAAS,gBAAiBA,EAAS,QAASF,CAAO,EACpD,gBAAiBE,EAAS,eAAgB,EACpE,OAAI,OAAOH,GAAQ,WACR,CAAE,GAAGM,EAAQ,IAAKH,EAAS,GAAI,EAEnCG,CACX,CCGA,IAAME,GACF,oEAEJ,eAAsBC,EAClBC,EACAC,EACgB,CAEhB,GAAIA,EAAI,YAAc,OAClB,MAAO,GAIX,IAAMC,EAAQJ,GAAiB,KAAKE,EAAI,QAAQ,IAAI,eAAe,GAAK,EAAE,EAC1E,GAAI,CAACE,GAAS,CAACA,EAAM,CAAC,EAClB,MAAO,GAIX,IAAMC,EAAK,MAAMC,EAAWH,EAAI,WAAY,OAAO,EACnD,GAAI,CACA,MAAMI,GAAUH,EAAM,CAAC,EAAGC,EAAI,CAC1B,OAAQ,qBACR,SAAUF,EAAI,eACd,WAAY,CAAC,OAAO,EAEpB,eAAgB,GACpB,CAAC,CACL,OAASK,EAAP,CACE,eAAQ,MAAM,2BAA6BA,CAAG,EACvC,EACX,CAEA,MAAO,EACX,CChDI,IAAAC,GAAW,WCgBf,IAAMC,GAASC,GAAO,EAEjB,IACG,yBACA,MAAOC,EAAeC,IAAwC,CAE1D,GAAI,CADS,MAAMC,EAAiBF,EAAKC,CAAG,EAExC,OAAO,IAAI,SAAS,eAAgB,CAAE,OAAQ,GAAI,CAAC,EAIvD,IAAME,EAAmB,CAAC,EACpBC,EAAe,CAAC,EAChBC,EAAe,CAAC,EAChBC,EAAM,OAAO,KAAKL,CAAG,EAC3B,QAASM,EAAI,EAAGA,EAAID,EAAI,OAAQC,IAAK,CACjC,GAAI,CAACD,EAAIC,CAAC,EACN,SAEJ,IAAMC,EAAMP,EAAIK,EAAIC,CAAC,CAAC,EACtB,GAAI,GAACC,GAAO,OAAOA,GAAO,UAAY,CAACA,EAAI,aAG3C,OAAQA,EAAI,YAAY,KAAM,CAC1B,IAAK,cACL,IAAK,cACDJ,EAAG,KAAKE,EAAIC,CAAC,CAAC,EACd,MACJ,IAAK,cACL,IAAK,QACDJ,EAAO,KAAKG,EAAIC,CAAC,CAAC,EAClB,MACJ,IAAK,WAEDF,EAAG,KAAKC,EAAIC,CAAC,CAAC,EACd,KACR,EAGJ,IAAME,EAAM,KAAK,UAAU,CACvB,QAAAC,GACA,OAAQP,GAAUA,EAAO,OAASA,EAAS,OAC3C,GAAIC,GAAMA,EAAG,OAASA,EAAK,OAC3B,GAAIC,GAAMA,EAAG,OAASA,EAAK,MAC/B,CAAC,EACD,OAAO,IAAI,SAASI,EAAK,CACrB,QAAS,CACL,eAAgB,kBACpB,CACJ,CAAC,CACL,CACJ,EAGC,IACG,sBACA,MAAOT,EAAeC,IAAwC,CAC1D,GAAM,CAAE,UAAAU,EAAW,IAAAC,EAAK,SAAAC,CAAS,EAAI,MAAMC,GAAed,EAAKC,CAAG,EAClE,GAAIY,EACA,OAAOA,EAGX,IAAME,EAAM,MAAMJ,EAAW,IAAIC,EAAM,QAAQ,EAC/C,OAAKG,EAIE,IAAI,SAASA,EAAK,CAAE,OAAQ,GAAI,CAAC,EAH7B,IAAI,SAAS,GAAI,CAAE,OAAQ,GAAI,CAAC,CAI/C,CACJ,EAGC,KACG,sBACA,MAAOf,EAAeC,IAAwC,CAC1D,GAAM,CAAE,UAAAU,EAAW,IAAAC,EAAK,SAAAC,CAAS,EAAI,MAAMC,GAAed,EAAKC,CAAG,EAClE,GAAIY,EACA,OAAOA,EAGX,IAAIG,EACEC,EAAS,IAAI,IAAIjB,EAAI,GAAG,EACxBkB,EAAW,SAASD,EAAO,aAAa,IAAI,KAAK,GAAK,GAAI,EAAE,EAClE,OAAIC,EAAW,IACXF,EAAgBE,GAEpB,MAAMP,EAAW,IAAIC,EAAMZ,EAAI,KAAO,CAAE,cAAAgB,CAAc,CAAC,EAEhD,IAAI,SAAS,GAAI,CAAE,OAAQ,GAAI,CAAC,CAC3C,CACJ,EAGC,OACG,sBACA,MAAOhB,EAAeC,IAAwC,CAC1D,GAAM,CAAE,UAAAU,EAAW,IAAAC,EAAK,SAAAC,CAAS,EAAI,MAAMC,GAAed,EAAKC,CAAG,EAClE,OAAIY,IAIJ,MAAMF,EAAW,OAAOC,CAAI,EAErB,IAAI,SAAS,GAAI,CAAE,OAAQ,GAAI,CAAC,EAC3C,CACJ,EAGC,KACG,iBACA,MAAOZ,EAAeC,IAAwC,CAC1D,GAAM,CAAE,MAAAkB,EAAO,SAAAN,CAAS,EAAI,MAAMO,GAAkBpB,EAAKC,CAAG,EAC5D,GAAIY,EACA,OAAOA,EAGX,IAAIQ,EAAU,MAAMrB,EAAI,KAAK,EAC7B,aAAMmB,EAAO,KAAKE,CAAO,EAClB,IAAI,SAAS,GAAI,CAAE,OAAQ,GAAI,CAAC,CAC3C,CACJ,EAGC,IAAI,IAAK,IACC,IAAI,SAAS,YAAa,CAAE,OAAQ,GAAI,CAAC,CACnD,EAGL,eAAeP,GACXd,EACAC,EAKD,CACC,GAAI,CAACD,GAAK,MAAQ,CAACA,EAAI,QAAQ,WAAa,CAACA,EAAI,QAAQ,IACrD,MAAO,CAAE,SAAU,IAAI,SAAS,cAAe,CAAE,OAAQ,GAAI,CAAC,CAAE,EAEpE,IAAMW,EAAYV,EAAID,EAAI,OAAO,SAAS,EAC1C,OACI,OAAOW,GAAa,UACpB,CAAC,CAAC,cAAe,aAAa,EAAE,SAASA,GAAW,aAAa,IAAI,EAE9D,CACH,SAAU,IAAI,SACV,8BAA8BX,EAAI,OAAO,MACzC,CAAE,OAAQ,GAAI,CAClB,CACJ,EAGS,MAAME,EAAiBF,EAAKC,CAAG,EAKrC,CAAE,UAAAU,EAAW,IAAKX,EAAI,OAAO,GAAI,EAH7B,CAAE,SAAU,IAAI,SAAS,eAAgB,CAAE,OAAQ,GAAI,CAAC,CAAE,CAIzE,CAGA,eAAeoB,GACXpB,EACAC,EACuD,CACvD,GAAI,CAACD,GAAK,MAAQ,CAACA,EAAI,QAAQ,MAC3B,MAAO,CAAE,SAAU,IAAI,SAAS,cAAe,CAAE,OAAQ,GAAI,CAAC,CAAE,EAEpE,IAAMmB,EAAQlB,EAAID,EAAI,OAAO,KAAK,EAClC,OACI,OAAOmB,GAAS,UAChB,CAAC,CAAC,cAAe,OAAO,EAAE,SAASA,GAAO,aAAa,IAAI,EAEpD,CACH,SAAU,IAAI,SACV,iCAAiCnB,EAAI,OAAO,SAC5C,CAAE,OAAQ,GAAI,CAClB,CACJ,EAGS,MAAME,EAAiBF,EAAKC,CAAG,EAKrC,CAAE,MAAAkB,CAAM,EAHJ,CAAE,SAAU,IAAI,SAAS,eAAgB,CAAE,OAAQ,GAAI,CAAC,CAAE,CAIzE,CAEA,IAAOG,GAAQ,CACX,MAAOxB,GAAO,MAClB", - "names": ["e", "r", "a", "o", "t", "p", "l", "s", "c", "webcrypto_default", "isCryptoKey", "key", "encoder", "decoder", "MAX_INT32", "concat", "buffers", "size", "acc", "length", "buf", "i", "buffer", "decodeBase64", "encoded", "binary", "bytes", "i", "decode", "input", "decoder", "JOSEError", "message", "_a", "JWTClaimValidationFailed", "claim", "reason", "JWTExpired", "JOSEAlgNotAllowed", "JOSENotSupported", "JWSInvalid", "JOSEError", "JWTInvalid", "JWSSignatureVerificationFailed", "JOSEError", "random_default", "webcrypto_default", "unusable", "name", "prop", "isAlgorithm", "algorithm", "getHashLength", "hash", "getNamedCurve", "alg", "checkUsage", "key", "usages", "expected", "msg", "last", "checkSigCryptoKey", "message", "msg", "actual", "types", "last", "invalid_key_input_default", "withAlg", "alg", "is_key_like_default", "key", "isCryptoKey", "types", "isDisjoint", "headers", "sources", "acc", "header", "parameters", "parameter", "is_disjoint_default", "isObjectLike", "value", "isObject", "input", "proto", "check_key_length_default", "alg", "key", "modulusLength", "findOid", "keyData", "oid", "from", "i", "sub", "value", "index", "getNamedCurve", "JOSENotSupported", "genericImport", "replace", "keyFormat", "pem", "alg", "options", "_a", "algorithm", "keyUsages", "c", "isPublic", "namedCurve", "webcrypto_default", "fromSPKI", "pem", "alg", "options", "genericImport", "importSPKI", "spki", "alg", "options", "fromSPKI", "symmetricTypeCheck", "alg", "key", "is_key_like_default", "withAlg", "types", "asymmetricTypeCheck", "usage", "checkKeyType", "check_key_type_default", "validateCrit", "Err", "recognizedDefault", "recognizedOption", "protectedHeader", "joseHeader", "input", "recognized", "parameter", "JOSENotSupported", "validate_crit_default", "validateAlgorithms", "option", "algorithms", "s", "validate_algorithms_default", "unprotected", "subtleDsa", "alg", "algorithm", "hash", "JOSENotSupported", "getCryptoKey", "alg", "key", "usage", "isCryptoKey", "checkSigCryptoKey", "invalid_key_input_default", "types", "webcrypto_default", "verify", "alg", "key", "signature", "data", "cryptoKey", "getCryptoKey", "check_key_length_default", "algorithm", "subtleDsa", "webcrypto_default", "verify_default", "flattenedVerify", "jws", "key", "options", "_a", "isObject", "JWSInvalid", "parsedProt", "protectedHeader", "decode", "decoder", "is_disjoint_default", "joseHeader", "extensions", "validate_crit_default", "b64", "alg", "algorithms", "validate_algorithms_default", "JOSEAlgNotAllowed", "resolvedKey", "check_key_type_default", "data", "concat", "encoder", "signature", "verify_default", "JWSSignatureVerificationFailed", "payload", "result", "compactVerify", "jws", "key", "options", "decoder", "JWSInvalid", "protectedHeader", "payload", "signature", "length", "verified", "flattenedVerify", "result", "epoch_default", "date", "REGEX", "secs_default", "str", "matched", "value", "normalizeTyp", "value", "checkAudiencePresence", "audPayload", "audOption", "jwt_claims_set_default", "protectedHeader", "encodedPayload", "options", "typ", "JWTClaimValidationFailed", "payload", "decoder", "isObject", "JWTInvalid", "requiredClaims", "issuer", "subject", "audience", "maxTokenAge", "claim", "tolerance", "secs_default", "currentDate", "now", "epoch_default", "JWTExpired", "age", "max", "jwtVerify", "jwt", "key", "options", "_a", "verified", "compactVerify", "JWTInvalid", "result", "jwt_claims_set_default", "tokenHeaderMatch", "AuthorizeRequest", "req", "env", "match", "pk", "importSPKI", "jwtVerify", "err", "version", "router", "e", "req", "env", "AuthorizeRequest", "queues", "kv", "r2", "all", "i", "obj", "res", "version", "namespace", "key", "errorRes", "setupKVRequest", "val", "expirationTtl", "reqUrl", "ttlParam", "queue", "setupQueueRequest", "message", "worker_default"] + "sources": ["../../worker-src/node_modules/itty-router/dist/itty-router.mjs", "../../worker-src/node_modules/jose/dist/browser/runtime/webcrypto.js", "../../worker-src/node_modules/jose/dist/browser/lib/buffer_utils.js", "../../worker-src/node_modules/jose/dist/browser/runtime/base64url.js", "../../worker-src/node_modules/jose/dist/browser/util/errors.js", "../../worker-src/node_modules/jose/dist/browser/runtime/random.js", "../../worker-src/node_modules/jose/dist/browser/lib/crypto_key.js", "../../worker-src/node_modules/jose/dist/browser/lib/invalid_key_input.js", "../../worker-src/node_modules/jose/dist/browser/runtime/is_key_like.js", "../../worker-src/node_modules/jose/dist/browser/lib/is_disjoint.js", "../../worker-src/node_modules/jose/dist/browser/lib/is_object.js", "../../worker-src/node_modules/jose/dist/browser/runtime/check_key_length.js", "../../worker-src/node_modules/jose/dist/browser/runtime/asn1.js", "../../worker-src/node_modules/jose/dist/browser/key/import.js", "../../worker-src/node_modules/jose/dist/browser/lib/check_key_type.js", "../../worker-src/node_modules/jose/dist/browser/lib/validate_crit.js", "../../worker-src/node_modules/jose/dist/browser/lib/validate_algorithms.js", "../../worker-src/node_modules/jose/dist/browser/runtime/subtle_dsa.js", "../../worker-src/node_modules/jose/dist/browser/runtime/get_sign_verify_key.js", "../../worker-src/node_modules/jose/dist/browser/runtime/verify.js", "../../worker-src/node_modules/jose/dist/browser/jws/flattened/verify.js", "../../worker-src/node_modules/jose/dist/browser/jws/compact/verify.js", "../../worker-src/node_modules/jose/dist/browser/lib/epoch.js", "../../worker-src/node_modules/jose/dist/browser/lib/secs.js", "../../worker-src/node_modules/jose/dist/browser/lib/jwt_claims_set.js", "../../worker-src/node_modules/jose/dist/browser/jwt/verify.js", "../../worker-src/lib/jwt-auth.ts", "../../worker-src/package.json", "../../worker-src/worker.ts"], + "sourcesContent": ["const e=({base:e=\"\",routes:r=[]}={})=>({__proto__:new Proxy({},{get:(a,o,t)=>(a,...p)=>r.push([o.toUpperCase(),RegExp(`^${(e+a).replace(/(\\/?)\\*/g,\"($1.*)?\").replace(/(\\/$)|((?<=\\/)\\/)/,\"\").replace(/(:(\\w+)\\+)/,\"(?<$2>.*)\").replace(/:(\\w+)(\\?)?(\\.)?/g,\"$2(?<$1>[^/]+)$2$3\").replace(/\\.(?=[\\w(])/,\"\\\\.\").replace(/\\)\\.\\?\\(([^\\[]+)\\[\\^/g,\"?)\\\\.?($1(?<=\\\\.)[^\\\\.\")}/*$`),p])&&t}),routes:r,async handle(e,...a){let o,t,p=new URL(e.url),l=e.query={};for(let[e,r]of p.searchParams)l[e]=void 0===l[e]?r:[l[e],r].flat();for(let[l,s,c]of r)if((l===e.method||\"ALL\"===l)&&(t=p.pathname.match(s))){e.params=t.groups||{};for(let r of c)if(void 0!==(o=await r(e.proxy||e,...a)))return o}}});export{e as Router};\n", "export default crypto;\nexport const isCryptoKey = (key) => key instanceof CryptoKey;\n", "import digest from '../runtime/digest.js';\nexport const encoder = new TextEncoder();\nexport const decoder = new TextDecoder();\nconst MAX_INT32 = 2 ** 32;\nexport function concat(...buffers) {\n const size = buffers.reduce((acc, { length }) => acc + length, 0);\n const buf = new Uint8Array(size);\n let i = 0;\n buffers.forEach((buffer) => {\n buf.set(buffer, i);\n i += buffer.length;\n });\n return buf;\n}\nexport function p2s(alg, p2sInput) {\n return concat(encoder.encode(alg), new Uint8Array([0]), p2sInput);\n}\nfunction writeUInt32BE(buf, value, offset) {\n if (value < 0 || value >= MAX_INT32) {\n throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);\n }\n buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);\n}\nexport function uint64be(value) {\n const high = Math.floor(value / MAX_INT32);\n const low = value % MAX_INT32;\n const buf = new Uint8Array(8);\n writeUInt32BE(buf, high, 0);\n writeUInt32BE(buf, low, 4);\n return buf;\n}\nexport function uint32be(value) {\n const buf = new Uint8Array(4);\n writeUInt32BE(buf, value);\n return buf;\n}\nexport function lengthAndInput(input) {\n return concat(uint32be(input.length), input);\n}\nexport async function concatKdf(secret, bits, value) {\n const iterations = Math.ceil((bits >> 3) / 32);\n const res = new Uint8Array(iterations * 32);\n for (let iter = 0; iter < iterations; iter++) {\n const buf = new Uint8Array(4 + secret.length + value.length);\n buf.set(uint32be(iter + 1));\n buf.set(secret, 4);\n buf.set(value, 4 + secret.length);\n res.set(await digest('sha256', buf), iter * 32);\n }\n return res.slice(0, bits >> 3);\n}\n", "import { encoder, decoder } from '../lib/buffer_utils.js';\nexport const encodeBase64 = (input) => {\n let unencoded = input;\n if (typeof unencoded === 'string') {\n unencoded = encoder.encode(unencoded);\n }\n const CHUNK_SIZE = 0x8000;\n const arr = [];\n for (let i = 0; i < unencoded.length; i += CHUNK_SIZE) {\n arr.push(String.fromCharCode.apply(null, unencoded.subarray(i, i + CHUNK_SIZE)));\n }\n return btoa(arr.join(''));\n};\nexport const encode = (input) => {\n return encodeBase64(input).replace(/=/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');\n};\nexport const decodeBase64 = (encoded) => {\n const binary = atob(encoded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n};\nexport const decode = (input) => {\n let encoded = input;\n if (encoded instanceof Uint8Array) {\n encoded = decoder.decode(encoded);\n }\n encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/\\s/g, '');\n try {\n return decodeBase64(encoded);\n }\n catch (_a) {\n throw new TypeError('The input to be decoded is not correctly encoded.');\n }\n};\n", "export class JOSEError extends Error {\n static get code() {\n return 'ERR_JOSE_GENERIC';\n }\n constructor(message) {\n var _a;\n super(message);\n this.code = 'ERR_JOSE_GENERIC';\n this.name = this.constructor.name;\n (_a = Error.captureStackTrace) === null || _a === void 0 ? void 0 : _a.call(Error, this, this.constructor);\n }\n}\nexport class JWTClaimValidationFailed extends JOSEError {\n static get code() {\n return 'ERR_JWT_CLAIM_VALIDATION_FAILED';\n }\n constructor(message, claim = 'unspecified', reason = 'unspecified') {\n super(message);\n this.code = 'ERR_JWT_CLAIM_VALIDATION_FAILED';\n this.claim = claim;\n this.reason = reason;\n }\n}\nexport class JWTExpired extends JOSEError {\n static get code() {\n return 'ERR_JWT_EXPIRED';\n }\n constructor(message, claim = 'unspecified', reason = 'unspecified') {\n super(message);\n this.code = 'ERR_JWT_EXPIRED';\n this.claim = claim;\n this.reason = reason;\n }\n}\nexport class JOSEAlgNotAllowed extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JOSE_ALG_NOT_ALLOWED';\n }\n static get code() {\n return 'ERR_JOSE_ALG_NOT_ALLOWED';\n }\n}\nexport class JOSENotSupported extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JOSE_NOT_SUPPORTED';\n }\n static get code() {\n return 'ERR_JOSE_NOT_SUPPORTED';\n }\n}\nexport class JWEDecryptionFailed extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWE_DECRYPTION_FAILED';\n this.message = 'decryption operation failed';\n }\n static get code() {\n return 'ERR_JWE_DECRYPTION_FAILED';\n }\n}\nexport class JWEDecompressionFailed extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWE_DECOMPRESSION_FAILED';\n this.message = 'decompression operation failed';\n }\n static get code() {\n return 'ERR_JWE_DECOMPRESSION_FAILED';\n }\n}\nexport class JWEInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWE_INVALID';\n }\n static get code() {\n return 'ERR_JWE_INVALID';\n }\n}\nexport class JWSInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWS_INVALID';\n }\n static get code() {\n return 'ERR_JWS_INVALID';\n }\n}\nexport class JWTInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWT_INVALID';\n }\n static get code() {\n return 'ERR_JWT_INVALID';\n }\n}\nexport class JWKInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWK_INVALID';\n }\n static get code() {\n return 'ERR_JWK_INVALID';\n }\n}\nexport class JWKSInvalid extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWKS_INVALID';\n }\n static get code() {\n return 'ERR_JWKS_INVALID';\n }\n}\nexport class JWKSNoMatchingKey extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWKS_NO_MATCHING_KEY';\n this.message = 'no applicable key found in the JSON Web Key Set';\n }\n static get code() {\n return 'ERR_JWKS_NO_MATCHING_KEY';\n }\n}\nexport class JWKSMultipleMatchingKeys extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';\n this.message = 'multiple matching keys found in the JSON Web Key Set';\n }\n static get code() {\n return 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';\n }\n}\nSymbol.asyncIterator;\nexport class JWKSTimeout extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWKS_TIMEOUT';\n this.message = 'request timed out';\n }\n static get code() {\n return 'ERR_JWKS_TIMEOUT';\n }\n}\nexport class JWSSignatureVerificationFailed extends JOSEError {\n constructor() {\n super(...arguments);\n this.code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';\n this.message = 'signature verification failed';\n }\n static get code() {\n return 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';\n }\n}\n", "import crypto from './webcrypto.js';\nexport default crypto.getRandomValues.bind(crypto);\n", "function unusable(name, prop = 'algorithm.name') {\n return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);\n}\nfunction isAlgorithm(algorithm, name) {\n return algorithm.name === name;\n}\nfunction getHashLength(hash) {\n return parseInt(hash.name.slice(4), 10);\n}\nfunction getNamedCurve(alg) {\n switch (alg) {\n case 'ES256':\n return 'P-256';\n case 'ES384':\n return 'P-384';\n case 'ES512':\n return 'P-521';\n default:\n throw new Error('unreachable');\n }\n}\nfunction checkUsage(key, usages) {\n if (usages.length && !usages.some((expected) => key.usages.includes(expected))) {\n let msg = 'CryptoKey does not support this operation, its usages must include ';\n if (usages.length > 2) {\n const last = usages.pop();\n msg += `one of ${usages.join(', ')}, or ${last}.`;\n }\n else if (usages.length === 2) {\n msg += `one of ${usages[0]} or ${usages[1]}.`;\n }\n else {\n msg += `${usages[0]}.`;\n }\n throw new TypeError(msg);\n }\n}\nexport function checkSigCryptoKey(key, alg, ...usages) {\n switch (alg) {\n case 'HS256':\n case 'HS384':\n case 'HS512': {\n if (!isAlgorithm(key.algorithm, 'HMAC'))\n throw unusable('HMAC');\n const expected = parseInt(alg.slice(2), 10);\n const actual = getHashLength(key.algorithm.hash);\n if (actual !== expected)\n throw unusable(`SHA-${expected}`, 'algorithm.hash');\n break;\n }\n case 'RS256':\n case 'RS384':\n case 'RS512': {\n if (!isAlgorithm(key.algorithm, 'RSASSA-PKCS1-v1_5'))\n throw unusable('RSASSA-PKCS1-v1_5');\n const expected = parseInt(alg.slice(2), 10);\n const actual = getHashLength(key.algorithm.hash);\n if (actual !== expected)\n throw unusable(`SHA-${expected}`, 'algorithm.hash');\n break;\n }\n case 'PS256':\n case 'PS384':\n case 'PS512': {\n if (!isAlgorithm(key.algorithm, 'RSA-PSS'))\n throw unusable('RSA-PSS');\n const expected = parseInt(alg.slice(2), 10);\n const actual = getHashLength(key.algorithm.hash);\n if (actual !== expected)\n throw unusable(`SHA-${expected}`, 'algorithm.hash');\n break;\n }\n case 'EdDSA': {\n if (key.algorithm.name !== 'Ed25519' && key.algorithm.name !== 'Ed448') {\n throw unusable('Ed25519 or Ed448');\n }\n break;\n }\n case 'ES256':\n case 'ES384':\n case 'ES512': {\n if (!isAlgorithm(key.algorithm, 'ECDSA'))\n throw unusable('ECDSA');\n const expected = getNamedCurve(alg);\n const actual = key.algorithm.namedCurve;\n if (actual !== expected)\n throw unusable(expected, 'algorithm.namedCurve');\n break;\n }\n default:\n throw new TypeError('CryptoKey does not support this operation');\n }\n checkUsage(key, usages);\n}\nexport function checkEncCryptoKey(key, alg, ...usages) {\n switch (alg) {\n case 'A128GCM':\n case 'A192GCM':\n case 'A256GCM': {\n if (!isAlgorithm(key.algorithm, 'AES-GCM'))\n throw unusable('AES-GCM');\n const expected = parseInt(alg.slice(1, 4), 10);\n const actual = key.algorithm.length;\n if (actual !== expected)\n throw unusable(expected, 'algorithm.length');\n break;\n }\n case 'A128KW':\n case 'A192KW':\n case 'A256KW': {\n if (!isAlgorithm(key.algorithm, 'AES-KW'))\n throw unusable('AES-KW');\n const expected = parseInt(alg.slice(1, 4), 10);\n const actual = key.algorithm.length;\n if (actual !== expected)\n throw unusable(expected, 'algorithm.length');\n break;\n }\n case 'ECDH': {\n switch (key.algorithm.name) {\n case 'ECDH':\n case 'X25519':\n case 'X448':\n break;\n default:\n throw unusable('ECDH, X25519, or X448');\n }\n break;\n }\n case 'PBES2-HS256+A128KW':\n case 'PBES2-HS384+A192KW':\n case 'PBES2-HS512+A256KW':\n if (!isAlgorithm(key.algorithm, 'PBKDF2'))\n throw unusable('PBKDF2');\n break;\n case 'RSA-OAEP':\n case 'RSA-OAEP-256':\n case 'RSA-OAEP-384':\n case 'RSA-OAEP-512': {\n if (!isAlgorithm(key.algorithm, 'RSA-OAEP'))\n throw unusable('RSA-OAEP');\n const expected = parseInt(alg.slice(9), 10) || 1;\n const actual = getHashLength(key.algorithm.hash);\n if (actual !== expected)\n throw unusable(`SHA-${expected}`, 'algorithm.hash');\n break;\n }\n default:\n throw new TypeError('CryptoKey does not support this operation');\n }\n checkUsage(key, usages);\n}\n", "function message(msg, actual, ...types) {\n if (types.length > 2) {\n const last = types.pop();\n msg += `one of type ${types.join(', ')}, or ${last}.`;\n }\n else if (types.length === 2) {\n msg += `one of type ${types[0]} or ${types[1]}.`;\n }\n else {\n msg += `of type ${types[0]}.`;\n }\n if (actual == null) {\n msg += ` Received ${actual}`;\n }\n else if (typeof actual === 'function' && actual.name) {\n msg += ` Received function ${actual.name}`;\n }\n else if (typeof actual === 'object' && actual != null) {\n if (actual.constructor && actual.constructor.name) {\n msg += ` Received an instance of ${actual.constructor.name}`;\n }\n }\n return msg;\n}\nexport default (actual, ...types) => {\n return message('Key must be ', actual, ...types);\n};\nexport function withAlg(alg, actual, ...types) {\n return message(`Key for the ${alg} algorithm must be `, actual, ...types);\n}\n", "import { isCryptoKey } from './webcrypto.js';\nexport default (key) => {\n return isCryptoKey(key);\n};\nexport const types = ['CryptoKey'];\n", "const isDisjoint = (...headers) => {\n const sources = headers.filter(Boolean);\n if (sources.length === 0 || sources.length === 1) {\n return true;\n }\n let acc;\n for (const header of sources) {\n const parameters = Object.keys(header);\n if (!acc || acc.size === 0) {\n acc = new Set(parameters);\n continue;\n }\n for (const parameter of parameters) {\n if (acc.has(parameter)) {\n return false;\n }\n acc.add(parameter);\n }\n }\n return true;\n};\nexport default isDisjoint;\n", "function isObjectLike(value) {\n return typeof value === 'object' && value !== null;\n}\nexport default function isObject(input) {\n if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') {\n return false;\n }\n if (Object.getPrototypeOf(input) === null) {\n return true;\n }\n let proto = input;\n while (Object.getPrototypeOf(proto) !== null) {\n proto = Object.getPrototypeOf(proto);\n }\n return Object.getPrototypeOf(input) === proto;\n}\n", "export default (alg, key) => {\n if (alg.startsWith('RS') || alg.startsWith('PS')) {\n const { modulusLength } = key.algorithm;\n if (typeof modulusLength !== 'number' || modulusLength < 2048) {\n throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);\n }\n }\n};\n", "import crypto, { isCryptoKey } from './webcrypto.js';\nimport invalidKeyInput from '../lib/invalid_key_input.js';\nimport { encodeBase64, decodeBase64 } from './base64url.js';\nimport formatPEM from '../lib/format_pem.js';\nimport { JOSENotSupported } from '../util/errors.js';\nimport { types } from './is_key_like.js';\nconst genericExport = async (keyType, keyFormat, key) => {\n if (!isCryptoKey(key)) {\n throw new TypeError(invalidKeyInput(key, ...types));\n }\n if (!key.extractable) {\n throw new TypeError('CryptoKey is not extractable');\n }\n if (key.type !== keyType) {\n throw new TypeError(`key is not a ${keyType} key`);\n }\n return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`);\n};\nexport const toSPKI = (key) => {\n return genericExport('public', 'spki', key);\n};\nexport const toPKCS8 = (key) => {\n return genericExport('private', 'pkcs8', key);\n};\nconst findOid = (keyData, oid, from = 0) => {\n if (from === 0) {\n oid.unshift(oid.length);\n oid.unshift(0x06);\n }\n let i = keyData.indexOf(oid[0], from);\n if (i === -1)\n return false;\n const sub = keyData.subarray(i, i + oid.length);\n if (sub.length !== oid.length)\n return false;\n return sub.every((value, index) => value === oid[index]) || findOid(keyData, oid, i + 1);\n};\nconst getNamedCurve = (keyData) => {\n switch (true) {\n case findOid(keyData, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]):\n return 'P-256';\n case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x22]):\n return 'P-384';\n case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]):\n return 'P-521';\n case findOid(keyData, [0x2b, 0x65, 0x6e]):\n return 'X25519';\n case findOid(keyData, [0x2b, 0x65, 0x6f]):\n return 'X448';\n case findOid(keyData, [0x2b, 0x65, 0x70]):\n return 'Ed25519';\n case findOid(keyData, [0x2b, 0x65, 0x71]):\n return 'Ed448';\n default:\n throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type');\n }\n};\nconst genericImport = async (replace, keyFormat, pem, alg, options) => {\n var _a;\n let algorithm;\n let keyUsages;\n const keyData = new Uint8Array(atob(pem.replace(replace, ''))\n .split('')\n .map((c) => c.charCodeAt(0)));\n const isPublic = keyFormat === 'spki';\n switch (alg) {\n case 'PS256':\n case 'PS384':\n case 'PS512':\n algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'RS256':\n case 'RS384':\n case 'RS512':\n algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'RSA-OAEP':\n case 'RSA-OAEP-256':\n case 'RSA-OAEP-384':\n case 'RSA-OAEP-512':\n algorithm = {\n name: 'RSA-OAEP',\n hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`,\n };\n keyUsages = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];\n break;\n case 'ES256':\n algorithm = { name: 'ECDSA', namedCurve: 'P-256' };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'ES384':\n algorithm = { name: 'ECDSA', namedCurve: 'P-384' };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'ES512':\n algorithm = { name: 'ECDSA', namedCurve: 'P-521' };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n case 'ECDH-ES':\n case 'ECDH-ES+A128KW':\n case 'ECDH-ES+A192KW':\n case 'ECDH-ES+A256KW': {\n const namedCurve = getNamedCurve(keyData);\n algorithm = namedCurve.startsWith('P-') ? { name: 'ECDH', namedCurve } : { name: namedCurve };\n keyUsages = isPublic ? [] : ['deriveBits'];\n break;\n }\n case 'EdDSA':\n algorithm = { name: getNamedCurve(keyData) };\n keyUsages = isPublic ? ['verify'] : ['sign'];\n break;\n default:\n throw new JOSENotSupported('Invalid or unsupported \"alg\" (Algorithm) value');\n }\n return crypto.subtle.importKey(keyFormat, keyData, algorithm, (_a = options === null || options === void 0 ? void 0 : options.extractable) !== null && _a !== void 0 ? _a : false, keyUsages);\n};\nexport const fromPKCS8 = (pem, alg, options) => {\n return genericImport(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\\s)/g, 'pkcs8', pem, alg, options);\n};\nexport const fromSPKI = (pem, alg, options) => {\n return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\\s)/g, 'spki', pem, alg, options);\n};\nfunction getElement(seq) {\n let result = [];\n let next = 0;\n while (next < seq.length) {\n let nextPart = parseElement(seq.subarray(next));\n result.push(nextPart);\n next += nextPart.byteLength;\n }\n return result;\n}\nfunction parseElement(bytes) {\n let position = 0;\n let tag = bytes[0] & 0x1f;\n position++;\n if (tag === 0x1f) {\n tag = 0;\n while (bytes[position] >= 0x80) {\n tag = tag * 128 + bytes[position] - 0x80;\n position++;\n }\n tag = tag * 128 + bytes[position] - 0x80;\n position++;\n }\n let length = 0;\n if (bytes[position] < 0x80) {\n length = bytes[position];\n position++;\n }\n else if (length === 0x80) {\n length = 0;\n while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) {\n if (length > bytes.byteLength) {\n throw new TypeError('invalid indefinite form length');\n }\n length++;\n }\n const byteLength = position + length + 2;\n return {\n byteLength,\n contents: bytes.subarray(position, position + length),\n raw: bytes.subarray(0, byteLength),\n };\n }\n else {\n let numberOfDigits = bytes[position] & 0x7f;\n position++;\n length = 0;\n for (let i = 0; i < numberOfDigits; i++) {\n length = length * 256 + bytes[position];\n position++;\n }\n }\n const byteLength = position + length;\n return {\n byteLength,\n contents: bytes.subarray(position, byteLength),\n raw: bytes.subarray(0, byteLength),\n };\n}\nfunction spkiFromX509(buf) {\n const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents);\n return encodeBase64(tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw);\n}\nfunction getSPKI(x509) {\n const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\\s)/g, '');\n const raw = decodeBase64(pem);\n return formatPEM(spkiFromX509(raw), 'PUBLIC KEY');\n}\nexport const fromX509 = (pem, alg, options) => {\n let spki;\n try {\n spki = getSPKI(pem);\n }\n catch (cause) {\n throw new TypeError('Failed to parse the X.509 certificate', { cause });\n }\n return fromSPKI(spki, alg, options);\n};\n", "import { decode as decodeBase64URL } from '../runtime/base64url.js';\nimport { fromSPKI, fromPKCS8, fromX509 } from '../runtime/asn1.js';\nimport asKeyObject from '../runtime/jwk_to_key.js';\nimport { JOSENotSupported } from '../util/errors.js';\nimport isObject from '../lib/is_object.js';\nexport async function importSPKI(spki, alg, options) {\n if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) {\n throw new TypeError('\"spki\" must be SPKI formatted string');\n }\n return fromSPKI(spki, alg, options);\n}\nexport async function importX509(x509, alg, options) {\n if (typeof x509 !== 'string' || x509.indexOf('-----BEGIN CERTIFICATE-----') !== 0) {\n throw new TypeError('\"x509\" must be X.509 formatted string');\n }\n return fromX509(x509, alg, options);\n}\nexport async function importPKCS8(pkcs8, alg, options) {\n if (typeof pkcs8 !== 'string' || pkcs8.indexOf('-----BEGIN PRIVATE KEY-----') !== 0) {\n throw new TypeError('\"pkcs8\" must be PKCS#8 formatted string');\n }\n return fromPKCS8(pkcs8, alg, options);\n}\nexport async function importJWK(jwk, alg, octAsKeyObject) {\n var _a;\n if (!isObject(jwk)) {\n throw new TypeError('JWK must be an object');\n }\n alg || (alg = jwk.alg);\n switch (jwk.kty) {\n case 'oct':\n if (typeof jwk.k !== 'string' || !jwk.k) {\n throw new TypeError('missing \"k\" (Key Value) Parameter value');\n }\n octAsKeyObject !== null && octAsKeyObject !== void 0 ? octAsKeyObject : (octAsKeyObject = jwk.ext !== true);\n if (octAsKeyObject) {\n return asKeyObject({ ...jwk, alg, ext: (_a = jwk.ext) !== null && _a !== void 0 ? _a : false });\n }\n return decodeBase64URL(jwk.k);\n case 'RSA':\n if (jwk.oth !== undefined) {\n throw new JOSENotSupported('RSA JWK \"oth\" (Other Primes Info) Parameter value is not supported');\n }\n case 'EC':\n case 'OKP':\n return asKeyObject({ ...jwk, alg });\n default:\n throw new JOSENotSupported('Unsupported \"kty\" (Key Type) Parameter value');\n }\n}\n", "import { withAlg as invalidKeyInput } from './invalid_key_input.js';\nimport isKeyLike, { types } from '../runtime/is_key_like.js';\nconst symmetricTypeCheck = (alg, key) => {\n if (key instanceof Uint8Array)\n return;\n if (!isKeyLike(key)) {\n throw new TypeError(invalidKeyInput(alg, key, ...types, 'Uint8Array'));\n }\n if (key.type !== 'secret') {\n throw new TypeError(`${types.join(' or ')} instances for symmetric algorithms must be of type \"secret\"`);\n }\n};\nconst asymmetricTypeCheck = (alg, key, usage) => {\n if (!isKeyLike(key)) {\n throw new TypeError(invalidKeyInput(alg, key, ...types));\n }\n if (key.type === 'secret') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithms must not be of type \"secret\"`);\n }\n if (usage === 'sign' && key.type === 'public') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm signing must be of type \"private\"`);\n }\n if (usage === 'decrypt' && key.type === 'public') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm decryption must be of type \"private\"`);\n }\n if (key.algorithm && usage === 'verify' && key.type === 'private') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm verifying must be of type \"public\"`);\n }\n if (key.algorithm && usage === 'encrypt' && key.type === 'private') {\n throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm encryption must be of type \"public\"`);\n }\n};\nconst checkKeyType = (alg, key, usage) => {\n const symmetric = alg.startsWith('HS') ||\n alg === 'dir' ||\n alg.startsWith('PBES2') ||\n /^A\\d{3}(?:GCM)?KW$/.test(alg);\n if (symmetric) {\n symmetricTypeCheck(alg, key);\n }\n else {\n asymmetricTypeCheck(alg, key, usage);\n }\n};\nexport default checkKeyType;\n", "import { JOSENotSupported } from '../util/errors.js';\nfunction validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {\n if (joseHeader.crit !== undefined && protectedHeader.crit === undefined) {\n throw new Err('\"crit\" (Critical) Header Parameter MUST be integrity protected');\n }\n if (!protectedHeader || protectedHeader.crit === undefined) {\n return new Set();\n }\n if (!Array.isArray(protectedHeader.crit) ||\n protectedHeader.crit.length === 0 ||\n protectedHeader.crit.some((input) => typeof input !== 'string' || input.length === 0)) {\n throw new Err('\"crit\" (Critical) Header Parameter MUST be an array of non-empty strings when present');\n }\n let recognized;\n if (recognizedOption !== undefined) {\n recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]);\n }\n else {\n recognized = recognizedDefault;\n }\n for (const parameter of protectedHeader.crit) {\n if (!recognized.has(parameter)) {\n throw new JOSENotSupported(`Extension Header Parameter \"${parameter}\" is not recognized`);\n }\n if (joseHeader[parameter] === undefined) {\n throw new Err(`Extension Header Parameter \"${parameter}\" is missing`);\n }\n else if (recognized.get(parameter) && protectedHeader[parameter] === undefined) {\n throw new Err(`Extension Header Parameter \"${parameter}\" MUST be integrity protected`);\n }\n }\n return new Set(protectedHeader.crit);\n}\nexport default validateCrit;\n", "const validateAlgorithms = (option, algorithms) => {\n if (algorithms !== undefined &&\n (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== 'string'))) {\n throw new TypeError(`\"${option}\" option must be an array of strings`);\n }\n if (!algorithms) {\n return undefined;\n }\n return new Set(algorithms);\n};\nexport default validateAlgorithms;\n", "import { JOSENotSupported } from '../util/errors.js';\nexport default function subtleDsa(alg, algorithm) {\n const hash = `SHA-${alg.slice(-3)}`;\n switch (alg) {\n case 'HS256':\n case 'HS384':\n case 'HS512':\n return { hash, name: 'HMAC' };\n case 'PS256':\n case 'PS384':\n case 'PS512':\n return { hash, name: 'RSA-PSS', saltLength: alg.slice(-3) >> 3 };\n case 'RS256':\n case 'RS384':\n case 'RS512':\n return { hash, name: 'RSASSA-PKCS1-v1_5' };\n case 'ES256':\n case 'ES384':\n case 'ES512':\n return { hash, name: 'ECDSA', namedCurve: algorithm.namedCurve };\n case 'EdDSA':\n return { name: algorithm.name };\n default:\n throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);\n }\n}\n", "import crypto, { isCryptoKey } from './webcrypto.js';\nimport { checkSigCryptoKey } from '../lib/crypto_key.js';\nimport invalidKeyInput from '../lib/invalid_key_input.js';\nimport { types } from './is_key_like.js';\nexport default function getCryptoKey(alg, key, usage) {\n if (isCryptoKey(key)) {\n checkSigCryptoKey(key, alg, usage);\n return key;\n }\n if (key instanceof Uint8Array) {\n if (!alg.startsWith('HS')) {\n throw new TypeError(invalidKeyInput(key, ...types));\n }\n return crypto.subtle.importKey('raw', key, { hash: `SHA-${alg.slice(-3)}`, name: 'HMAC' }, false, [usage]);\n }\n throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array'));\n}\n", "import subtleAlgorithm from './subtle_dsa.js';\nimport crypto from './webcrypto.js';\nimport checkKeyLength from './check_key_length.js';\nimport getVerifyKey from './get_sign_verify_key.js';\nconst verify = async (alg, key, signature, data) => {\n const cryptoKey = await getVerifyKey(alg, key, 'verify');\n checkKeyLength(alg, cryptoKey);\n const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);\n try {\n return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);\n }\n catch (_a) {\n return false;\n }\n};\nexport default verify;\n", "import { decode as base64url } from '../../runtime/base64url.js';\nimport verify from '../../runtime/verify.js';\nimport { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from '../../util/errors.js';\nimport { concat, encoder, decoder } from '../../lib/buffer_utils.js';\nimport isDisjoint from '../../lib/is_disjoint.js';\nimport isObject from '../../lib/is_object.js';\nimport checkKeyType from '../../lib/check_key_type.js';\nimport validateCrit from '../../lib/validate_crit.js';\nimport validateAlgorithms from '../../lib/validate_algorithms.js';\nexport async function flattenedVerify(jws, key, options) {\n var _a;\n if (!isObject(jws)) {\n throw new JWSInvalid('Flattened JWS must be an object');\n }\n if (jws.protected === undefined && jws.header === undefined) {\n throw new JWSInvalid('Flattened JWS must have either of the \"protected\" or \"header\" members');\n }\n if (jws.protected !== undefined && typeof jws.protected !== 'string') {\n throw new JWSInvalid('JWS Protected Header incorrect type');\n }\n if (jws.payload === undefined) {\n throw new JWSInvalid('JWS Payload missing');\n }\n if (typeof jws.signature !== 'string') {\n throw new JWSInvalid('JWS Signature missing or incorrect type');\n }\n if (jws.header !== undefined && !isObject(jws.header)) {\n throw new JWSInvalid('JWS Unprotected Header incorrect type');\n }\n let parsedProt = {};\n if (jws.protected) {\n try {\n const protectedHeader = base64url(jws.protected);\n parsedProt = JSON.parse(decoder.decode(protectedHeader));\n }\n catch (_b) {\n throw new JWSInvalid('JWS Protected Header is invalid');\n }\n }\n if (!isDisjoint(parsedProt, jws.header)) {\n throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');\n }\n const joseHeader = {\n ...parsedProt,\n ...jws.header,\n };\n const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options === null || options === void 0 ? void 0 : options.crit, parsedProt, joseHeader);\n let b64 = true;\n if (extensions.has('b64')) {\n b64 = parsedProt.b64;\n if (typeof b64 !== 'boolean') {\n throw new JWSInvalid('The \"b64\" (base64url-encode payload) Header Parameter must be a boolean');\n }\n }\n const { alg } = joseHeader;\n if (typeof alg !== 'string' || !alg) {\n throw new JWSInvalid('JWS \"alg\" (Algorithm) Header Parameter missing or invalid');\n }\n const algorithms = options && validateAlgorithms('algorithms', options.algorithms);\n if (algorithms && !algorithms.has(alg)) {\n throw new JOSEAlgNotAllowed('\"alg\" (Algorithm) Header Parameter not allowed');\n }\n if (b64) {\n if (typeof jws.payload !== 'string') {\n throw new JWSInvalid('JWS Payload must be a string');\n }\n }\n else if (typeof jws.payload !== 'string' && !(jws.payload instanceof Uint8Array)) {\n throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance');\n }\n let resolvedKey = false;\n if (typeof key === 'function') {\n key = await key(parsedProt, jws);\n resolvedKey = true;\n }\n checkKeyType(alg, key, 'verify');\n const data = concat(encoder.encode((_a = jws.protected) !== null && _a !== void 0 ? _a : ''), encoder.encode('.'), typeof jws.payload === 'string' ? encoder.encode(jws.payload) : jws.payload);\n let signature;\n try {\n signature = base64url(jws.signature);\n }\n catch (_c) {\n throw new JWSInvalid('Failed to base64url decode the signature');\n }\n const verified = await verify(alg, key, signature, data);\n if (!verified) {\n throw new JWSSignatureVerificationFailed();\n }\n let payload;\n if (b64) {\n try {\n payload = base64url(jws.payload);\n }\n catch (_d) {\n throw new JWSInvalid('Failed to base64url decode the payload');\n }\n }\n else if (typeof jws.payload === 'string') {\n payload = encoder.encode(jws.payload);\n }\n else {\n payload = jws.payload;\n }\n const result = { payload };\n if (jws.protected !== undefined) {\n result.protectedHeader = parsedProt;\n }\n if (jws.header !== undefined) {\n result.unprotectedHeader = jws.header;\n }\n if (resolvedKey) {\n return { ...result, key };\n }\n return result;\n}\n", "import { flattenedVerify } from '../flattened/verify.js';\nimport { JWSInvalid } from '../../util/errors.js';\nimport { decoder } from '../../lib/buffer_utils.js';\nexport async function compactVerify(jws, key, options) {\n if (jws instanceof Uint8Array) {\n jws = decoder.decode(jws);\n }\n if (typeof jws !== 'string') {\n throw new JWSInvalid('Compact JWS must be a string or Uint8Array');\n }\n const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.');\n if (length !== 3) {\n throw new JWSInvalid('Invalid Compact JWS');\n }\n const verified = await flattenedVerify({ payload, protected: protectedHeader, signature }, key, options);\n const result = { payload: verified.payload, protectedHeader: verified.protectedHeader };\n if (typeof key === 'function') {\n return { ...result, key: verified.key };\n }\n return result;\n}\n", "export default (date) => Math.floor(date.getTime() / 1000);\n", "const minute = 60;\nconst hour = minute * 60;\nconst day = hour * 24;\nconst week = day * 7;\nconst year = day * 365.25;\nconst REGEX = /^(\\d+|\\d+\\.\\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i;\nexport default (str) => {\n const matched = REGEX.exec(str);\n if (!matched) {\n throw new TypeError('Invalid time period format');\n }\n const value = parseFloat(matched[1]);\n const unit = matched[2].toLowerCase();\n switch (unit) {\n case 'sec':\n case 'secs':\n case 'second':\n case 'seconds':\n case 's':\n return Math.round(value);\n case 'minute':\n case 'minutes':\n case 'min':\n case 'mins':\n case 'm':\n return Math.round(value * minute);\n case 'hour':\n case 'hours':\n case 'hr':\n case 'hrs':\n case 'h':\n return Math.round(value * hour);\n case 'day':\n case 'days':\n case 'd':\n return Math.round(value * day);\n case 'week':\n case 'weeks':\n case 'w':\n return Math.round(value * week);\n default:\n return Math.round(value * year);\n }\n};\n", "import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from '../util/errors.js';\nimport { decoder } from './buffer_utils.js';\nimport epoch from './epoch.js';\nimport secs from './secs.js';\nimport isObject from './is_object.js';\nconst normalizeTyp = (value) => value.toLowerCase().replace(/^application\\//, '');\nconst checkAudiencePresence = (audPayload, audOption) => {\n if (typeof audPayload === 'string') {\n return audOption.includes(audPayload);\n }\n if (Array.isArray(audPayload)) {\n return audOption.some(Set.prototype.has.bind(new Set(audPayload)));\n }\n return false;\n};\nexport default (protectedHeader, encodedPayload, options = {}) => {\n const { typ } = options;\n if (typ &&\n (typeof protectedHeader.typ !== 'string' ||\n normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) {\n throw new JWTClaimValidationFailed('unexpected \"typ\" JWT header value', 'typ', 'check_failed');\n }\n let payload;\n try {\n payload = JSON.parse(decoder.decode(encodedPayload));\n }\n catch (_a) {\n }\n if (!isObject(payload)) {\n throw new JWTInvalid('JWT Claims Set must be a top-level JSON object');\n }\n const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;\n if (maxTokenAge !== undefined)\n requiredClaims.push('iat');\n if (audience !== undefined)\n requiredClaims.push('aud');\n if (subject !== undefined)\n requiredClaims.push('sub');\n if (issuer !== undefined)\n requiredClaims.push('iss');\n for (const claim of new Set(requiredClaims.reverse())) {\n if (!(claim in payload)) {\n throw new JWTClaimValidationFailed(`missing required \"${claim}\" claim`, claim, 'missing');\n }\n }\n if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {\n throw new JWTClaimValidationFailed('unexpected \"iss\" claim value', 'iss', 'check_failed');\n }\n if (subject && payload.sub !== subject) {\n throw new JWTClaimValidationFailed('unexpected \"sub\" claim value', 'sub', 'check_failed');\n }\n if (audience &&\n !checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) {\n throw new JWTClaimValidationFailed('unexpected \"aud\" claim value', 'aud', 'check_failed');\n }\n let tolerance;\n switch (typeof options.clockTolerance) {\n case 'string':\n tolerance = secs(options.clockTolerance);\n break;\n case 'number':\n tolerance = options.clockTolerance;\n break;\n case 'undefined':\n tolerance = 0;\n break;\n default:\n throw new TypeError('Invalid clockTolerance option type');\n }\n const { currentDate } = options;\n const now = epoch(currentDate || new Date());\n if ((payload.iat !== undefined || maxTokenAge) && typeof payload.iat !== 'number') {\n throw new JWTClaimValidationFailed('\"iat\" claim must be a number', 'iat', 'invalid');\n }\n if (payload.nbf !== undefined) {\n if (typeof payload.nbf !== 'number') {\n throw new JWTClaimValidationFailed('\"nbf\" claim must be a number', 'nbf', 'invalid');\n }\n if (payload.nbf > now + tolerance) {\n throw new JWTClaimValidationFailed('\"nbf\" claim timestamp check failed', 'nbf', 'check_failed');\n }\n }\n if (payload.exp !== undefined) {\n if (typeof payload.exp !== 'number') {\n throw new JWTClaimValidationFailed('\"exp\" claim must be a number', 'exp', 'invalid');\n }\n if (payload.exp <= now - tolerance) {\n throw new JWTExpired('\"exp\" claim timestamp check failed', 'exp', 'check_failed');\n }\n }\n if (maxTokenAge) {\n const age = now - payload.iat;\n const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge);\n if (age - tolerance > max) {\n throw new JWTExpired('\"iat\" claim timestamp check failed (too far in the past)', 'iat', 'check_failed');\n }\n if (age < 0 - tolerance) {\n throw new JWTClaimValidationFailed('\"iat\" claim timestamp check failed (it should be in the past)', 'iat', 'check_failed');\n }\n }\n return payload;\n};\n", "import { compactVerify } from '../jws/compact/verify.js';\nimport jwtPayload from '../lib/jwt_claims_set.js';\nimport { JWTInvalid } from '../util/errors.js';\nexport async function jwtVerify(jwt, key, options) {\n var _a;\n const verified = await compactVerify(jwt, key, options);\n if (((_a = verified.protectedHeader.crit) === null || _a === void 0 ? void 0 : _a.includes('b64')) && verified.protectedHeader.b64 === false) {\n throw new JWTInvalid('JWTs MUST NOT use unencoded payload');\n }\n const payload = jwtPayload(verified.protectedHeader, verified.payload, options);\n const result = { payload, protectedHeader: verified.protectedHeader };\n if (typeof key === 'function') {\n return { ...result, key: verified.key };\n }\n return result;\n}\n", "/*\nCopyright 2022 The Dapr Authors\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { importSPKI, jwtVerify } from 'jose'\nimport { IRequest } from 'itty-router'\n\nimport { Environment } from '$lib/environment'\n\nconst tokenHeaderMatch =\n /^(?:Bearer )?([A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]+\\.[A-Za-z0-9_\\-]+)/i\n\nexport async function AuthorizeRequest(\n req: IRequest,\n env: Environment\n): Promise {\n // If \"SKIP_AUTH\" is set, we can allow skipping authorization\n if (env.SKIP_AUTH === 'true') {\n return true\n }\n\n // Ensure we have an Authorization header with a bearer JWT token\n const match = tokenHeaderMatch.exec(req.headers.get('authorization') || '')\n if (!match || !match[1]) {\n return false\n }\n\n // Validate the JWT\n const pk = await importSPKI(env.PUBLIC_KEY, 'EdDSA')\n try {\n await jwtVerify(match[1], pk, {\n issuer: 'dapr.io/cloudflare',\n audience: env.TOKEN_AUDIENCE,\n algorithms: ['EdDSA'],\n // Allow 5 mins of clock skew\n clockTolerance: 300,\n })\n } catch (err) {\n console.error('Failed to validate JWT: ' + err)\n return false\n }\n\n return true\n}\n", "{\n \"private\": true,\n \"name\": \"dapr-cfworkers-client\",\n \"description\": \"Client code for Dapr to interact with Cloudflare Workers\",\n \"version\": \"20230517\",\n \"main\": \"worker.ts\",\n \"scripts\": {\n \"build\": \"esbuild --bundle --minify --outfile=../workers/code/worker.js --format=esm --platform=browser --sourcemap worker.ts\",\n \"start\": \"wrangler dev\",\n \"format\": \"prettier --write .\"\n },\n \"author\": \"Dapr authors\",\n \"license\": \"Apache2\",\n \"devDependencies\": {\n \"@cloudflare/workers-types\": \"^4.20230511.0\",\n \"esbuild\": \"^0.27.2\",\n \"prettier\": \"^2.8.8\",\n \"typescript\": \"^5.0.4\",\n \"wrangler\": \"^4.61.0\"\n },\n \"dependencies\": {\n \"itty-router\": \"3.0.12\",\n \"jose\": \"4.15.5\"\n }\n}\n", "/*\nCopyright 2022 The Dapr Authors\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { Router, IRequest } from 'itty-router'\n\nimport { Environment } from '$lib/environment'\nimport { AuthorizeRequest } from '$lib/jwt-auth'\n\nimport { version } from './package.json'\n\nconst router = Router()\n // Handle the info endpoint\n .get(\n '/.well-known/dapr/info',\n async (req: IRequest, env: Environment): Promise => {\n const auth = await AuthorizeRequest(req, env)\n if (!auth) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Filter all bindings by type\n const queues: string[] = []\n const kv: string[] = []\n const r2: string[] = []\n const all = Object.keys(env)\n for (let i = 0; i < all.length; i++) {\n if (!all[i]) {\n continue\n }\n const obj = env[all[i]]\n if (!obj || typeof obj != 'object' || !obj.constructor) {\n continue\n }\n switch (obj.constructor.name) {\n case 'KvNamespace':\n case 'KVNamespace':\n kv.push(all[i])\n break\n case 'WorkerQueue':\n case 'Queue':\n queues.push(all[i])\n break\n case 'R2Bucket':\n // Note that we currently don't support R2 yet\n r2.push(all[i])\n break\n }\n }\n\n const res = JSON.stringify({\n version,\n queues: queues && queues.length ? queues : undefined,\n kv: kv && kv.length ? kv : undefined,\n r2: r2 && r2.length ? r2 : undefined,\n })\n return new Response(res, {\n headers: {\n 'content-type': 'application/json',\n },\n })\n }\n )\n\n // Retrieve a value from KV\n .get(\n '/kv/:namespace/:key',\n async (req: IRequest, env: Environment): Promise => {\n const { namespace, key, errorRes } = await setupKVRequest(req, env)\n if (errorRes) {\n return errorRes\n }\n\n const val = await namespace!.get(key!, 'stream')\n if (!val) {\n return new Response('', { status: 404 })\n }\n\n return new Response(val, { status: 200 })\n }\n )\n\n // Store a value in KV\n .post(\n '/kv/:namespace/:key',\n async (req: IRequest, env: Environment): Promise => {\n const { namespace, key, errorRes } = await setupKVRequest(req, env)\n if (errorRes) {\n return errorRes\n }\n\n let expirationTtl: number | undefined = undefined\n const reqUrl = new URL(req.url)\n const ttlParam = parseInt(reqUrl.searchParams.get('ttl') || '', 10)\n if (ttlParam > 0) {\n expirationTtl = ttlParam\n }\n await namespace!.put(key!, req.body!, { expirationTtl })\n\n return new Response('', { status: 201 })\n }\n )\n\n // Delete a value from KV\n .delete(\n '/kv/:namespace/:key',\n async (req: IRequest, env: Environment): Promise => {\n const { namespace, key, errorRes } = await setupKVRequest(req, env)\n if (errorRes) {\n return errorRes\n }\n\n await namespace!.delete(key!)\n\n return new Response('', { status: 204 })\n }\n )\n\n // Publish a message in a queue\n .post(\n '/queues/:queue',\n async (req: IRequest, env: Environment): Promise => {\n const { queue, errorRes } = await setupQueueRequest(req, env)\n if (errorRes) {\n return errorRes\n }\n\n let message = await req.text()\n await queue!.send(message)\n return new Response('', { status: 201 })\n }\n )\n\n // Catch-all route to handle 404s\n .all('*', (): Response => {\n return new Response('Not found', { status: 404 })\n })\n\n// Performs the init setps for a KV request. Returns a Response object in case of error.\nasync function setupKVRequest(\n req: IRequest,\n env: Environment\n): Promise<{\n namespace?: KVNamespace\n key?: string\n errorRes?: Response\n}> {\n if (!req?.text || !req.params?.namespace || !req.params?.key) {\n return { errorRes: new Response('Bad request', { status: 400 }) }\n }\n const namespace = env[req.params.namespace] as KVNamespace\n if (\n typeof namespace != 'object' ||\n !['KVNamespace', 'KvNamespace'].includes(namespace?.constructor?.name)\n ) {\n return {\n errorRes: new Response(\n `Worker is not bound to KV '${req.params.kv}'`,\n { status: 412 }\n ),\n }\n }\n\n const auth = await AuthorizeRequest(req, env)\n if (!auth) {\n return { errorRes: new Response('Unauthorized', { status: 401 }) }\n }\n\n return { namespace, key: req.params.key }\n}\n\n// Performs the init setps for a Queue request. Returns a Response object in case of error.\nasync function setupQueueRequest(\n req: IRequest,\n env: Environment\n): Promise<{ queue?: Queue; errorRes?: Response }> {\n if (!req?.text || !req.params?.queue) {\n return { errorRes: new Response('Bad request', { status: 400 }) }\n }\n const queue = env[req.params.queue] as Queue\n if (\n typeof queue != 'object' ||\n !['WorkerQueue', 'Queue'].includes(queue?.constructor?.name)\n ) {\n return {\n errorRes: new Response(\n `Worker is not bound to queue '${req.params.queue}'`,\n { status: 412 }\n ),\n }\n }\n\n const auth = await AuthorizeRequest(req, env)\n if (!auth) {\n return { errorRes: new Response('Unauthorized', { status: 401 }) }\n }\n\n return { queue }\n}\n\nexport default {\n fetch: router.handle,\n}\n"], + "mappings": "AAAA,IAAMA,GAAE,CAAC,CAAC,KAAK,EAAE,GAAG,OAAOC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,CAACC,EAAEC,EAAEC,IAAI,CAACF,KAAKG,IAAIJ,EAAE,KAAK,CAACE,EAAE,YAAY,EAAE,OAAO,KAAK,EAAED,GAAG,QAAQ,WAAW,SAAS,EAAE,QAAQ,oBAAoB,EAAE,EAAE,QAAQ,aAAa,WAAW,EAAE,QAAQ,oBAAoB,oBAAoB,EAAE,QAAQ,cAAc,KAAK,EAAE,QAAQ,wBAAwB,wBAAwB,CAAC,KAAK,EAAEG,CAAC,CAAC,GAAGD,CAAC,CAAC,EAAE,OAAOH,EAAE,MAAM,OAAOD,KAAKE,EAAE,CAAC,IAAI,EAAEE,EAAEC,EAAE,IAAI,IAAIL,EAAE,GAAG,EAAEM,EAAEN,EAAE,MAAM,CAAC,EAAE,OAAO,CAACA,EAAEC,CAAC,IAAII,EAAE,aAAaC,EAAEN,CAAC,EAAWM,EAAEN,CAAC,IAAZ,OAAcC,EAAE,CAACK,EAAEN,CAAC,EAAEC,CAAC,EAAE,KAAK,EAAE,OAAO,CAACK,EAAEC,EAAEC,CAAC,IAAIP,EAAE,IAAIK,IAAIN,EAAE,QAAgBM,IAAR,SAAaF,EAAEC,EAAE,SAAS,MAAME,CAAC,GAAG,CAACP,EAAE,OAAOI,EAAE,QAAQ,CAAC,EAAE,QAAQH,KAAKO,EAAE,IAAa,EAAE,MAAMP,EAAED,EAAE,OAAOA,EAAE,GAAGE,CAAC,KAAnC,OAAsC,OAAO,CAAC,CAAC,CAAC,GCAjqB,IAAOO,EAAQ,OACFC,EAAeC,GAAQA,aAAe,UCA5C,IAAMC,EAAU,IAAI,YACdC,EAAU,IAAI,YACrBC,GAAY,GAAK,GAChB,SAASC,KAAUC,EAAS,CAC/B,IAAMC,EAAOD,EAAQ,OAAO,CAACE,EAAK,CAAE,OAAAC,CAAO,IAAMD,EAAMC,EAAQ,CAAC,EAC1DC,EAAM,IAAI,WAAWH,CAAI,EAC3BI,EAAI,EACR,OAAAL,EAAQ,QAASM,GAAW,CACxBF,EAAI,IAAIE,EAAQD,CAAC,EACjBA,GAAKC,EAAO,MAChB,CAAC,EACMF,CACX,CCGO,IAAMG,GAAgBC,GAAY,CACrC,IAAMC,EAAS,KAAKD,CAAO,EACrBE,EAAQ,IAAI,WAAWD,EAAO,MAAM,EAC1C,QAASE,EAAI,EAAGA,EAAIF,EAAO,OAAQE,IAC/BD,EAAMC,CAAC,EAAIF,EAAO,WAAWE,CAAC,EAElC,OAAOD,CACX,EACaE,EAAUC,GAAU,CAC7B,IAAIL,EAAUK,EACVL,aAAmB,aACnBA,EAAUM,EAAQ,OAAON,CAAO,GAEpCA,EAAUA,EAAQ,QAAQ,KAAM,GAAG,EAAE,QAAQ,KAAM,GAAG,EAAE,QAAQ,MAAO,EAAE,EACzE,GAAI,CACA,OAAOD,GAAaC,CAAO,CAC/B,MACW,CACP,MAAM,IAAI,UAAU,mDAAmD,CAC3E,CACJ,ECpCO,IAAMO,EAAN,cAAwB,KAAM,CACjC,WAAW,MAAO,CACd,MAAO,kBACX,CACA,YAAYC,EAAS,CACjB,IAAIC,EACJ,MAAMD,CAAO,EACb,KAAK,KAAO,mBACZ,KAAK,KAAO,KAAK,YAAY,MAC5BC,EAAK,MAAM,qBAAuB,MAAQA,IAAO,QAAkBA,EAAG,KAAK,MAAO,KAAM,KAAK,WAAW,CAC7G,CACJ,EACaC,EAAN,cAAuCH,CAAU,CACpD,WAAW,MAAO,CACd,MAAO,iCACX,CACA,YAAYC,EAASG,EAAQ,cAAeC,EAAS,cAAe,CAChE,MAAMJ,CAAO,EACb,KAAK,KAAO,kCACZ,KAAK,MAAQG,EACb,KAAK,OAASC,CAClB,CACJ,EACaC,EAAN,cAAyBN,CAAU,CACtC,WAAW,MAAO,CACd,MAAO,iBACX,CACA,YAAYC,EAASG,EAAQ,cAAeC,EAAS,cAAe,CAChE,MAAMJ,CAAO,EACb,KAAK,KAAO,kBACZ,KAAK,MAAQG,EACb,KAAK,OAASC,CAClB,CACJ,EACaE,EAAN,cAAgCP,CAAU,CAC7C,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,0BAChB,CACA,WAAW,MAAO,CACd,MAAO,0BACX,CACJ,EACaQ,EAAN,cAA+BR,CAAU,CAC5C,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,wBAChB,CACA,WAAW,MAAO,CACd,MAAO,wBACX,CACJ,EA8BO,IAAMS,EAAN,cAAyBC,CAAU,CACtC,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,iBAChB,CACA,WAAW,MAAO,CACd,MAAO,iBACX,CACJ,EACaC,EAAN,cAAyBD,CAAU,CACtC,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,iBAChB,CACA,WAAW,MAAO,CACd,MAAO,iBACX,CACJ,EAkDO,IAAME,EAAN,cAA6CC,CAAU,CAC1D,aAAc,CACV,MAAM,GAAG,SAAS,EAClB,KAAK,KAAO,wCACZ,KAAK,QAAU,+BACnB,CACA,WAAW,MAAO,CACd,MAAO,uCACX,CACJ,EC5JA,IAAOC,EAAQC,EAAO,gBAAgB,KAAKA,CAAM,ECDjD,SAASC,EAASC,EAAMC,EAAO,iBAAkB,CAC7C,OAAO,IAAI,UAAU,kDAAkDA,CAAI,YAAYD,CAAI,EAAE,CACjG,CACA,SAASE,EAAYC,EAAWH,EAAM,CAClC,OAAOG,EAAU,OAASH,CAC9B,CACA,SAASI,EAAcC,EAAM,CACzB,OAAO,SAASA,EAAK,KAAK,MAAM,CAAC,EAAG,EAAE,CAC1C,CACA,SAASC,GAAcC,EAAK,CACxB,OAAQA,EAAK,CACT,IAAK,QACD,MAAO,QACX,IAAK,QACD,MAAO,QACX,IAAK,QACD,MAAO,QACX,QACI,MAAM,IAAI,MAAM,aAAa,CACrC,CACJ,CACA,SAASC,GAAWC,EAAKC,EAAQ,CAC7B,GAAIA,EAAO,QAAU,CAACA,EAAO,KAAMC,GAAaF,EAAI,OAAO,SAASE,CAAQ,CAAC,EAAG,CAC5E,IAAIC,EAAM,sEACV,GAAIF,EAAO,OAAS,EAAG,CACnB,IAAMG,EAAOH,EAAO,IAAI,EACxBE,GAAO,UAAUF,EAAO,KAAK,IAAI,CAAC,QAAQG,CAAI,GAClD,MACSH,EAAO,SAAW,EACvBE,GAAO,UAAUF,EAAO,CAAC,CAAC,OAAOA,EAAO,CAAC,CAAC,IAG1CE,GAAO,GAAGF,EAAO,CAAC,CAAC,IAEvB,MAAM,IAAI,UAAUE,CAAG,CAC3B,CACJ,CACO,SAASE,GAAkBL,EAAKF,KAAQG,EAAQ,CACnD,OAAQH,EAAK,CACT,IAAK,QACL,IAAK,QACL,IAAK,QAAS,CACV,GAAI,CAACL,EAAYO,EAAI,UAAW,MAAM,EAClC,MAAMV,EAAS,MAAM,EACzB,IAAMY,EAAW,SAASJ,EAAI,MAAM,CAAC,EAAG,EAAE,EAE1C,GADeH,EAAcK,EAAI,UAAU,IAAI,IAChCE,EACX,MAAMZ,EAAS,OAAOY,CAAQ,GAAI,gBAAgB,EACtD,KACJ,CACA,IAAK,QACL,IAAK,QACL,IAAK,QAAS,CACV,GAAI,CAACT,EAAYO,EAAI,UAAW,mBAAmB,EAC/C,MAAMV,EAAS,mBAAmB,EACtC,IAAMY,EAAW,SAASJ,EAAI,MAAM,CAAC,EAAG,EAAE,EAE1C,GADeH,EAAcK,EAAI,UAAU,IAAI,IAChCE,EACX,MAAMZ,EAAS,OAAOY,CAAQ,GAAI,gBAAgB,EACtD,KACJ,CACA,IAAK,QACL,IAAK,QACL,IAAK,QAAS,CACV,GAAI,CAACT,EAAYO,EAAI,UAAW,SAAS,EACrC,MAAMV,EAAS,SAAS,EAC5B,IAAMY,EAAW,SAASJ,EAAI,MAAM,CAAC,EAAG,EAAE,EAE1C,GADeH,EAAcK,EAAI,UAAU,IAAI,IAChCE,EACX,MAAMZ,EAAS,OAAOY,CAAQ,GAAI,gBAAgB,EACtD,KACJ,CACA,IAAK,QAAS,CACV,GAAIF,EAAI,UAAU,OAAS,WAAaA,EAAI,UAAU,OAAS,QAC3D,MAAMV,EAAS,kBAAkB,EAErC,KACJ,CACA,IAAK,QACL,IAAK,QACL,IAAK,QAAS,CACV,GAAI,CAACG,EAAYO,EAAI,UAAW,OAAO,EACnC,MAAMV,EAAS,OAAO,EAC1B,IAAMY,EAAWL,GAAcC,CAAG,EAElC,GADeE,EAAI,UAAU,aACdE,EACX,MAAMZ,EAASY,EAAU,sBAAsB,EACnD,KACJ,CACA,QACI,MAAM,IAAI,UAAU,2CAA2C,CACvE,CACAH,GAAWC,EAAKC,CAAM,CAC1B,CC7FA,SAASK,GAAQC,EAAKC,KAAWC,EAAO,CACpC,GAAIA,EAAM,OAAS,EAAG,CAClB,IAAMC,EAAOD,EAAM,IAAI,EACvBF,GAAO,eAAeE,EAAM,KAAK,IAAI,CAAC,QAAQC,CAAI,GACtD,MACSD,EAAM,SAAW,EACtBF,GAAO,eAAeE,EAAM,CAAC,CAAC,OAAOA,EAAM,CAAC,CAAC,IAG7CF,GAAO,WAAWE,EAAM,CAAC,CAAC,IAE9B,OAAID,GAAU,KACVD,GAAO,aAAaC,CAAM,GAErB,OAAOA,GAAW,YAAcA,EAAO,KAC5CD,GAAO,sBAAsBC,EAAO,IAAI,GAEnC,OAAOA,GAAW,UAAYA,GAAU,MACzCA,EAAO,aAAeA,EAAO,YAAY,OACzCD,GAAO,4BAA4BC,EAAO,YAAY,IAAI,IAG3DD,CACX,CACA,IAAOI,EAAQ,CAACH,KAAWC,IAChBH,GAAQ,eAAgBE,EAAQ,GAAGC,CAAK,EAE5C,SAASG,EAAQC,EAAKL,KAAWC,EAAO,CAC3C,OAAOH,GAAQ,eAAeO,CAAG,sBAAuBL,EAAQ,GAAGC,CAAK,CAC5E,CC5BA,IAAOK,EAASC,GACLC,EAAYD,CAAG,EAEbE,EAAQ,CAAC,WAAW,ECJjC,IAAMC,GAAa,IAAIC,IAAY,CAC/B,IAAMC,EAAUD,EAAQ,OAAO,OAAO,EACtC,GAAIC,EAAQ,SAAW,GAAKA,EAAQ,SAAW,EAC3C,MAAO,GAEX,IAAIC,EACJ,QAAWC,KAAUF,EAAS,CAC1B,IAAMG,EAAa,OAAO,KAAKD,CAAM,EACrC,GAAI,CAACD,GAAOA,EAAI,OAAS,EAAG,CACxBA,EAAM,IAAI,IAAIE,CAAU,EACxB,QACJ,CACA,QAAWC,KAAaD,EAAY,CAChC,GAAIF,EAAI,IAAIG,CAAS,EACjB,MAAO,GAEXH,EAAI,IAAIG,CAAS,CACrB,CACJ,CACA,MAAO,EACX,EACOC,EAAQP,GCrBf,SAASQ,GAAaC,EAAO,CACzB,OAAO,OAAOA,GAAU,UAAYA,IAAU,IAClD,CACe,SAARC,EAA0BC,EAAO,CACpC,GAAI,CAACH,GAAaG,CAAK,GAAK,OAAO,UAAU,SAAS,KAAKA,CAAK,IAAM,kBAClE,MAAO,GAEX,GAAI,OAAO,eAAeA,CAAK,IAAM,KACjC,MAAO,GAEX,IAAIC,EAAQD,EACZ,KAAO,OAAO,eAAeC,CAAK,IAAM,MACpCA,EAAQ,OAAO,eAAeA,CAAK,EAEvC,OAAO,OAAO,eAAeD,CAAK,IAAMC,CAC5C,CCfA,IAAOC,EAAQ,CAACC,EAAKC,IAAQ,CACzB,GAAID,EAAI,WAAW,IAAI,GAAKA,EAAI,WAAW,IAAI,EAAG,CAC9C,GAAM,CAAE,cAAAE,CAAc,EAAID,EAAI,UAC9B,GAAI,OAAOC,GAAkB,UAAYA,EAAgB,KACrD,MAAM,IAAI,UAAU,GAAGF,CAAG,uDAAuD,CAEzF,CACJ,ECiBA,IAAMG,EAAU,CAACC,EAASC,EAAKC,EAAO,IAAM,CACpCA,IAAS,IACTD,EAAI,QAAQA,EAAI,MAAM,EACtBA,EAAI,QAAQ,CAAI,GAEpB,IAAIE,EAAIH,EAAQ,QAAQC,EAAI,CAAC,EAAGC,CAAI,EACpC,GAAIC,IAAM,GACN,MAAO,GACX,IAAMC,EAAMJ,EAAQ,SAASG,EAAGA,EAAIF,EAAI,MAAM,EAC9C,OAAIG,EAAI,SAAWH,EAAI,OACZ,GACJG,EAAI,MAAM,CAACC,EAAOC,IAAUD,IAAUJ,EAAIK,CAAK,CAAC,GAAKP,EAAQC,EAASC,EAAKE,EAAI,CAAC,CAC3F,EACMI,GAAiBP,GAAY,CAC/B,OAAQ,GAAM,CACV,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAM,IAAM,GAAM,EAAM,EAAM,CAAI,CAAC,EAClE,MAAO,QACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,EAAM,EAAM,EAAI,CAAC,EAChD,MAAO,QACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,EAAM,EAAM,EAAI,CAAC,EAChD,MAAO,QACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAI,CAAC,EACpC,MAAO,SACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAI,CAAC,EACpC,MAAO,OACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAI,CAAC,EACpC,MAAO,UACX,KAAKD,EAAQC,EAAS,CAAC,GAAM,IAAM,GAAI,CAAC,EACpC,MAAO,QACX,QACI,MAAM,IAAIQ,EAAiB,yDAAyD,CAC5F,CACJ,EACMC,GAAgB,MAAOC,EAASC,EAAWC,EAAKC,EAAKC,IAAY,CACnE,IAAIC,EACJ,IAAIC,EACAC,EACEjB,EAAU,IAAI,WAAW,KAAKY,EAAI,QAAQF,EAAS,EAAE,CAAC,EACvD,MAAM,EAAE,EACR,IAAKQ,GAAMA,EAAE,WAAW,CAAC,CAAC,CAAC,EAC1BC,EAAWR,IAAc,OAC/B,OAAQE,EAAK,CACT,IAAK,QACL,IAAK,QACL,IAAK,QACDG,EAAY,CAAE,KAAM,UAAW,KAAM,OAAOH,EAAI,MAAM,EAAE,CAAC,EAAG,EAC5DI,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,QACL,IAAK,QACL,IAAK,QACDH,EAAY,CAAE,KAAM,oBAAqB,KAAM,OAAOH,EAAI,MAAM,EAAE,CAAC,EAAG,EACtEI,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,WACL,IAAK,eACL,IAAK,eACL,IAAK,eACDH,EAAY,CACR,KAAM,WACN,KAAM,OAAO,SAASH,EAAI,MAAM,EAAE,EAAG,EAAE,GAAK,CAAC,EACjD,EACAI,EAAYE,EAAW,CAAC,UAAW,SAAS,EAAI,CAAC,UAAW,WAAW,EACvE,MACJ,IAAK,QACDH,EAAY,CAAE,KAAM,QAAS,WAAY,OAAQ,EACjDC,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,QACDH,EAAY,CAAE,KAAM,QAAS,WAAY,OAAQ,EACjDC,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,QACDH,EAAY,CAAE,KAAM,QAAS,WAAY,OAAQ,EACjDC,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,IAAK,UACL,IAAK,iBACL,IAAK,iBACL,IAAK,iBAAkB,CACnB,IAAMC,EAAab,GAAcP,CAAO,EACxCgB,EAAYI,EAAW,WAAW,IAAI,EAAI,CAAE,KAAM,OAAQ,WAAAA,CAAW,EAAI,CAAE,KAAMA,CAAW,EAC5FH,EAAYE,EAAW,CAAC,EAAI,CAAC,YAAY,EACzC,KACJ,CACA,IAAK,QACDH,EAAY,CAAE,KAAMT,GAAcP,CAAO,CAAE,EAC3CiB,EAAYE,EAAW,CAAC,QAAQ,EAAI,CAAC,MAAM,EAC3C,MACJ,QACI,MAAM,IAAIX,EAAiB,gDAAgD,CACnF,CACA,OAAOa,EAAO,OAAO,UAAUV,EAAWX,EAASgB,GAAYD,EAAuDD,GAAQ,eAAiB,MAAQC,IAAO,OAASA,EAAK,GAAOE,CAAS,CAChM,EAIO,IAAMK,GAAW,CAACC,EAAKC,EAAKC,IACxBC,GAAc,6CAA8C,OAAQH,EAAKC,EAAKC,CAAO,ECrHhG,eAAsBE,GAAWC,EAAMC,EAAKC,EAAS,CACjD,GAAI,OAAOF,GAAS,UAAYA,EAAK,QAAQ,4BAA4B,IAAM,EAC3E,MAAM,IAAI,UAAU,sCAAsC,EAE9D,OAAOG,GAASH,EAAMC,EAAKC,CAAO,CACtC,CCRA,IAAME,GAAqB,CAACC,EAAKC,IAAQ,CACrC,GAAI,EAAAA,aAAe,YAEnB,IAAI,CAACC,EAAUD,CAAG,EACd,MAAM,IAAI,UAAUE,EAAgBH,EAAKC,EAAK,GAAGG,EAAO,YAAY,CAAC,EAEzE,GAAIH,EAAI,OAAS,SACb,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,CAAC,8DAA8D,EAE/G,EACMC,GAAsB,CAACL,EAAKC,EAAKK,IAAU,CAC7C,GAAI,CAACJ,EAAUD,CAAG,EACd,MAAM,IAAI,UAAUE,EAAgBH,EAAKC,EAAK,GAAGG,CAAK,CAAC,EAE3D,GAAIH,EAAI,OAAS,SACb,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,CAAC,mEAAmE,EAEhH,GAAIE,IAAU,QAAUL,EAAI,OAAS,SACjC,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,CAAC,uEAAuE,EAEpH,GAAIE,IAAU,WAAaL,EAAI,OAAS,SACpC,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,CAAC,0EAA0E,EAEvH,GAAIH,EAAI,WAAaK,IAAU,UAAYL,EAAI,OAAS,UACpD,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,CAAC,wEAAwE,EAErH,GAAIH,EAAI,WAAaK,IAAU,WAAaL,EAAI,OAAS,UACrD,MAAM,IAAI,UAAU,GAAGG,EAAM,KAAK,MAAM,CAAC,yEAAyE,CAE1H,EACMG,GAAe,CAACP,EAAKC,EAAKK,IAAU,CACpBN,EAAI,WAAW,IAAI,GACjCA,IAAQ,OACRA,EAAI,WAAW,OAAO,GACtB,qBAAqB,KAAKA,CAAG,EAE7BD,GAAmBC,EAAKC,CAAG,EAG3BI,GAAoBL,EAAKC,EAAKK,CAAK,CAE3C,EACOE,EAAQD,GC3Cf,SAASE,GAAaC,EAAKC,EAAmBC,EAAkBC,EAAiBC,EAAY,CACzF,GAAIA,EAAW,OAAS,QAAaD,EAAgB,OAAS,OAC1D,MAAM,IAAIH,EAAI,gEAAgE,EAElF,GAAI,CAACG,GAAmBA,EAAgB,OAAS,OAC7C,OAAO,IAAI,IAEf,GAAI,CAAC,MAAM,QAAQA,EAAgB,IAAI,GACnCA,EAAgB,KAAK,SAAW,GAChCA,EAAgB,KAAK,KAAME,GAAU,OAAOA,GAAU,UAAYA,EAAM,SAAW,CAAC,EACpF,MAAM,IAAIL,EAAI,uFAAuF,EAEzG,IAAIM,EACAJ,IAAqB,OACrBI,EAAa,IAAI,IAAI,CAAC,GAAG,OAAO,QAAQJ,CAAgB,EAAG,GAAGD,EAAkB,QAAQ,CAAC,CAAC,EAG1FK,EAAaL,EAEjB,QAAWM,KAAaJ,EAAgB,KAAM,CAC1C,GAAI,CAACG,EAAW,IAAIC,CAAS,EACzB,MAAM,IAAIC,EAAiB,+BAA+BD,CAAS,qBAAqB,EAE5F,GAAIH,EAAWG,CAAS,IAAM,OAC1B,MAAM,IAAIP,EAAI,+BAA+BO,CAAS,cAAc,EAEnE,GAAID,EAAW,IAAIC,CAAS,GAAKJ,EAAgBI,CAAS,IAAM,OACjE,MAAM,IAAIP,EAAI,+BAA+BO,CAAS,+BAA+B,CAE7F,CACA,OAAO,IAAI,IAAIJ,EAAgB,IAAI,CACvC,CACA,IAAOM,EAAQV,GCjCf,IAAMW,GAAqB,CAACC,EAAQC,IAAe,CAC/C,GAAIA,IAAe,SACd,CAAC,MAAM,QAAQA,CAAU,GAAKA,EAAW,KAAMC,GAAM,OAAOA,GAAM,QAAQ,GAC3E,MAAM,IAAI,UAAU,IAAIF,CAAM,sCAAsC,EAExE,GAAKC,EAGL,OAAO,IAAI,IAAIA,CAAU,CAC7B,EACOE,GAAQJ,GCTA,SAARK,EAA2BC,EAAKC,EAAW,CAC9C,IAAMC,EAAO,OAAOF,EAAI,MAAM,EAAE,CAAC,GACjC,OAAQA,EAAK,CACT,IAAK,QACL,IAAK,QACL,IAAK,QACD,MAAO,CAAE,KAAAE,EAAM,KAAM,MAAO,EAChC,IAAK,QACL,IAAK,QACL,IAAK,QACD,MAAO,CAAE,KAAAA,EAAM,KAAM,UAAW,WAAYF,EAAI,MAAM,EAAE,GAAK,CAAE,EACnE,IAAK,QACL,IAAK,QACL,IAAK,QACD,MAAO,CAAE,KAAAE,EAAM,KAAM,mBAAoB,EAC7C,IAAK,QACL,IAAK,QACL,IAAK,QACD,MAAO,CAAE,KAAAA,EAAM,KAAM,QAAS,WAAYD,EAAU,UAAW,EACnE,IAAK,QACD,MAAO,CAAE,KAAMA,EAAU,IAAK,EAClC,QACI,MAAM,IAAIE,EAAiB,OAAOH,CAAG,6DAA6D,CAC1G,CACJ,CCrBe,SAARI,EAA8BC,EAAKC,EAAKC,EAAO,CAClD,GAAIC,EAAYF,CAAG,EACf,OAAAG,GAAkBH,EAAKD,EAAKE,CAAK,EAC1BD,EAEX,GAAIA,aAAe,WAAY,CAC3B,GAAI,CAACD,EAAI,WAAW,IAAI,EACpB,MAAM,IAAI,UAAUK,EAAgBJ,EAAK,GAAGK,CAAK,CAAC,EAEtD,OAAOC,EAAO,OAAO,UAAU,MAAON,EAAK,CAAE,KAAM,OAAOD,EAAI,MAAM,EAAE,CAAC,GAAI,KAAM,MAAO,EAAG,GAAO,CAACE,CAAK,CAAC,CAC7G,CACA,MAAM,IAAI,UAAUG,EAAgBJ,EAAK,GAAGK,EAAO,YAAY,CAAC,CACpE,CCZA,IAAME,GAAS,MAAOC,EAAKC,EAAKC,EAAWC,IAAS,CAChD,IAAMC,EAAY,MAAMC,EAAaL,EAAKC,EAAK,QAAQ,EACvDK,EAAeN,EAAKI,CAAS,EAC7B,IAAMG,EAAYC,EAAgBR,EAAKI,EAAU,SAAS,EAC1D,GAAI,CACA,OAAO,MAAMK,EAAO,OAAO,OAAOF,EAAWH,EAAWF,EAAWC,CAAI,CAC3E,MACW,CACP,MAAO,EACX,CACJ,EACOO,GAAQX,GCNf,eAAsBY,EAAgBC,EAAKC,EAAKC,EAAS,CACrD,IAAIC,EACJ,GAAI,CAACC,EAASJ,CAAG,EACb,MAAM,IAAIK,EAAW,iCAAiC,EAE1D,GAAIL,EAAI,YAAc,QAAaA,EAAI,SAAW,OAC9C,MAAM,IAAIK,EAAW,uEAAuE,EAEhG,GAAIL,EAAI,YAAc,QAAa,OAAOA,EAAI,WAAc,SACxD,MAAM,IAAIK,EAAW,qCAAqC,EAE9D,GAAIL,EAAI,UAAY,OAChB,MAAM,IAAIK,EAAW,qBAAqB,EAE9C,GAAI,OAAOL,EAAI,WAAc,SACzB,MAAM,IAAIK,EAAW,yCAAyC,EAElE,GAAIL,EAAI,SAAW,QAAa,CAACI,EAASJ,EAAI,MAAM,EAChD,MAAM,IAAIK,EAAW,uCAAuC,EAEhE,IAAIC,EAAa,CAAC,EAClB,GAAIN,EAAI,UACJ,GAAI,CACA,IAAMO,EAAkBC,EAAUR,EAAI,SAAS,EAC/CM,EAAa,KAAK,MAAMG,EAAQ,OAAOF,CAAe,CAAC,CAC3D,MACW,CACP,MAAM,IAAIF,EAAW,iCAAiC,CAC1D,CAEJ,GAAI,CAACK,EAAWJ,EAAYN,EAAI,MAAM,EAClC,MAAM,IAAIK,EAAW,2EAA2E,EAEpG,IAAMM,EAAa,CACf,GAAGL,EACH,GAAGN,EAAI,MACX,EACMY,EAAaC,EAAaR,EAAY,IAAI,IAAI,CAAC,CAAC,MAAO,EAAI,CAAC,CAAC,EAAqDH,GAAQ,KAAMI,EAAYK,CAAU,EACxJG,EAAM,GACV,GAAIF,EAAW,IAAI,KAAK,IACpBE,EAAMR,EAAW,IACb,OAAOQ,GAAQ,WACf,MAAM,IAAIT,EAAW,yEAAyE,EAGtG,GAAM,CAAE,IAAAU,CAAI,EAAIJ,EAChB,GAAI,OAAOI,GAAQ,UAAY,CAACA,EAC5B,MAAM,IAAIV,EAAW,2DAA2D,EAEpF,IAAMW,EAAad,GAAWe,GAAmB,aAAcf,EAAQ,UAAU,EACjF,GAAIc,GAAc,CAACA,EAAW,IAAID,CAAG,EACjC,MAAM,IAAIG,EAAkB,gDAAgD,EAEhF,GAAIJ,GACA,GAAI,OAAOd,EAAI,SAAY,SACvB,MAAM,IAAIK,EAAW,8BAA8B,UAGlD,OAAOL,EAAI,SAAY,UAAY,EAAEA,EAAI,mBAAmB,YACjE,MAAM,IAAIK,EAAW,wDAAwD,EAEjF,IAAIc,EAAc,GACd,OAAOlB,GAAQ,aACfA,EAAM,MAAMA,EAAIK,EAAYN,CAAG,EAC/BmB,EAAc,IAElBC,EAAaL,EAAKd,EAAK,QAAQ,EAC/B,IAAMoB,EAAOC,EAAOC,EAAQ,QAAQpB,EAAKH,EAAI,aAAe,MAAQG,IAAO,OAASA,EAAK,EAAE,EAAGoB,EAAQ,OAAO,GAAG,EAAG,OAAOvB,EAAI,SAAY,SAAWuB,EAAQ,OAAOvB,EAAI,OAAO,EAAIA,EAAI,OAAO,EAC1LwB,EACJ,GAAI,CACAA,EAAYhB,EAAUR,EAAI,SAAS,CACvC,MACW,CACP,MAAM,IAAIK,EAAW,0CAA0C,CACnE,CAEA,GAAI,CADa,MAAMoB,GAAOV,EAAKd,EAAKuB,EAAWH,CAAI,EAEnD,MAAM,IAAIK,EAEd,IAAIC,EACJ,GAAIb,EACA,GAAI,CACAa,EAAUnB,EAAUR,EAAI,OAAO,CACnC,MACW,CACP,MAAM,IAAIK,EAAW,wCAAwC,CACjE,MAEK,OAAOL,EAAI,SAAY,SAC5B2B,EAAUJ,EAAQ,OAAOvB,EAAI,OAAO,EAGpC2B,EAAU3B,EAAI,QAElB,IAAM4B,EAAS,CAAE,QAAAD,CAAQ,EAOzB,OANI3B,EAAI,YAAc,SAClB4B,EAAO,gBAAkBtB,GAEzBN,EAAI,SAAW,SACf4B,EAAO,kBAAoB5B,EAAI,QAE/BmB,EACO,CAAE,GAAGS,EAAQ,IAAA3B,CAAI,EAErB2B,CACX,CC/GA,eAAsBC,GAAcC,EAAKC,EAAKC,EAAS,CAInD,GAHIF,aAAe,aACfA,EAAMG,EAAQ,OAAOH,CAAG,GAExB,OAAOA,GAAQ,SACf,MAAM,IAAII,EAAW,4CAA4C,EAErE,GAAM,CAAE,EAAGC,EAAiB,EAAGC,EAAS,EAAGC,EAAW,OAAAC,CAAO,EAAIR,EAAI,MAAM,GAAG,EAC9E,GAAIQ,IAAW,EACX,MAAM,IAAIJ,EAAW,qBAAqB,EAE9C,IAAMK,EAAW,MAAMC,EAAgB,CAAE,QAAAJ,EAAS,UAAWD,EAAiB,UAAAE,CAAU,EAAGN,EAAKC,CAAO,EACjGS,EAAS,CAAE,QAASF,EAAS,QAAS,gBAAiBA,EAAS,eAAgB,EACtF,OAAI,OAAOR,GAAQ,WACR,CAAE,GAAGU,EAAQ,IAAKF,EAAS,GAAI,EAEnCE,CACX,CCpBA,IAAOC,GAASC,GAAS,KAAK,MAAMA,EAAK,QAAQ,EAAI,GAAI,ECKzD,IAAMC,GAAQ,sGACPC,EAASC,GAAQ,CACpB,IAAMC,EAAUH,GAAM,KAAKE,CAAG,EAC9B,GAAI,CAACC,EACD,MAAM,IAAI,UAAU,4BAA4B,EAEpD,IAAMC,EAAQ,WAAWD,EAAQ,CAAC,CAAC,EAEnC,OADaA,EAAQ,CAAC,EAAE,YAAY,EACtB,CACV,IAAK,MACL,IAAK,OACL,IAAK,SACL,IAAK,UACL,IAAK,IACD,OAAO,KAAK,MAAMC,CAAK,EAC3B,IAAK,SACL,IAAK,UACL,IAAK,MACL,IAAK,OACL,IAAK,IACD,OAAO,KAAK,MAAMA,EAAQ,EAAM,EACpC,IAAK,OACL,IAAK,QACL,IAAK,KACL,IAAK,MACL,IAAK,IACD,OAAO,KAAK,MAAMA,EAAQ,IAAI,EAClC,IAAK,MACL,IAAK,OACL,IAAK,IACD,OAAO,KAAK,MAAMA,EAAQ,KAAG,EACjC,IAAK,OACL,IAAK,QACL,IAAK,IACD,OAAO,KAAK,MAAMA,EAAQ,MAAI,EAClC,QACI,OAAO,KAAK,MAAMA,EAAQ,QAAI,CACtC,CACJ,ECtCA,IAAMC,GAAgBC,GAAUA,EAAM,YAAY,EAAE,QAAQ,iBAAkB,EAAE,EAC1EC,GAAwB,CAACC,EAAYC,IACnC,OAAOD,GAAe,SACfC,EAAU,SAASD,CAAU,EAEpC,MAAM,QAAQA,CAAU,EACjBC,EAAU,KAAK,IAAI,UAAU,IAAI,KAAK,IAAI,IAAID,CAAU,CAAC,CAAC,EAE9D,GAEJE,EAAQ,CAACC,EAAiBC,EAAgBC,EAAU,CAAC,IAAM,CAC9D,GAAM,CAAE,IAAAC,CAAI,EAAID,EAChB,GAAIC,IACC,OAAOH,EAAgB,KAAQ,UAC5BN,GAAaM,EAAgB,GAAG,IAAMN,GAAaS,CAAG,GAC1D,MAAM,IAAIC,EAAyB,oCAAqC,MAAO,cAAc,EAEjG,IAAIC,EACJ,GAAI,CACAA,EAAU,KAAK,MAAMC,EAAQ,OAAOL,CAAc,CAAC,CACvD,MACW,CACX,CACA,GAAI,CAACM,EAASF,CAAO,EACjB,MAAM,IAAIG,EAAW,gDAAgD,EAEzE,GAAM,CAAE,eAAAC,EAAiB,CAAC,EAAG,OAAAC,EAAQ,QAAAC,EAAS,SAAAC,EAAU,YAAAC,CAAY,EAAIX,EACpEW,IAAgB,QAChBJ,EAAe,KAAK,KAAK,EACzBG,IAAa,QACbH,EAAe,KAAK,KAAK,EACzBE,IAAY,QACZF,EAAe,KAAK,KAAK,EACzBC,IAAW,QACXD,EAAe,KAAK,KAAK,EAC7B,QAAWK,KAAS,IAAI,IAAIL,EAAe,QAAQ,CAAC,EAChD,GAAI,EAAEK,KAAST,GACX,MAAM,IAAID,EAAyB,qBAAqBU,CAAK,UAAWA,EAAO,SAAS,EAGhG,GAAIJ,GAAU,EAAE,MAAM,QAAQA,CAAM,EAAIA,EAAS,CAACA,CAAM,GAAG,SAASL,EAAQ,GAAG,EAC3E,MAAM,IAAID,EAAyB,+BAAgC,MAAO,cAAc,EAE5F,GAAIO,GAAWN,EAAQ,MAAQM,EAC3B,MAAM,IAAIP,EAAyB,+BAAgC,MAAO,cAAc,EAE5F,GAAIQ,GACA,CAAChB,GAAsBS,EAAQ,IAAK,OAAOO,GAAa,SAAW,CAACA,CAAQ,EAAIA,CAAQ,EACxF,MAAM,IAAIR,EAAyB,+BAAgC,MAAO,cAAc,EAE5F,IAAIW,EACJ,OAAQ,OAAOb,EAAQ,eAAgB,CACnC,IAAK,SACDa,EAAYC,EAAKd,EAAQ,cAAc,EACvC,MACJ,IAAK,SACDa,EAAYb,EAAQ,eACpB,MACJ,IAAK,YACDa,EAAY,EACZ,MACJ,QACI,MAAM,IAAI,UAAU,oCAAoC,CAChE,CACA,GAAM,CAAE,YAAAE,CAAY,EAAIf,EAClBgB,EAAMC,GAAMF,GAAe,IAAI,IAAM,EAC3C,IAAKZ,EAAQ,MAAQ,QAAaQ,IAAgB,OAAOR,EAAQ,KAAQ,SACrE,MAAM,IAAID,EAAyB,+BAAgC,MAAO,SAAS,EAEvF,GAAIC,EAAQ,MAAQ,OAAW,CAC3B,GAAI,OAAOA,EAAQ,KAAQ,SACvB,MAAM,IAAID,EAAyB,+BAAgC,MAAO,SAAS,EAEvF,GAAIC,EAAQ,IAAMa,EAAMH,EACpB,MAAM,IAAIX,EAAyB,qCAAsC,MAAO,cAAc,CAEtG,CACA,GAAIC,EAAQ,MAAQ,OAAW,CAC3B,GAAI,OAAOA,EAAQ,KAAQ,SACvB,MAAM,IAAID,EAAyB,+BAAgC,MAAO,SAAS,EAEvF,GAAIC,EAAQ,KAAOa,EAAMH,EACrB,MAAM,IAAIK,EAAW,qCAAsC,MAAO,cAAc,CAExF,CACA,GAAIP,EAAa,CACb,IAAMQ,EAAMH,EAAMb,EAAQ,IACpBiB,EAAM,OAAOT,GAAgB,SAAWA,EAAcG,EAAKH,CAAW,EAC5E,GAAIQ,EAAMN,EAAYO,EAClB,MAAM,IAAIF,EAAW,2DAA4D,MAAO,cAAc,EAE1G,GAAIC,EAAM,EAAIN,EACV,MAAM,IAAIX,EAAyB,gEAAiE,MAAO,cAAc,CAEjI,CACA,OAAOC,CACX,EClGA,eAAsBkB,GAAUC,EAAKC,EAAKC,EAAS,CAC/C,IAAIC,EACJ,IAAMC,EAAW,MAAMC,GAAcL,EAAKC,EAAKC,CAAO,EACtD,GAAM,GAAAC,EAAKC,EAAS,gBAAgB,QAAU,MAAQD,IAAO,SAAkBA,EAAG,SAAS,KAAK,GAAMC,EAAS,gBAAgB,MAAQ,GACnI,MAAM,IAAIE,EAAW,qCAAqC,EAG9D,IAAMC,EAAS,CAAE,QADDC,EAAWJ,EAAS,gBAAiBA,EAAS,QAASF,CAAO,EACpD,gBAAiBE,EAAS,eAAgB,EACpE,OAAI,OAAOH,GAAQ,WACR,CAAE,GAAGM,EAAQ,IAAKH,EAAS,GAAI,EAEnCG,CACX,CCGA,IAAME,GACF,oEAEJ,eAAsBC,EAClBC,EACAC,EACgB,CAEhB,GAAIA,EAAI,YAAc,OAClB,MAAO,GAIX,IAAMC,EAAQJ,GAAiB,KAAKE,EAAI,QAAQ,IAAI,eAAe,GAAK,EAAE,EAC1E,GAAI,CAACE,GAAS,CAACA,EAAM,CAAC,EAClB,MAAO,GAIX,IAAMC,EAAK,MAAMC,GAAWH,EAAI,WAAY,OAAO,EACnD,GAAI,CACA,MAAMI,GAAUH,EAAM,CAAC,EAAGC,EAAI,CAC1B,OAAQ,qBACR,SAAUF,EAAI,eACd,WAAY,CAAC,OAAO,EAEpB,eAAgB,GACpB,CAAC,CACL,OAASK,EAAK,CACV,eAAQ,MAAM,2BAA6BA,CAAG,EACvC,EACX,CAEA,MAAO,EACX,CChDI,IAAAC,GAAW,WCgBf,IAAMC,GAASC,GAAO,EAEjB,IACG,yBACA,MAAOC,EAAeC,IAAwC,CAE1D,GAAI,CADS,MAAMC,EAAiBF,EAAKC,CAAG,EAExC,OAAO,IAAI,SAAS,eAAgB,CAAE,OAAQ,GAAI,CAAC,EAIvD,IAAME,EAAmB,CAAC,EACpBC,EAAe,CAAC,EAChBC,EAAe,CAAC,EAChBC,EAAM,OAAO,KAAKL,CAAG,EAC3B,QAASM,EAAI,EAAGA,EAAID,EAAI,OAAQC,IAAK,CACjC,GAAI,CAACD,EAAIC,CAAC,EACN,SAEJ,IAAMC,EAAMP,EAAIK,EAAIC,CAAC,CAAC,EACtB,GAAI,GAACC,GAAO,OAAOA,GAAO,UAAY,CAACA,EAAI,aAG3C,OAAQA,EAAI,YAAY,KAAM,CAC1B,IAAK,cACL,IAAK,cACDJ,EAAG,KAAKE,EAAIC,CAAC,CAAC,EACd,MACJ,IAAK,cACL,IAAK,QACDJ,EAAO,KAAKG,EAAIC,CAAC,CAAC,EAClB,MACJ,IAAK,WAEDF,EAAG,KAAKC,EAAIC,CAAC,CAAC,EACd,KACR,CACJ,CAEA,IAAME,EAAM,KAAK,UAAU,CACvB,QAAAC,GACA,OAAQP,GAAUA,EAAO,OAASA,EAAS,OAC3C,GAAIC,GAAMA,EAAG,OAASA,EAAK,OAC3B,GAAIC,GAAMA,EAAG,OAASA,EAAK,MAC/B,CAAC,EACD,OAAO,IAAI,SAASI,EAAK,CACrB,QAAS,CACL,eAAgB,kBACpB,CACJ,CAAC,CACL,CACJ,EAGC,IACG,sBACA,MAAOT,EAAeC,IAAwC,CAC1D,GAAM,CAAE,UAAAU,EAAW,IAAAC,EAAK,SAAAC,CAAS,EAAI,MAAMC,GAAed,EAAKC,CAAG,EAClE,GAAIY,EACA,OAAOA,EAGX,IAAME,EAAM,MAAMJ,EAAW,IAAIC,EAAM,QAAQ,EAC/C,OAAKG,EAIE,IAAI,SAASA,EAAK,CAAE,OAAQ,GAAI,CAAC,EAH7B,IAAI,SAAS,GAAI,CAAE,OAAQ,GAAI,CAAC,CAI/C,CACJ,EAGC,KACG,sBACA,MAAOf,EAAeC,IAAwC,CAC1D,GAAM,CAAE,UAAAU,EAAW,IAAAC,EAAK,SAAAC,CAAS,EAAI,MAAMC,GAAed,EAAKC,CAAG,EAClE,GAAIY,EACA,OAAOA,EAGX,IAAIG,EACEC,EAAS,IAAI,IAAIjB,EAAI,GAAG,EACxBkB,EAAW,SAASD,EAAO,aAAa,IAAI,KAAK,GAAK,GAAI,EAAE,EAClE,OAAIC,EAAW,IACXF,EAAgBE,GAEpB,MAAMP,EAAW,IAAIC,EAAMZ,EAAI,KAAO,CAAE,cAAAgB,CAAc,CAAC,EAEhD,IAAI,SAAS,GAAI,CAAE,OAAQ,GAAI,CAAC,CAC3C,CACJ,EAGC,OACG,sBACA,MAAOhB,EAAeC,IAAwC,CAC1D,GAAM,CAAE,UAAAU,EAAW,IAAAC,EAAK,SAAAC,CAAS,EAAI,MAAMC,GAAed,EAAKC,CAAG,EAClE,OAAIY,IAIJ,MAAMF,EAAW,OAAOC,CAAI,EAErB,IAAI,SAAS,GAAI,CAAE,OAAQ,GAAI,CAAC,EAC3C,CACJ,EAGC,KACG,iBACA,MAAOZ,EAAeC,IAAwC,CAC1D,GAAM,CAAE,MAAAkB,EAAO,SAAAN,CAAS,EAAI,MAAMO,GAAkBpB,EAAKC,CAAG,EAC5D,GAAIY,EACA,OAAOA,EAGX,IAAIQ,EAAU,MAAMrB,EAAI,KAAK,EAC7B,aAAMmB,EAAO,KAAKE,CAAO,EAClB,IAAI,SAAS,GAAI,CAAE,OAAQ,GAAI,CAAC,CAC3C,CACJ,EAGC,IAAI,IAAK,IACC,IAAI,SAAS,YAAa,CAAE,OAAQ,GAAI,CAAC,CACnD,EAGL,eAAeP,GACXd,EACAC,EAKD,CACC,GAAI,CAACD,GAAK,MAAQ,CAACA,EAAI,QAAQ,WAAa,CAACA,EAAI,QAAQ,IACrD,MAAO,CAAE,SAAU,IAAI,SAAS,cAAe,CAAE,OAAQ,GAAI,CAAC,CAAE,EAEpE,IAAMW,EAAYV,EAAID,EAAI,OAAO,SAAS,EAC1C,OACI,OAAOW,GAAa,UACpB,CAAC,CAAC,cAAe,aAAa,EAAE,SAASA,GAAW,aAAa,IAAI,EAE9D,CACH,SAAU,IAAI,SACV,8BAA8BX,EAAI,OAAO,EAAE,IAC3C,CAAE,OAAQ,GAAI,CAClB,CACJ,EAGS,MAAME,EAAiBF,EAAKC,CAAG,EAKrC,CAAE,UAAAU,EAAW,IAAKX,EAAI,OAAO,GAAI,EAH7B,CAAE,SAAU,IAAI,SAAS,eAAgB,CAAE,OAAQ,GAAI,CAAC,CAAE,CAIzE,CAGA,eAAeoB,GACXpB,EACAC,EACuD,CACvD,GAAI,CAACD,GAAK,MAAQ,CAACA,EAAI,QAAQ,MAC3B,MAAO,CAAE,SAAU,IAAI,SAAS,cAAe,CAAE,OAAQ,GAAI,CAAC,CAAE,EAEpE,IAAMmB,EAAQlB,EAAID,EAAI,OAAO,KAAK,EAClC,OACI,OAAOmB,GAAS,UAChB,CAAC,CAAC,cAAe,OAAO,EAAE,SAASA,GAAO,aAAa,IAAI,EAEpD,CACH,SAAU,IAAI,SACV,iCAAiCnB,EAAI,OAAO,KAAK,IACjD,CAAE,OAAQ,GAAI,CAClB,CACJ,EAGS,MAAME,EAAiBF,EAAKC,CAAG,EAKrC,CAAE,MAAAkB,CAAM,EAHJ,CAAE,SAAU,IAAI,SAAS,eAAgB,CAAE,OAAQ,GAAI,CAAC,CAAE,CAIzE,CAEA,IAAOG,GAAQ,CACX,MAAOxB,GAAO,MAClB", + "names": ["e", "r", "a", "o", "t", "p", "l", "s", "c", "webcrypto_default", "isCryptoKey", "key", "encoder", "decoder", "MAX_INT32", "concat", "buffers", "size", "acc", "length", "buf", "i", "buffer", "decodeBase64", "encoded", "binary", "bytes", "i", "decode", "input", "decoder", "JOSEError", "message", "_a", "JWTClaimValidationFailed", "claim", "reason", "JWTExpired", "JOSEAlgNotAllowed", "JOSENotSupported", "JWSInvalid", "JOSEError", "JWTInvalid", "JWSSignatureVerificationFailed", "JOSEError", "random_default", "webcrypto_default", "unusable", "name", "prop", "isAlgorithm", "algorithm", "getHashLength", "hash", "getNamedCurve", "alg", "checkUsage", "key", "usages", "expected", "msg", "last", "checkSigCryptoKey", "message", "msg", "actual", "types", "last", "invalid_key_input_default", "withAlg", "alg", "is_key_like_default", "key", "isCryptoKey", "types", "isDisjoint", "headers", "sources", "acc", "header", "parameters", "parameter", "is_disjoint_default", "isObjectLike", "value", "isObject", "input", "proto", "check_key_length_default", "alg", "key", "modulusLength", "findOid", "keyData", "oid", "from", "i", "sub", "value", "index", "getNamedCurve", "JOSENotSupported", "genericImport", "replace", "keyFormat", "pem", "alg", "options", "_a", "algorithm", "keyUsages", "c", "isPublic", "namedCurve", "webcrypto_default", "fromSPKI", "pem", "alg", "options", "genericImport", "importSPKI", "spki", "alg", "options", "fromSPKI", "symmetricTypeCheck", "alg", "key", "is_key_like_default", "withAlg", "types", "asymmetricTypeCheck", "usage", "checkKeyType", "check_key_type_default", "validateCrit", "Err", "recognizedDefault", "recognizedOption", "protectedHeader", "joseHeader", "input", "recognized", "parameter", "JOSENotSupported", "validate_crit_default", "validateAlgorithms", "option", "algorithms", "s", "validate_algorithms_default", "subtleDsa", "alg", "algorithm", "hash", "JOSENotSupported", "getCryptoKey", "alg", "key", "usage", "isCryptoKey", "checkSigCryptoKey", "invalid_key_input_default", "types", "webcrypto_default", "verify", "alg", "key", "signature", "data", "cryptoKey", "getCryptoKey", "check_key_length_default", "algorithm", "subtleDsa", "webcrypto_default", "verify_default", "flattenedVerify", "jws", "key", "options", "_a", "isObject", "JWSInvalid", "parsedProt", "protectedHeader", "decode", "decoder", "is_disjoint_default", "joseHeader", "extensions", "validate_crit_default", "b64", "alg", "algorithms", "validate_algorithms_default", "JOSEAlgNotAllowed", "resolvedKey", "check_key_type_default", "data", "concat", "encoder", "signature", "verify_default", "JWSSignatureVerificationFailed", "payload", "result", "compactVerify", "jws", "key", "options", "decoder", "JWSInvalid", "protectedHeader", "payload", "signature", "length", "verified", "flattenedVerify", "result", "epoch_default", "date", "REGEX", "secs_default", "str", "matched", "value", "normalizeTyp", "value", "checkAudiencePresence", "audPayload", "audOption", "jwt_claims_set_default", "protectedHeader", "encodedPayload", "options", "typ", "JWTClaimValidationFailed", "payload", "decoder", "isObject", "JWTInvalid", "requiredClaims", "issuer", "subject", "audience", "maxTokenAge", "claim", "tolerance", "secs_default", "currentDate", "now", "epoch_default", "JWTExpired", "age", "max", "jwtVerify", "jwt", "key", "options", "_a", "verified", "compactVerify", "JWTInvalid", "result", "jwt_claims_set_default", "tokenHeaderMatch", "AuthorizeRequest", "req", "env", "match", "pk", "importSPKI", "jwtVerify", "err", "version", "router", "e", "req", "env", "AuthorizeRequest", "queues", "kv", "r2", "all", "i", "obj", "res", "version", "namespace", "key", "errorRes", "setupKVRequest", "val", "expirationTtl", "reqUrl", "ttlParam", "queue", "setupQueueRequest", "message", "worker_default"] } diff --git a/common/component/kafka/auth.go b/common/component/kafka/auth.go index ea8cc43fac..d84c577c3d 100644 --- a/common/component/kafka/auth.go +++ b/common/component/kafka/auth.go @@ -88,3 +88,20 @@ func updateOidcAuthInfo(config *sarama.Config, metadata *KafkaMetadata) error { return nil } + +func updateOidcPrivateKeyJWTAuthInfo(config *sarama.Config, metadata *KafkaMetadata) error { + tokenProvider := metadata.getOAuthTokenSourcePrivateKeyJWT() + + if metadata.TLSCaCert != "" { + err := tokenProvider.addCa(metadata.TLSCaCert) + if err != nil { + return fmt.Errorf("kafka: error setting oauth client trusted CA: %w", err) + } + } + + config.Net.SASL.Enable = true + config.Net.SASL.Mechanism = sarama.SASLTypeOAuth + config.Net.SASL.TokenProvider = tokenProvider + + return nil +} diff --git a/common/component/kafka/auth_test.go b/common/component/kafka/auth_test.go index 41afc5f634..10c0bc1a15 100644 --- a/common/component/kafka/auth_test.go +++ b/common/component/kafka/auth_test.go @@ -18,14 +18,20 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" + "encoding/base64" + "encoding/json" "encoding/pem" "fmt" "math/big" + "net/http" + "net/http/httptest" + "strings" "testing" "time" "github.com/IBM/sarama" "github.com/stretchr/testify/require" + "golang.org/x/oauth2" ) func getAuthBaseMetadata() map[string]string { @@ -122,4 +128,53 @@ func TestAuth(t *testing.T) { require.False(t, mockConfig.Net.TLS.Enable) require.Nil(t, mockConfig.Net.TLS.Config) }) + + t.Run("oidc private key jwt uses flattened audience", func(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + keyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + certPEM, _, err := createTestCert() + require.NoError(t, err) + + var receivedAssertion string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseForm()) + receivedAssertion = r.FormValue("client_assertion") + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "access_token": "test-token", + "expires_in": 3600, + }) + })) + defer server.Close() + + ts := &OAuthTokenSourcePrivateKeyJWT{ + TokenEndpoint: oauth2.Endpoint{TokenURL: server.URL}, + ClientID: "test-client", + ClientAssertionCert: string(certPEM), + ClientAssertionKey: string(keyPEM), + } + + _, err = ts.Token() + require.NoError(t, err) + require.NotEmpty(t, receivedAssertion) + + parts := strings.Split(receivedAssertion, ".") + require.Len(t, parts, 3, "JWT should have 3 parts") + + decodedPayload, err := base64.RawURLEncoding.DecodeString(parts[1]) + require.NoError(t, err) + + var rawClaims map[string]interface{} + err = json.Unmarshal(decodedPayload, &rawClaims) + require.NoError(t, err) + + audValue := rawClaims["aud"] + require.IsType(t, "", audValue) + }) } diff --git a/common/component/kafka/clients.go b/common/component/kafka/clients.go index 71cb5f2b22..ff571f3b12 100644 --- a/common/component/kafka/clients.go +++ b/common/component/kafka/clients.go @@ -22,6 +22,10 @@ func (k *Kafka) latestClients() (*clients, error) { // case 1: use aws clients with refreshable tokens in the cfg case k.awsConfig != nil: + if k.clients != nil { + return k.clients, nil + } + awsKafkaOpts := KafkaOptions{ Config: k.config, ConsumerGroup: k.consumerGroup, @@ -34,10 +38,12 @@ func (k *Kafka) latestClients() (*clients, error) { if err != nil { return nil, fmt.Errorf("failed to get AWS IAM Kafka clients: %w", err) } - return &clients{ + + k.clients = &clients{ consumerGroup: awsKafkaClients.ConsumerGroup, producer: awsKafkaClients.Producer, - }, nil + } + return k.clients, nil // case 2: normal static auth profile clients default: diff --git a/common/component/kafka/kafka.go b/common/component/kafka/kafka.go index 76a40c002c..9b5bab50f3 100644 --- a/common/component/kafka/kafka.go +++ b/common/component/kafka/kafka.go @@ -72,6 +72,9 @@ type Kafka struct { latestSchemaCacheWriteLock sync.RWMutex latestSchemaCacheReadLock sync.Mutex + // Whether to encode/decode Avro into Avro JSON or standard JSON + useAvroJSON bool + // used for background logic that cannot use the context passed to the Init function internalContext context.Context internalContextCancel func() @@ -181,6 +184,12 @@ func (k *Kafka) Init(ctx context.Context, metadata map[string]string) error { if err != nil { return err } + case oidcPrivateKeyJWTAuthType: + k.logger.Info("Configuring SASL OAuth2/OIDC authentication with private key JWT") + err = updateOidcPrivateKeyJWTAuthInfo(config, meta) + if err != nil { + return err + } case passwordAuthType: k.logger.Info("Configuring SASL Password authentication") k.saslUsername = meta.SaslUsername @@ -223,10 +232,12 @@ func (k *Kafka) Init(ctx context.Context, metadata map[string]string) error { } k.consumeRetryEnabled = meta.ConsumeRetryEnabled k.consumeRetryInterval = meta.ConsumeRetryInterval - if meta.SchemaRegistryURL != "" { k.logger.Infof("Schema registry URL '%s' provided. Configuring the Schema Registry client.", meta.SchemaRegistryURL) k.srClient = srclient.CreateSchemaRegistryClient(meta.SchemaRegistryURL) + k.srClient.CodecCreationEnabled(true) + k.srClient.CodecJsonEnabled(!meta.UseAvroJSON) + k.useAvroJSON = meta.UseAvroJSON // Empty password is a possibility if meta.SchemaRegistryAPIKey != "" { k.srClient.SetCredentials(meta.SchemaRegistryAPIKey, meta.SchemaRegistryAPISecret) @@ -294,12 +305,16 @@ func (k *Kafka) ValidateAWS(metadata map[string]string) (awsAuth.Options, error) } return awsAuth.Options{ + Logger: k.logger, Region: region, AccessKey: accessKey, SecretKey: secretKey, AssumeRoleArn: role, AssumeRoleSessionName: session, SessionToken: token, + TrustAnchorArn: metadata["trustAnchorArn"], + TrustProfileArn: metadata["trustProfileArn"], + Properties: metadata, }, nil } @@ -364,12 +379,7 @@ func (k *Kafka) DeserializeValue(message *sarama.ConsumerMessage, config Subscri if err != nil { return nil, err } - // The data coming through is standard JSON. The version currently supported by srclient doesn't support this yet - // Use this specific codec instead. - codec, err := goavro.NewCodecForStandardJSONFull(schema.Schema()) - if err != nil { - return nil, err - } + codec := schema.Codec() // The value returned in Avro JSON format native, _, err := codec.NativeFromBinary(message.Value[5:]) if err != nil { return nil, err @@ -405,24 +415,24 @@ func (k *Kafka) getLatestSchema(topic string) (*srclient.Schema, *goavro.Codec, if errSchema != nil { return nil, nil, errSchema } - // New JSON standard serialization/Deserialization is not integrated in srclient yet. - // Since standard json is passed from dapr, it is needed. - codec, errCodec := goavro.NewCodecForStandardJSONFull(schema.Schema()) + + codec, errCodec := k.getCodec(schema) if errCodec != nil { return nil, nil, errCodec } + defer k.latestSchemaCacheWriteLock.Unlock() k.latestSchemaCacheWriteLock.Lock() k.latestSchemaCache[subject] = SchemaCacheEntry{schema: schema, codec: codec, expirationTime: time.Now().Add(k.latestSchemaCacheTTL)} - k.latestSchemaCacheWriteLock.Unlock() + return schema, codec, nil } schema, err := srClient.GetLatestSchema(getSchemaSubject(topic)) if err != nil { return nil, nil, err } - codec, err := goavro.NewCodecForStandardJSONFull(schema.Schema()) - if err != nil { - return nil, nil, err + codec, errCodec := k.getCodec(schema) + if errCodec != nil { + return nil, nil, errCodec } return schema, codec, nil @@ -436,6 +446,17 @@ func (k *Kafka) getSchemaRegistyClient() (srclient.ISchemaRegistryClient, error) return k.srClient, nil } +func (k *Kafka) getCodec(schema *srclient.Schema) (*goavro.Codec, error) { + // The data coming through is either Avro JSON or standard JSON. + // Force creation of a new codec instance for serialization and deserialization to avoid state mutation issues. + // https://github.com/linkedin/goavro/issues/299 + // Once the bug is fixed, we can remove this and use the codec directly from schema.Codec() + if k.useAvroJSON { + return goavro.NewCodec(schema.Schema()) + } + return goavro.NewCodecForStandardJSONFull(schema.Schema()) +} + func (k *Kafka) SerializeValue(topic string, data []byte, metadata map[string]string) ([]byte, error) { // Null Data is valid and a tombstone record. // It should be converted to NULL and not go through schema validation & encoding diff --git a/common/component/kafka/kafka_test.go b/common/component/kafka/kafka_test.go index c6375d94e2..2647e91109 100644 --- a/common/component/kafka/kafka_test.go +++ b/common/component/kafka/kafka_test.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "encoding/json" "errors" + "math/big" "testing" "time" @@ -51,57 +52,94 @@ func TestGetValueSchemaType(t *testing.T) { } var ( - testSchema1 = `{"type": "record", "name": "cupcake", "fields": [{"name": "flavor", "type": "string"}, {"name": "created_date", "type": ["null",{"type": "long","logicalType": "timestamp-millis"}],"default": null}]}` - testValue1 = map[string]interface{}{"flavor": "chocolate", "created_date": float64(time.Now().UnixMilli())} - invValue = map[string]string{"xxx": "chocolate"} + now = time.Now() + testSchema1 = `{ + "type": "record", + "name": "cupcake", + "fields": [ + {"name": "flavor", "type": "string"}, + {"name": "weight", "type": {"type": "bytes", "logicalType": "decimal", "precision": 5, "scale": 2}}, + {"name": "created_date", "type": ["null", {"type": "long", "logicalType": "timestamp-millis"}], "default": null} + ] + }` + testValue1 = map[string]any{"flavor": "chocolate", "weight": "100.24", "created_date": float64(now.UnixMilli())} + testValue1AvroJSON = map[string]any{"flavor": "chocolate", "weight": big.NewRat(10024, 100), "created_date": goavro.Union("long.timestamp-millis", float64(now.UnixMilli()))} + invValue = map[string]string{"xxx": "chocolate"} ) func TestDeserializeValue(t *testing.T) { - registry := srclient.CreateMockSchemaRegistryClient("http://localhost:8081") - schema, _ := registry.CreateSchema("my-topic-value", testSchema1, srclient.Avro) handlerConfig := SubscriptionHandlerConfig{ IsBulkSubscribe: false, ValueSchemaType: Avro, } - k := Kafka{ - srClient: registry, + + registryJSON := srclient.CreateMockSchemaRegistryClient("http://localhost:8081") + kJSON := Kafka{ + srClient: registryJSON, schemaCachingEnabled: true, logger: logger.NewLogger("kafka_test"), } + kJSON.srClient.CodecJsonEnabled(true) + kJSON.useAvroJSON = false + schemaJSON, _ := registryJSON.CreateSchema("my-topic-value", testSchema1, srclient.Avro) - schemaIDBytes := make([]byte, 4) - binary.BigEndian.PutUint32(schemaIDBytes, uint32(schema.ID())) //nolint:gosec - - valJSON, _ := json.Marshal(testValue1) + // set up for Standard JSON codec, _ := goavro.NewCodecForStandardJSONFull(testSchema1) + valJSON, _ := json.Marshal(testValue1) native, _, _ := codec.NativeFromTextual(valJSON) - valueBytes, _ := codec.BinaryFromNative(nil, native) + valueBytesFromJSON, _ := codec.BinaryFromNative(nil, native) + recordValueFromJSON := formatByteRecord(schemaJSON.ID(), valueBytesFromJSON) - var recordValue []byte - recordValue = append(recordValue, byte(0)) - recordValue = append(recordValue, schemaIDBytes...) - recordValue = append(recordValue, valueBytes...) + // setup for Avro JSON + registryAvroJSON := srclient.CreateMockSchemaRegistryClient("http://localhost:8081") + kAvroJSON := Kafka{ + srClient: registryAvroJSON, + schemaCachingEnabled: true, + logger: logger.NewLogger("kafka_test"), + } + kAvroJSON.srClient.CodecJsonEnabled(false) + schemaAvroJSON, _ := registryAvroJSON.CreateSchema("my-topic-value", testSchema1, srclient.Avro) + + codecAvroJSON, _ := goavro.NewCodec(testSchema1) + valueBytesFromAvroJSON, _ := codecAvroJSON.BinaryFromNative(nil, testValue1AvroJSON) + valueAvroJSON, _ := codecAvroJSON.TextualFromNative(nil, testValue1AvroJSON) + recordValueFromAvroJSON := formatByteRecord(schemaAvroJSON.ID(), valueBytesFromAvroJSON) t.Run("Schema found, return value", func(t *testing.T) { msg := sarama.ConsumerMessage{ Key: []byte("my_key"), - Value: recordValue, + Value: recordValueFromJSON, Topic: "my-topic", } - act, err := k.DeserializeValue(&msg, handlerConfig) + act, err := kJSON.DeserializeValue(&msg, handlerConfig) var actMap map[string]any json.Unmarshal(act, &actMap) require.Equal(t, testValue1, actMap) require.NoError(t, err) }) + t.Run("Schema found and useAvroJson, return value as Avro Json", func(t *testing.T) { + msg := sarama.ConsumerMessage{ + Key: []byte("my_key"), + Value: recordValueFromAvroJSON, + Topic: "my-topic", + } + + actText, err := kAvroJSON.DeserializeValue(&msg, handlerConfig) + // test if flaky if comparing the textual version. Convert to native for comparison + exp, _, _ := codecAvroJSON.NativeFromTextual(valueAvroJSON) + act, _, _ := codecAvroJSON.NativeFromTextual(actText) + require.Equal(t, exp, act) + require.NoError(t, err) + }) + t.Run("Data null, return as JSON null", func(t *testing.T) { msg := sarama.ConsumerMessage{ Key: []byte("my_key"), Value: nil, Topic: "my-topic", } - act, err := k.DeserializeValue(&msg, handlerConfig) + act, err := kJSON.DeserializeValue(&msg, handlerConfig) require.Equal(t, []byte("null"), act) require.NoError(t, err) }) @@ -112,7 +150,7 @@ func TestDeserializeValue(t *testing.T) { Value: []byte("xxxx"), Topic: "my-topic", } - _, err := k.DeserializeValue(&msg, handlerConfig) + _, err := kJSON.DeserializeValue(&msg, handlerConfig) require.Error(t, err) }) @@ -123,7 +161,7 @@ func TestDeserializeValue(t *testing.T) { Value: []byte("xxxxx"), Topic: "my-topic", } - _, err := k.DeserializeValue(&msg, handlerConfig) + _, err := kJSON.DeserializeValue(&msg, handlerConfig) require.Error(t, err) }) @@ -131,7 +169,7 @@ func TestDeserializeValue(t *testing.T) { t.Run("Invalid data, return error", func(t *testing.T) { var invalidVal []byte invalidVal = append(invalidVal, byte(0)) - invalidVal = append(invalidVal, schemaIDBytes...) + invalidVal = append(invalidVal, recordValueFromJSON[0:5]...) invalidVal = append(invalidVal, []byte("xxx")...) msg := sarama.ConsumerMessage{ @@ -139,7 +177,7 @@ func TestDeserializeValue(t *testing.T) { Value: invalidVal, Topic: "my-topic", } - _, err := k.DeserializeValue(&msg, handlerConfig) + _, err := kJSON.DeserializeValue(&msg, handlerConfig) require.Error(t, err) }) @@ -151,16 +189,62 @@ func TestDeserializeValue(t *testing.T) { } msg := sarama.ConsumerMessage{ Key: []byte("my_key"), - Value: recordValue, + Value: recordValueFromJSON, Topic: "my-topic", } _, err := kInv.DeserializeValue(&msg, handlerConfig) require.Error(t, err, "schema registry details not set") }) + + t.Run("verifying issue with union types due to codec state mutation is fixed", func(t *testing.T) { + // Arrange + testSchemaUnion := `["null", "long"]` + + // In happy path, codec is initialized and NativeFromBinary is called first, which sets the states of the codec + codecCard1, err := goavro.NewCodecForStandardJSONFull(testSchemaUnion) + require.NoError(t, err) + + datum1, _, err := codecCard1.NativeFromBinary([]byte{0x02, 0x06}) + require.NoError(t, err) + + // As expected, the datum is a long with value 3 + require.Equal(t, int64(3), datum1.(map[string]any)["long"]) + + // Reproducing the error when NativeFromTextual is called before NativeFromBinary, which changes the states of the codec + codecCard2, err := goavro.NewCodecForStandardJSONFull(testSchemaUnion) + require.NoError(t, err) + + // Trigger textual path that mutates states + codecCard2.NativeFromTextual([]byte("1")) + + // Binary for union index 1 (long) with value 3: 0x02 0x06 + datum, _, err := codecCard2.NativeFromBinary([]byte{0x02, 0x06}) + require.NoError(t, err) + + // Prior to bug fix, the datum would be returned as a {"null", 3} but should return '{"long":3}'! + require.Nil(t, datum.(map[string]any)["null"]) + require.Equal(t, int64(3), datum.(map[string]any)["long"]) + + // As a result, next call to TextualFromNative would fail with "Cannot encode textual union: cannot encode textual null: expected: Go nil; received: int64" + act, err := codecCard2.TextualFromNative(nil, datum) + require.NoError(t, err) + require.Equal(t, []byte("3"), act) + }) } -func assertValueSerialized(t *testing.T, act []byte, valJSON []byte, schema *srclient.Schema) { - require.NotEqual(t, act, valJSON) +func formatByteRecord(schemaID int, valueBytes []byte) []byte { + schemaIDBytes := make([]byte, 4) + binary.BigEndian.PutUint32(schemaIDBytes, uint32(schemaID)) //nolint:gosec + + var recordValue []byte + recordValue = append(recordValue, byte(0)) + recordValue = append(recordValue, schemaIDBytes...) + recordValue = append(recordValue, valueBytes...) + return recordValue +} + +func assertValueSerialized(t *testing.T, act []byte, expJSON []byte, schema *srclient.Schema) { + require.NotEqual(t, expJSON, act) actSchemaID := int(binary.BigEndian.Uint32(act[1:5])) codec, _ := goavro.NewCodecForStandardJSONFull(schema.Schema()) @@ -168,87 +252,111 @@ func assertValueSerialized(t *testing.T, act []byte, valJSON []byte, schema *src actJSON, _ := codec.TextualFromNative(nil, native) var actMap map[string]any json.Unmarshal(actJSON, &actMap) + var expMap map[string]any + json.Unmarshal(expJSON, &expMap) require.Equal(t, schema.ID(), actSchemaID) - require.Equal(t, testValue1, actMap) + require.Equal(t, expMap, actMap) } func TestSerializeValueCachingDisabled(t *testing.T) { - registry := srclient.CreateMockSchemaRegistryClient("http://localhost:8081") - schema, _ := registry.CreateSchema("my-topic-value", testSchema1, srclient.Avro) + registryJSON := srclient.CreateMockSchemaRegistryClient("http://localhost:8081") + registryJSON.CodecJsonEnabled(true) + schemaJSON, _ := registryJSON.CreateSchema("my-topic-value", testSchema1, srclient.Avro) - k := Kafka{ - srClient: registry, + schemaIDBytes := make([]byte, 4) + binary.BigEndian.PutUint32(schemaIDBytes, uint32(schemaJSON.ID())) //nolint:gosec + + // set up for Avro JSON + registryAvroJSON := srclient.CreateMockSchemaRegistryClient("http://localhost:8081") + registryAvroJSON.CodecJsonEnabled(false) + codecAvroJSON, _ := goavro.NewCodec(testSchema1) + schemaAvroJSON, _ := registryAvroJSON.CreateSchema("my-topic-value", testSchema1, srclient.Avro) + schemaAvroJSONIDBytes := make([]byte, 4) + binary.BigEndian.PutUint32(schemaAvroJSONIDBytes, uint32(schemaAvroJSON.ID())) //nolint:gosec + valueBytesFromAvroJSON, _ := codecAvroJSON.BinaryFromNative(nil, testValue1AvroJSON) + valueAvroJSON, _ := codecAvroJSON.TextualFromNative(nil, testValue1AvroJSON) + var recordValueFromAvroJSON []byte + recordValueFromAvroJSON = append(recordValueFromAvroJSON, byte(0)) + recordValueFromAvroJSON = append(recordValueFromAvroJSON, schemaAvroJSONIDBytes...) + recordValueFromAvroJSON = append(recordValueFromAvroJSON, valueBytesFromAvroJSON...) + + kJSON := Kafka{ + srClient: registryJSON, + schemaCachingEnabled: false, + logger: logger.NewLogger("kafka_test"), + useAvroJSON: false, + } + + kAvroJSON := Kafka{ + srClient: registryAvroJSON, schemaCachingEnabled: false, logger: logger.NewLogger("kafka_test"), + useAvroJSON: true, } t.Run("valueSchemaType not set, leave value as is", func(t *testing.T) { valJSON, _ := json.Marshal(testValue1) - - act, err := k.SerializeValue("my-topic", valJSON, map[string]string{}) - + act, err := kJSON.SerializeValue("my-topic", valJSON, map[string]string{}) require.JSONEq(t, string(valJSON), string(act)) require.NoError(t, err) }) t.Run("valueSchemaType set to None, leave value as is", func(t *testing.T) { valJSON, _ := json.Marshal(testValue1) - - act, err := k.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "None"}) - + act, err := kJSON.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "None"}) require.JSONEq(t, string(valJSON), string(act)) require.NoError(t, err) }) t.Run("valueSchemaType set to None, leave value as is", func(t *testing.T) { valJSON, _ := json.Marshal(testValue1) - - act, err := k.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "NONE"}) - + act, err := kJSON.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "NONE"}) require.JSONEq(t, string(valJSON), string(act)) require.NoError(t, err) }) t.Run("valueSchemaType invalid, return error", func(t *testing.T) { valJSON, _ := json.Marshal(testValue1) - - _, err := k.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "xx"}) - + _, err := kJSON.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "xx"}) require.Error(t, err, "error parsing schema type. 'xx' is not a supported value") }) t.Run("schema found, serialize value as Avro binary", func(t *testing.T) { valJSON, _ := json.Marshal(testValue1) - act, err := k.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "Avro"}) - assertValueSerialized(t, act, valJSON, schema) + act, err := kJSON.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "Avro"}) + assertValueSerialized(t, act, valJSON, schemaJSON) require.NoError(t, err) }) - t.Run("value published 'null', no error", func(t *testing.T) { - act, err := k.SerializeValue("my-topic", []byte("null"), map[string]string{"valueSchemaType": "Avro"}) + t.Run("schema found, useAvroJson=true, serialize value as Avro binary", func(t *testing.T) { + act, err := kAvroJSON.SerializeValue("my-topic", valueAvroJSON, map[string]string{"valueSchemaType": "Avro"}) + require.Equal(t, act[:5], recordValueFromAvroJSON[:5]) + require.NoError(t, err) + }) + t.Run("value published 'null', no error", func(t *testing.T) { + act, err := kJSON.SerializeValue("my-topic", []byte("null"), map[string]string{"valueSchemaType": "Avro"}) require.Nil(t, act) require.NoError(t, err) }) t.Run("value published nil, no error", func(t *testing.T) { - act, err := k.SerializeValue("my-topic", nil, map[string]string{"valueSchemaType": "Avro"}) - + act, err := kJSON.SerializeValue("my-topic", nil, map[string]string{"valueSchemaType": "Avro"}) require.Nil(t, act) require.NoError(t, err) }) t.Run("invalid data, return error", func(t *testing.T) { valJSON, _ := json.Marshal(invValue) - _, err := k.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "Avro"}) - + _, err := kJSON.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "Avro"}) require.Error(t, err, "cannot decode textual record \"cupcake\": cannot decode textual map: cannot determine codec: \"xxx\"") }) } func TestSerializeValueCachingEnabled(t *testing.T) { registry := srclient.CreateMockSchemaRegistryClient("http://localhost:8081") + registry.CodecJsonEnabled(true) schema, _ := registry.CreateSchema("my-topic-value", testSchema1, srclient.Avro) k := Kafka{ @@ -257,6 +365,7 @@ func TestSerializeValueCachingEnabled(t *testing.T) { latestSchemaCache: make(map[string]SchemaCacheEntry), latestSchemaCacheTTL: time.Minute * 5, logger: logger.NewLogger("kafka_test"), + useAvroJSON: false, } t.Run("valueSchemaType not set, leave value as is", func(t *testing.T) { @@ -272,12 +381,85 @@ func TestSerializeValueCachingEnabled(t *testing.T) { assertValueSerialized(t, act, valJSON, schema) require.NoError(t, err) }) + + t.Run("serialize with complex avro schema", func(t *testing.T) { + testSchemaOcr := `{ + "type": "record", + "name": "ocr_requested", + "namespace": "foo.cmd.image_processing", + "fields": [ + { + "name": "id", + "type": "string", + "doc": "Idempotency key" + }, + { + "name": "document_metadata", + "type": { + "type": "record", + "name": "DocumentMetadata", + "fields": [ + { + "name": "content_type", + "type": "string" + }, + { + "name": "original_filename", + "type": ["null", "string"], + "default": null + }, + { + "name": "source", + "type": { + "type": "enum", + "name": "DocumentSource", + "symbols": [ + "Unknown", + "Import", + "PatientUpload", + "UserUpload", + "UserUploadFax" + ] + } + }, + { + "name": "type", + "type": { + "type": "enum", + "name": "DocumentType", + "symbols": ["Unknown", "InsuranceCard", "MiscReport"] + } + } + ] + } + }, + { + "name": "s3_path", + "type": { + "type": "record", + "name": "S3Path", + "fields": [ + { "name": "bucket", "type": "string" }, + { "name": "key", "type": "string" } + ] + } + } + ] +}` + schemaOcr, _ := registry.CreateSchema("my-ocr-topic-value", testSchemaOcr, srclient.Avro) + valueOcr := map[string]any{"id": "123", "document_metadata": map[string]any{"content_type": "application/pdf", "original_filename": nil, "source": "UserUpload", "type": "InsuranceCard"}, "s3_path": map[string]any{"bucket": "test-bucket", "key": "test-key"}} + valJSONOcr, _ := json.Marshal(valueOcr) + act, err := k.SerializeValue("my-ocr-topic", valJSONOcr, map[string]string{"valueSchemaType": "Avro"}) + assertValueSerialized(t, act, valJSONOcr, schemaOcr) + require.NoError(t, err) + }) } func TestLatestSchemaCaching(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() registry := srclient.CreateMockSchemaRegistryClient("http://locahost:8081") + registry.CodecJsonEnabled(true) m := mock_srclient.NewMockISchemaRegistryClient(ctrl) schema, _ := registry.CreateSchema("my-topic-value", testSchema1, srclient.Avro) @@ -339,9 +521,7 @@ func TestLatestSchemaCaching(t *testing.T) { } m.EXPECT().GetLatestSchema(gomock.Eq("my-topic-value")).Return(schema, nil).Times(2) - valJSON, _ := json.Marshal(testValue1) - act, err := k.SerializeValue("my-topic", valJSON, map[string]string{"valueSchemaType": "Avro"}) assertValueSerialized(t, act, valJSON, schema) @@ -379,6 +559,14 @@ func TestValidateAWS(t *testing.T) { AssumeRoleArn: "testRoleArn", AssumeRoleSessionName: "testSessionName", SessionToken: "testSessionToken", + Properties: map[string]string{ + "region": "us-east-1", + "accessKey": "testAccessKey", + "secretKey": "testSecretKey", + "assumeRoleArn": "testRoleArn", + "sessionName": "testSessionName", + "sessionToken": "testSessionToken", + }, }, err: nil, }, @@ -399,6 +587,14 @@ func TestValidateAWS(t *testing.T) { AssumeRoleArn: "awsRoleArn", AssumeRoleSessionName: "awsSessionName", SessionToken: "awsSessionToken", + Properties: map[string]string{ + "awsRegion": "us-west-2", + "awsAccessKey": "awsAccessKey", + "awsSecretKey": "awsSecretKey", + "awsIamRoleArn": "awsRoleArn", + "awsStsSessionName": "awsSessionName", + "awsSessionToken": "awsSessionToken", + }, }, err: nil, }, diff --git a/common/component/kafka/metadata.go b/common/component/kafka/metadata.go index 2453037583..9fa87fb45e 100644 --- a/common/component/kafka/metadata.go +++ b/common/component/kafka/metadata.go @@ -42,6 +42,7 @@ const ( authType = "authType" passwordAuthType = "password" oidcAuthType = "oidc" + oidcPrivateKeyJWTAuthType = "oidc_private_key_jwt" mtlsAuthType = "mtls" awsIAMAuthType = "awsiam" noAuthType = "none" @@ -65,36 +66,41 @@ const ( ) type KafkaMetadata struct { - Brokers string `mapstructure:"brokers"` - internalBrokers []string `mapstructure:"-"` - ConsumerGroup string `mapstructure:"consumerGroup"` - ClientID string `mapstructure:"clientId"` - AuthType string `mapstructure:"authType"` - SaslUsername string `mapstructure:"saslUsername"` - SaslPassword string `mapstructure:"saslPassword"` - SaslMechanism string `mapstructure:"saslMechanism"` - InitialOffset string `mapstructure:"initialOffset"` - internalInitialOffset int64 `mapstructure:"-"` - MaxMessageBytes int `mapstructure:"maxMessageBytes"` - OidcTokenEndpoint string `mapstructure:"oidcTokenEndpoint"` - OidcClientID string `mapstructure:"oidcClientID"` - OidcClientSecret string `mapstructure:"oidcClientSecret"` - OidcScopes string `mapstructure:"oidcScopes"` - OidcExtensions string `mapstructure:"oidcExtensions"` - internalOidcScopes []string `mapstructure:"-"` - TLSDisable bool `mapstructure:"disableTls"` - TLSSkipVerify bool `mapstructure:"skipVerify"` - TLSCaCert string `mapstructure:"caCert"` - TLSClientCert string `mapstructure:"clientCert"` - TLSClientKey string `mapstructure:"clientKey"` - ConsumeRetryEnabled bool `mapstructure:"consumeRetryEnabled"` - ConsumeRetryInterval time.Duration `mapstructure:"consumeRetryInterval"` - HeartbeatInterval time.Duration `mapstructure:"heartbeatInterval"` - SessionTimeout time.Duration `mapstructure:"sessionTimeout"` - Version string `mapstructure:"version"` - EscapeHeaders bool `mapstructure:"escapeHeaders"` - internalVersion sarama.KafkaVersion `mapstructure:"-"` - internalOidcExtensions map[string]string `mapstructure:"-"` + Brokers string `mapstructure:"brokers"` + internalBrokers []string `mapstructure:"-"` + ConsumerGroup string `mapstructure:"consumerGroup"` + ClientID string `mapstructure:"clientId"` + AuthType string `mapstructure:"authType"` + SaslUsername string `mapstructure:"saslUsername"` + SaslPassword string `mapstructure:"saslPassword"` + SaslMechanism string `mapstructure:"saslMechanism"` + InitialOffset string `mapstructure:"initialOffset"` + internalInitialOffset int64 `mapstructure:"-"` + MaxMessageBytes int `mapstructure:"maxMessageBytes"` + OidcTokenEndpoint string `mapstructure:"oidcTokenEndpoint"` + OidcClientID string `mapstructure:"oidcClientID"` + OidcClientSecret string `mapstructure:"oidcClientSecret"` + OidcScopes string `mapstructure:"oidcScopes"` + OidcExtensions string `mapstructure:"oidcExtensions"` + OidcClientAssertionCert string `mapstructure:"oidcClientAssertionCert"` + OidcClientAssertionKey string `mapstructure:"oidcClientAssertionKey"` + OidcResource string `mapstructure:"oidcResource"` + OidcAudience string `mapstructure:"oidcAudience"` + OidcKid string `mapstructure:"oidcKid"` + internalOidcScopes []string `mapstructure:"-"` + TLSDisable bool `mapstructure:"disableTls"` + TLSSkipVerify bool `mapstructure:"skipVerify"` + TLSCaCert string `mapstructure:"caCert"` + TLSClientCert string `mapstructure:"clientCert"` + TLSClientKey string `mapstructure:"clientKey"` + ConsumeRetryEnabled bool `mapstructure:"consumeRetryEnabled"` + ConsumeRetryInterval time.Duration `mapstructure:"consumeRetryInterval"` + HeartbeatInterval time.Duration `mapstructure:"heartbeatInterval"` + SessionTimeout time.Duration `mapstructure:"sessionTimeout"` + Version string `mapstructure:"version"` + EscapeHeaders bool `mapstructure:"escapeHeaders"` + internalVersion sarama.KafkaVersion `mapstructure:"-"` + internalOidcExtensions map[string]string `mapstructure:"-"` // configs for kafka client ClientConnectionTopicMetadataRefreshInterval time.Duration `mapstructure:"clientConnectionTopicMetadataRefreshInterval"` @@ -116,6 +122,7 @@ type KafkaMetadata struct { SchemaRegistryAPISecret string `mapstructure:"schemaRegistryAPISecret"` SchemaCachingEnabled bool `mapstructure:"schemaCachingEnabled"` SchemaLatestVersionCacheTTL time.Duration `mapstructure:"schemaLatestVersionCacheTTL"` + UseAvroJSON bool `mapstructure:"useAvroJSON"` // header from/to metadata excluded keys regex ExcludeHeaderMetaRegex string `mapstructure:"excludeHeaderMetaRegex"` @@ -171,6 +178,7 @@ func (k *Kafka) getKafkaMetadata(meta map[string]string) (*KafkaMetadata, error) SchemaCachingEnabled: true, SchemaLatestVersionCacheTTL: 5 * time.Minute, EscapeHeaders: false, + UseAvroJSON: false, ExcludeHeaderMetaRegex: "", } @@ -249,6 +257,32 @@ func (k *Kafka) getKafkaMetadata(meta map[string]string) (*KafkaMetadata, error) } } k.logger.Debug("Configuring SASL token authentication via OIDC.") + case oidcPrivateKeyJWTAuthType: + if m.OidcTokenEndpoint == "" { + return nil, errors.New("kafka error: missing OIDC Token Endpoint for authType 'oidc_private_key_jwt'") + } + if m.OidcClientID == "" { + return nil, errors.New("kafka error: missing OIDC Client ID for authType 'oidc_private_key_jwt'") + } + if m.OidcClientAssertionCert == "" { + return nil, errors.New("kafka error: missing OIDC Client Assertion Cert for authType 'oidc_private_key_jwt'") + } + if m.OidcClientAssertionKey == "" { + return nil, errors.New("kafka error: missing OIDC Client Assertion Key for authType 'oidc_private_key_jwt'") + } + if m.OidcScopes != "" { + m.internalOidcScopes = strings.Split(m.OidcScopes, ",") + } else { + k.logger.Warn("Warning: no OIDC scopes specified, using default 'openid' scope only. This is a security risk for token reuse.") + m.internalOidcScopes = []string{"openid"} + } + if m.OidcExtensions != "" { + err = json.Unmarshal([]byte(m.OidcExtensions), &m.internalOidcExtensions) + if err != nil || len(m.internalOidcExtensions) < 1 { + return nil, errors.New("kafka error: improper OIDC Extensions format for authType 'oidc_private_key_jwt'") + } + } + k.logger.Debug("Configuring SASL token authentication via OIDC with private_key_jwt.") case mtlsAuthType: if m.TLSClientCert != "" { if !isValidPEM(m.TLSClientCert) { diff --git a/common/component/kafka/metadata_test.go b/common/component/kafka/metadata_test.go index bdba431a46..f1c49a2c1c 100644 --- a/common/component/kafka/metadata_test.go +++ b/common/component/kafka/metadata_test.go @@ -216,7 +216,7 @@ func TestMissingSaslValuesOnUpgrade(t *testing.T) { require.Equal(t, fmt.Sprintf("kafka error: missing SASL Username for authType '%s'", passwordAuthType), err.Error()) } -func TestMissingOidcValues(t *testing.T) { +func TestMissingOidcClientSecretValues(t *testing.T) { k := getKafka() m := map[string]string{"brokers": "akfak.com:9092", "authType": oidcAuthType} meta, err := k.getKafkaMetadata(m) @@ -243,6 +243,43 @@ func TestMissingOidcValues(t *testing.T) { require.Contains(t, meta.internalOidcScopes, "openid") } +func TestMissingOidcPrivateKeyJwtValues(t *testing.T) { + k := getKafka() + m := map[string]string{"brokers": "akfak.com:9092", "authType": oidcPrivateKeyJWTAuthType} + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + require.Equal(t, "kafka error: missing OIDC Token Endpoint for authType 'oidc_private_key_jwt'", err.Error()) + + m["oidcTokenEndpoint"] = "https://sassa.fra/" + meta, err = k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + require.Equal(t, "kafka error: missing OIDC Client ID for authType 'oidc_private_key_jwt'", err.Error()) + + m["oidcClientID"] = "sassafras" + meta, err = k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + require.Equal(t, "kafka error: missing OIDC Client Assertion Cert for authType 'oidc_private_key_jwt'", err.Error()) + + m["oidcClientAssertionCert"] = "sassapass" + meta, err = k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + require.Equal(t, "kafka error: missing OIDC Client Assertion Key for authType 'oidc_private_key_jwt'", err.Error()) + + m["oidcClientAssertionKey"] = "sassapass" + meta, err = k.getKafkaMetadata(m) + require.NoError(t, err) + require.Contains(t, meta.internalOidcScopes, "openid") + + m["oidcKid"] = "1234567890" + meta, err = k.getKafkaMetadata(m) + require.NoError(t, err) + require.Equal(t, "1234567890", meta.OidcKid) +} + func TestPresentSaslValues(t *testing.T) { k := getKafka() m := map[string]string{ diff --git a/common/component/kafka/mocks/mock_ISchemaRegistryClient.go b/common/component/kafka/mocks/mock_ISchemaRegistryClient.go index 0e7d3bb72c..a07b6a2470 100644 --- a/common/component/kafka/mocks/mock_ISchemaRegistryClient.go +++ b/common/component/kafka/mocks/mock_ISchemaRegistryClient.go @@ -1,7 +1,7 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: /Users/patrick.assuied/go/pkg/mod/github.com/riferrei/srclient@v0.6.0/schemaRegistryClient.go +// Source: /Users/patrick.assuied/go/pkg/mod/github.com/riferrei/srclient@v0.7.2/schemaRegistryClient.go -// Package mock_srclient is a generated GoMock package. +// Package mocks is a generated GoMock package. package mocks import ( @@ -74,6 +74,18 @@ func (mr *MockISchemaRegistryClientMockRecorder) CodecCreationEnabled(value inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodecCreationEnabled", reflect.TypeOf((*MockISchemaRegistryClient)(nil).CodecCreationEnabled), value) } +// CodecJsonEnabled mocks base method. +func (m *MockISchemaRegistryClient) CodecJsonEnabled(value bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "CodecJsonEnabled", value) +} + +// CodecJsonEnabled indicates an expected call of CodecJsonEnabled. +func (mr *MockISchemaRegistryClientMockRecorder) CodecJsonEnabled(value interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodecJsonEnabled", reflect.TypeOf((*MockISchemaRegistryClient)(nil).CodecJsonEnabled), value) +} + // CreateSchema mocks base method. func (m *MockISchemaRegistryClient) CreateSchema(subject, schema string, schemaType srclient.SchemaType, references ...srclient.Reference) (*srclient.Schema, error) { m.ctrl.T.Helper() @@ -197,6 +209,20 @@ func (mr *MockISchemaRegistryClientMockRecorder) GetSchemaByVersion(subject, ver return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemaByVersion", reflect.TypeOf((*MockISchemaRegistryClient)(nil).GetSchemaByVersion), subject, version) } +// GetSchemaRegistryURL mocks base method. +func (m *MockISchemaRegistryClient) GetSchemaRegistryURL() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSchemaRegistryURL") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetSchemaRegistryURL indicates an expected call of GetSchemaRegistryURL. +func (mr *MockISchemaRegistryClientMockRecorder) GetSchemaRegistryURL() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemaRegistryURL", reflect.TypeOf((*MockISchemaRegistryClient)(nil).GetSchemaRegistryURL)) +} + // GetSchemaVersions mocks base method. func (m *MockISchemaRegistryClient) GetSchemaVersions(subject string) ([]int, error) { m.ctrl.T.Helper() @@ -212,6 +238,21 @@ func (mr *MockISchemaRegistryClientMockRecorder) GetSchemaVersions(subject inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemaVersions", reflect.TypeOf((*MockISchemaRegistryClient)(nil).GetSchemaVersions), subject) } +// GetSubjectVersionsById mocks base method. +func (m *MockISchemaRegistryClient) GetSubjectVersionsById(schemaID int) (srclient.SubjectVersionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubjectVersionsById", schemaID) + ret0, _ := ret[0].(srclient.SubjectVersionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubjectVersionsById indicates an expected call of GetSubjectVersionsById. +func (mr *MockISchemaRegistryClientMockRecorder) GetSubjectVersionsById(schemaID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubjectVersionsById", reflect.TypeOf((*MockISchemaRegistryClient)(nil).GetSubjectVersionsById), schemaID) +} + // GetSubjects mocks base method. func (m *MockISchemaRegistryClient) GetSubjects() ([]string, error) { m.ctrl.T.Helper() diff --git a/common/component/kafka/sasl_oauthbearer.go b/common/component/kafka/sasl_oauthbearer.go index 125956617c..eb9a684b63 100644 --- a/common/component/kafka/sasl_oauthbearer.go +++ b/common/component/kafka/sasl_oauthbearer.go @@ -17,12 +17,13 @@ import ( ctx "context" "crypto/tls" "crypto/x509" - "encoding/pem" "errors" "fmt" "net/http" "time" + "github.com/dapr/kit/crypto/pem" + "github.com/IBM/sarama" "golang.org/x/oauth2" ccred "golang.org/x/oauth2/clientcredentials" @@ -51,26 +52,21 @@ func (m KafkaMetadata) getOAuthTokenSource() *OAuthTokenSource { } } -var tokenRequestTimeout, _ = time.ParseDuration("30s") - func (ts *OAuthTokenSource) addCa(caPem string) error { pemBytes := []byte(caPem) - block, _ := pem.Decode(pemBytes) - - if block == nil || block.Type != "CERTIFICATE" { - return errors.New("PEM data not valid or not of a valid type (CERTIFICATE)") - } - - caCert, err := x509.ParseCertificate(block.Bytes) + caCerts, err := pem.DecodePEMCertificates(pemBytes) if err != nil { return fmt.Errorf("error parsing PEM certificate: %w", err) } + if len(caCerts) > 1 { + return fmt.Errorf("expected 1 certificate, got %d", len(caCerts)) + } if ts.trustedCas == nil { ts.trustedCas = make([]*x509.Certificate, 0) } - ts.trustedCas = append(ts.trustedCas, caCert) + ts.trustedCas = append(ts.trustedCas, caCerts[0]) return nil } @@ -113,9 +109,15 @@ func (ts *OAuthTokenSource) Token() (*sarama.AccessToken, error) { return nil, errors.New("cannot generate token, OAuthTokenSource not fully configured") } - oidcCfg := ccred.Config{ClientID: ts.ClientID, ClientSecret: ts.ClientSecret, Scopes: ts.Scopes, TokenURL: ts.TokenEndpoint.TokenURL, AuthStyle: ts.TokenEndpoint.AuthStyle} + oidcCfg := ccred.Config{ + ClientID: ts.ClientID, + ClientSecret: ts.ClientSecret, + Scopes: ts.Scopes, + TokenURL: ts.TokenEndpoint.TokenURL, + AuthStyle: ts.TokenEndpoint.AuthStyle, + } - timeoutCtx, cancel := ctx.WithTimeout(ctx.TODO(), tokenRequestTimeout) + timeoutCtx, cancel := ctx.WithTimeout(ctx.TODO(), 30*time.Second) defer cancel() ts.configureClient() diff --git a/common/component/kafka/sasl_oauthbearer_private_key_jwt.go b/common/component/kafka/sasl_oauthbearer_private_key_jwt.go new file mode 100644 index 0000000000..1c3cd4091e --- /dev/null +++ b/common/component/kafka/sasl_oauthbearer_private_key_jwt.go @@ -0,0 +1,232 @@ +/* +Copyright 2025 The Dapr Authors +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 kafka + +import ( + ctx "context" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/dapr/kit/crypto/pem" + + "github.com/IBM/sarama" + "github.com/google/uuid" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" + "golang.org/x/oauth2" +) + +type OAuthTokenSourcePrivateKeyJWT struct { + CachedToken oauth2.Token + Extensions map[string]string + TokenEndpoint oauth2.Endpoint + ClientID string + ClientSecret string + Scopes []string + httpClient *http.Client + trustedCas []*x509.Certificate + skipCaVerify bool + ClientAuthMethod string + ClientAssertionCert string + ClientAssertionKey string + Resource string + Audience string + Kid string +} + +type tokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int64 `json:"expires_in"` +} + +func (m KafkaMetadata) getOAuthTokenSourcePrivateKeyJWT() *OAuthTokenSourcePrivateKeyJWT { + return &OAuthTokenSourcePrivateKeyJWT{ + TokenEndpoint: oauth2.Endpoint{TokenURL: m.OidcTokenEndpoint}, + ClientID: m.OidcClientID, + ClientSecret: m.OidcClientSecret, + Scopes: m.internalOidcScopes, + Extensions: m.internalOidcExtensions, + skipCaVerify: m.TLSSkipVerify, + ClientAuthMethod: m.AuthType, + ClientAssertionCert: m.OidcClientAssertionCert, + ClientAssertionKey: m.OidcClientAssertionKey, + Resource: m.OidcResource, + Audience: m.OidcAudience, + Kid: m.OidcKid, + } +} + +func (ts *OAuthTokenSourcePrivateKeyJWT) addCa(caPem string) error { + pemBytes := []byte(caPem) + + caCerts, err := pem.DecodePEMCertificates(pemBytes) + if err != nil { + return fmt.Errorf("error parsing PEM certificate: %w", err) + } + if len(caCerts) > 1 { + return fmt.Errorf("expected 1 certificate, got %d", len(caCerts)) + } + + if ts.trustedCas == nil { + ts.trustedCas = make([]*x509.Certificate, 0) + } + ts.trustedCas = append(ts.trustedCas, caCerts[0]) + + return nil +} + +func (ts *OAuthTokenSourcePrivateKeyJWT) configureClient() { + if ts.httpClient != nil { + return + } + + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: ts.skipCaVerify, //nolint:gosec + } + + if ts.trustedCas != nil { + caPool, err := x509.SystemCertPool() + if err != nil { + caPool = x509.NewCertPool() + } + + for _, c := range ts.trustedCas { + caPool.AddCert(c) + } + tlsConfig.RootCAs = caPool + } + + ts.httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } +} + +// At the time of writing this, the oauth2 package does not support the client assertion authentication method. +// Ref: https://github.com/golang/oauth2/issues/744 +func (ts *OAuthTokenSourcePrivateKeyJWT) Token() (*sarama.AccessToken, error) { + if ts.CachedToken.Valid() { + return ts.asSaramaToken(), nil + } + + if ts.TokenEndpoint.TokenURL == "" || ts.ClientID == "" { + return nil, errors.New("cannot generate token, OAuthTokenSourcePrivateKeyJWT not fully configured") + } + + if ts.ClientAssertionCert == "" || ts.ClientAssertionKey == "" { + return nil, errors.New("client_jwt requires client assertion cert and key") + } + + pk, err := pem.DecodePEMPrivateKey([]byte(ts.ClientAssertionKey)) + if err != nil { + return nil, fmt.Errorf("unable to parse private key: %w", err) + } + rsaKey, ok := pk.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("client_jwt requires RSA private key") + } + + now := time.Now() + aud := ts.TokenEndpoint.TokenURL + + audClaim := aud + if ts.Audience != "" { + audClaim = ts.Audience + } + + token, err := jwt.NewBuilder(). + Issuer(ts.ClientID). + Subject(ts.ClientID). + Audience([]string{audClaim}). + IssuedAt(now). + Expiration(now.Add(1 * time.Minute)). + JwtID(uuid.New().String()). + NotBefore(now). + Build() + if err != nil { + return nil, fmt.Errorf("failed to build token: %w", err) + } + + // Some IdPs require the audience to be set as a single string + token.Options().Enable(jwt.FlattenAudience) + + var signOptions []jwt.Option + if ts.Kid != "" { + headers := jws.NewHeaders() + if err = headers.Set("kid", ts.Kid); err != nil { + return nil, fmt.Errorf("error setting JWT kid header: %w", err) + } + signOptions = append(signOptions, jws.WithProtectedHeaders(headers)) + } + assertion, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, rsaKey, signOptions...)) + if err != nil { + return nil, fmt.Errorf("error signing client assertion: %w", err) + } + + urlValues := &url.Values{} + urlValues.Set("grant_type", "client_credentials") + urlValues.Set("client_id", ts.ClientID) + urlValues.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + urlValues.Set("client_assertion", string(assertion)) + if ts.Audience != "" { + urlValues.Set("audience", ts.Audience) + } + if ts.Resource != "" { + urlValues.Set("resource", ts.Resource) + } + if len(ts.Scopes) > 0 { + urlValues.Set("scope", strings.Join(ts.Scopes, " ")) + } + + timeoutCtx, cancel := ctx.WithTimeout(ctx.TODO(), 30*time.Second) + defer cancel() + ts.configureClient() + req, err := http.NewRequestWithContext(timeoutCtx, http.MethodPost, ts.TokenEndpoint.TokenURL, strings.NewReader(urlValues.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := ts.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("token endpoint returned %d", resp.StatusCode) + } + var tr tokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil { + return nil, err + } + if tr.AccessToken == "" { + return nil, errors.New("no access_token in response") + } + ts.CachedToken = oauth2.Token{AccessToken: tr.AccessToken, Expiry: time.Now().Add(time.Duration(tr.ExpiresIn) * time.Second)} + return ts.asSaramaToken(), nil +} + +func (ts *OAuthTokenSourcePrivateKeyJWT) asSaramaToken() *sarama.AccessToken { + return &(sarama.AccessToken{Token: ts.CachedToken.AccessToken, Extensions: ts.Extensions}) +} diff --git a/common/component/kafka/subscriber_test.go b/common/component/kafka/subscriber_test.go index 135df6f9cb..0c3751f99a 100644 --- a/common/component/kafka/subscriber_test.go +++ b/common/component/kafka/subscriber_test.go @@ -482,9 +482,9 @@ func Test_Subscribe(t *testing.T) { cancel() assert.Eventually(t, func() bool { - return consumeCalled.Load() == 3 + return cancelCalled.Load() == 3 }, time.Second, time.Millisecond) - assert.Equal(t, int64(3), cancelCalled.Load()) + assert.Equal(t, int64(3), consumeCalled.Load()) k.Subscribe(ctx, SubscriptionHandlerConfig{}) assert.Nil(t, k.consumerCancel) diff --git a/common/component/postgresql/v1/metadata.go b/common/component/postgresql/v1/metadata.go index 7bef937115..67cba27ef2 100644 --- a/common/component/postgresql/v1/metadata.go +++ b/common/component/postgresql/v1/metadata.go @@ -17,8 +17,8 @@ import ( "errors" "time" - "github.com/dapr/components-contrib/common/authentication/aws" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" + "github.com/dapr/components-contrib/common/aws" "github.com/dapr/components-contrib/state" "github.com/dapr/kit/metadata" "github.com/dapr/kit/ptr" diff --git a/common/component/postgresql/v1/postgresql.go b/common/component/postgresql/v1/postgresql.go index 636c19a493..e4967799d6 100644 --- a/common/component/postgresql/v1/postgresql.go +++ b/common/component/postgresql/v1/postgresql.go @@ -21,6 +21,7 @@ import ( "fmt" "reflect" "strconv" + "strings" "time" "github.com/jackc/pgx/v5" @@ -28,8 +29,9 @@ import ( "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsAuth "github.com/dapr/components-contrib/common/aws/auth" pginterfaces "github.com/dapr/components-contrib/common/component/postgresql/interfaces" pgtransactions "github.com/dapr/components-contrib/common/component/postgresql/transactions" commonsql "github.com/dapr/components-contrib/common/component/sql" @@ -55,8 +57,6 @@ type PostgreSQL struct { etagColumn string enableAzureAD bool enableAWSIAM bool - - awsAuthProvider awsAuth.Provider } type Options struct { @@ -115,13 +115,20 @@ func (p *PostgreSQL) Init(ctx context.Context, meta state.Metadata) error { return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr) } - var provider awsAuth.Provider - provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts)) - if err != nil { + authOpts := awsAuth.Options{ + Logger: p.logger, + Properties: meta.Properties, + Region: opts.Region, + AccessKey: opts.AccessKey, + SecretKey: opts.SecretKey, + SessionToken: opts.SessionToken, + AssumeRoleArn: opts.AssumeRoleArn, + AssumeRoleSessionName: opts.AssumeRoleSessionName, + Endpoint: opts.Endpoint, + } + if err = awsCommon.ConfigurePostgresIAM(ctx, config, authOpts); err != nil { return err } - p.awsAuthProvider = provider - p.awsAuthProvider.UpdatePostgres(ctx, config) } connCtx, connCancel := context.WithTimeout(ctx, p.metadata.Timeout) @@ -182,6 +189,7 @@ func (p *PostgreSQL) Features() []state.Feature { state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, + state.FeatureKeysLike, } } @@ -502,22 +510,17 @@ func (p *PostgreSQL) CleanupExpired() error { return nil } -// Close implements io.Close. +// Close implements io.Closer. func (p *PostgreSQL) Close() error { if p.db != nil { p.db.Close() p.db = nil } - errs := make([]error, 2) if p.gc != nil { - errs[0] = p.gc.Close() - } - - if p.awsAuthProvider != nil { - errs[1] = p.awsAuthProvider.Close() + return p.gc.Close() } - return errors.Join(errs...) + return nil } // GetCleanupInterval returns the cleanupInterval property. @@ -531,3 +534,88 @@ func (p *PostgreSQL) GetComponentMetadata() (metadataInfo metadata.MetadataMap) metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) return } + +func (p *PostgreSQL) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + // Match with backslash-escaping for % and _ + where := []string{ + `key LIKE $1 ESCAPE '\'`, + `(expiredate IS NULL OR expiredate > CURRENT_TIMESTAMP)`, + } + args := []any{req.Pattern} + + // Pagination: resume strictly AFTER the last returned row_id + if req.ContinuationToken != nil && *req.ContinuationToken != "" { + rid, err := strconv.ParseInt(*req.ContinuationToken, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid continue token: %w", err) + } + where = append(where, fmt.Sprintf("row_id > $%d", len(args)+1)) + args = append(args, rid) + } + + // Optional LIMIT: fetch one extra row to detect "has next" + limitClause := "" + var pageSize uint32 + if req.PageSize != nil && *req.PageSize > 0 { + pageSize = *req.PageSize + limitClause = fmt.Sprintf(" LIMIT $%d", len(args)+1) + args = append(args, pageSize+1) + } + + query := fmt.Sprintf(` + SELECT key, row_id + FROM %s + WHERE %s + ORDER BY row_id ASC%s`, + p.metadata.TableName, + strings.Join(where, " AND "), + limitClause, + ) + + rows, err := p.db.Query(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + type rec struct { + key string + rowID uint64 + } + list := make([]rec, 0, 256) + + for rows.Next() { + var k string + var rid uint64 + if err := rows.Scan(&k, &rid); err != nil { + return nil, err + } + list = append(list, rec{key: k, rowID: rid}) + } + if err := rows.Err(); err != nil { + return nil, err + } + + resp := &state.KeysLikeResponse{ + Keys: make([]string, 0, len(list)), + } + + // If we fetched more than a page, set the token to the last returned row's row_id + //nolint:gosec + if pageSize > 0 && uint32(len(list)) > pageSize { + lastReturned := list[pageSize-1].rowID + tok := strconv.FormatUint(lastReturned, 10) + resp.ContinuationToken = &tok + list = list[:pageSize] + } + + for _, r := range list { + resp.Keys = append(resp.Keys, r.key) + } + + return resp, nil +} diff --git a/common/component/postgresql/v1/postgresql_query.go b/common/component/postgresql/v1/postgresql_query.go index cb487448e0..77910648f9 100644 --- a/common/component/postgresql/v1/postgresql_query.go +++ b/common/component/postgresql/v1/postgresql_query.go @@ -49,12 +49,7 @@ func NewPostgreSQLQueryStateStore(logger logger.Logger, opts Options) state.Stor // Features returns the features available in this component. func (p *PostgreSQLQuery) Features() []state.Feature { - return []state.Feature{ - state.FeatureETag, - state.FeatureTransactional, - state.FeatureQueryAPI, - state.FeatureTTL, - } + return append(p.PostgreSQL.Features(), state.FeatureQueryAPI) } // Query executes a query against store. diff --git a/common/component/postgresql/v1/postgresql_test.go b/common/component/postgresql/v1/postgresql_test.go index a018cbd7aa..c725201cfd 100644 --- a/common/component/postgresql/v1/postgresql_test.go +++ b/common/component/postgresql/v1/postgresql_test.go @@ -18,6 +18,7 @@ package postgresql import ( "context" "encoding/json" + "errors" "testing" "time" @@ -218,6 +219,25 @@ func TestMultiOperationOrder(t *testing.T) { require.NoError(t, err) } +type fakeGC struct { + err error +} + +func (f *fakeGC) CleanupExpired() error { return nil } +func (f *fakeGC) Close() error { return f.err } + +func TestClose_NoGC(t *testing.T) { + p := &PostgreSQL{} + require.NoError(t, p.Close()) +} + +func TestClose_WithGC(t *testing.T) { + expErr := errors.New("gc failure") + p := &PostgreSQL{gc: &fakeGC{err: expErr}} + err := p.Close() + require.Equal(t, expErr, err) +} + func createSetRequest() state.SetRequest { return state.SetRequest{ Key: randomKey(), diff --git a/common/component/redis/redis.go b/common/component/redis/redis.go index f7d701f817..f603bb1c4d 100644 --- a/common/component/redis/redis.go +++ b/common/component/redis/redis.go @@ -70,7 +70,7 @@ type RedisPipeliner interface { //nolint:interfacebloat type RedisClient interface { - GetNilValueError() RedisError + IsNilValueError(error) bool Context() context.Context DoRead(ctx context.Context, args ...interface{}) (interface{}, error) DoWrite(ctx context.Context, args ...interface{}) error diff --git a/common/component/redis/redis_test.go b/common/component/redis/redis_test.go index a4788e1292..eb78e60b62 100644 --- a/common/component/redis/redis_test.go +++ b/common/component/redis/redis_test.go @@ -50,7 +50,7 @@ const ( func getFakeProperties() map[string]string { return map[string]string{ - host: "fake.redis.com", + host: "fake.redis.com:6379", password: "fakePassword", username: "fakeUsername", sentinelUsername: "fakeSentinelUsername", diff --git a/common/component/redis/settings.go b/common/component/redis/settings.go index 13cba00a3c..1b73cfd365 100644 --- a/common/component/redis/settings.go +++ b/common/component/redis/settings.go @@ -16,15 +16,21 @@ package redis import ( "crypto/tls" "fmt" + "net" "strconv" + "strings" "time" "github.com/dapr/kit/config" ) +const defaultRedisPort = "6379" + type Settings struct { // The Redis host Host string `mapstructure:"redisHost"` + // The Redis port (optional, appended to Host if Host does not already contain a port) + Port uint16 `mapstructure:"redisPort"` // The Redis password Password string `mapstructure:"redisPassword"` // The Redis username @@ -119,9 +125,71 @@ func (s *Settings) Decode(in interface{}) error { return fmt.Errorf("decode failed. %w", err) } + resolved, err := resolveHost(s.Host, s.Port) + if err != nil { + return err + } + s.Host = resolved + return nil } +// resolveHost ensures Host contains a port. If Host already includes a port +// (contains ":"), it is returned unchanged. Otherwise, the separate Port value +// is appended; when Port is empty the Redis default (6379) is used. +// For comma-separated host lists (cluster/sentinel), each entry is resolved +// individually. +// Returns an error if any address already contains a port that conflicts with +// a separately configured port value. +func resolveHost(host string, port uint16) (string, error) { + if host == "" { + return host, nil + } + + // Comma-separated addresses (cluster or sentinel mode). + if strings.Contains(host, ",") { + parts := strings.Split(host, ",") + addrs := make([]string, len(parts)) + for i, addr := range parts { + resolved, err := resolveAddr(strings.TrimSpace(addr), port) + if err != nil { + return "", err + } + addrs[i] = resolved + } + return strings.Join(addrs, ","), nil + } + + return resolveAddr(host, port) +} + +// resolveAddr appends port to a single address when it does not already +// contain one. Returns an error if the address already contains a port that +// conflicts with the separately configured port value. +func resolveAddr(addr string, port uint16) (string, error) { + if addr == "" { + return addr, nil + } + + portStr := strconv.FormatUint(uint64(port), 10) + + // Already has a port. + if _, existingPort, err := net.SplitHostPort(addr); err == nil { + if port != 0 && portStr != existingPort { + return "", fmt.Errorf( + "redis host %q already contains port %s, but redisPort is set to %s; "+ + "either remove the port from redisHost or remove the redisPort setting", + addr, existingPort, portStr) + } + return addr, nil + } + + if port == 0 { + portStr = defaultRedisPort + } + return net.JoinHostPort(addr, portStr), nil +} + func (s *Settings) SetCertificate(fn func(cert *tls.Certificate)) error { if s.ClientCert == "" || s.ClientKey == "" { return nil diff --git a/common/component/redis/settings_test.go b/common/component/redis/settings_test.go index 9b6ebc291c..80f111aeea 100644 --- a/common/component/redis/settings_test.go +++ b/common/component/redis/settings_test.go @@ -8,6 +8,223 @@ import ( "github.com/stretchr/testify/require" ) +func TestResolveHost(t *testing.T) { + tests := []struct { + name string + host string + port uint16 + want string + wantErr bool + }{ + { + name: "host:port already present", + host: "redis-master:6379", + port: 0, + want: "redis-master:6379", + }, + { + name: "host:port matching separate port", + host: "redis-master:6379", + port: 6379, + want: "redis-master:6379", + }, + { + name: "host:port conflicts with separate port", + host: "redis-master:6379", + port: 6380, + wantErr: true, + }, + { + name: "host only with separate port", + host: "redis-master", + port: 6380, + want: "redis-master:6380", + }, + { + name: "host only defaults to 6379", + host: "redis-master", + port: 0, + want: "redis-master:6379", + }, + { + name: "empty host returns empty", + host: "", + port: 6379, + want: "", + }, + { + name: "cluster hosts all with ports", + host: "node1:6379,node2:6379,node3:6379", + port: 0, + want: "node1:6379,node2:6379,node3:6379", + }, + { + name: "cluster hosts without ports get the specified port applied", + host: "node1,node2,node3", + port: 6380, + want: "node1:6380,node2:6380,node3:6380", + }, + { + name: "cluster hosts without ports default to 6379", + host: "node1,node2,node3", + port: 0, + want: "node1:6379,node2:6379,node3:6379", + }, + { + name: "cluster hosts one conflicts with separate port", + host: "node1:6379,node2,node3:6381", + port: 6380, + wantErr: true, + }, + { + name: "cluster hosts with spaces around commas", + host: "node1 , node2 , node3", + port: 6380, + want: "node1:6380,node2:6380,node3:6380", + }, + { + name: "mixed cluster hosts: some with matching port, some without", + host: "node1:6380,node2,node3:6380", + port: 6380, + want: "node1:6380,node2:6380,node3:6380", + }, + { + name: "mixed cluster hosts: some with port, some without, using default 6379", + host: "node1:6379,node2,node3", + port: 0, + want: "node1:6379,node2:6379,node3:6379", + }, + { + name: "mixed cluster hosts: one entry conflicts with redisPort", + host: "node1:6380,node2,node3:9999", + port: 6380, + wantErr: true, + }, + { + name: "sentinel addresses without ports", + host: "sentinel1,sentinel2,sentinel3", + port: 26379, + want: "sentinel1:26379,sentinel2:26379,sentinel3:26379", + }, + { + name: "IPv6 address with port in brackets", + host: "[::1]:6379", + port: 0, + want: "[::1]:6379", + }, + { + name: "IPv6 address with port conflicts with separate port", + host: "[::1]:6379", + port: 6380, + wantErr: true, + }, + { + name: "FQDN host without port", + host: "redis.staging.example.com", + port: 6379, + want: "redis.staging.example.com:6379", + }, + { + name: "valid port: upper boundary", + host: "redis-master", + port: 65535, + want: "redis-master:65535", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := resolveHost(tt.host, tt.port) + if tt.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), "redisPort") + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func TestSettingsDecodeResolvesHostPort(t *testing.T) { + t.Run("decode with separate port", func(t *testing.T) { + s := Settings{} + err := s.Decode(map[string]string{ + "redisHost": "redis-master", + "redisPort": "6380", + }) + require.NoError(t, err) + require.Equal(t, "redis-master:6380", s.Host) + }) + + t.Run("decode with host:port matching separate port", func(t *testing.T) { + s := Settings{} + err := s.Decode(map[string]string{ + "redisHost": "redis-master:6379", + "redisPort": "6379", + }) + require.NoError(t, err) + require.Equal(t, "redis-master:6379", s.Host) + }) + + t.Run("decode with host:port conflicting separate port errors", func(t *testing.T) { + s := Settings{} + err := s.Decode(map[string]string{ + "redisHost": "redis-master:6379", + "redisPort": "6380", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "redisPort") + }) + + t.Run("decode with host only defaults port", func(t *testing.T) { + s := Settings{} + err := s.Decode(map[string]string{ + "redisHost": "redis-master", + }) + require.NoError(t, err) + require.Equal(t, "redis-master:6379", s.Host) + }) + + t.Run("decode cluster hosts with separate port", func(t *testing.T) { + s := Settings{} + err := s.Decode(map[string]string{ + "redisHost": "node1,node2,node3", + "redisPort": "6380", + }) + require.NoError(t, err) + require.Equal(t, "node1:6380,node2:6380,node3:6380", s.Host) + }) + + t.Run("decode cluster host conflict errors", func(t *testing.T) { + s := Settings{} + err := s.Decode(map[string]string{ + "redisHost": "node1:6379,node2,node3", + "redisPort": "6380", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "redisPort") + }) + + t.Run("decode with invalid port rejects at decode", func(t *testing.T) { + s := Settings{} + err := s.Decode(map[string]string{ + "redisHost": "redis-master", + "redisPort": "abc", + }) + require.Error(t, err) + }) + + t.Run("decode with out-of-range port rejects at decode", func(t *testing.T) { + s := Settings{} + err := s.Decode(map[string]string{ + "redisHost": "redis-master", + "redisPort": "99999", + }) + require.Error(t, err) + }) +} + func TestSettings(t *testing.T) { t.Run("test set certificate, missing data", func(t *testing.T) { var c *tls.Certificate diff --git a/common/component/redis/v8client.go b/common/component/redis/v8client.go index dd67a7be4b..e9063e998e 100644 --- a/common/component/redis/v8client.go +++ b/common/component/redis/v8client.go @@ -16,6 +16,7 @@ package redis import ( "context" "crypto/tls" + "errors" "strings" "time" @@ -106,8 +107,8 @@ func (c v8Client) Get(ctx context.Context, key string) (string, error) { return c.client.Get(ctx, key).Result() } -func (c v8Client) GetNilValueError() RedisError { - return RedisError(v8.Nil.Error()) +func (c v8Client) IsNilValueError(err error) bool { + return errors.Is(err, v8.Nil) } func (c v8Client) Context() context.Context { diff --git a/common/component/redis/v9client.go b/common/component/redis/v9client.go index 23b20d8ac7..077ac74681 100644 --- a/common/component/redis/v9client.go +++ b/common/component/redis/v9client.go @@ -16,6 +16,7 @@ package redis import ( "context" "crypto/tls" + "errors" "strings" "time" @@ -106,8 +107,8 @@ func (c v9Client) Get(ctx context.Context, key string) (string, error) { return c.client.Get(ctx, key).Result() } -func (c v9Client) GetNilValueError() RedisError { - return RedisError(v9.Nil.Error()) +func (c v9Client) IsNilValueError(err error) bool { + return errors.Is(err, v9.Nil) } func (c v9Client) Context() context.Context { diff --git a/common/proto/state/sqlserver/test.pb.go b/common/proto/state/sqlserver/test.pb.go new file mode 100644 index 0000000000..1c936adb57 --- /dev/null +++ b/common/proto/state/sqlserver/test.pb.go @@ -0,0 +1,162 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.25.4 +// source: test.proto + +package sqlserver + +import ( + "reflect" + "sync" + + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/runtime/protoimpl" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TestEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + EventId int32 `protobuf:"varint,1,opt,name=eventId,proto3" json:"eventId,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *TestEvent) Reset() { + *x = TestEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestEvent) ProtoMessage() {} + +func (x *TestEvent) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestEvent.ProtoReflect.Descriptor instead. +func (*TestEvent) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{0} +} + +func (x *TestEvent) GetEventId() int32 { + if x != nil { + return x.EventId + } + return 0 +} + +func (x *TestEvent) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +var File_test_proto protoreflect.FileDescriptor + +var file_test_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5f, 0x0a, + 0x09, 0x54, 0x65, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x49, 0x64, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x41, + 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x61, 0x70, + 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2d, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x69, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2f, 0x73, 0x71, 0x6c, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_test_proto_rawDescOnce sync.Once + file_test_proto_rawDescData = file_test_proto_rawDesc +) + +func file_test_proto_rawDescGZIP() []byte { + file_test_proto_rawDescOnce.Do(func() { + file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData) + }) + return file_test_proto_rawDescData +} + +var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_test_proto_goTypes = []interface{}{ + (*TestEvent)(nil), // 0: TestEvent + (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp +} +var file_test_proto_depIdxs = []int32{ + 1, // 0: TestEvent.timestamp:type_name -> google.protobuf.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_test_proto_init() } +func file_test_proto_init() { + if File_test_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_test_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_test_proto_goTypes, + DependencyIndexes: file_test_proto_depIdxs, + MessageInfos: file_test_proto_msgTypes, + }.Build() + File_test_proto = out.File + file_test_proto_rawDesc = nil + file_test_proto_goTypes = nil + file_test_proto_depIdxs = nil +} diff --git a/common/proto/state/sqlserver/test.proto b/common/proto/state/sqlserver/test.proto new file mode 100644 index 0000000000..7c84802598 --- /dev/null +++ b/common/proto/state/sqlserver/test.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +option go_package = "github.com/dapr/components-contrib/common/proto/state/sqlserver"; + +import "google/protobuf/timestamp.proto"; + +message TestEvent { + int32 eventId = 1; + google.protobuf.Timestamp timestamp = 2; +} \ No newline at end of file diff --git a/component-metadata-schema.json b/component-metadata-schema.json index 1ed919ab4b..1981d80592 100644 --- a/component-metadata-schema.json +++ b/component-metadata-schema.json @@ -214,6 +214,7 @@ "lock", "middleware", "crypto", + "nameresolution", "conversation" ], "description": "Component type, of one of the allowed values." diff --git a/configuration/postgres/metadata.go b/configuration/postgres/metadata.go index 9ec52a0908..08ede2c469 100644 --- a/configuration/postgres/metadata.go +++ b/configuration/postgres/metadata.go @@ -18,8 +18,8 @@ import ( "fmt" "time" - "github.com/dapr/components-contrib/common/authentication/aws" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" + "github.com/dapr/components-contrib/common/aws" kitmd "github.com/dapr/kit/metadata" ) @@ -63,6 +63,11 @@ func (m *metadata) InitWithMetadata(meta map[string]string) error { return fmt.Errorf("invalid table name '%s'. non-alphanumerics or upper cased table names are not supported", m.ConfigTable) } + // Timeout + if m.Timeout < 1*time.Second { + return errors.New("invalid value for 'timeout': must be greater than 1s") + } + opts := pgauth.InitWithMetadataOpts{ AzureADEnabled: true, AWSIAMEnabled: true, diff --git a/configuration/postgres/postgres.go b/configuration/postgres/postgres.go index 734a199f0d..00b9238935 100644 --- a/configuration/postgres/postgres.go +++ b/configuration/postgres/postgres.go @@ -25,14 +25,16 @@ import ( "strings" "sync" "sync/atomic" + "time" + "github.com/aws/aws-sdk-go-v2/feature/rds/auth" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" + awsAuth "github.com/dapr/components-contrib/common/aws/auth" "github.com/dapr/components-contrib/configuration" contribMetadata "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" @@ -52,7 +54,7 @@ type ConfigurationStore struct { enableAzureAD bool enableAWSIAM bool - awsAuthProvider awsAuth.Provider + awsAuthProvider awsAuth.CredentialProvider } type subscription struct { @@ -76,9 +78,24 @@ var ( allowedTableNameChars = regexp.MustCompile(`^[a-z0-9./_]*$`) ) +type Options struct { + // Disables support for authenticating with Azure AD + NoAzureAD bool + + // Disables support for authenticating with AWS IAM + NoAWSIAM bool +} + func NewPostgresConfigurationStore(logger logger.Logger) configuration.Store { + return NewPostgresConfigurationStoreWithOptions(logger, Options{}) +} + +// NewPostgresConfigurationStoreWithOptions creates a new instance of PostgreSQL store with options. +func NewPostgresConfigurationStoreWithOptions(logger logger.Logger, opts Options) configuration.Store { return &ConfigurationStore{ - logger: logger, + logger: logger, + enableAzureAD: !opts.NoAzureAD, + enableAWSIAM: !opts.NoAWSIAM, } } @@ -105,30 +122,51 @@ func (p *ConfigurationStore) Init(ctx context.Context, metadata configuration.Me return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr) } - var provider awsAuth.Provider - provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts)) - if err != nil { - return err + configOpts := awsAuth.Options{ + Logger: p.logger, + Properties: metadata.Properties, + Region: opts.Region, + AccessKey: opts.AccessKey, + SecretKey: opts.SecretKey, + SessionToken: opts.SessionToken, + AssumeRoleArn: opts.AssumeRoleArn, + AssumeRoleSessionName: opts.AssumeRoleSessionName, + Endpoint: opts.Endpoint, + } + + provider, providerErr := awsAuth.NewCredentialProvider(ctx, configOpts, nil) + if providerErr != nil { + return providerErr } + p.awsAuthProvider = provider - p.awsAuthProvider.UpdatePostgres(ctx, config) + region := configOpts.Region + config.MaxConnLifetime = time.Minute * 10 + config.BeforeConnect = func(ctx context.Context, pgConfig *pgx.ConnConfig) error { + pwd, tokenErr := auth.BuildAuthToken(ctx, fmt.Sprintf("%s:%d", pgConfig.Host, pgConfig.Port), region, pgConfig.User, p.awsAuthProvider) + if tokenErr != nil { + return fmt.Errorf("failed to get database token: %w", tokenErr) + } + + pgConfig.Password = pwd + return nil + } } - pool, err := pgxpool.NewWithConfig(ctx, config) + connCtx, connCancel := context.WithTimeout(ctx, p.metadata.Timeout) + defer connCancel() + p.client, err = pgxpool.NewWithConfig(connCtx, config) if err != nil { return fmt.Errorf("PostgreSQL configuration store connection error: %w", err) } - err = pool.Ping(ctx) + pingCtx, pingCancel := context.WithTimeout(ctx, p.metadata.Timeout) + defer pingCancel() + err = p.client.Ping(pingCtx) if err != nil { return fmt.Errorf("PostgreSQL configuration store ping error: %w", err) } - p.client = pool - err = p.client.Ping(ctx) - if err != nil { - return fmt.Errorf("unable to connect to configuration store: '%w'", err) - } // check if table exists exists := false err = p.client.QueryRow(ctx, QueryTableExists, p.metadata.ConfigTable).Scan(&exists) @@ -453,9 +491,5 @@ func (p *ConfigurationStore) Close() error { p.client.Close() } - errs := make([]error, 1) - if p.awsAuthProvider != nil { - errs[0] = p.awsAuthProvider.Close() - } - return errors.Join(errs...) + return nil } diff --git a/configuration/postgres/postgres_test.go b/configuration/postgres/postgres_test.go index f3d12ceb8e..59c4740001 100644 --- a/configuration/postgres/postgres_test.go +++ b/configuration/postgres/postgres_test.go @@ -14,15 +14,25 @@ limitations under the License. package postgres import ( + "fmt" + "net" "regexp" + "runtime" "testing" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/rds/auth" + "github.com/jackc/pgx/v5" "github.com/pashagolub/pgxmock/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" "github.com/dapr/components-contrib/configuration" + metadatapkg "github.com/dapr/components-contrib/metadata" + "github.com/dapr/kit/logger" ) func TestSelectAllQuery(t *testing.T) { @@ -115,3 +125,125 @@ func TestValidateInput(t *testing.T) { keys3 := []string{"Name 1=1"} require.Error(t, validateInput(keys3), "invalid key : 'Name 1=1'") } + +func TestPostgresConfigurationWithIAM(t *testing.T) { + // testcontainers spins up Linux containers (moto, postgres) that rely on the + // bridge network driver. On Windows, Docker runs in Windows-container mode and + // does not ship the bridge plugin, so container creation fails even when the + // Docker daemon is reachable. Skip explicitly rather than letting the test hang + // or produce a confusing "plugin not found" error. + if runtime.GOOS != "linux" { + t.Skip("testcontainers bridge network unavailable on non-Linux platforms") + } + + ctx := t.Context() + + // Testing use of moto to mock AWS services for IAM authentication + motoContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "motoserver/moto:5.1.19", + ExposedPorts: []string{"5000/tcp"}, + WaitingFor: wait.ForLog("Running on http://127.0.0.1:5000"), + }, + Started: true, + }) + require.NoError(t, err) + defer func() { _ = motoContainer.Terminate(ctx) }() + + motoHost, err := motoContainer.Host(ctx) + require.NoError(t, err) + motoPort, err := motoContainer.MappedPort(ctx, "5000") + require.NoError(t, err) + motoURL := "http://" + net.JoinHostPort(motoHost, motoPort.Port()) + + t.Setenv("AWS_ENDPOINT_URL", motoURL) + t.Setenv("AWS_ACCESS_KEY_ID", "testing") + t.Setenv("AWS_SECRET_ACCESS_KEY", "testing") + t.Setenv("AWS_REGION", "us-east-1") + + pgContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "postgres:18", + ExposedPorts: []string{"5432/tcp"}, + Env: map[string]string{ + "POSTGRES_PASSWORD": "password", + "POSTGRES_DB": "testdb", + }, + WaitingFor: wait.ForAll( + wait.ForListeningPort("5432/tcp"), + wait.ForLog("database system is ready to accept connections"), + ), + }, + Started: true, + }) + require.NoError(t, err) + defer func() { _ = pgContainer.Terminate(ctx) }() + + pgHost, err := pgContainer.Host(ctx) + require.NoError(t, err) + pgPort, err := pgContainer.MappedPort(ctx, "5432") + require.NoError(t, err) + + pgConnString := "postgres://postgres:password@" + net.JoinHostPort(pgHost, pgPort.Port()) + "/testdb?sslmode=disable" + + pgConn, err := pgx.Connect(ctx, pgConnString) + require.NoError(t, err) + defer func() { _ = pgConn.Close(ctx) }() + + cfg, err := config.LoadDefaultConfig(ctx) + require.NoError(t, err) + + token, err := auth.BuildAuthToken(ctx, net.JoinHostPort(pgHost, pgPort.Port()), "us-east-1", "testuser", cfg.Credentials) + require.NoError(t, err) + + _, err = pgConn.Exec(ctx, fmt.Sprintf("CREATE USER testuser WITH PASSWORD '%s'", token)) + require.NoError(t, err) + + _, err = pgConn.Exec(ctx, "CREATE TABLE config_table (key TEXT PRIMARY KEY, value TEXT, version TEXT, metadata JSONB)") + require.NoError(t, err) + + _, err = pgConn.Exec(ctx, "GRANT ALL ON config_table TO testuser") + require.NoError(t, err) + + t.Run("Valid IAM Authentication", func(t *testing.T) { + metadata := map[string]string{ + "connectionString": "postgres://testuser@" + net.JoinHostPort(pgHost, pgPort.Port()) + "/testdb?sslmode=disable", + "table": "config_table", + "useAWSIAM": "true", + "awsRegion": "us-east-1", + "awsAccessKey": "testing", + "awsSecretKey": "testing", + } + + store := NewPostgresConfigurationStore(logger.NewLogger("test")) + err := store.Init(ctx, configuration.Metadata{Base: metadatapkg.Base{Properties: metadata}}) + require.NoError(t, err) + + t.Run("Get Request", func(t *testing.T) { + resp, err2 := store.Get(ctx, &configuration.GetRequest{}) + require.NoError(t, err2) + assert.Empty(t, resp.Items) + }) + + err = store.Close() + require.NoError(t, err) + }) + + t.Run("Invalid IAM Authentication", func(t *testing.T) { + metadata := map[string]string{ + "connectionString": "postgres://testuser@" + net.JoinHostPort(pgHost, pgPort.Port()) + "/testdb?sslmode=disable", + "table": "config_table", + "useAWSIAM": "true", + "awsRegion": "us-east-1", + "awsAccessKey": "testingincorrect", + "awsSecretKey": "testingincorrect", + } + + store := NewPostgresConfigurationStore(logger.NewLogger("test")) + err := store.Init(ctx, configuration.Metadata{Base: metadatapkg.Base{Properties: metadata}}) + require.Error(t, err) + + err = store.Close() + require.NoError(t, err) + }) +} diff --git a/configuration/redis/metadata.yaml b/configuration/redis/metadata.yaml index 30643bcff5..0291cfbe6f 100644 --- a/configuration/redis/metadata.yaml +++ b/configuration/redis/metadata.yaml @@ -57,9 +57,23 @@ authenticationProfiles: metadata: - name: redisHost required: true - description: Connection-string for the Redis host + description: | + Connection-string for the Redis host. If "redisType" is "cluster" it + can be multiple hosts separated by commas or just a single host. + The port can be included in the host string (e.g. "host:6379") or + provided separately via "redisPort". example: "redis-master.default.svc.cluster.local:6379" type: string + - name: redisPort + required: false + description: | + The Redis port. Optional: if "redisHost" already contains a port, this + field must either match or be omitted. When "redisHost" does not include + a port and this field is not set, the default Redis port 6379 is used. + In cluster mode, this port is applied to every host in the + comma-separated list. + example: "6379" + type: string - name: enableTLS type: bool required: false diff --git a/configuration/redis/redis_test.go b/configuration/redis/redis_test.go index 46edca4898..de84cbfc08 100644 --- a/configuration/redis/redis_test.go +++ b/configuration/redis/redis_test.go @@ -254,7 +254,7 @@ func Test_parseRedisMetadata(t *testing.T) { testProperties["sentinelMasterName"] = "tesSentinelMasterName" testProperties["redisDB"] = "1" testSettings := redisComponent.Settings{ - Host: "testHost", + Host: "testHost:6379", Password: "testPassword", SentinelUsername: "testSentinelUsername", SentinelPassword: "testSentinelPassword", @@ -270,7 +270,7 @@ func Test_parseRedisMetadata(t *testing.T) { testDefaultProperties := make(map[string]string) testDefaultProperties["redisHost"] = "testHost" defaultSettings := redisComponent.Settings{ - Host: "testHost", + Host: "testHost:6379", Password: "", SentinelUsername: "", SentinelPassword: "", diff --git a/conversation/anthropic/anthropic.go b/conversation/anthropic/anthropic.go index fc7d4638e6..db99df7dc0 100644 --- a/conversation/anthropic/anthropic.go +++ b/conversation/anthropic/anthropic.go @@ -41,8 +41,6 @@ func NewAnthropic(logger logger.Logger) conversation.Conversation { return a } -const defaultModel = "claude-3-5-sonnet-20240620" - func (a *Anthropic) Init(ctx context.Context, meta conversation.Metadata) error { m := conversation.LangchainMetadata{} err := kmeta.DecodeMetadata(meta.Properties, &m) @@ -50,23 +48,27 @@ func (a *Anthropic) Init(ctx context.Context, meta conversation.Metadata) error return err } - model := defaultModel - if m.Model != "" { - model = m.Model - } + // Resolve model via central helper (uses metadata, then env var, then default) + model := conversation.GetAnthropicModel(m.Model) - llm, err := anthropic.New( + options := []anthropic.Option{ anthropic.WithModel(model), anthropic.WithToken(m.Key), - ) + } + + if httpClient := conversation.BuildHTTPClient(); httpClient != nil { + options = append(options, anthropic.WithHTTPClient(httpClient)) + } + + llm, err := anthropic.New(options...) if err != nil { return err } a.LLM.Model = llm - if m.CacheTTL != "" { - cachedModel, cacheErr := conversation.CacheModel(ctx, m.CacheTTL, a.LLM.Model) + if m.ResponseCacheTTL != nil { + cachedModel, cacheErr := conversation.CacheResponses(ctx, m.ResponseCacheTTL, a.LLM.Model) if cacheErr != nil { return cacheErr } diff --git a/conversation/anthropic/metadata.yaml b/conversation/anthropic/metadata.yaml index 729971ca47..db46bb8916 100644 --- a/conversation/anthropic/metadata.yaml +++ b/conversation/anthropic/metadata.yaml @@ -24,10 +24,10 @@ metadata: - name: model required: false description: | - The Anthropic LLM to use. + The Anthropic LLM to use. Configurable via ANTHROPIC_MODEL environment variable. type: string - example: 'claude-3-5-sonnet-20240620' - default: 'claude-3-5-sonnet-20240620' + example: 'claude-sonnet-4-20250514' + default: 'claude-sonnet-4-20250514' - name: cacheTTL required: false description: | diff --git a/conversation/aws/bedrock/bedrock.go b/conversation/aws/bedrock/bedrock.go index 6dc0498d60..1b62920915 100644 --- a/conversation/aws/bedrock/bedrock.go +++ b/conversation/aws/bedrock/bedrock.go @@ -17,8 +17,11 @@ package bedrock import ( "context" "reflect" + "time" + + awsCommon "github.com/dapr/components-contrib/common/aws" + awsCommonAuth "github.com/dapr/components-contrib/common/aws/auth" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" "github.com/dapr/components-contrib/conversation" "github.com/dapr/components-contrib/conversation/langchaingokit" "github.com/dapr/components-contrib/metadata" @@ -37,13 +40,18 @@ type AWSBedrock struct { } type AWSBedrockMetadata struct { - Region string `json:"region"` - Endpoint string `json:"endpoint"` - AccessKey string `json:"accessKey"` - SecretKey string `json:"secretKey"` - SessionToken string `json:"sessionToken"` - Model string `json:"model"` - CacheTTL string `json:"cacheTTL"` + Region string `json:"region"` + Endpoint string `json:"endpoint"` + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` + SessionToken string `json:"sessionToken"` + Model string `json:"model"` + ResponseCacheTTL *time.Duration `json:"responseCacheTTL,omitempty" mapstructure:"responseCacheTTL" mapstructurealiases:"cacheTTL" mdaliases:"cacheTTL"` + + // TODO: @mikeee - Consider exporting awsCommonAuth.awsRAOpts and using it here + AssumeRoleArn string `json:"assumeRoleArn"` + TrustAnchorArn string `json:"trustAnchorArn"` + TrustProfileArn string `json:"trustProfileArn"` } func NewAWSBedrock(logger logger.Logger) conversation.Conversation { @@ -61,7 +69,20 @@ func (b *AWSBedrock) Init(ctx context.Context, meta conversation.Metadata) error return err } - awsConfig, err := awsAuth.GetConfigV2(m.AccessKey, m.SecretKey, m.SessionToken, m.Region, m.Endpoint) + configOpts := awsCommonAuth.Options{ + Logger: b.logger, + Properties: nil, + Region: m.Region, + AccessKey: m.AccessKey, + SecretKey: m.SecretKey, + SessionToken: m.SessionToken, + AssumeRoleArn: m.AssumeRoleArn, + TrustAnchorArn: m.TrustAnchorArn, + TrustProfileArn: m.TrustProfileArn, + Endpoint: m.Endpoint, + } + + awsConfig, err := awsCommon.NewConfig(ctx, configOpts) if err != nil { return err } @@ -83,8 +104,8 @@ func (b *AWSBedrock) Init(ctx context.Context, meta conversation.Metadata) error b.LLM.Model = llm - if m.CacheTTL != "" { - cachedModel, cacheErr := conversation.CacheModel(ctx, m.CacheTTL, b.LLM.Model) + if m.ResponseCacheTTL != nil { + cachedModel, cacheErr := conversation.CacheResponses(ctx, m.ResponseCacheTTL, b.LLM.Model) if cacheErr != nil { return cacheErr } @@ -96,7 +117,7 @@ func (b *AWSBedrock) Init(ctx context.Context, meta conversation.Metadata) error func (b *AWSBedrock) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := AWSBedrockMetadata{} - metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.ConversationType) + _ = metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.ConversationType) return } diff --git a/conversation/aws/bedrock/bedrock_test.go b/conversation/aws/bedrock/bedrock_test.go new file mode 100644 index 0000000000..81fa1977dd --- /dev/null +++ b/conversation/aws/bedrock/bedrock_test.go @@ -0,0 +1,166 @@ +/* +Copyright 2025 The Dapr Authors +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 bedrock + +import ( + "os" + "testing" + + "github.com/tmc/langchaingo/llms" + + "github.com/dapr/components-contrib/conversation" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/kit/logger" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewAWSBedrock(t *testing.T) { + lg := logger.NewLogger("bedrock test") + b := NewAWSBedrock(lg) + assert.NotNil(t, b) + assert.Implements(t, (*conversation.Conversation)(nil), b) +} + +func TestInit(t *testing.T) { + testCases := []struct { + name string + metadata map[string]string + expectError bool + errorMsg string + }{ + { + name: "missing region", + metadata: map[string]string{ + "model": "amazon.titan-text-lite-v1", + }, + expectError: false, + }, + { + name: "valid metadata", + metadata: map[string]string{ + "region": "us-east-1", + "model": "amazon.titan-text-lite-v1", + }, + expectError: false, + }, + { + name: "with access key and secret", + metadata: map[string]string{ + "region": "us-east-1", + "accessKey": "test-key", + "secretKey": "test-secret", + "model": "amazon.titan-text-lite-v1", + }, + expectError: false, + }, + { + name: "with responseCacheTTL", + metadata: map[string]string{ + "region": "us-east-1", + "model": "amazon.titan-text-lite-v1", + "responseCacheTTL": "5m", + }, + expectError: false, + }, + { + name: "invalid responseCacheTTL", + metadata: map[string]string{ + "region": "us-east-1", + "model": "amazon.titan-text-lite-v1", + "responseCacheTTL": "invalid", + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b := &AWSBedrock{ + logger: logger.NewLogger("bedrock test"), + } + err := b.Init(t.Context(), conversation.Metadata{ + Base: metadata.Base{ + Properties: tc.metadata, + }, + }) + if tc.expectError { + require.Error(t, err) + if tc.errorMsg != "" { + assert.Contains(t, err.Error(), tc.errorMsg) + } + } else { + // TODO Implement mocks + if err != nil { + t.Skipf("Skipping test due to AWS config error: %v", err) + } + assert.NotNil(t, b.LLM.Model) + assert.Equal(t, tc.metadata["model"], b.model) + } + }) + } +} + +func TestGetComponentMetadata(t *testing.T) { + b := &AWSBedrock{} + md := b.GetComponentMetadata() + assert.NotEmpty(t, md) + + expectedFields := []string{"Region", "Endpoint", "AccessKey", "SecretKey", "SessionToken", "Model", "responseCacheTTL", "AssumeRoleArn", "TrustAnchorArn", "TrustProfileArn"} + for _, field := range expectedFields { + _, exists := md[field] + assert.True(t, exists, "Field %s should exist in metadata", field) + } +} + +func TestClose(t *testing.T) { + b := &AWSBedrock{} + err := b.Close() + assert.NoError(t, err) +} + +func TestConverse(t *testing.T) { + // Skip if no AWS credentials + if os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "" { + t.Skip("Skipping Converse test: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY not set") + } + + b := NewAWSBedrock(logger.NewLogger("bedrock test")) + err := b.Init(t.Context(), conversation.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "region": "us-east-1", + "model": "amazon.titan-text-lite-v1", + }, + }, + }) + require.NoError(t, err) + + resp, err := b.Converse(t.Context(), &conversation.Request{ + Message: &[]llms.MessageContent{ + { + Role: llms.ChatMessageTypeHuman, + Parts: []llms.ContentPart{ + llms.TextContent{Text: "Hello"}, + }, + }, + }, + }) + require.NoError(t, err) + assert.NotNil(t, resp) + assert.NotEmpty(t, resp.Outputs) +} diff --git a/conversation/aws/bedrock/metadata.yaml b/conversation/aws/bedrock/metadata.yaml index aa1c19d668..2bd6c56c33 100644 --- a/conversation/aws/bedrock/metadata.yaml +++ b/conversation/aws/bedrock/metadata.yaml @@ -21,12 +21,16 @@ metadata: - name: model required: false description: | - The LLM to use. Defaults to Bedrock's default provider model from Amazon. + The model identifier or inference profile ARN to use. Defaults to Bedrock's default provider model from Amazon. + You can specify either: + - A model ID (e.g., "amazon.titan-text-express-v1") that supports on-demand throughput + - An inference profile ARN for models that require it (found in the AWS Bedrock console under "Cross-Region Inference") type: string example: 'amazon.titan-text-express-v1' - - name: cacheTTL + - name: responseCacheTTL required: false description: | - A time-to-live value for a prompt cache to expire. Uses Golang durations + A time-to-live value for a prompt/response cache to expire. Uses Go duration strings (e.g. "5m", "1h"). + The component also supports the legacy key `cacheTTL` via mapstructure aliases. type: string example: '10m' diff --git a/conversation/converse.go b/conversation/converse.go index ad19586e39..7bf7d43c4d 100644 --- a/conversation/converse.go +++ b/conversation/converse.go @@ -17,6 +17,7 @@ package conversation import ( "context" "io" + "time" "github.com/tmc/langchaingo/llms" "google.golang.org/protobuf/types/known/anypb" @@ -36,23 +37,28 @@ type Conversation interface { type Request struct { // Message can be user input prompt/instructions and/or tool call responses. - Message *[]llms.MessageContent - Tools *[]llms.Tool - ToolChoice *string + Message *[]llms.MessageContent + Tools *[]llms.Tool + ToolChoice *string + Temperature float64 `json:"temperature"` + + // Metadata fields that are separate from the actual component metadata fields + // that get passed to the LLM through the conversation. + // https://github.com/openai/openai-go/blob/main/chatcompletion.go#L3010 + Metadata map[string]string `json:"metadata"` + ResponseFormatAsJSONSchema map[string]any `json:"responseFormatAsJsonSchema"` + PromptCacheRetention *time.Duration `json:"promptCacheRetention,omitempty"` + + // TODO: rm these in future PR as they are not used Parameters map[string]*anypb.Any `json:"parameters"` ConversationContext string `json:"conversationContext"` - Temperature float64 `json:"temperature"` - - // from metadata - Key string `json:"key"` - Model string `json:"model"` - Endpoints []string `json:"endpoints"` - Policy string `json:"loadBalancingPolicy"` } type Response struct { - ConversationContext string `json:"conversationContext"` Outputs []Result `json:"outputs"` + Model string `json:"model"` + ConversationContext string `json:"conversationContext,omitempty"` + Usage *Usage `json:"usage,omitempty"` } type Result struct { diff --git a/conversation/deepseek/deepseek.go b/conversation/deepseek/deepseek.go index 1ed6c075fd..8139a33fea 100644 --- a/conversation/deepseek/deepseek.go +++ b/conversation/deepseek/deepseek.go @@ -63,12 +63,7 @@ func (d *Deepseek) Init(ctx context.Context, meta conversation.Metadata) error { md.Endpoint = defaultEndpoint } - options := []openai.Option{ - openai.WithModel(model), - openai.WithToken(md.Key), - openai.WithBaseURL(md.Endpoint), - } - + options := conversation.BuildOpenAIClientOptions(model, md.Key, md.Endpoint) llm, err := openai.New(options...) if err != nil { return err diff --git a/conversation/deepseek/metadata.go b/conversation/deepseek/metadata.go index c3c73ba0ac..85c40dc9e5 100644 --- a/conversation/deepseek/metadata.go +++ b/conversation/deepseek/metadata.go @@ -17,9 +17,10 @@ limitations under the License. package deepseek +import "github.com/dapr/components-contrib/conversation" + type DeepseekMetadata struct { - Key string `json:"key"` - MaxTokens int `json:"maxTokens"` - Model string `json:"model"` - Endpoint string `json:"endpoint"` + conversation.LangchainMetadata `json:",inline" mapstructure:",squash"` + Key string `json:"key"` + MaxTokens int `json:"maxTokens"` } diff --git a/conversation/echo/echo.go b/conversation/echo/echo.go index 43fadc67f5..5bcbe3176e 100644 --- a/conversation/echo/echo.go +++ b/conversation/echo/echo.go @@ -17,7 +17,7 @@ package echo import ( "context" "fmt" - "reflect" + "sort" "strconv" "strings" @@ -26,12 +26,10 @@ import ( "github.com/dapr/components-contrib/conversation" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" - kmeta "github.com/dapr/kit/metadata" ) // Echo implement is only for test. type Echo struct { - model string logger logger.Logger } @@ -44,29 +42,36 @@ func NewEcho(logger logger.Logger) conversation.Conversation { } func (e *Echo) Init(ctx context.Context, meta conversation.Metadata) error { - r := &conversation.Request{} - err := kmeta.DecodeMetadata(meta.Properties, r) - if err != nil { - return err - } - - e.model = r.Model - + // Echo component has no metadata return nil } func (e *Echo) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { - metadataStruct := conversation.Request{} - metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) + // Echo component has no metadata return } +// approximateTokensFromWords estimates the number of tokens based on word count. +// ref: https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them +func approximateTokensFromWords(text string) uint64 { + if text == "" { + return 0 + } + + // split on whitespace to count words + return uint64(len(strings.Fields(text))) +} + // Converse returns one output per input message. func (e *Echo) Converse(ctx context.Context, r *conversation.Request) (res *conversation.Response, err error) { if r == nil || r.Message == nil { + var conversationContext string + if r != nil { + conversationContext = r.ConversationContext + } return &conversation.Response{ - ConversationContext: r.ConversationContext, Outputs: []conversation.Result{}, + ConversationContext: conversationContext, }, nil } @@ -102,6 +107,8 @@ func (e *Echo) Converse(ctx context.Context, r *conversation.Request) (res *conv for argName := range propMap { argNames = append(argNames, argName) } + // sort the arg names to keep deterministic order for tests + sort.Strings(argNames) } } @@ -136,6 +143,7 @@ func (e *Echo) Converse(ctx context.Context, r *conversation.Request) (res *conv } } + responseContent := strings.Join(contentFromMessaged, "\n") stopReason := "stop" if len(toolCalls) > 0 { stopReason = "tool_calls" @@ -145,7 +153,7 @@ func (e *Echo) Converse(ctx context.Context, r *conversation.Request) (res *conv FinishReason: stopReason, Index: 0, Message: conversation.Message{ - Content: strings.Join(contentFromMessaged, "\n"), + Content: responseContent, }, } @@ -158,9 +166,17 @@ func (e *Echo) Converse(ctx context.Context, r *conversation.Request) (res *conv Choices: []conversation.Choice{choice}, } + tokenCount := approximateTokensFromWords(responseContent) + usage := &conversation.Usage{ + CompletionTokens: tokenCount, + PromptTokens: tokenCount, + TotalTokens: tokenCount + tokenCount, + } + res = &conversation.Response{ ConversationContext: r.ConversationContext, Outputs: []conversation.Result{output}, + Usage: usage, } return res, nil diff --git a/conversation/echo/echo_test.go b/conversation/echo/echo_test.go index 410c623eec..f683348136 100644 --- a/conversation/echo/echo_test.go +++ b/conversation/echo/echo_test.go @@ -183,6 +183,69 @@ func TestConverseAlpha2(t *testing.T) { }, }, }, + { + name: "tool call request with multiple arguments alphabetic ordering of arguments", + messages: []llms.MessageContent{ + { + Role: llms.ChatMessageTypeHuman, + Parts: []llms.ContentPart{ + llms.TextContent{Text: "hello echo"}, + }, + }, + }, + tools: []llms.Tool{ + { + Type: "function", + Function: &llms.FunctionDefinition{ + Name: "myfunc", + Description: "A function that does something", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "unit": map[string]any{ + "type": "string", + "description": "unit should come last", + }, + "name": map[string]any{ + "type": "string", + "description": "The name to process, should come second", + }, + "location": map[string]any{ + "type": "string", + "description": "location should come first", + }, + }, + }, + }, + }, + }, + expected: &conversation.Response{ + Outputs: []conversation.Result{ + { + StopReason: "tool_calls", + Choices: []conversation.Choice{ + { + FinishReason: "tool_calls", + Index: 0, + Message: conversation.Message{ + Content: "hello echo", + ToolCallRequest: &[]llms.ToolCall{ + { + ID: "0", // ID is auto-generated by the echo component + Type: "function", + FunctionCall: &llms.FunctionCall{ + Name: "myfunc", + Arguments: "location,name,unit", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, { name: "text message with tool call response", // echo responds with the text message and tool call response appended to the message content diff --git a/conversation/googleai/googleai.go b/conversation/googleai/googleai.go index 58aad5c368..1f6261afb5 100644 --- a/conversation/googleai/googleai.go +++ b/conversation/googleai/googleai.go @@ -18,13 +18,13 @@ import ( "context" "reflect" + "github.com/tmc/langchaingo/llms/openai" + "github.com/dapr/components-contrib/conversation" "github.com/dapr/components-contrib/conversation/langchaingokit" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" kmeta "github.com/dapr/kit/metadata" - - "github.com/tmc/langchaingo/llms/googleai" ) type GoogleAI struct { @@ -41,8 +41,6 @@ func NewGoogleAI(logger logger.Logger) conversation.Conversation { return g } -const defaultModel = "gemini-1.5-flash" - func (g *GoogleAI) Init(ctx context.Context, meta conversation.Metadata) error { md := conversation.LangchainMetadata{} err := kmeta.DecodeMetadata(meta.Properties, &md) @@ -50,27 +48,23 @@ func (g *GoogleAI) Init(ctx context.Context, meta conversation.Metadata) error { return err } - model := defaultModel - if md.Model != "" { - model = md.Model - } + // Resolve model via central helper (uses metadata, then env var, then default) + model := conversation.GetGoogleAIModel(md.Model) + key, _ := meta.GetProperty("key") - opts := []googleai.Option{ - googleai.WithAPIKey(md.Key), - googleai.WithDefaultModel(model), - } - llm, err := googleai.New( - ctx, - opts..., - ) + // endpoint from https://ai.google.dev/gemini-api/docs/openai + const endpoint = "https://generativelanguage.googleapis.com/v1beta/openai/" + opts := conversation.BuildOpenAIClientOptions(model, key, endpoint) + + llm, err := openai.New(opts...) if err != nil { return err } g.LLM.Model = llm - if md.CacheTTL != "" { - cachedModel, cacheErr := conversation.CacheModel(ctx, md.CacheTTL, g.LLM.Model) + if md.ResponseCacheTTL != nil { + cachedModel, cacheErr := conversation.CacheResponses(ctx, md.ResponseCacheTTL, g.LLM.Model) if cacheErr != nil { return cacheErr } diff --git a/conversation/googleai/metadata.yaml b/conversation/googleai/metadata.yaml index 848b864c73..61703b5ac9 100644 --- a/conversation/googleai/metadata.yaml +++ b/conversation/googleai/metadata.yaml @@ -24,10 +24,10 @@ metadata: - name: model required: false description: | - The GoogleAI LLM to use. + The GoogleAI LLM to use. Configurable via GOOGLEAI_MODEL environment variable. type: string - example: 'gemini-2.0-flash' - default: 'gemini-2.0-flash' + example: 'gemini-2.5-flash-lite' + default: 'gemini-2.5-flash-lite' - name: cacheTTL required: false description: | diff --git a/conversation/huggingface/huggingface.go b/conversation/huggingface/huggingface.go index 0ef727f874..3aaae761a9 100644 --- a/conversation/huggingface/huggingface.go +++ b/conversation/huggingface/huggingface.go @@ -42,9 +42,6 @@ func NewHuggingface(logger logger.Logger) conversation.Conversation { return h } -// Default model - using a popular and reliable model -const defaultModel = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" - // Default HuggingFace OpenAI-compatible endpoint const defaultEndpoint = "https://router.huggingface.co/hf-inference/models/{{model}}/v1" @@ -55,11 +52,8 @@ func (h *Huggingface) Init(ctx context.Context, meta conversation.Metadata) erro return err } - model := defaultModel - if m.Model != "" { - model = m.Model - } - + // Resolve model via central helper (uses metadata, then env var, then default) + model := conversation.GetHuggingFaceModel(m.Model) endpoint := strings.Replace(defaultEndpoint, "{{model}}", model, 1) if m.Endpoint != "" { endpoint = m.Endpoint @@ -67,11 +61,7 @@ func (h *Huggingface) Init(ctx context.Context, meta conversation.Metadata) erro // Create options for OpenAI client using HuggingFace's OpenAI-compatible API // This is a workaround for issues with the native HuggingFace langchaingo implementation - options := []openai.Option{ - openai.WithModel(model), - openai.WithToken(m.Key), - openai.WithBaseURL(endpoint), - } + options := conversation.BuildOpenAIClientOptions(model, m.Key, endpoint) llm, err := openai.New(options...) if err != nil { @@ -80,8 +70,8 @@ func (h *Huggingface) Init(ctx context.Context, meta conversation.Metadata) erro h.LLM.Model = llm - if m.CacheTTL != "" { - cachedModel, cacheErr := conversation.CacheModel(ctx, m.CacheTTL, h.LLM.Model) + if m.ResponseCacheTTL != nil { + cachedModel, cacheErr := conversation.CacheResponses(ctx, m.ResponseCacheTTL, h.LLM.Model) if cacheErr != nil { return cacheErr } diff --git a/conversation/huggingface/metadata.yaml b/conversation/huggingface/metadata.yaml index 89ec7d01a5..e37ccc6c6c 100644 --- a/conversation/huggingface/metadata.yaml +++ b/conversation/huggingface/metadata.yaml @@ -24,7 +24,7 @@ metadata: - name: model required: false description: | - The Huggingface model to use. Uses OpenAI-compatible API. + The Huggingface model to use. Uses OpenAI-compatible API. Configurable via HUGGINGFACE_MODEL environment variable. type: string example: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-32B' default: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-32B' diff --git a/conversation/langchaingokit/model.go b/conversation/langchaingokit/model.go index 61684953aa..60406abd05 100644 --- a/conversation/langchaingokit/model.go +++ b/conversation/langchaingokit/model.go @@ -16,19 +16,24 @@ package langchaingokit import ( "context" + "fmt" "github.com/tmc/langchaingo/llms" + "github.com/dapr/kit/logger" + "github.com/dapr/components-contrib/conversation" ) // LLM is a helper struct that wraps a LangChain Go model type LLM struct { llms.Model + model string + logger logger.Logger } func (a *LLM) Converse(ctx context.Context, r *conversation.Request) (res *conversation.Response, err error) { - opts := getOptionsFromRequest(r) + opts := getOptionsFromRequest(r, a.logger) var messages []llms.MessageContent if r.Message != nil { @@ -40,40 +45,69 @@ func (a *LLM) Converse(ctx context.Context, r *conversation.Request) (res *conve return nil, err } - outputs := make([]conversation.Result, 0, len(resp.Choices)) - for i := range resp.Choices { + outputs, usage, err := a.NormalizeConverseResult(resp.Choices) + if err != nil { + return nil, err + } + + return &conversation.Response{ + Model: a.model, + Outputs: outputs, + Usage: usage, + }, nil +} + +// NOTE: ollama does not provide a stop reason at all, +// so server side best we can do is say unknown if this is empty. +func normalizeFinishReason(stopReason string) string { + if stopReason == "" { + return "unknown" + } + return stopReason +} + +func (a *LLM) NormalizeConverseResult(choices []*llms.ContentChoice) ([]conversation.Result, *conversation.Usage, error) { + if len(choices) == 0 { + return nil, nil, nil + } + + // Extract usage from the first choice's GenerationInfo (all choices share the same usage) + var usage *conversation.Usage + if len(choices) > 0 && choices[0].GenerationInfo != nil { + var err error + usage, err = extractUsageFromLangchainGenerationInfo(choices[0].GenerationInfo) + if err != nil { + return nil, nil, fmt.Errorf("failed to extract usage metrics: %v", err) + } + } + + outputs := make([]conversation.Result, 0, len(choices)) + for i := range choices { choice := conversation.Choice{ - FinishReason: resp.Choices[i].StopReason, + FinishReason: normalizeFinishReason(choices[i].StopReason), Index: int64(i), } - if resp.Choices[i].Content != "" { - choice.Message.Content = resp.Choices[i].Content + if choices[i].Content != "" { + choice.Message.Content = choices[i].Content } - if resp.Choices[i].ToolCalls != nil { - choice.Message.ToolCallRequest = &resp.Choices[i].ToolCalls + if choices[i].ToolCalls != nil { + choice.Message.ToolCallRequest = &choices[i].ToolCalls } output := conversation.Result{ - StopReason: resp.Choices[i].StopReason, + StopReason: normalizeFinishReason(choices[i].StopReason), Choices: []conversation.Choice{choice}, } outputs = append(outputs, output) } - res = &conversation.Response{ - // TODO: Fix this, we never used this ConversationContext field to begin with. - // This needs improvements to be useful. - ConversationContext: r.ConversationContext, - Outputs: outputs, - } - - return res, nil + return outputs, usage, nil } -func getOptionsFromRequest(r *conversation.Request, opts ...llms.CallOption) []llms.CallOption { +func getOptionsFromRequest(r *conversation.Request, logger logger.Logger, opts ...llms.CallOption) []llms.CallOption { if opts == nil { opts = make([]llms.CallOption, 0) } @@ -90,5 +124,55 @@ func getOptionsFromRequest(r *conversation.Request, opts ...llms.CallOption) []l opts = append(opts, llms.WithToolChoice(r.ToolChoice)) } + if r.ResponseFormatAsJSONSchema != nil { + structuredOutput, err := convertToStructuredOutputDefinition(r.ResponseFormatAsJSONSchema) + if err != nil { + logger.Warnf("failed to convert response format to structured output, will continue without structured output: %v", err) + } else { + opts = append(opts, llms.WithStructuredOutput(structuredOutput)) + } + // Note: WithJSONMode() is not needed when using WithStructuredOutput, + // as structured output already returns JSON so do NOT add that here in this block! + } + + // NOTE: we can add these in future! There are others... + // llms.WithThinkingMode() + // llms.WithCacheControl() + // llms.WithMaxLength() + // llms.WithMinLength() + // llms.WithMaxTokens() + + // Handle prompt cache retention for OpenAI's extended prompt caching feature + if r.PromptCacheRetention != nil { + if r.Metadata == nil { + r.Metadata = make(map[string]string) + } + // OpenAI expects this as a top-level parameter, but we are forced to pass it via metadata, + // and langchaingo should forward it to the OpenAI client. + // NOTE: This is absolutely a complete hack that I guarantee you does work. + // In langchain there is a llms.WithPromptCaching(true) option that is incompatible with Openai yielding an err bc then it tries to use a bool instead of a string, + // because openai expects this to be a time duration string but used with langchain with their llms.WithPromptCachine(true) does not translate properly. + // When Langchain fixes this then we can update accordingly :) + const metadataPromptCacheKey = "prompt_cache_retention" + r.Metadata[metadataPromptCacheKey] = r.PromptCacheRetention.String() + } + + // Openai accepts this as map[string]string but langchain expects map[string]any, + // so we go with openai for our type opinion here, and therefore I convert accordingly. + if r.Metadata != nil { + opts = append(opts, llms.WithMetadata(stringMapToAny(r.Metadata))) + } + return opts } + +func stringMapToAny(m map[string]string) map[string]any { + if m == nil { + return nil + } + out := make(map[string]any, len(m)) + for k, v := range m { + out[k] = v + } + return out +} diff --git a/conversation/langchaingokit/translate.go b/conversation/langchaingokit/translate.go new file mode 100644 index 0000000000..5dfcd42cfc --- /dev/null +++ b/conversation/langchaingokit/translate.go @@ -0,0 +1,162 @@ +/* +Copyright 2026 The Dapr Authors +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 langchaingokit + +import ( + "fmt" + + "github.com/dapr/components-contrib/conversation" +) + +// NOTE: These are all translations due to langchaingo data types. + +// exposing to use also in tests +const ( + completionKey = "CompletionTokens" + promptKey = "PromptTokens" + totalKey = "TotalTokens" + completionAcceptedKey = "CompletionAcceptedPredictionTokens" + completionAudioKey = "CompletionAudioTokens" + reasoningKey = "CompletionReasoningTokens" + rejectedKey = "CompletionRejectedPredictionTokens" + promptAudioKey = "PromptAudioTokens" + promptCachedKey = "PromptCachedTokens" +) + +// extractUint64FromGenInfo extracts a uint64 value from genInfo map to extract usage data from langchaingo's GenerationInfo map in the choices response. +func extractUint64FromGenInfo(genInfo map[string]any, key string) (uint64, error) { + if v, ok := genInfo[key]; ok { + switch val := v.(type) { + case uint64: + return val, nil + case int: + if val < 0 { + return 0, fmt.Errorf("negative value for %s: %d", key, val) + } + return uint64(val), nil + case int64: + if val < 0 { + return 0, fmt.Errorf("negative value for %s: %d", key, val) + } + return uint64(val), nil + case int32: + if val < 0 { + return 0, fmt.Errorf("negative value for %s: %d", key, val) + } + return uint64(val), nil + case float64: + if val < 0 { + return 0, fmt.Errorf("negative value for %s: %f", key, val) + } + return uint64(val), nil + case float32: + if val < 0 { + return 0, fmt.Errorf("negative value for %s: %f", key, val) + } + return uint64(val), nil + default: + return 0, fmt.Errorf("failed to extract usage metrics for type: %T", val) + } + } + return 0, nil +} + +// extractUsageFromLangchainGenerationInfo extracts usage statistics from langchaingo's GenerationInfo map. +// Magic strings are based on the fields here: +// ref: https://github.com/openai/openai-go/blob/main/completion.go#L192 for CompletionUsageCompletionTokensDetails +// ref: https://github.com/openai/openai-go/blob/main/completion.go#L162 for CompletionUsagePromptTokensDetails +func extractUsageFromLangchainGenerationInfo(genInfo map[string]any) (*conversation.Usage, error) { + if genInfo == nil { + return nil, nil + } + + usage := &conversation.Usage{} + completionTokens, err := extractUint64FromGenInfo(genInfo, completionKey) + if err != nil { + return nil, fmt.Errorf("failed to extract completion tokens: %v", err) + } + usage.CompletionTokens = completionTokens + + promptTokens, err := extractUint64FromGenInfo(genInfo, promptKey) + if err != nil { + return nil, fmt.Errorf("failed to extract prompt tokens: %v", err) + } + usage.PromptTokens = promptTokens + + totalTokens, err := extractUint64FromGenInfo(genInfo, totalKey) + if err != nil { + return nil, fmt.Errorf("failed to extract total tokens: %v", err) + } + usage.TotalTokens = totalTokens + + acceptedTokens, err := extractUint64FromGenInfo(genInfo, completionAcceptedKey) + if err != nil { + return nil, fmt.Errorf("failed to extract completion accepted prediction tokens: %v", err) + } + + audioTokens, err := extractUint64FromGenInfo(genInfo, completionAudioKey) + if err != nil { + return nil, fmt.Errorf("failed to extract completion audio tokens: %v", err) + } + + reasoningTokens, err := extractUint64FromGenInfo(genInfo, reasoningKey) + if err != nil { + return nil, fmt.Errorf("failed to extract completion reasoning tokens: %v", err) + } + + rejectedTokens, err := extractUint64FromGenInfo(genInfo, rejectedKey) + if err != nil { + return nil, fmt.Errorf("failed to extract completion rejected prediction tokens: %v", err) + } + + completionDetails := &conversation.CompletionTokensDetails{ + AcceptedPredictionTokens: acceptedTokens, + AudioTokens: audioTokens, + ReasoningTokens: reasoningTokens, + RejectedPredictionTokens: rejectedTokens, + } + + if completionDetails.AcceptedPredictionTokens > 0 || completionDetails.AudioTokens > 0 || + completionDetails.ReasoningTokens > 0 || completionDetails.RejectedPredictionTokens > 0 { + usage.CompletionTokensDetails = completionDetails + } + + promptAudioTokens, err := extractUint64FromGenInfo(genInfo, promptAudioKey) + if err != nil { + return nil, fmt.Errorf("failed to extract prompt audio tokens: %v", err) + } + + promptCachedTokens, err := extractUint64FromGenInfo(genInfo, promptCachedKey) + if err != nil { + return nil, fmt.Errorf("failed to extract prompt cached tokens: %v", err) + } + + promptDetails := &conversation.PromptTokensDetails{ + AudioTokens: promptAudioTokens, + CachedTokens: promptCachedTokens, + } + + if promptDetails.AudioTokens > 0 || promptDetails.CachedTokens > 0 { + usage.PromptTokensDetails = promptDetails + } + + // Only return usage if we have at least some data + if usage.CompletionTokens > 0 || usage.PromptTokens > 0 || usage.TotalTokens > 0 { + return usage, nil + } + + return nil, nil +} diff --git a/conversation/langchaingokit/translate_test.go b/conversation/langchaingokit/translate_test.go new file mode 100644 index 0000000000..504dd86043 --- /dev/null +++ b/conversation/langchaingokit/translate_test.go @@ -0,0 +1,339 @@ +/* +Copyright 2026 The Dapr Authors +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 langchaingokit + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/conversation" +) + +func TestExtractInt64FromGenInfo(t *testing.T) { + tests := []struct { + name string + genInfo map[string]any + key string + expected uint64 + expectedErr bool + }{ + { + name: "extract int64 value", + genInfo: map[string]any{ + completionKey: int64(100), + }, + key: completionKey, + expected: uint64(100), + expectedErr: false, + }, + { + name: "missing key", + genInfo: map[string]any{ + "OtherKey": int64(50), + }, + key: completionKey, + expected: uint64(0), + expectedErr: false, + }, + { + name: "nil genInfo returns zero", + genInfo: nil, + key: completionKey, + expected: uint64(0), + expectedErr: false, + }, + { + name: "wrong type returns zero", + genInfo: map[string]any{ + completionKey: "not an int", + }, + key: completionKey, + expected: uint64(0), + expectedErr: true, + }, + { + name: "zero value", + genInfo: map[string]any{ + completionKey: int64(0), + }, + key: completionKey, + expected: uint64(0), + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := extractUint64FromGenInfo(tt.genInfo, tt.key) + if tt.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestExtractUsageFromLangchainGenerationInfo(t *testing.T) { + tests := []struct { + name string + genInfo map[string]any + validate func(t *testing.T, result *conversation.Usage, err error) + }{ + { + name: "nil genInfo", + genInfo: nil, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + assert.Nil(t, result) + }, + }, + { + name: "empty genInfo", + genInfo: map[string]any{}, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + assert.Nil(t, result) + }, + }, + { + name: "basic usage", + genInfo: map[string]any{ + completionKey: int64(100), + promptKey: int64(50), + totalKey: int64(150), + }, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, uint64(100), result.CompletionTokens) + assert.Equal(t, uint64(50), result.PromptTokens) + assert.Equal(t, uint64(150), result.TotalTokens) + assert.Nil(t, result.CompletionTokensDetails) + assert.Nil(t, result.PromptTokensDetails) + }, + }, + { + name: "usage with completion token details", + genInfo: map[string]any{ + completionKey: int64(200), + promptKey: int64(100), + totalKey: int64(300), + completionAcceptedKey: int64(10), + completionAudioKey: int64(5), + reasoningKey: int64(15), + rejectedKey: int64(2), + }, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, uint64(200), result.CompletionTokens) + assert.NotNil(t, result.CompletionTokensDetails) + assert.Equal(t, uint64(10), result.CompletionTokensDetails.AcceptedPredictionTokens) + assert.Equal(t, uint64(5), result.CompletionTokensDetails.AudioTokens) + assert.Equal(t, uint64(15), result.CompletionTokensDetails.ReasoningTokens) + assert.Equal(t, uint64(2), result.CompletionTokensDetails.RejectedPredictionTokens) + assert.Nil(t, result.PromptTokensDetails) + }, + }, + { + name: "usage with prompt token details", + genInfo: map[string]any{ + completionKey: int64(150), + promptKey: int64(75), + totalKey: int64(225), + promptAudioKey: int64(10), + promptCachedKey: int64(20), + }, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, uint64(150), result.CompletionTokens) + assert.Nil(t, result.CompletionTokensDetails) + assert.NotNil(t, result.PromptTokensDetails) + assert.Equal(t, uint64(10), result.PromptTokensDetails.AudioTokens) + assert.Equal(t, uint64(20), result.PromptTokensDetails.CachedTokens) + }, + }, + { + name: "usage with all details", + genInfo: map[string]any{ + completionKey: int64(250), + promptKey: int64(125), + totalKey: int64(375), + completionAcceptedKey: int64(20), + completionAudioKey: int64(8), + reasoningKey: int64(25), + rejectedKey: int64(3), + promptAudioKey: int64(15), + promptCachedKey: int64(30), + }, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, uint64(250), result.CompletionTokens) + assert.Equal(t, uint64(125), result.PromptTokens) + assert.Equal(t, uint64(375), result.TotalTokens) + assert.NotNil(t, result.CompletionTokensDetails) + assert.Equal(t, uint64(20), result.CompletionTokensDetails.AcceptedPredictionTokens) + assert.Equal(t, uint64(8), result.CompletionTokensDetails.AudioTokens) + assert.Equal(t, uint64(25), result.CompletionTokensDetails.ReasoningTokens) + assert.Equal(t, uint64(3), result.CompletionTokensDetails.RejectedPredictionTokens) + assert.NotNil(t, result.PromptTokensDetails) + assert.Equal(t, uint64(15), result.PromptTokensDetails.AudioTokens) + assert.Equal(t, uint64(30), result.PromptTokensDetails.CachedTokens) + }, + }, + { + name: "completion details with zero values are not included", + genInfo: map[string]any{ + completionKey: int64(100), + promptKey: int64(50), + totalKey: int64(150), + completionAcceptedKey: int64(0), + completionAudioKey: int64(0), + reasoningKey: int64(0), + rejectedKey: int64(0), + }, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Nil(t, result.CompletionTokensDetails) + }, + }, + { + name: "prompt details with zero values are not included", + genInfo: map[string]any{ + completionKey: int64(100), + promptKey: int64(50), + totalKey: int64(150), + promptAudioKey: int64(0), + promptCachedKey: int64(0), + }, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Nil(t, result.PromptTokensDetails) + }, + }, + { + name: "completion details included if any field is non-zero", + genInfo: map[string]any{ + completionKey: int64(100), + promptKey: int64(50), + totalKey: int64(150), + completionAcceptedKey: int64(0), + completionAudioKey: int64(5), + reasoningKey: int64(0), + rejectedKey: int64(0), + }, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.NotNil(t, result.CompletionTokensDetails) + assert.Equal(t, uint64(5), result.CompletionTokensDetails.AudioTokens) + }, + }, + { + name: "invalid type", + genInfo: map[string]any{ + completionKey: int64(100), + promptKey: int64(50), + totalKey: int64(150), + completionAcceptedKey: int64(0), + completionAudioKey: int64(5), + reasoningKey: int64(0), + rejectedKey: "i am the invalid type here", + }, + validate: func(t *testing.T, result *conversation.Usage, err error) { + require.Error(t, err) + require.Nil(t, result) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := extractUsageFromLangchainGenerationInfo(tt.genInfo) + tt.validate(t, result, err) + }) + } +} + +func TestStringMapToAny(t *testing.T) { + tests := []struct { + name string + input map[string]string + validate func(t *testing.T, result map[string]any) + }{ + { + name: "nil map returns nil", + input: nil, + validate: func(t *testing.T, result map[string]any) { + assert.Nil(t, result) + }, + }, + { + name: "empty map returns empty map", + input: map[string]string{}, + validate: func(t *testing.T, result map[string]any) { + assert.NotNil(t, result) + assert.Empty(t, result) + }, + }, + { + name: "converts map[string]string to map[string]any", + input: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + validate: func(t *testing.T, result map[string]any) { + require.NotNil(t, result) + assert.Len(t, result, 3) + assert.Equal(t, "value1", result["key1"]) + assert.Equal(t, "value2", result["key2"]) + assert.Equal(t, "value3", result["key3"]) + // Verify types + for _, v := range result { + _, ok := v.(string) + assert.True(t, ok, "All values should be strings") + } + }, + }, + { + name: "preserves all key-value pairs", + input: map[string]string{ + "metadata1": "data1", + "metadata2": "data2", + }, + validate: func(t *testing.T, result map[string]any) { + assert.Equal(t, "data1", result["metadata1"]) + assert.Equal(t, "data2", result["metadata2"]) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := stringMapToAny(tt.input) + tt.validate(t, result) + }) + } +} diff --git a/conversation/langchaingokit/workarounds.go b/conversation/langchaingokit/workarounds.go new file mode 100644 index 0000000000..6ea633b2b9 --- /dev/null +++ b/conversation/langchaingokit/workarounds.go @@ -0,0 +1,190 @@ +package langchaingokit + +import ( + "errors" + "fmt" + "strings" + + "github.com/tmc/langchaingo/llms" +) + +// NOTE: These are all workarounds due to langchaingo limitations, +// or limitations of certain components (so far only mistral). + +// CreateToolCallPart creates mistral and ollama api compatible tool call messages. +// Most LLM providers can handle tool calls using the tool call object; +// however, mistral and ollama requires it as text in conversation history. +// This is due to langchaingo limitations. +func CreateToolCallPart(toolCall *llms.ToolCall) llms.ContentPart { + if toolCall == nil { + return nil + } + + if toolCall.FunctionCall == nil { + return llms.TextContent{ + Text: "Tool call [ID: " + toolCall.ID + "]: ", + } + } + + return llms.TextContent{ + Text: "Tool call [ID: " + toolCall.ID + "]: " + toolCall.FunctionCall.Name + "(" + toolCall.FunctionCall.Arguments + ")", + } +} + +// CreateToolResponseMessage creates mistral and ollama api compatible tool response message +// using the human role specifically otherwise mistral will reject the tool response message. +// Most LLM providers can handle tool call responses using the tool call response object; +// however, mistral and ollama requires it as text in conversation history. +// This is due to langchaingo limitations. +func CreateToolResponseMessage(responses ...llms.ContentPart) llms.MessageContent { + msg := llms.MessageContent{ + Role: llms.ChatMessageTypeHuman, + } + if len(responses) == 0 { + return msg + } + var toolID, name string + + mistralContentParts := make([]string, 0, len(responses)) + for _, response := range responses { + if resp, ok := response.(llms.ToolCallResponse); ok { + if toolID == "" { + toolID = resp.ToolCallID + } + if name == "" { + name = resp.Name + } + mistralContentParts = append(mistralContentParts, resp.Content) + } + } + if len(mistralContentParts) > 0 { + msg.Parts = []llms.ContentPart{ + llms.TextContent{ + Text: "Tool response [ID: " + toolID + ", Name: " + name + "]: " + strings.Join(mistralContentParts, "\n"), + }, + } + } + return msg +} + +// convertToStructuredOutputSchema is an internal helper that converts a JSON schema map to langchaingo's llms.StructuredOutputSchema. +// It recursively processes nested objects and arrays to build the complete schema structure. +func convertToStructuredOutputSchema(schemaMap map[string]any) (*llms.StructuredOutputSchema, error) { + if schemaMap == nil { + return nil, errors.New("schema map cannot be nil") + } + + schemaTypeStr, ok := schemaMap["type"].(string) + if !ok { + return nil, errors.New("schema type is required") + } + + schema := &llms.StructuredOutputSchema{ + Type: llms.SchemaType(schemaTypeStr), + } + + if desc, ok := schemaMap["description"].(string); ok { + schema.Description = desc + } + + if required, ok := schemaMap["required"].([]any); ok { + schema.Required = make([]string, 0, len(required)) + for _, r := range required { + if rStr, ok := r.(string); ok { + schema.Required = append(schema.Required, rStr) + } + } + } + + if additionalProps, ok := schemaMap["additionalProperties"].(bool); ok { + schema.AdditionalProperties = additionalProps + } + + // extract enum - convert to []string if needed + if enum, ok := schemaMap["enum"].([]any); ok { + enumStrings := make([]string, 0, len(enum)) + for _, e := range enum { + if eStr, ok := e.(string); ok { + enumStrings = append(enumStrings, eStr) + } else { + // if enum contains non-string values, convert to string + enumStrings = append(enumStrings, fmt.Sprintf("%v", e)) + } + } + if len(enumStrings) > 0 { + schema.Enum = enumStrings + } + } + + if schema.Type == llms.SchemaTypeObject { + if properties, ok := schemaMap["properties"].(map[string]any); ok { + schema.Properties = make(map[string]*llms.StructuredOutputSchema) + for propName, propValue := range properties { + if propMap, ok := propValue.(map[string]any); ok { + propSchema, err := convertToStructuredOutputSchema(propMap) + if err != nil { + return nil, fmt.Errorf("failed to convert property %q: %w", propName, err) + } + schema.Properties[propName] = propSchema + } else { + return nil, fmt.Errorf("property %q must be a map, got %T", propName, propValue) + } + } + } + } + + // handle array items + if schema.Type == llms.SchemaTypeArray { + if items, ok := schemaMap["items"].(map[string]any); ok { + itemsSchema, err := convertToStructuredOutputSchema(items) + if err != nil { + return nil, fmt.Errorf("failed to convert array items: %w", err) + } + schema.Items = itemsSchema + } + } + + return schema, nil +} + +// convertToStructuredOutputDefinition converts a JSON schema map to langchain's llms.StructuredOutputDefinition. +// Based on langchain's structured output implementation: +// https://github.com/tmc/langchaingo/commit/5b6a093e5995485fdf061609cf987be84be947e2#diff-bb2609d8b74a6201524e3f8c9408b2866e19620c7ee0af3c6c947a5302baf6a1 +func convertToStructuredOutputDefinition(jsonSchema map[string]any) (*llms.StructuredOutputDefinition, error) { + if jsonSchema == nil { + return nil, errors.New("json schema cannot be nil") + } + + if _, ok := jsonSchema["type"].(string); !ok { + return nil, errors.New("schema type is required and must be a string") + } + + schema, err := convertToStructuredOutputSchema(jsonSchema) + if err != nil { + return nil, fmt.Errorf("failed to convert schema: %w", err) + } + + // build StructuredOutputDefinition + // name is not a required json schema field, so we use a default value if not provided for langchaingo's llms.StructuredOutputDefinition.Name + name := "response" + if n, ok := jsonSchema["name"].(string); ok && n != "" { + name = n + } + + description := "" + if d, ok := jsonSchema["description"].(string); ok { + description = d + } + + strict := false + if s, ok := jsonSchema["strict"].(bool); ok { + strict = s + } + + return &llms.StructuredOutputDefinition{ + Name: name, + Description: description, + Schema: schema, + Strict: strict, + }, nil +} diff --git a/conversation/langchaingokit/workarounds_test.go b/conversation/langchaingokit/workarounds_test.go new file mode 100644 index 0000000000..978c86d53a --- /dev/null +++ b/conversation/langchaingokit/workarounds_test.go @@ -0,0 +1,548 @@ +package langchaingokit + +/* +Copyright 2025 The Dapr Authors +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. +*/ + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tmc/langchaingo/llms" +) + +func TestConvertToStructuredOutputDefinition(t *testing.T) { + tests := []struct { + name string + schema map[string]any + wantErr bool + validate func(t *testing.T, result *llms.StructuredOutputDefinition, err error) + }{ + { + name: "nil input", + schema: nil, + wantErr: true, + }, + { + name: "missing type", + schema: map[string]any{ + "description": "test schema", + }, + wantErr: true, + }, + { + name: "invalid type format", + schema: map[string]any{ + "type": 123, // not a string + }, + wantErr: true, + }, + { + name: "simple schema with defaults", + schema: map[string]any{ + "type": "string", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputDefinition, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "response", result.Name) + assert.Equal(t, "", result.Description) + assert.False(t, result.Strict) + assert.NotNil(t, result.Schema) + assert.Equal(t, llms.SchemaTypeString, result.Schema.Type) + }, + }, + { + name: "full strict schema", + schema: map[string]any{ + "type": "object", + "name": "user_info", + "description": "User information schema", + "strict": true, + "properties": map[string]any{ + "name": map[string]any{ + "type": "string", + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputDefinition, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "user_info", result.Name) + assert.Equal(t, "User information schema", result.Description) + assert.True(t, result.Strict) + assert.NotNil(t, result.Schema) + assert.Equal(t, llms.SchemaTypeObject, result.Schema.Type) + }, + }, + { + name: "empty name uses default", + schema: map[string]any{ + "type": "string", + "name": "", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputDefinition, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "response", result.Name) + }, + }, + { + name: "complex nested object schema", + schema: map[string]any{ + "type": "object", + "name": "complex_schema", + "description": "Complex nested schema", + "properties": map[string]any{ + "user": map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{ + "type": "string", + }, + "age": map[string]any{ + "type": "integer", + }, + }, + "required": []any{"name"}, + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputDefinition, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "complex_schema", result.Name) + assert.NotNil(t, result.Schema.Properties) + assert.NotNil(t, result.Schema.Properties["user"]) + assert.Equal(t, llms.SchemaTypeObject, result.Schema.Properties["user"].Type) + }, + }, + { + name: "array schema", + schema: map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "string", + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputDefinition, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "response", result.Name) + assert.Equal(t, llms.SchemaTypeArray, result.Schema.Type) + assert.NotNil(t, result.Schema.Items) + assert.Equal(t, llms.SchemaTypeString, result.Schema.Items.Type) + }, + }, + { + name: "invalid property format", + schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "invalid": "not a map", // invalid property format here + "valid": map[string]any{ + "type": "string", + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputDefinition, err error) { + require.Error(t, err) + assert.Nil(t, result) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := convertToStructuredOutputDefinition(tt.schema) + if tt.wantErr { + assert.Nil(t, result) + assert.Error(t, err) + } else if tt.validate != nil { + tt.validate(t, result, err) + } + }) + } +} + +func TestConvertToStructuredOutputSchema(t *testing.T) { + tests := []struct { + name string + schema map[string]any + wantErr bool + validate func(t *testing.T, result *llms.StructuredOutputSchema, err error) + }{ + { + name: "nil input", + schema: nil, + wantErr: true, + }, + { + name: "missing type", + schema: map[string]any{ + "description": "test", + }, + wantErr: true, + }, + { + name: "invalid type format", + schema: map[string]any{ + "type": 123, + }, + wantErr: true, + }, + { + name: "simple string type", + schema: map[string]any{ + "type": "string", + "description": "A string field", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, llms.SchemaTypeString, result.Type) + assert.Equal(t, "A string field", result.Description) + }, + }, + { + name: "integer type", + schema: map[string]any{ + "type": "integer", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, llms.SchemaTypeInteger, result.Type) + }, + }, + { + name: "number type", + schema: map[string]any{ + "type": "number", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, llms.SchemaTypeNumber, result.Type) + }, + }, + { + name: "boolean type", + schema: map[string]any{ + "type": "boolean", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, llms.SchemaTypeBoolean, result.Type) + }, + }, + { + name: "object with properties", + schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{ + "type": "string", + }, + "age": map[string]any{ + "type": "integer", + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, llms.SchemaTypeObject, result.Type) + assert.NotNil(t, result.Properties) + assert.Len(t, result.Properties, 2) + assert.Equal(t, llms.SchemaTypeString, result.Properties["name"].Type) + assert.Equal(t, llms.SchemaTypeInteger, result.Properties["age"].Type) + }, + }, + { + name: "object with required fields", + schema: map[string]any{ + "type": "object", + "required": []any{"name", "email"}, + "properties": map[string]any{ + "name": map[string]any{ + "type": "string", + }, + "email": map[string]any{ + "type": "string", + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, []string{"name", "email"}, result.Required) + }, + }, + { + name: "required fields with non-string values are skipped", + schema: map[string]any{ + "type": "object", + "required": []any{"name", 123, "email"}, + "properties": map[string]any{ + "name": map[string]any{ + "type": "string", + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, []string{"name", "email"}, result.Required) + }, + }, + { + name: "additionalProperties false", + schema: map[string]any{ + "type": "object", + "additionalProperties": false, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.False(t, result.AdditionalProperties) + }, + }, + { + name: "additionalProperties true", + schema: map[string]any{ + "type": "object", + "additionalProperties": true, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.True(t, result.AdditionalProperties) + }, + }, + { + name: "enum with string values", + schema: map[string]any{ + "type": "string", + "enum": []any{"option1", "option2", "option3"}, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, []string{"option1", "option2", "option3"}, result.Enum) + }, + }, + { + name: "enum with mixed types converts to strings", + schema: map[string]any{ + "type": "string", + "enum": []any{"option1", 123, true}, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, []string{"option1", "123", "true"}, result.Enum) + }, + }, + { + name: "array with items", + schema: map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "string", + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, llms.SchemaTypeArray, result.Type) + assert.NotNil(t, result.Items) + assert.Equal(t, llms.SchemaTypeString, result.Items.Type) + }, + }, + { + name: "nested object properties", + schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "user": map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{ + "type": "string", + }, + "address": map[string]any{ + "type": "object", + "properties": map[string]any{ + "street": map[string]any{ + "type": "string", + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.NotNil(t, result.Properties["user"]) + assert.NotNil(t, result.Properties["user"].Properties["address"]) + assert.NotNil(t, result.Properties["user"].Properties["address"].Properties["street"]) + }, + }, + { + name: "array of objects", + schema: map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "object", + "properties": map[string]any{ + "id": map[string]any{ + "type": "integer", + }, + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, llms.SchemaTypeArray, result.Type) + assert.Equal(t, llms.SchemaTypeObject, result.Items.Type) + assert.NotNil(t, result.Items.Properties["id"]) + }, + }, + { + name: "invalid property format", + schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "invalid": "not a map", // invalid property format here + "valid": map[string]any{ + "type": "string", + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.Error(t, err) + assert.Nil(t, result) + }, + }, + { + name: "invalid array items format", + schema: map[string]any{ + "type": "array", + "items": "not a map", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Nil(t, result.Items) + }, + }, + { + name: "nested property with invalid format", + schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "nested": map[string]any{ + "type": "object", + "properties": map[string]any{ + "invalid": "not a map", // invalid property format here + "valid": map[string]any{ + "type": "string", + }, + }, + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.Error(t, err) + assert.Nil(t, result) + }, + }, + { + name: "nested array items with invalid property", + schema: map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "object", + "properties": map[string]any{ + "invalid": "not a map", // invalid property format here + "valid": map[string]any{ + "type": "string", + }, + }, + }, + }, + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.Error(t, err) + assert.Nil(t, result) + }, + }, + { + name: "object without properties", + schema: map[string]any{ + "type": "object", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, llms.SchemaTypeObject, result.Type) + assert.Nil(t, result.Properties) + }, + }, + { + name: "array without items", + schema: map[string]any{ + "type": "array", + }, + wantErr: false, + validate: func(t *testing.T, result *llms.StructuredOutputSchema, err error) { + require.NoError(t, err) + assert.Equal(t, llms.SchemaTypeArray, result.Type) + assert.Nil(t, result.Items) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := convertToStructuredOutputSchema(tt.schema) + if tt.wantErr { + assert.Nil(t, result) + assert.Error(t, err) + } else if tt.validate != nil { + tt.validate(t, result, err) + } + }) + } +} diff --git a/conversation/metadata.go b/conversation/metadata.go index d3ed4f706a..ed5d422135 100644 --- a/conversation/metadata.go +++ b/conversation/metadata.go @@ -14,7 +14,11 @@ limitations under the License. */ package conversation -import "github.com/dapr/components-contrib/metadata" +import ( + "time" + + "github.com/dapr/components-contrib/metadata" +) // Metadata represents a set of conversation specific properties. type Metadata struct { @@ -23,8 +27,8 @@ type Metadata struct { // LangchainMetadata is a common metadata structure for langchain supported implementations. type LangchainMetadata struct { - Key string `json:"key"` - Model string `json:"model"` - CacheTTL string `json:"cacheTTL"` - Endpoint string `json:"endpoint"` + Key string `json:"key" mapstructure:"key"` + Model string `json:"model" mapstructure:"model"` + ResponseCacheTTL *time.Duration `json:"responseCacheTTL,omitempty" mapstructure:"responseCacheTTL" mapstructurealiases:"cacheTTL"` + Endpoint string `json:"endpoint" mapstructure:"endpoint"` } diff --git a/conversation/metadata_test.go b/conversation/metadata_test.go index 58edab76e2..ca435e496b 100644 --- a/conversation/metadata_test.go +++ b/conversation/metadata_test.go @@ -16,6 +16,7 @@ package conversation import ( "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,11 +24,12 @@ import ( func TestLangchainMetadata(t *testing.T) { t.Run("json marshaling with endpoint", func(t *testing.T) { + ttl := 10 * time.Minute metadata := LangchainMetadata{ - Key: "test-key", - Model: "gpt-4", - CacheTTL: "10m", - Endpoint: "https://custom-endpoint.example.com", + Key: "test-key", + Model: DefaultOpenAIModel, + ResponseCacheTTL: &ttl, + Endpoint: "https://custom-endpoint.example.com", } bytes, err := json.Marshal(metadata) @@ -39,7 +41,7 @@ func TestLangchainMetadata(t *testing.T) { assert.Equal(t, metadata.Key, unmarshaled.Key) assert.Equal(t, metadata.Model, unmarshaled.Model) - assert.Equal(t, metadata.CacheTTL, unmarshaled.CacheTTL) + assert.Equal(t, metadata.ResponseCacheTTL, unmarshaled.ResponseCacheTTL) assert.Equal(t, metadata.Endpoint, unmarshaled.Endpoint) }) diff --git a/conversation/mistral/metadata.yaml b/conversation/mistral/metadata.yaml index 329379dc24..28e2e1b47f 100644 --- a/conversation/mistral/metadata.yaml +++ b/conversation/mistral/metadata.yaml @@ -24,7 +24,7 @@ metadata: - name: model required: false description: | - The Mistral LLM to use. + The Mistral LLM to use. Configurable via MISTRAL_MODEL environment variable. type: string example: 'open-mistral-7b' default: 'open-mistral-7b' diff --git a/conversation/mistral/mistral.go b/conversation/mistral/mistral.go index c319319747..09355a58b2 100644 --- a/conversation/mistral/mistral.go +++ b/conversation/mistral/mistral.go @@ -16,17 +16,17 @@ package mistral import ( "context" + "errors" "reflect" - "strings" + + "github.com/tmc/langchaingo/llms" + "github.com/tmc/langchaingo/llms/mistral" "github.com/dapr/components-contrib/conversation" "github.com/dapr/components-contrib/conversation/langchaingokit" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" kmeta "github.com/dapr/kit/metadata" - - "github.com/tmc/langchaingo/llms" - "github.com/tmc/langchaingo/llms/mistral" ) type Mistral struct { @@ -43,8 +43,6 @@ func NewMistral(logger logger.Logger) conversation.Conversation { return m } -const defaultModel = "open-mistral-7b" - func (m *Mistral) Init(ctx context.Context, meta conversation.Metadata) error { md := conversation.LangchainMetadata{} err := kmeta.DecodeMetadata(meta.Properties, &md) @@ -52,23 +50,29 @@ func (m *Mistral) Init(ctx context.Context, meta conversation.Metadata) error { return err } - model := defaultModel - if md.Model != "" { - model = md.Model + if md.Key == "" { + return errors.New("mistral api key is required") } - llm, err := mistral.New( + // Resolve model via central helper (uses metadata, then env var, then default) + model := conversation.GetMistralModel(md.Model) + options := []mistral.Option{ mistral.WithModel(model), mistral.WithAPIKey(md.Key), - ) + } + + // NOTE: Mistral has a WithTimeout option; however, it is not used or added to the mistral client so we do not add it, + // and this is another case of Mistral being an outlier for the conversation components. + + llm, err := mistral.New(options...) if err != nil { return err } m.LLM.Model = llm - if md.CacheTTL != "" { - cachedModel, cacheErr := conversation.CacheModel(ctx, md.CacheTTL, m.LLM.Model) + if md.ResponseCacheTTL != nil { + cachedModel, cacheErr := conversation.CacheResponses(ctx, md.ResponseCacheTTL, m.LLM.Model) if cacheErr != nil { return cacheErr } @@ -88,56 +92,16 @@ func (m *Mistral) Close() error { return nil } -// CreateToolCallPart creates mistral api compatible tool call messages. -// Most LLM providers can handle tool calls using the tool call object; -// however, mistral requires it as text in conversation history. +// CreateToolCallPart creates mistral and ollama api compatible tool call messages. +// This is a wrapper around langchaingokit.CreateToolCallPart for runtime compatibility. +// TODO: rm this in future PR to use the langchaingokit.CreateToolCallPart directly. func CreateToolCallPart(toolCall *llms.ToolCall) llms.ContentPart { - if toolCall == nil { - return nil - } - - if toolCall.FunctionCall == nil { - return llms.TextContent{ - Text: "Tool call [ID: " + toolCall.ID + "]: ", - } - } - - return llms.TextContent{ - Text: "Tool call [ID: " + toolCall.ID + "]: " + toolCall.FunctionCall.Name + "(" + toolCall.FunctionCall.Arguments + ")", - } + return langchaingokit.CreateToolCallPart(toolCall) } -// CreateToolResponseMessage creates mistral api compatible tool response message -// using the human role specifically otherwise mistral will reject the tool response message. -// Most LLM providers can handle tool call responses using the tool call response object; -// however, mistral requires it as text in conversation history. +// CreateToolResponseMessage creates mistral and ollama api compatible tool response message. +// This is a wrapper around langchaingokit.CreateToolResponseMessage for runtime compatibility. +// TODO: rm this in future PR to use the langchaingokit.CreateToolResponseMessage directly. func CreateToolResponseMessage(responses ...llms.ContentPart) llms.MessageContent { - msg := llms.MessageContent{ - Role: llms.ChatMessageTypeHuman, - } - if len(responses) == 0 { - return msg - } - var toolID, name string - - mistralContentParts := make([]string, 0, len(responses)) - for _, response := range responses { - if resp, ok := response.(llms.ToolCallResponse); ok { - if toolID == "" { - toolID = resp.ToolCallID - } - if name == "" { - name = resp.Name - } - mistralContentParts = append(mistralContentParts, resp.Content) - } - } - if len(mistralContentParts) > 0 { - msg.Parts = []llms.ContentPart{ - llms.TextContent{ - Text: "Tool response [ID: " + toolID + ", Name: " + name + "]: " + strings.Join(mistralContentParts, "\n"), - }, - } - } - return msg + return langchaingokit.CreateToolResponseMessage(responses...) } diff --git a/conversation/models.go b/conversation/models.go new file mode 100644 index 0000000000..3042c0fbd5 --- /dev/null +++ b/conversation/models.go @@ -0,0 +1,85 @@ +/* +Copyright 2025 The Dapr Authors +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 conversation + +import ( + "os" +) + +// Default models for conversation components +// These can be overridden via environment variables for runtime configuration +const ( + // Environment variable names + envOpenAIModel = "OPENAI_MODEL" + envAzureOpenAIModel = "AZURE_OPENAI_MODEL" + envAnthropicModel = "ANTHROPIC_MODEL" + envGoogleAIModel = "GOOGLEAI_MODEL" + envMistralModel = "MISTRAL_MODEL" + envHuggingFaceModel = "HUGGINGFACE_MODEL" + envOllamaModel = "OLLAMA_MODEL" +) + +// Exported default model constants for consumers of the conversation package. +// These are used as fallbacks when env vars and metadata are not set. +const ( + DefaultOpenAIModel = "gpt-5-nano" // Enable GPT-5 (Preview) for all clients + DefaultAzureOpenAIModel = "gpt-4.1-nano" // Default Azure OpenAI model + DefaultAnthropicModel = "claude-sonnet-4-20250514" + DefaultGoogleAIModel = "gemini-2.5-flash-lite" + DefaultMistralModel = "open-mistral-7b" + DefaultHuggingFaceModel = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" + DefaultOllamaModel = "llama3.2:latest" +) + +// getModel returns the value of an environment variable or a default value +func getModel(envVar, defaultValue, metadataValue string) string { + if value := os.Getenv(envVar); value != "" { + return value + } + if metadataValue != "" { + return metadataValue + } + return defaultValue +} + +// Example usage for model getters with metadata support: +// Pass metadataValue from your metadata file/struct, or "" if not set. +func GetOpenAIModel(metadataValue string) string { + return getModel(envOpenAIModel, DefaultOpenAIModel, metadataValue) +} + +func GetAzureOpenAIModel(metadataValue string) string { + return getModel(envAzureOpenAIModel, DefaultAzureOpenAIModel, metadataValue) +} + +func GetAnthropicModel(metadataValue string) string { + return getModel(envAnthropicModel, DefaultAnthropicModel, metadataValue) +} + +func GetGoogleAIModel(metadataValue string) string { + return getModel(envGoogleAIModel, DefaultGoogleAIModel, metadataValue) +} + +func GetMistralModel(metadataValue string) string { + return getModel(envMistralModel, DefaultMistralModel, metadataValue) +} + +func GetHuggingFaceModel(metadataValue string) string { + return getModel(envHuggingFaceModel, DefaultHuggingFaceModel, metadataValue) +} + +func GetOllamaModel(metadataValue string) string { + return getModel(envOllamaModel, DefaultOllamaModel, metadataValue) +} diff --git a/conversation/ollama/metadata.yaml b/conversation/ollama/metadata.yaml index 113c7fbcd6..3f120aa6cc 100644 --- a/conversation/ollama/metadata.yaml +++ b/conversation/ollama/metadata.yaml @@ -12,7 +12,7 @@ metadata: - name: model required: false description: | - The Ollama LLM to use. + The Ollama LLM to use. Configurable via OLLAMA_MODEL environment variable. type: string example: 'llama3.2:latest' default: 'llama3.2:latest' diff --git a/conversation/ollama/ollama.go b/conversation/ollama/ollama.go index d3f7aa0913..181144bc4b 100644 --- a/conversation/ollama/ollama.go +++ b/conversation/ollama/ollama.go @@ -18,13 +18,13 @@ import ( "context" "reflect" + "github.com/tmc/langchaingo/llms/openai" + "github.com/dapr/components-contrib/conversation" "github.com/dapr/components-contrib/conversation/langchaingokit" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" kmeta "github.com/dapr/kit/metadata" - - "github.com/tmc/langchaingo/llms/ollama" ) type Ollama struct { @@ -33,6 +33,10 @@ type Ollama struct { logger logger.Logger } +const ( + defaultEndpoint = "http://localhost:11434/v1" +) + func NewOllama(logger logger.Logger) conversation.Conversation { o := &Ollama{ logger: logger, @@ -41,8 +45,6 @@ func NewOllama(logger logger.Logger) conversation.Conversation { return o } -const defaultModel = "llama3.2:latest" - func (o *Ollama) Init(ctx context.Context, meta conversation.Metadata) error { md := conversation.LangchainMetadata{} err := kmeta.DecodeMetadata(meta.Properties, &md) @@ -50,22 +52,27 @@ func (o *Ollama) Init(ctx context.Context, meta conversation.Metadata) error { return err } - model := defaultModel - if md.Model != "" { - model = md.Model + // The key is ignored for ollama, but required by openai. + // Therefore, we set a default to prevent an err. + // ref: https://docs.ollama.com/api/openai-compatibility + if md.Key == "" { + md.Key = "ollama" + } + + if md.Endpoint == "" { + md.Endpoint = defaultEndpoint } - llm, err := ollama.New( - ollama.WithModel(model), - ) + options := conversation.BuildOpenAIClientOptions(conversation.GetOllamaModel(md.Model), md.Key, md.Endpoint) + llm, err := openai.New(options...) if err != nil { return err } o.LLM.Model = llm - if md.CacheTTL != "" { - cachedModel, cacheErr := conversation.CacheModel(ctx, md.CacheTTL, o.LLM.Model) + if md.ResponseCacheTTL != nil { + cachedModel, cacheErr := conversation.CacheResponses(ctx, md.ResponseCacheTTL, o.LLM.Model) if cacheErr != nil { return cacheErr } diff --git a/conversation/openai/metadata.go b/conversation/openai/metadata.go index d7a699f5ca..347905b9cf 100644 --- a/conversation/openai/metadata.go +++ b/conversation/openai/metadata.go @@ -15,7 +15,9 @@ limitations under the License. package openai -import "github.com/dapr/components-contrib/conversation" +import ( + "github.com/dapr/components-contrib/conversation" +) // OpenAILangchainMetadata extends LangchainMetadata with OpenAI-specific properties. type OpenAILangchainMetadata struct { diff --git a/conversation/openai/metadata.yaml b/conversation/openai/metadata.yaml index dc01eb4ea4..0fe7f04f66 100644 --- a/conversation/openai/metadata.yaml +++ b/conversation/openai/metadata.yaml @@ -24,10 +24,10 @@ metadata: - name: model required: false description: | - The OpenAI LLM to use. + The OpenAI LLM to use. Configurable via OPENAI_MODEL environment variable. type: string - example: 'gpt-4-turbo' - default: 'gpt-4o' + default: 'gpt-5-nano' + example: 'gpt-5-nano' - name: endpoint required: false description: | @@ -52,5 +52,8 @@ metadata: description: | The type of API to use for the OpenAI service. This is required when using Azure OpenAI. type: string + allowedValues: + - "open_ai" + - "azure" example: 'azure' - default: '' \ No newline at end of file + default: 'open_ai' \ No newline at end of file diff --git a/conversation/openai/metadata_test.go b/conversation/openai/metadata_test.go index e7a5b4b211..ba786e53b1 100644 --- a/conversation/openai/metadata_test.go +++ b/conversation/openai/metadata_test.go @@ -16,6 +16,7 @@ package openai import ( "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,12 +26,13 @@ import ( func TestOpenaiLangchainMetadata(t *testing.T) { t.Run("json marshaling with endpoint", func(t *testing.T) { + ttl := 10 * time.Minute metadata := OpenAILangchainMetadata{ LangchainMetadata: conversation.LangchainMetadata{ - Key: "test-key", - Model: "gpt-4", - CacheTTL: "10m", - Endpoint: "https://custom-endpoint.openai.azure.com/", + Key: "test-key", + Model: "gpt-4", + ResponseCacheTTL: &ttl, + Endpoint: "https://custom-endpoint.openai.azure.com/", }, APIType: "azure", APIVersion: "2025-01-01-preview", @@ -45,7 +47,7 @@ func TestOpenaiLangchainMetadata(t *testing.T) { assert.Equal(t, metadata.Key, unmarshaled.Key) assert.Equal(t, metadata.Model, unmarshaled.Model) - assert.Equal(t, metadata.CacheTTL, unmarshaled.CacheTTL) + assert.Equal(t, metadata.ResponseCacheTTL, unmarshaled.ResponseCacheTTL) assert.Equal(t, metadata.Endpoint, unmarshaled.Endpoint) assert.Equal(t, metadata.APIType, unmarshaled.APIType) assert.Equal(t, metadata.APIVersion, unmarshaled.APIVersion) diff --git a/conversation/openai/openai.go b/conversation/openai/openai.go index 9dfea30310..ff49c7455d 100644 --- a/conversation/openai/openai.go +++ b/conversation/openai/openai.go @@ -18,6 +18,7 @@ import ( "context" "errors" "reflect" + "strings" "github.com/dapr/components-contrib/conversation" "github.com/dapr/components-contrib/conversation/langchaingokit" @@ -32,6 +33,7 @@ type OpenAI struct { langchaingokit.LLM logger logger.Logger + md OpenAILangchainMetadata } func NewOpenAI(logger logger.Logger) conversation.Conversation { @@ -42,7 +44,39 @@ func NewOpenAI(logger logger.Logger) conversation.Conversation { return o } -const defaultModel = "gpt-4o" +func (o *OpenAI) buildClientOptions(md OpenAILangchainMetadata) ([]openai.Option, error) { + // Resolve model via central helper (uses metadata, then env var, then default) + var model string + // we support lowercase and uppercase here + if strings.EqualFold(md.APIType, "azure") { + model = conversation.GetAzureOpenAIModel(md.Model) + } else { + model = conversation.GetOpenAIModel(md.Model) + } + options := conversation.BuildOpenAIClientOptions(model, md.Key, md.Endpoint) + + // apply options specifically for azure openai + // TODO: in future, there is also an openai.APITypeAzureAD that we can add. + if strings.EqualFold(md.APIType, "azure") { + if md.Endpoint == "" || md.APIVersion == "" { + return nil, errors.New("endpoint and apiVersion must be provided when apiType is set to 'azure'") + } + options = append(options, + openai.WithAPIType(openai.APITypeAzure), + openai.WithAPIVersion(md.APIVersion), + + // apparently this is required for azure openai (but not for openai) + // https://github.com/tmc/langchaingo/blob/509308ff01c13e662d5613d3aea793fabe18edd2/llms/openai/openaillm_option.go#L78 + openai.WithEmbeddingModel(md.Model), + ) + + // NOTE: This is also an option here. + // https://github.com/tmc/langchaingo/blob/509308ff01c13e662d5613d3aea793fabe18edd2/llms/openai/openaillm_option.go#L89 + // openai.WithEmbeddingDimentions(), + } + + return options, nil +} func (o *OpenAI) Init(ctx context.Context, meta conversation.Metadata) error { md := OpenAILangchainMetadata{} @@ -50,28 +84,11 @@ func (o *OpenAI) Init(ctx context.Context, meta conversation.Metadata) error { if err != nil { return err } + o.md = md - model := defaultModel - if md.Model != "" { - model = md.Model - } - // Create options for OpenAI client - options := []openai.Option{ - openai.WithModel(model), - openai.WithToken(md.Key), - } - - // Add custom endpoint if provided - if md.Endpoint != "" { - options = append(options, openai.WithBaseURL(md.Endpoint)) - } - - if md.APIType == "azure" { - if md.Endpoint == "" || md.APIVersion == "" { - return errors.New("endpoint and apiVersion must be provided when apiType is set to 'azure'") - } - - options = append(options, openai.WithAPIType(openai.APITypeAzure), openai.WithAPIVersion(md.APIVersion)) + options, err := o.buildClientOptions(md) + if err != nil { + return err } llm, err := openai.New(options...) @@ -81,14 +98,15 @@ func (o *OpenAI) Init(ctx context.Context, meta conversation.Metadata) error { o.LLM.Model = llm - if md.CacheTTL != "" { - cachedModel, cacheErr := conversation.CacheModel(ctx, md.CacheTTL, o.LLM.Model) + if md.ResponseCacheTTL != nil { + cachedModel, cacheErr := conversation.CacheResponses(ctx, md.ResponseCacheTTL, o.LLM.Model) if cacheErr != nil { return cacheErr } o.LLM.Model = cachedModel } + return nil } diff --git a/conversation/openai/openai_test.go b/conversation/openai/openai_test.go index 1e9a7102ec..97cfd5715b 100644 --- a/conversation/openai/openai_test.go +++ b/conversation/openai/openai_test.go @@ -34,7 +34,7 @@ func TestInit(t *testing.T) { name: "with default endpoint", metadata: map[string]string{ "key": "test-key", - "model": "gpt-4", + "model": conversation.DefaultOpenAIModel, }, testFn: func(t *testing.T, o *OpenAI, err error) { require.NoError(t, err) @@ -45,7 +45,7 @@ func TestInit(t *testing.T) { name: "with custom endpoint", metadata: map[string]string{ "key": "test-key", - "model": "gpt-4", + "model": conversation.DefaultOpenAIModel, "endpoint": "https://api.openai.com/v1", }, testFn: func(t *testing.T, o *OpenAI, err error) { @@ -59,7 +59,7 @@ func TestInit(t *testing.T) { name: "with apiType azure and missing apiVersion", metadata: map[string]string{ "key": "test-key", - "model": "gpt-4", + "model": conversation.DefaultOpenAIModel, "apiType": "azure", "endpoint": "https://custom-endpoint.openai.azure.com/", }, @@ -72,7 +72,7 @@ func TestInit(t *testing.T) { name: "with apiType azure and custom apiVersion", metadata: map[string]string{ "key": "test-key", - "model": "gpt-4", + "model": conversation.DefaultOpenAIModel, "apiType": "azure", "endpoint": "https://custom-endpoint.openai.azure.com/", "apiVersion": "2025-01-01-preview", @@ -86,7 +86,7 @@ func TestInit(t *testing.T) { name: "with apiType azure but missing endpoint", metadata: map[string]string{ "key": "test-key", - "model": "gpt-4", + "model": conversation.DefaultOpenAIModel, "apiType": "azure", "apiVersion": "2025-01-01-preview", }, @@ -128,7 +128,6 @@ func TestEndpointInMetadata(t *testing.T) { return keys }()) - // Verify endpoint field exists (note: field names are capitalized in metadata) - _, exists := md["Endpoint"] - assert.True(t, exists, "Endpoint field should exist in metadata") + _, exists := md["endpoint"] + assert.True(t, exists, "endpoint field should exist in metadata") } diff --git a/conversation/opts.go b/conversation/opts.go index c8c0d5db21..7d9a0f9948 100644 --- a/conversation/opts.go +++ b/conversation/opts.go @@ -17,24 +17,61 @@ package conversation import ( "context" "fmt" + "net/http" "time" + "github.com/tmc/langchaingo/httputil" "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms/cache" "github.com/tmc/langchaingo/llms/cache/inmemory" + "github.com/tmc/langchaingo/llms/openai" ) -// CacheModel creates a prompt query cache with a configured TTL -func CacheModel(ctx context.Context, ttl string, model llms.Model) (llms.Model, error) { - d, err := time.ParseDuration(ttl) - if err != nil { - return model, fmt.Errorf("failed to parse cacheTTL duration: %s", err) +// BuildOpenAIClientOptions is a helper function that is used by conversation components that use the OpenAI client under the hood. +// HTTP client timeout is set from resiliency policy configuration. +func BuildOpenAIClientOptions(model, key, endpoint string) []openai.Option { + options := []openai.Option{ + openai.WithModel(model), + openai.WithToken(key), + } + + if endpoint != "" { + options = append(options, openai.WithBaseURL(endpoint)) } - mem, err := inmemory.New(ctx, inmemory.WithExpiration(d)) + if httpClient := BuildHTTPClient(); httpClient != nil { + options = append(options, openai.WithHTTPClient(httpClient)) + } + + return options +} + +// CacheResponses creates a response cache with a configured TTL. +// This caches the final LLM responses (outputs) based on the input messages and call options. +// When the same prompt with the same options is requested, the cached response is returned +// without making an API call to the LLM provider, reducing latency and cost. +func CacheResponses(ctx context.Context, ttl *time.Duration, model llms.Model) (llms.Model, error) { + mem, err := inmemory.New(ctx, inmemory.WithExpiration(*ttl)) if err != nil { return model, fmt.Errorf("failed to create llm cache: %s", err) } return cache.New(model, mem), nil } + +// BuildHTTPClient creates an HTTP client with timeout set to 0 to rely on context deadlines. +// The context deadline will be respected via http.NewRequestWithContext within Langchain. +// This allows resiliency policy timeouts from runtime to propagate through to the HTTP client for the LLM provider. +func BuildHTTPClient() *http.Client { + httpClient := &http.Client{ + // wrap with httputil.Transport to preserve user-agent + Transport: &httputil.Transport{ + Transport: http.DefaultTransport, + }, + // Timeout is set to 0 to rely on context deadlines set by any configured resiliency policies + // The context deadline will be respected via http.NewRequestWithContext in Langchain. + Timeout: 0, + } + + return httpClient +} diff --git a/conversation/usage.go b/conversation/usage.go new file mode 100644 index 0000000000..9719488c88 --- /dev/null +++ b/conversation/usage.go @@ -0,0 +1,37 @@ +/* +Copyright 2025 The Dapr Authors +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 conversation + +// Usage represents token usage statistics for a completion request +type Usage struct { + CompletionTokens uint64 `json:"completionTokens"` + PromptTokens uint64 `json:"promptTokens"` + TotalTokens uint64 `json:"totalTokens"` + CompletionTokensDetails *CompletionTokensDetails `json:"completionTokensDetails,omitempty"` + PromptTokensDetails *PromptTokensDetails `json:"promptTokensDetails,omitempty"` +} + +// CompletionTokensDetails provides breakdown of completion tokens +type CompletionTokensDetails struct { + AcceptedPredictionTokens uint64 `json:"acceptedPredictionTokens"` + AudioTokens uint64 `json:"audioTokens"` + ReasoningTokens uint64 `json:"reasoningTokens"` + RejectedPredictionTokens uint64 `json:"rejectedPredictionTokens"` +} + +// PromptTokensDetails provides breakdown of prompt tokens +type PromptTokensDetails struct { + AudioTokens uint64 `json:"audioTokens"` + CachedTokens uint64 `json:"cachedTokens"` +} diff --git a/crypto/azure/keyvault/metadata.yaml b/crypto/azure/keyvault/metadata.yaml index 703e3afcd3..6cd91bea8d 100644 --- a/crypto/azure/keyvault/metadata.yaml +++ b/crypto/azure/keyvault/metadata.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../../../component-metadata-schema.json schemaVersion: v1 -type: cryptography +type: crypto name: azure.keyvault version: v1 status: alpha diff --git a/crypto/jwks/metadata.yaml b/crypto/jwks/metadata.yaml index c0975723ff..2713e4a3f0 100644 --- a/crypto/jwks/metadata.yaml +++ b/crypto/jwks/metadata.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../../../component-metadata-schema.json schemaVersion: v1 -type: cryptography +type: crypto name: jwks version: v1 status: alpha diff --git a/crypto/kubernetes/secrets/metadata.yaml b/crypto/kubernetes/secrets/metadata.yaml index e0ebe11b5f..eac54ec6d7 100644 --- a/crypto/kubernetes/secrets/metadata.yaml +++ b/crypto/kubernetes/secrets/metadata.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../../../component-metadata-schema.json schemaVersion: v1 -type: cryptography +type: crypto name: kubernetes.secrets version: v1 status: alpha diff --git a/crypto/localstorage/metadata.yaml b/crypto/localstorage/metadata.yaml index 1a037f543a..8e409dd45d 100644 --- a/crypto/localstorage/metadata.yaml +++ b/crypto/localstorage/metadata.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../../../component-metadata-schema.json schemaVersion: v1 -type: cryptography +type: crypto name: localstorage version: v1 status: alpha diff --git a/crypto/pubkey_cache_test.go b/crypto/pubkey_cache_test.go index da40044dc8..738d17eadd 100644 --- a/crypto/pubkey_cache_test.go +++ b/crypto/pubkey_cache_test.go @@ -211,14 +211,6 @@ func TestPubKeyCacheGetKey(t *testing.T) { return cache.pubKeys["key"].ctx.Size() == 2 }, time.Second*5, time.Millisecond) - t.Cleanup(func() { - select { - case <-getKeyReturned: - case <-time.After(1 * time.Second): - assert.Fail(t, "expected GetKey to return from cancelled context in time") - } - }) - assert.Equal(t, "key", i) cancel1(assert.AnError) select { @@ -240,6 +232,16 @@ func TestPubKeyCacheGetKey(t *testing.T) { result, err := cache.GetKey(ctx1, "key") assert.Equal(t, context.Canceled, err) assert.Nil(t, result) + + // Wait for the background goroutine to finish before the test + // returns, otherwise t.Context() (used by ctx2) gets cancelled + // when the test function exits, causing a race where ctx2's + // GetKey sees a cancelled context instead of the resolved value. + select { + case <-getKeyReturned: + case <-time.After(5 * time.Second): + assert.Fail(t, "expected GetKey to return from cancelled context in time") + } }) t.Run("if all callers give cancelled contexts, the underlying context should also be cancelled", func(t *testing.T) { diff --git a/docs/developing-component.md b/docs/developing-component.md index baf3670cc2..46f113114d 100644 --- a/docs/developing-component.md +++ b/docs/developing-component.md @@ -51,6 +51,22 @@ make test ```bash make lint ``` +> ⚠️ **Warning**: You may run into version incompatibilities between the installed Go version and the golangci-lint version and see the following error: +> ``` +> [!] Your locally installed version of golangci-lint is different from the pipeline +> [!] This will likely cause linting issues for you locally +> [!] Yours: v1.64.8 +> [!] Theirs: v1.64.6 +> [!] Upgrade: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /bin v1.64.6 +> golangci-lint run --timeout=20m --max-same-issues 0 --max-issues-per-linter 0 +> Error: can't load config: the Go language version (go1.23) used to build golangci-lint is lower than the targeted Go version (1.24.1) +> Failed executing command with error: can't load config: the Go language version (go1.23) used to build golangci-lint is lower than the targeted Go version (1.24.1) +>? make: *** [lint] Error 3 +> ``` +> In this case, make sure to install the exact versions of go requested and the version of golangci-lint using the command: +> ``` +> go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.6 +> ``` ## Validating with Dapr core diff --git a/go.mod b/go.mod index 9d61f17c01..8a5c424cf6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/dapr/components-contrib -go 1.24.4 +go 1.24.13 require ( cloud.google.com/go/datastore v1.20.0 @@ -26,49 +26,58 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/IBM/sarama v1.45.2 github.com/aerospike/aerospike-client-go/v6 v6.12.0 + github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 + github.com/akeylesslabs/akeyless-go/v5 v5.0.16 github.com/alibaba/sentinel-golang v1.0.4 github.com/alibabacloud-go/darabonba-openapi v0.2.1 github.com/alibabacloud-go/oos-20190601 v1.0.4 github.com/alibabacloud-go/tea v1.2.1 github.com/alibabacloud-go/tea-utils v1.4.5 - github.com/alicebob/miniredis/v2 v2.30.5 + github.com/alicebob/miniredis/v2 v2.36.1 github.com/aliyun/aliyun-log-go-sdk v0.1.54 github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible github.com/aliyun/aliyun-tablestore-go-sdk v1.7.10 github.com/apache/dubbo-go-hessian2 v1.11.5 - github.com/apache/pulsar-client-go v0.14.0 + github.com/apache/pulsar-client-go v0.18.0 github.com/apache/rocketmq-client-go/v2 v2.1.2-0.20230412142645-25003f6f083d - github.com/apache/thrift v0.13.0 github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1-0.20241125194140-078c08b8574a - github.com/aws/aws-sdk-go v1.55.6 - github.com/aws/aws-sdk-go-v2 v1.36.5 - github.com/aws/aws-sdk-go-v2/config v1.29.17 - github.com/aws/aws-sdk-go-v2/credentials v1.17.70 + github.com/aws/aws-sdk-go-v2 v1.41.1 + github.com/aws/aws-sdk-go-v2/config v1.32.9 + github.com/aws/aws-sdk-go-v2/credentials v1.19.9 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3 github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.3.10 - github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.17.3 + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.4 + github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.24.3 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4 + github.com/aws/aws-sdk-go-v2/service/kinesis v1.42.10 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8 + github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.21 + github.com/aws/aws-sdk-go-v2/service/ses v1.34.18 github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 - github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 - github.com/aws/rolesanywhere-credential-helper v1.0.4 + github.com/aws/aws-sdk-go-v2/service/ssm v1.60.2 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 + github.com/aws/smithy-go v1.24.0 github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 - github.com/camunda/zeebe/clients/go/v8 v8.2.12 + github.com/camunda/zeebe/clients/go/v8 v8.5.25 github.com/cenkalti/backoff/v4 v4.3.0 github.com/chebyrash/promise v0.0.0-20230709133807-42ec49ba1459 github.com/cinience/go_rocketmq v0.0.2 - github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0 + github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2 github.com/cloudevents/sdk-go/v2 v2.15.2 - github.com/cloudwego/kitex v0.5.0 + github.com/cloudwego/gopkg v0.1.8 + github.com/cloudwego/kitex v0.15.4 github.com/cloudwego/kitex-examples v0.1.1 - github.com/cyphar/filepath-securejoin v0.2.4 + github.com/cyphar/filepath-securejoin v0.6.1 github.com/dancannon/gorethink v4.0.0+incompatible - github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 + github.com/dapr/components-contrib/tests/certification v0.0.0-20260219105038-d0cf89ba8acf + github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d github.com/didip/tollbooth/v7 v7.0.1 - github.com/eclipse/paho.mqtt.golang v1.4.3 + github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/fasthttp-contrib/sessions v0.0.0-20160905201309-74f6ac73d5d5 github.com/go-redis/redis/v8 v8.11.5 - github.com/go-sql-driver/mysql v1.7.1 + github.com/go-sql-driver/mysql v1.8.1 github.com/go-zookeeper/zk v1.0.3 github.com/gocql/gocql v1.5.2 github.com/golang/mock v1.6.0 @@ -76,11 +85,10 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 github.com/gorilla/mux v1.8.1 github.com/grandcat/zeroconf v1.0.0 - github.com/hamba/avro/v2 v2.28.0 github.com/hashicorp/consul/api v1.25.1 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hazelcast/hazelcast-go-client v0.0.0-20190530123621-6cf767c2f31a - github.com/http-wasm/http-wasm-host-go v0.6.0 + github.com/http-wasm/http-wasm-host-go v0.7.0 github.com/huaweicloud/huaweicloud-sdk-go-obs v3.23.4+incompatible github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.56 github.com/influxdata/influxdb-client-go/v2 v2.12.3 @@ -91,91 +99,91 @@ require ( github.com/labd/commercetools-go-sdk v1.3.1 github.com/lestrrat-go/httprc v1.0.5 github.com/lestrrat-go/jwx/v2 v2.0.21 - github.com/linkedin/goavro/v2 v2.12.0 + github.com/linkedin/goavro/v2 v2.14.1 github.com/machinebox/graphql v0.2.2 github.com/matoous/go-nanoid/v2 v2.0.0 github.com/microsoft/go-mssqldb v1.6.0 github.com/mikeee/aws_credential_helper v0.0.1-alpha.2 github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 github.com/mrz1836/postmark v1.6.1 - github.com/nats-io/nats-server/v2 v2.9.23 - github.com/nats-io/nats.go v1.31.0 - github.com/nats-io/nkeys v0.4.6 + github.com/nats-io/nats-server/v2 v2.10.27 + github.com/nats-io/nats.go v1.39.1 + github.com/nats-io/nkeys v0.4.10 github.com/open-policy-agent/opa v1.4.2 github.com/oracle/coherence-go-client/v2 v2.2.0 github.com/oracle/oci-go-sdk/v54 v54.0.0 github.com/pashagolub/pgxmock/v2 v2.12.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/sftp v1.13.7 - github.com/puzpuzpuz/xsync/v3 v3.0.0 - github.com/rabbitmq/amqp091-go v1.9.0 + github.com/puzpuzpuz/xsync/v3 v3.5.1 + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427 github.com/redis/go-redis/v9 v9.6.3 - github.com/riferrei/srclient v0.6.0 + github.com/riferrei/srclient v0.7.3 github.com/sendgrid/sendgrid-go v3.13.0+incompatible github.com/sijms/go-ora/v2 v2.8.22 github.com/spf13/cast v1.8.0 - github.com/spiffe/go-spiffe/v2 v2.5.0 + github.com/spiffe/go-spiffe/v2 v2.6.0 github.com/stealthrocket/wasi-go v0.8.1-0.20230912180546-8efbab50fb58 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/supplyon/gremcos v0.1.40 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.732 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssm v1.0.732 - github.com/tetratelabs/wazero v1.7.0 - github.com/tmc/langchaingo v0.1.13 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/tetratelabs/wazero v1.8.0 + github.com/tmc/langchaingo v0.1.15-0.20251029190607-e35755df7084 github.com/valyala/fasthttp v1.53.0 - github.com/vmware/vmware-go-kcl v1.5.1 + github.com/vmware/vmware-go-kcl-v2 v1.0.0 github.com/xdg-go/scram v1.1.2 go.etcd.io/etcd/client/v3 v3.5.21 go.mongodb.org/mongo-driver v1.14.0 go.uber.org/goleak v1.3.0 go.uber.org/multierr v1.11.0 go.uber.org/ratelimit v0.3.0 - golang.org/x/crypto v0.39.0 - golang.org/x/mod v0.25.0 - golang.org/x/net v0.41.0 - golang.org/x/oauth2 v0.30.0 + golang.org/x/crypto v0.47.0 + golang.org/x/mod v0.31.0 + golang.org/x/net v0.49.0 + golang.org/x/oauth2 v0.34.0 google.golang.org/api v0.231.0 - google.golang.org/grpc v1.73.0 - google.golang.org/protobuf v1.36.6 + google.golang.org/grpc v1.78.0 + google.golang.org/protobuf v1.36.11 gopkg.in/couchbase/gocb.v1 v1.6.7 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.31.0 + k8s.io/api v0.32.3 k8s.io/apiextensions-apiserver v0.31.0 k8s.io/apimachinery v0.33.0 - k8s.io/client-go v0.31.0 + k8s.io/client-go v0.32.3 k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 modernc.org/sqlite v1.34.5 sigs.k8s.io/yaml v1.4.0 ) require ( - cel.dev/expr v0.23.0 // indirect + cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.120.0 // indirect - cloud.google.com/go/ai v0.7.0 // indirect - cloud.google.com/go/aiplatform v1.86.0 // indirect cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect - cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect - cloud.google.com/go/vertexai v0.12.0 // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect - github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect - github.com/99designs/keyring v1.2.1 // indirect - github.com/AthenZ/athenz v1.10.39 // indirect + dario.cat/mergo v1.0.2 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/AthenZ/athenz v1.12.13 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/Code-Hex/go-generics-cache v1.3.1 // indirect github.com/DataDog/zstd v1.5.2 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect - github.com/Microsoft/hcsshim v0.11.7 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RoaringBitmap/roaring v1.1.0 // indirect + github.com/RoaringBitmap/roaring/v2 v2.8.0 // indirect github.com/Workiva/go-datastructures v1.0.53 // indirect github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect @@ -185,80 +193,98 @@ require ( github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect github.com/alibabacloud-go/openapi-util v0.0.11 // indirect github.com/alibabacloud-go/tea-xml v1.1.2 // indirect - github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/aliyun/credentials-go v1.1.2 // indirect github.com/aliyunmq/mq-http-go-sdk v1.0.3 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/apache/dubbo-getty v1.4.9-0.20220610060150-8af010f3f3dc // indirect github.com/apache/rocketmq-client-go v1.2.5 // indirect + github.com/apache/thrift v0.13.0 // indirect github.com/ardielle/ardielle-go v1.5.2 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go v1.55.6 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect - github.com/aws/smithy-go v1.22.5 // indirect - github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect + github.com/awslabs/kinesis-aggregation/go/v2 v2.0.0-20211222152315-953b66f67407 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.4.0 // indirect + github.com/bits-and-blooms/bitset v1.12.0 // indirect github.com/bufbuild/protocompile v0.6.0 // indirect - github.com/bytedance/gopkg v0.0.0-20240711085056-a03554c296f8 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/choleraehyq/pid v0.0.20 // indirect github.com/clbanning/mxj/v2 v2.5.6 // indirect - github.com/cloudwego/fastpb v0.0.4-0.20230131074846-6fc453d58b96 // indirect - github.com/cloudwego/frugal v0.2.0 // indirect - github.com/cloudwego/gopkg v0.1.0 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/cloudwego/netpoll v0.3.2 // indirect - github.com/cloudwego/thriftgo v0.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cloudwego/configmanager v0.2.3 // indirect + github.com/cloudwego/dynamicgo v0.7.1 // indirect + github.com/cloudwego/fastpb v0.0.5 // indirect + github.com/cloudwego/frugal v0.3.0 // indirect + github.com/cloudwego/kitex/pkg/protocol/bthrift v0.0.0-20260112072316-5cf426cf9e1b // indirect + github.com/cloudwego/localsession v0.2.1 // indirect + github.com/cloudwego/netpoll v0.7.2 // indirect + github.com/cloudwego/runtimex v0.1.1 // indirect + github.com/cloudwego/thriftgo v0.4.3 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/creasty/defaults v1.5.2 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/deepmap/oapi-codegen v1.11.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dubbogo/gost v1.13.1 // indirect github.com/dubbogo/triple v1.1.8 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/eapache/go-resiliency v1.7.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect github.com/eapache/queue v1.1.0 // indirect + github.com/ebitengine/purego v0.9.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/fatih/color v1.17.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gage-technologies/mistral-go v1.1.0 // indirect github.com/gavv/httpexpect v2.0.0+incompatible // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/go-pkgz/expirable-cache v0.1.0 // indirect @@ -266,9 +292,9 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.0 // indirect github.com/go-resty/resty/v2 v2.11.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogap/errors v0.0.0-20200228125012-531a6449b28c // indirect github.com/gogap/stack v0.0.0-20150131034635-fef68dddd4f8 // indirect @@ -276,21 +302,20 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/google/generative-ai-go v0.15.1 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/s2a-go v0.1.9 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/hamba/avro/v2 v2.29.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect @@ -301,7 +326,7 @@ require ( github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect - github.com/imdario/mergo v0.3.16 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -319,7 +344,8 @@ require ( github.com/k0kubun/pp v3.0.1+incompatible // indirect github.com/kataras/go-errors v0.0.3 // indirect github.com/kataras/go-serializer v0.0.4 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/knadh/koanf v1.4.1 // indirect github.com/kr/fs v0.1.0 // indirect github.com/kubemq-io/protobuf v1.3.1 // indirect @@ -329,33 +355,40 @@ require ( github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matryer/is v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/miekg/dns v1.1.57 // indirect - github.com/minio/highwayhash v1.0.2 // indirect + github.com/minio/highwayhash v1.0.3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.0 // indirect + github.com/morikuni/aec v1.1.0 // indirect github.com/moul/http2curl v1.0.0 // indirect github.com/mschoch/smat v0.2.0 // indirect - github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect - github.com/nats-io/jwt/v2 v2.5.0 // indirect + github.com/nats-io/jwt/v2 v2.7.3 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/panjf2000/ants/v2 v2.8.1 // indirect + github.com/panjf2000/ants/v2 v2.11.3 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect @@ -365,7 +398,7 @@ require ( github.com/pkoukk/tiktoken-go v0.1.6 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.64.0 // indirect @@ -374,14 +407,14 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rs/zerolog v1.31.0 // indirect - github.com/russross/blackfriday v1.6.0 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect - github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shirou/gopsutil/v4 v4.25.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect @@ -389,12 +422,13 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tchap/go-patricia/v2 v2.3.2 // indirect - github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/gjson v1.17.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tjfoc/gmsm v1.3.2 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -407,41 +441,39 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect - github.com/yuin/gopher-lua v1.1.0 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect - github.com/zeebo/errs v1.4.0 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.etcd.io/etcd/api/v3 v3.5.21 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.opentelemetry.io/proto/otlp v1.6.0 // indirect - go.uber.org/atomic v1.10.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/arch v0.10.0 // indirect + golang.org/x/arch v0.14.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // 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 + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/tools v0.40.0 // indirect google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/couchbase/gocbcore.v7 v7.1.18 // indirect gopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4 // indirect gopkg.in/couchbaselabs/gojcbmock.v1 v1.0.4 // indirect gopkg.in/couchbaselabs/jsonx.v1 v1.0.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v4 v4.1.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index fb3f11c723..2a144e520e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= -cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 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= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -15,12 +15,13 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= -cloud.google.com/go/ai v0.7.0 h1:P6+b5p4gXlza5E+u7uvcgYlzZ7103ACg70YdZeC6oGE= -cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo= -cloud.google.com/go/aiplatform v1.86.0 h1:b8FVN8Jv4R0c1qMzqzURiJYXLp9R6Wx7d0q4MPGlTeM= -cloud.google.com/go/aiplatform v1.86.0/go.mod h1:xp3wFix8imliXkVpgMRkjnreJYTaNzLF44GOrnIENto= cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -31,8 +32,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= @@ -65,30 +66,29 @@ cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6Q cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= -cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8= -cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8= contrib.go.opencensus.io/exporter/prometheus v0.4.1/go.mod h1:t9wvfitlUjGXG2IXAZsuFq26mDGid/JwCEXp+gTG/9U= contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/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= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dubbo.apache.org/dubbo-go/v3 v3.0.3-0.20230118042253-4f159a2b38f3 h1:j08GKvXilDMHuVuGy+X0CMTL+Wxrte5a4XrWGDypZf0= dubbo.apache.org/dubbo-go/v3 v3.0.3-0.20230118042253-4f159a2b38f3/go.mod h1:bxe6StRQ4PVbZa+B5nsREuez4agzmWiELS9NhEoDscI= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= -github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= -github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= -github.com/AthenZ/athenz v1.10.39 h1:mtwHTF/v62ewY2Z5KWhuZgVXftBej1/Tn80zx4DcawY= -github.com/AthenZ/athenz v1.10.39/go.mod h1:3Tg8HLsiQZp81BJY58JBeU2BR6B/H4/0MQGfCwhHNEA= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AthenZ/athenz v1.12.13 h1:OhZNqZsoBXNrKBJobeUUEirPDnwt0HRo4kQMIO1UwwQ= +github.com/AthenZ/athenz v1.12.13/go.mod h1:XXDXXgaQzXaBXnJX6x/bH4yF6eon2lkyzQZ0z/dxprE= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.6.0 h1:FQOmDxJj1If0D0khZR00MDa2Eb+k9BBsSaK7cEbLwkk= github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.6.0/go.mod h1:X0+PSrHOZdTjkiEhgv53HS5gplbzVVl2jd6hQRYSS3c= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0 h1:AdaGDU3FgoUC2tsd3vsd9JblRrpFLUsS38yh1eLYfwM= @@ -97,6 +97,8 @@ github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.0.3 h1:gBWC0dYF3aO+7xGxL0 github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.0.3/go.mod h1:7LBWaO4KRASAo9VpfhpxQKkdY6PBwkv9UDKzL9Sajuw= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.2.0 h1:aJG+Jxd9/rrLwf8R1Ko0RlOBTJASs/lGQJ8b9AdlKTc= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.2.0/go.mod h1:41ONblJrPxDcnVr+voS+3xXWy/KnZLh+7zY5s6woAlQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.2.1 h1:0f6XnzroY1yCQQwxGf/n/2xlaBF02Qhof2as99dGNsY= @@ -125,8 +127,9 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.0 h1:lJwNFV+xYjHREUTH github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.0/go.mod h1:GfT0aGew8Qj5yiQVqOO5v7N8fanbJGyUoHqXg56qcVY= github.com/Azure/go-amqp v1.0.5 h1:po5+ljlcNSU8xtapHTe8gIc8yHxCzC03E8afH2g1ftU= github.com/Azure/go-amqp v1.0.5/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -140,8 +143,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0 h1:nNMpRpnkWDAaqcpxMJvxa/Ud98gjbYwayJY4/9bdjiU= @@ -154,13 +157,13 @@ github.com/IBM/sarama v1.45.2/go.mod h1:ppaoTcVdGv186/z6MEKsMm70A5fwJfRTpstI37kV github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= -github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d h1:wvStE9wLpws31NiWUx+38wny1msZ/tm+eL5xmm4Y7So= github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/RoaringBitmap/roaring v1.1.0 h1:b10lZrZXaY6Q6EKIRrmOF519FIyQQ5anPgGr3niw2yY= github.com/RoaringBitmap/roaring v1.1.0/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= +github.com/RoaringBitmap/roaring/v2 v2.8.0 h1:y1rdtixfXvaITKzkfiKvScI0hlBJHe9sfzJp8cgeM7w= +github.com/RoaringBitmap/roaring/v2 v2.8.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= @@ -177,10 +180,11 @@ github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KO github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 h1:ly0WKARATneFzwBlTZ2lUyjtLqoOEYqt1vOlf89za/4= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5/go.mod h1:W6DMNwPyIE3jpXDaJOvCKUT/kHPZrpl/BGiIVUILbMk= +github.com/akeylesslabs/akeyless-go/v5 v5.0.16 h1:nH0ExvPnfWMhHL3DovUQBXST/2Dj02KJxIHFYMqRauo= +github.com/akeylesslabs/akeyless-go/v5 v5.0.16/go.mod h1:4oo5+/uOcshVr/+hLxxL4UQIALyQNWwOCskLGgTL6nk= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -217,10 +221,8 @@ github.com/alibabacloud-go/tea-utils v1.4.5 h1:h0/6Xd2f3bPE4XHTvkpjwxowIwRCJAJOq github.com/alibabacloud-go/tea-utils v1.4.5/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= github.com/alibabacloud-go/tea-xml v1.1.2 h1:oLxa7JUXm2EDFzMg+7oRsYc+kutgCVwm+bZlhhmvW5M= github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.30.5 h1:3r6kTHdKnuP4fkS8k2IrvSfxpxUTcW1SOL0wN7b7Dt0= -github.com/alicebob/miniredis/v2 v2.30.5/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg= +github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI= +github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= github.com/aliyun/aliyun-log-go-sdk v0.1.54 h1:ejQygZTGBqTs4V9qQUunWYtFwyKUWXYryfgrX9OhOlg= @@ -244,8 +246,8 @@ github.com/apache/dubbo-go-hessian2 v1.9.3/go.mod h1:xQUjE7F8PX49nm80kChFvepA/Av github.com/apache/dubbo-go-hessian2 v1.11.0/go.mod h1:7rEw9guWABQa6Aqb8HeZcsYPHsOS7XT1qtJvkmI6c5w= github.com/apache/dubbo-go-hessian2 v1.11.5 h1:rcK22+yMw2Hejm6GRG7WrdZ0DinW2QMZc01c7YVZjcQ= github.com/apache/dubbo-go-hessian2 v1.11.5/go.mod h1:QP9Tc0w/B/mDopjusebo/c7GgEfl6Lz8jeuFg8JA6yw= -github.com/apache/pulsar-client-go v0.14.0 h1:P7yfAQhQ52OCAu8yVmtdbNQ81vV8bF54S2MLmCPJC9w= -github.com/apache/pulsar-client-go v0.14.0/go.mod h1:PNUE29x9G1EHMvm41Bs2vcqwgv7N8AEjeej+nEVYbX8= +github.com/apache/pulsar-client-go v0.18.0 h1:YsySoOds7WCXkRcOKHb85gk/v1Jndp+2oCkkRQEowUA= +github.com/apache/pulsar-client-go v0.18.0/go.mod h1:GKmTD1u5YLuhUnoVTNGdhdGNAYhoglWNWgwLJZTljAw= github.com/apache/rocketmq-client-go v1.2.5 h1:2hPoLHpMJy1a57HDNmx7PZKgvlgVYO1Alz925oeqphQ= github.com/apache/rocketmq-client-go v1.2.5/go.mod h1:Kap8oXIVLlHF50BGUbN9z97QUp1GaK1nOoCfsZnR2bw= github.com/apache/rocketmq-client-go/v2 v2.1.0/go.mod h1:oEZKFDvS7sz/RWU0839+dQBupazyBV7WX5cP6nrio0Q= @@ -259,7 +261,6 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/ardielle/ardielle-go v1.5.2 h1:TilHTpHIQJ27R1Tl/iITBzMwiUGSlVfiVhwDNGM3Zj4= github.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI= -github.com/ardielle/ardielle-tools v1.5.4/go.mod h1:oZN+JRMnqGiIhrzkRN9l26Cej9dEx4jeNG6A+AdkShk= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= @@ -274,70 +275,91 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1-0.20241125194140-078c08b8574a h1:QFemvMGPnajaeRBkFc1HoEA7qzVjUv+rkYb1/ps1/UE= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1-0.20241125194140-078c08b8574a/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI= -github.com/aws/aws-sdk-go v1.19.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.32.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.41.13/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= -github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= -github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= +github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A= +github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI= github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= -github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3 h1:xQYRnbQ+ypDMCLiFlLw5cF7Xd6K+oaL7jco2zwIMqTs= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3/go.mod h1:X7RC8FFkx0bjNJRBddd3xdoDaDmNLSxICFdIdJ7asqw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.3.10 h1:z6fAXB4HSuYjrE/P8RU3NdCaN+EPaeq/+80aisCjuF8= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.3.10/go.mod h1:PoPjOi7j+/DtKIGC58HRfcdWKBPYYXwdKnRG+po+hzo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.4 h1:X2X1hn9CQk9G8Nis/xBs3YWJaNJCpQYpxcGWpl5Kgg4= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.4/go.mod h1:Vg7AqclrUJtnnahELZ8ZFWMDHoUHvEwArxrE7rpri58= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= -github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.17.3 h1:PtP2Zzf3uy94EsVOW+tB7gNt63fFZEHuS9IRWg5q250= -github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.17.3/go.mod h1:4zuvYEUJm0Vq8tb3gcb2sl04A9I1AA5DKAefbYPA4VM= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.24.3 h1:GXQrb3kyg4EU94onCRH/oG2IsVjHMNE+IPE4RGkgSa4= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.24.3/go.mod h1:PKGlRhLmSZuA6iCbRD1oZKrTJHdm6NWwWBvHxfDNHTA= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4 h1:Rv6o9v2AfdEIKoAa7pQpJ5ch9ji2HevFUvGY6ufawlI= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 h1:QHaS/SHXfyNycuu4GiWb+AfW5T3bput6X5E3Ai/Q31M= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.6.0/go.mod h1:9O7UG2pELnP0hq35+Gd7XDjOLBkg7tmgRQ0y14ZjoJI= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.42.10 h1:9jBVTw8qxfekGSNtiFreb1e5m2vCz89XcC5C4pmDN9Y= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.42.10/go.mod h1:Fpex7CunMujL2O9qaKTDYG0xnl1ZP3pBZ68XyQCmhtA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8 h1:HD6R8K10gPbN9CNqRDOs42QombXlYeLOr4KkIxe2lQs= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8/go.mod h1:x66GdH8qjYTr6Kb4ik38Ewl6moLsg8igbceNsmxVxeA= +github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.21 h1:/YhTlE24/FbF2gmPITNfSx1X2UzTHTiDcv8DR5vxLdY= +github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.39.21/go.mod h1:6rO2Gn8dZ3wsaQUwKDNqU8nkL69VKkHnVduy+wc/11k= +github.com/aws/aws-sdk-go-v2/service/ses v1.34.18 h1:2Lnd3ZNTyWpFJJM55y0mP0aESovm+vFuFEwLijucUL8= +github.com/aws/aws-sdk-go-v2/service/ses v1.34.18/go.mod h1:BLwHw6wdkA6NfnW/cFaVcvpwdIXHLAkpe6nsLF9BVww= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE= github.com/aws/aws-sdk-go-v2/service/sns v1.34.7/go.mod h1:4WYoZAhHt+dWYpoOQUgkUKfuQbE6Gg/hW4oXE0pKS9U= github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ= github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI= +github.com/aws/aws-sdk-go-v2/service/ssm v1.60.2 h1:ZvLR/SUQGk8sR+bHl8vXT00zgJ+U1fHDzrlokzz9DDo= +github.com/aws/aws-sdk-go-v2/service/ssm v1.60.2/go.mod h1:H5QEq6SthlWMh8PXfSupp6uTg7iaJ3J36Cf15CPG5zE= github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= -github.com/aws/rolesanywhere-credential-helper v1.0.4 h1:kHIVVdyQQiFZoKBP+zywBdFilGCS8It+UvW5LolKbW8= -github.com/aws/rolesanywhere-credential-helper v1.0.4/go.mod h1:QVGNxlDlYhjR0/ZUee7uGl0hNChWidNpe2+GD87Buqk= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= -github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= -github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f h1:Pf0BjJDga7C98f0vhw+Ip5EaiE07S3lTKpIYPNS0nMo= -github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f/go.mod h1:SghidfnxvX7ribW6nHI7T+IBbc9puZ9kk5Tx/88h8P4= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/awslabs/kinesis-aggregation/go/v2 v2.0.0-20211222152315-953b66f67407 h1:p8Ubi4GEgfRc1xFn/WtGNkVG8RXxGHOsKiwGptufIo8= +github.com/awslabs/kinesis-aggregation/go/v2 v2.0.0-20211222152315-953b66f67407/go.mod h1:0Qr1uMHFmHsIYMcG4T7BJ9yrJtWadhOmpABCX69dwuc= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -352,13 +374,11 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bits-and-blooms/bitset v1.4.0 h1:+YZ8ePm+He2pU3dZlIZiOeAKfrBkXi1lSrXJ/Xzgbu8= -github.com/bits-and-blooms/bitset v1.4.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/brianvoe/gofakeit/v6 v6.16.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= @@ -379,14 +399,16 @@ github.com/bytedance/gopkg v0.0.0-20210910103821-e4efae9c17c3/go.mod h1:birsdqRC github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= github.com/bytedance/gopkg v0.0.0-20220509134931-d1878f638986/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= github.com/bytedance/gopkg v0.0.0-20220531084716-665b4f21126f/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= -github.com/bytedance/gopkg v0.0.0-20220817015305-b879a72dc90f/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= -github.com/bytedance/gopkg v0.0.0-20240711085056-a03554c296f8 h1:rDwLxYTMoKHaw4cS0bQhaTZnkXp5e6ediCggGcRD/CA= -github.com/bytedance/gopkg v0.0.0-20240711085056-a03554c296f8/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ= +github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/mockey v1.0.0-rc.0/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= -github.com/bytedance/mockey v1.2.0 h1:847+X2fBSM4s/AIN4loO5d16PCgEj53j7Q8YVB+8P6c= -github.com/bytedance/mockey v1.2.0/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= -github.com/camunda/zeebe/clients/go/v8 v8.2.12 h1:VWkbyhcZFXLqrLXLjICFrKk7Y4Hia9ldKIFoO5V5M8o= -github.com/camunda/zeebe/clients/go/v8 v8.2.12/go.mod h1:4mpOks0uLXPbOCW82g/H9ZHDfdr90ikvFBWGwDV+fG8= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/camunda/zeebe/clients/go/v8 v8.5.25 h1:CwQWKBSR4PvOzcV0FmkSaNAayazjqtgJ+yjVQ2NFY1s= +github.com/camunda/zeebe/clients/go/v8 v8.5.25/go.mod h1:oLhBlv65aO2sV5FJ/dfEREyeqbrPHbJdv0lr5wJujJ8= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= @@ -394,6 +416,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -407,9 +431,6 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/choleraehyq/pid v0.0.12/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= github.com/choleraehyq/pid v0.0.13/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= github.com/choleraehyq/pid v0.0.15/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= -github.com/choleraehyq/pid v0.0.16/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= -github.com/choleraehyq/pid v0.0.20 h1:FSOci0vLLkM/38cDpokosFPcYLpoSxjeTzYiipiu7is= -github.com/choleraehyq/pid v0.0.20/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -425,33 +446,41 @@ github.com/clbanning/mxj/v2 v2.5.6 h1:Jm4VaCI/+Ug5Q57IzEoZbwx4iQFA6wkXv72juUSeK+ github.com/clbanning/mxj/v2 v2.5.6/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0 h1:dEopBSOSjB5fM9r76ufM44AVj9Dnz2IOM0Xs6FVxZRM= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0/go.mod h1:qDSbb0fgIfFNjZrNTPtS5MOMScAGyQtn1KlSvoOdqYw= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2 h1:FIvfKlS2mcuP0qYY6yzdIU9xdrRd/YMP0bNwFjXd0u8= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2/go.mod h1:POsdVp/08Mki0WD9QvvgRRpg9CQ6zhjfRrBoEY8JFS8= github.com/cloudevents/sdk-go/v2 v2.15.2 h1:54+I5xQEnI73RBhWHxbI1XJcqOFOVJN85vb41+8mHUc= github.com/cloudevents/sdk-go/v2 v2.15.2/go.mod h1:lL7kSWAE/V8VI4Wh0jbL2v/jvqsm6tjmaQBSvxcv4uE= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudwego/configmanager v0.2.3 h1:P0YTBgqDBnKeI/VARvut/Dc9Rfxt9Bw1Nv7sk0Ru4u8= +github.com/cloudwego/configmanager v0.2.3/go.mod h1:4GeSKjH6JLvKx4/Hrbh5dse8fDqj1n/Up8HfU4wHJ+w= +github.com/cloudwego/dynamicgo v0.7.1 h1:ITStSu+SaqXd+oFjg+OA920VTOd9GpYTFaUg9upHBKk= +github.com/cloudwego/dynamicgo v0.7.1/go.mod h1:f9le2ULWbFFkQ8WoP+7pGl1zEI2xRLZhaaif6ROLwDw= github.com/cloudwego/fastpb v0.0.2/go.mod h1:/V13XFTq2TUkxj2qWReV8MwfPC4NnPcy6FsrojnsSG0= -github.com/cloudwego/fastpb v0.0.4-0.20230131074846-6fc453d58b96 h1:61PQT0CXNUuQDiDKv/QQ+pFi9uthExZLQz8b5WfS7Qw= -github.com/cloudwego/fastpb v0.0.4-0.20230131074846-6fc453d58b96/go.mod h1:/V13XFTq2TUkxj2qWReV8MwfPC4NnPcy6FsrojnsSG0= +github.com/cloudwego/fastpb v0.0.5 h1:vYnBPsfbAtU5TVz5+f9UTlmSCixG9F9vRwaqE0mZPZU= +github.com/cloudwego/fastpb v0.0.5/go.mod h1:Bho7aAKBUtT9RPD2cNVkTdx4yQumfSv3If7wYnm1izk= github.com/cloudwego/frugal v0.1.3/go.mod h1:b981ViPYdhI56aFYsoMjl9kv6yeqYSO+iEz2jrhkCgI= -github.com/cloudwego/frugal v0.1.6/go.mod h1:9ElktKsh5qd2zDBQ5ENhPSQV7F2dZ/mXlr1eaZGDBFs= -github.com/cloudwego/frugal v0.2.0 h1:0ETSzQYoYqVvdl7EKjqJ9aJnDoG6TzvNKV3PMQiQTS8= -github.com/cloudwego/frugal v0.2.0/go.mod h1:cpnV6kdRMjN3ylxRo63RNbZ9rBK6oxs70Zk6QZ4Enj4= -github.com/cloudwego/gopkg v0.1.0 h1:N7CE4FS5crkZg3w7shw3UR3TG4+uofXXabGuBNmSrlE= -github.com/cloudwego/gopkg v0.1.0/go.mod h1:32yKw2zkpTMtuX6amJR0EMK79f0vGPr67UcArCOlZLU= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/frugal v0.3.0 h1:tgAP0nytiJuyoIM3V3TDOGzjrSNRAIlNG1HHOAzZ3Cs= +github.com/cloudwego/frugal v0.3.0/go.mod h1:pMk46fFyAwUbW7q7lfdK7c6HsD6bWtu6/3Vhz63CgsY= +github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= +github.com/cloudwego/gopkg v0.1.8 h1:ma9oACsY3v6xJwQ8NUc/h19GLV2ZCIjx0P6hqaSIlt4= +github.com/cloudwego/gopkg v0.1.8/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= github.com/cloudwego/kitex v0.0.1/go.mod h1:NTTu8szFfMKY9pxa7JmI/4FZpD15p5YUHLTYMqsXj9o= github.com/cloudwego/kitex v0.0.4/go.mod h1:EIjPJ4Dom2ornk7xDCdKpUpOnf4Tulevimh4Tn05OGc= github.com/cloudwego/kitex v0.2.0/go.mod h1:1p4rtGIIiFZMOePYbSPgLkIhdhdfhEtVOJSti/k9vK4= github.com/cloudwego/kitex v0.3.1/go.mod h1:VZ+G2ILJC98uErzapZd+680LgU5B/hYBFp9pVwnkNuE= github.com/cloudwego/kitex v0.3.2/go.mod h1:/XD07VpUD9VQWmmoepASgZ6iw//vgWikVA9MpzLC5i0= github.com/cloudwego/kitex v0.4.3/go.mod h1:7CV4Cs5oi7dWqI3fFoDTLJHyt/18z7nlHIIHsQQNnbE= -github.com/cloudwego/kitex v0.5.0 h1:f/rip2gp8mdeTpi0WQFv7BdDdkdZn/Q0KvBCm9Mi+7c= -github.com/cloudwego/kitex v0.5.0/go.mod h1:yhw7XikNVG4RstmlQAidBuxMlZYpIiCLsDU8eHPGEMo= +github.com/cloudwego/kitex v0.15.4 h1:mFg3Vg21aEsoQRNc1FK/hOEtEm47EZEJGyorlnsP9nQ= +github.com/cloudwego/kitex v0.15.4/go.mod h1:Zsr4TATU+M3/t+R7CuK7FKQzT2jIhc8ytzEDqjTZmwk= github.com/cloudwego/kitex-examples v0.1.1 h1:5uGqbGEobl8pKSVKwaWgltuf/JAa8Fg2MioX4WmlCXw= github.com/cloudwego/kitex-examples v0.1.1/go.mod h1:5V7LsSJtY18KnceJdvpxYswOfgV3kXE0BGm5mRYyuAg= +github.com/cloudwego/kitex/pkg/protocol/bthrift v0.0.0-20260112072316-5cf426cf9e1b h1:NYQss6yhM1D54n/zlkIC52bNtPsux7x/GepCoOSJt84= +github.com/cloudwego/kitex/pkg/protocol/bthrift v0.0.0-20260112072316-5cf426cf9e1b/go.mod h1:OP63V8YwwSlPVFqHZblV3mJXLPIjcIdwkT6ZYjEggcI= +github.com/cloudwego/localsession v0.2.1 h1:obiuwSP2MQX+fFot3HjOQjvR5o7FlSc8Z4e5EM+NqRY= +github.com/cloudwego/localsession v0.2.1/go.mod h1:J4uams2YT/2d4t7OI6A7NF7EcG8OlHJsOX2LdPbqoyc= github.com/cloudwego/netpoll v0.0.2/go.mod h1:rZOiNI0FYjuvNybXKKhAPUja03loJi/cdv2F55AE6E8= github.com/cloudwego/netpoll v0.0.3/go.mod h1:rZOiNI0FYjuvNybXKKhAPUja03loJi/cdv2F55AE6E8= github.com/cloudwego/netpoll v0.1.0/go.mod h1:rZOiNI0FYjuvNybXKKhAPUja03loJi/cdv2F55AE6E8= @@ -459,17 +488,19 @@ github.com/cloudwego/netpoll v0.2.0/go.mod h1:rZOiNI0FYjuvNybXKKhAPUja03loJi/cdv github.com/cloudwego/netpoll v0.2.2/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= github.com/cloudwego/netpoll v0.2.4/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= github.com/cloudwego/netpoll v0.2.6/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= -github.com/cloudwego/netpoll v0.3.2 h1:/998ICrNMVBo4mlul4j7qcIeY7QnEfuCCPPwck9S3X4= -github.com/cloudwego/netpoll v0.3.2/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +github.com/cloudwego/netpoll v0.7.2 h1:4qDBGQ6CG2SvEXhZSDxMdtqt/NLDxjAVk0PC/biKiJo= +github.com/cloudwego/netpoll v0.7.2/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= github.com/cloudwego/netpoll-http2 v0.0.4/go.mod h1:iFr5SzJCXIYgBg0ubL0fZiCQ6W36s9p0KjXpV04lmoY= github.com/cloudwego/netpoll-http2 v0.0.6/go.mod h1:+bjPyu2Cd4GDzKa0IegPgp1hjMjpZ6/kXTsSjIsmUk8= +github.com/cloudwego/runtimex v0.1.1 h1:lheZjFOyKpsq8TsGGfmX9/4O7F0TKpWmB8on83k7GE8= +github.com/cloudwego/runtimex v0.1.1/go.mod h1:23vL/HGV0W8nSCHbe084AgEBdDV4rvXenEUMnUNvUd8= github.com/cloudwego/thriftgo v0.0.1/go.mod h1:LzeafuLSiHA9JTiWC8TIMIq64iadeObgRUhmVG1OC/w= github.com/cloudwego/thriftgo v0.1.2/go.mod h1:LzeafuLSiHA9JTiWC8TIMIq64iadeObgRUhmVG1OC/w= github.com/cloudwego/thriftgo v0.2.1/go.mod h1:8i9AF5uDdWHGqzUhXDlubCjx4MEfKvWXGQlMWyR0tM4= -github.com/cloudwego/thriftgo v0.2.8/go.mod h1:dAyXHEmKXo0LfMCrblVEY3mUZsdeuA5+i0vF5f09j7E= -github.com/cloudwego/thriftgo v0.3.0 h1:BBb9hVcqmu9p4iKUP/PSIaDB21Vfutgd7k2zgK37Q9Q= -github.com/cloudwego/thriftgo v0.3.0/go.mod h1:AvH0iEjvKHu3cdxG7JvhSAaffkS4h2f4/ZxpJbm48W4= +github.com/cloudwego/thriftgo v0.4.3 h1:Ig80u/nQdOiB4K36BG4oqud2f8LMykZkbnk4R4QywiM= +github.com/cloudwego/thriftgo v0.4.3/go.mod h1:/D4zRAEj1t3/Tq1bVGDMnRt3wxpHfalXfZWvq/n4YmY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -477,15 +508,17 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= -github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= -github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= @@ -504,24 +537,26 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creasty/defaults v1.5.2 h1:/VfB6uxpyp6h0fr7SPp7n8WJBoV8jfxQXPCnkVSjyls= github.com/creasty/defaults v1.5.2/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/dancannon/gorethink v4.0.0+incompatible h1:KFV7Gha3AuqT+gr0B/eKvGhbjmUv0qGF43aKCIKVE9A= github.com/dancannon/gorethink v4.0.0+incompatible/go.mod h1:BLvkat9KmZc1efyYwhz3WnybhRZtgF1K929FD8z1avU= -github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 h1:Q26gmPxs6WnnBYoudOlznPHsmrbTawcYEpHg4VoB7v8= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= +github.com/dapr/components-contrib/tests/certification v0.0.0-20260219105038-d0cf89ba8acf h1:vRT6BytcXSseSg+VZthVm93niltGVOFbhLeRXbwyWKI= +github.com/dapr/components-contrib/tests/certification v0.0.0-20260219105038-d0cf89ba8acf/go.mod h1:NawmfMN065wKn8Jk39E1iwwgWe3kti/MfHBy1jQmSZs= +github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d h1:csljij9d1IO6u9nqbg+TuSRmTZ+OXT8G49yh6zie1yI= +github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= github.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -550,12 +585,14 @@ github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4w github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= -github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dubbogo/go-zookeeper v1.0.3/go.mod h1:fn6n2CAEer3novYgk9ULLwAjuV8/g4DdC2ENwRb6E+c= @@ -576,8 +613,6 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= -github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= @@ -586,9 +621,12 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4A github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= -github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= +github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -599,16 +637,17 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.0/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -628,10 +667,11 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= @@ -663,18 +703,13 @@ github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-co-op/gocron v1.9.0/go.mod h1:DbJm9kdgr1sEvWpHCA7dFFs/PGHPMil9/97EXCRPr4k= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= @@ -683,8 +718,6 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -694,28 +727,25 @@ github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNV github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= -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/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pkgz/expirable-cache v0.1.0 h1:3bw0m8vlTK8qlwz5KXuygNBTkiKRTPrAGXU0Ej2AC1g= github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= @@ -735,9 +765,8 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= @@ -746,8 +775,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -757,8 +786,6 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gocql/gocql v1.5.2 h1:WnKf8xRQImcT/KLaEWG2pjEeryDB7K0qQN9mPs1C58Q= github.com/gocql/gocql v1.5.2/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= @@ -775,8 +802,8 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= @@ -791,8 +818,9 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -818,6 +846,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -843,8 +872,6 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/generative-ai-go v0.15.1 h1:n8aQUpvhPOlGVuM2DRkJ2jvx04zpp42B778AROJa+pQ= -github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -862,7 +889,6 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -873,6 +899,7 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -882,6 +909,10 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20220608213341-c488b8fa1db3/go.mod h1:gSuNB+gJaOiQKLEZ+q+PK9Mq3SOzhRcw2GsGS/FhYDk= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= @@ -889,10 +920,13 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAx github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1/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/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= @@ -911,13 +945,13 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -933,15 +967,13 @@ github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4G 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.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hamba/avro/v2 v2.28.0 h1:E8J5D27biyAulWKNiEBhV85QPc9xRMCUCGJewS0KYCE= -github.com/hamba/avro/v2 v2.28.0/go.mod h1:9TVrlt1cG1kkTUtm9u2eO5Qb7rZXlYzoKqPt8TSH+TA= +github.com/hamba/avro/v2 v2.29.0 h1:fkqoWEPxfygZxrkktgSHEpd0j/P7RKTBTDbcEeMdVEY= +github.com/hamba/avro/v2 v2.29.0/go.mod h1:Pk3T+x74uJoJOFmHrdJ8PRdgSEL/kEKteJ31NytCKxI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE= @@ -1030,19 +1062,21 @@ github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKe github.com/hazelcast/hazelcast-go-client v0.0.0-20190530123621-6cf767c2f31a h1:j6SSiw7fWemWfrJL801xiQ6xRT7ZImika50xvmPN+tg= github.com/hazelcast/hazelcast-go-client v0.0.0-20190530123621-6cf767c2f31a/go.mod h1:VhwtcZ7sg3xq7REqGzEy7ylSWGKz4jZd05eCJropNzI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/http-wasm/http-wasm-host-go v0.6.0 h1:Vd4XvcFB3NMgWp2VLCQaiqYgLneN2lChbyN9NGoNDro= -github.com/http-wasm/http-wasm-host-go v0.6.0/go.mod h1:zQB3w+df4hryDEqBorGyA1DwPJ86LfKIASNLFuj6CuI= +github.com/http-wasm/http-wasm-host-go v0.7.0 h1:+1KrRyOO6tWiDB24QrtSYyDmzFLBBs3jioKaUT0mq1c= +github.com/http-wasm/http-wasm-host-go v0.7.0/go.mod h1:adXKcLmL7yuavH/e0kBAp7b3TgAHTo/enCduyN5bXGM= github.com/huaweicloud/huaweicloud-sdk-go-obs v3.23.4+incompatible h1:XRAk4HBDLCYEdPLWtKf5iZhOi7lfx17aY0oSO9+mcg8= github.com/huaweicloud/huaweicloud-sdk-go-obs v3.23.4+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.56 h1:ULzGSSe95hkOdh17NsiPV3lw58D0SC6M/6askOwF12Q= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.56/go.mod h1:bsqx6o47Kl4YsniIjPwuoeqiIB5Fc3JbSpB2b3o3WFQ= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -1061,8 +1095,6 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 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/jawher/mow.cli v1.0.4/go.mod h1:5hQj2V8g+qYmLUVWqu4Wuja1pI57M83EChYLVZ0sMKk= -github.com/jawher/mow.cli v1.2.0/go.mod h1:y+pcA3jBAdo/GIZx/0rFjw/K2bVEODP9rfZOfaiq8Ko= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -1084,7 +1116,6 @@ github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4E github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -1112,7 +1143,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= @@ -1130,13 +1160,15 @@ github.com/kitex-contrib/monitor-prometheus v0.0.0-20210817080809-024dd7bd51e1/g github.com/kitex-contrib/obs-opentelemetry v0.0.0-20220601144657-c60210e3c928/go.mod h1:VvMzPMfgL7iUG92eVZGuRybGVMKzuSrsfMvHHpL7/Ac= github.com/kitex-contrib/obs-opentelemetry/logging/logrus v0.0.0-20220601144657-c60210e3c928/go.mod h1:Eml/0Z+CqgGIPf9JXzLGu+N9NJoy2x5pqypN+hmKArE= github.com/kitex-contrib/tracer-opentracing v0.0.2/go.mod h1:mprt5pxqywFQxlHb7ugfiMdKbABTLI9YrBYs9WmlK5Q= +github.com/kjk/httplogproxy v0.0.0-20190214011443-6743ea9a2d3d/go.mod h1:kkVhzcC9maw+0jdT2UfGGikRmobjydsBiD6ElexuTLk= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -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/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knadh/koanf v1.4.1 h1:Z0VGW/uo8NJmjd+L1Dc3S5frq6c62w5xQ9Yf4Mg3wFQ= github.com/knadh/koanf v1.4.1/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= @@ -1160,6 +1192,7 @@ github.com/kubemq-io/kubemq-go v1.7.9 h1:dGTcs+cwmoLnnBX1H3xrKU2qd37JODNO/LHRk6V github.com/kubemq-io/kubemq-go v1.7.9/go.mod h1:f6n4qByudW/018Ymol/3s5sjJvt6flEN+ZgP1VVVv0U= github.com/kubemq-io/protobuf v1.3.1 h1:b4QcnpujV8U3go8pa2+FTESl6ygU6hY8APYibRtyemo= github.com/kubemq-io/protobuf v1.3.1/go.mod h1:mzbGBI05R+GhFLD520xweEIvDM+m4nI7ruJDhgEncas= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 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/labd/commercetools-go-sdk v1.3.1 h1:EZnym91AutZXLZ+D1x52kZF35Wq51ZUEMewGCXdoje8= @@ -1192,18 +1225,19 @@ github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f/go.mod github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042/go.mod h1:TPpsiPUEh0zFL1Snz4crhMlBe60PYxRHr5oFF3rRYg0= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/linkedin/goavro/v2 v2.11.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= -github.com/linkedin/goavro/v2 v2.12.0 h1:rIQQSj8jdAUlKQh6DttK8wCRv4t4QO09g1C4aBWXslg= -github.com/linkedin/goavro/v2 v2.12.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/linkedin/goavro/v2 v2.13.1/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= +github.com/linkedin/goavro/v2 v2.14.1 h1:/8VjDpd38PRsy02JS0jflAu7JZPfJcGTwqWgMkFS2iI= +github.com/linkedin/goavro/v2 v2.14.1/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo= github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -1249,8 +1283,8 @@ github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mikeee/aws_credential_helper v0.0.1-alpha.2 h1:qhP1AlQCklni6kNbvGVI5lAicAVef4cGP1JVMaWUDqc= github.com/mikeee/aws_credential_helper v0.0.1-alpha.2/go.mod h1:ql8URJDxt5h47BSezC1QUi3QWRn/FjVWn858+b1h+xU= -github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -1277,16 +1311,20 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= -github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1294,20 +1332,19 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mrz1836/postmark v1.6.1 h1:UHAs9WuZEBZj12MdZ/iVRyoC4tq3ODTdYhE17OhJeJ4= github.com/mrz1836/postmark v1.6.1/go.mod h1:6z5MxAH00Kj44owtQaryv9Pbqp5OKT3wWcRSydB0p0A= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= -github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -1319,18 +1356,18 @@ github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4 github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak= -github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= +github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE= +github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU= -github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0= +github.com/nats-io/nats-server/v2 v2.10.27 h1:A/i3JqtrP897UHc2/Jia/mqaXkqj9+HGdpz+R0mC+sM= +github.com/nats-io/nats-server/v2 v2.10.27/go.mod h1:SGzoWGU8wUVnMr/HJhEMv4R8U4f7hF4zDygmRxpNsvg= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= -github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/nats.go v1.39.1 h1:oTkfKBmz7W047vRxV762M67ZdXeOtUgvbBaNoQ+3PPk= +github.com/nats-io/nats.go v1.39.1/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= -github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= +github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc= +github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -1405,8 +1442,8 @@ github.com/oracle/coherence-go-client/v2 v2.2.0/go.mod h1:IUOIVsyaeccST2AZa/F3/P github.com/oracle/oci-go-sdk/v54 v54.0.0 h1:CDLjeSejv2aDpElAJrhKpi6zvT/zhZCZuXchUUZ+LS4= github.com/oracle/oci-go-sdk/v54 v54.0.0/go.mod h1:+t+yvcFGVp+3ZnztnyxqXfQDsMlq8U25faBLa+mqCMc= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/panjf2000/ants/v2 v2.8.1 h1:C+n/f++aiW8kHCExKlpX6X+okmxKXP7DWLutxuAPuwQ= -github.com/panjf2000/ants/v2 v2.8.1/go.mod h1:KIBmYG9QQX5U2qzFP/yQJaq/nSb6rahS9iEHkrCMgM8= +github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -1423,9 +1460,6 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -1434,6 +1468,7 @@ github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9F github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -1454,8 +1489,9 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/polarismesh/polaris-go v1.1.0/go.mod h1:tquawfjEKp1W3ffNJQSzhfditjjoZ7tvhOCElN7Efzs= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -1511,10 +1547,12 @@ github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3M github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/puzpuzpuz/xsync/v3 v3.0.0 h1:QwUcmah+dZZxy6va/QSU26M6O6Q422afP9jO8JlnRSA= -github.com/puzpuzpuz/xsync/v3 v3.0.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= -github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427 h1:hOnThDlsq0e4M7Sl3A3MnMlazYJsNuuDDqywa5mI7wQ= +github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427/go.mod h1:Zhu1DOotWGZcjom6CZH+8mJ2AD3fOx0QjVIrbpMxN04= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -1523,24 +1561,22 @@ github.com/redis/go-redis/v9 v9.6.3/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= -github.com/riferrei/srclient v0.6.0 h1:60LWpQW66AAL5TtWuMPZEplwgWLUdCK3OBUbag/JWFg= -github.com/riferrei/srclient v0.6.0/go.mod h1:e3nZcDdaOSsaYqiO18INPBK4qnJTjEEyL2rlJcsTtrA= +github.com/riferrei/srclient v0.7.3 h1:JRR6jgfINWUcYZhBRHEg/NAFv7giVmjkoouRbWbakgw= +github.com/riferrei/srclient v0.7.3/go.mod h1:byIzLF4UNZzclmzQXXr++Oe1GEH/hNFahUOSTXc7uSc= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= @@ -1564,8 +1600,10 @@ github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NF github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= github.com/shirou/gopsutil/v3 v3.22.2/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY= -github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= -github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY= +github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -1581,8 +1619,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= @@ -1620,8 +1658,8 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stealthrocket/wasi-go v0.8.1-0.20230912180546-8efbab50fb58 h1:mTC4gyv3lcJ1XpzZMAckqkvWUqeT5Bva4RAT1IoHAAA= github.com/stealthrocket/wasi-go v0.8.1-0.20230912180546-8efbab50fb58/go.mod h1:ZAYCOqLJkc9P6fcq14TV4cf+gJ2fHthp9kCGxBViagE= github.com/stealthrocket/wazergo v0.19.1 h1:BPrITETPgSFwiytwmToO0MbUC/+RGC39JScz1JmmG6c= @@ -1648,8 +1686,10 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO 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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -1663,16 +1703,16 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.732 h1:19TN github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.732/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssm v1.0.732 h1:JcnkgQlkfUYWl21YHGXdQKmmfpvp+ynG/c8ClF/euIE= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssm v1.0.732/go.mod h1:WRkZJqoHH9P4KixWWLdUng99v8X5sr0BQglhVffornM= -github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME= -github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E= -github.com/tetratelabs/wazero v1.7.0 h1:jg5qPydno59wqjpGrHph81lbtHzTrWzwwtD4cD88+hQ= -github.com/tetratelabs/wazero v1.7.0/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= +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/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= +github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/tevid/gohamcrest v1.1.1/go.mod h1:3UvtWlqm8j5JbwYZh80D/PVBt0mJ1eJiYgZMibh0H/k= github.com/tidwall/gjson v1.2.1/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA= github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= @@ -1686,21 +1726,23 @@ github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= -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/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= -github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= +github.com/tmc/langchaingo v0.1.15-0.20251029190607-e35755df7084 h1:e7m315AqnlqGh/c7Dc1+pn8rFNONmXToKgaUrXdj2hM= +github.com/tmc/langchaingo v0.1.15-0.20251029190607-e35755df7084/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/uber/jaeger-client-go v2.29.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -1718,8 +1760,8 @@ github.com/valyala/fasthttp v1.53.0 h1:lW/+SUkOxCx2vlIu0iaImv4JLrVRnbbkpCoaawvA4 github.com/valyala/fasthttp v1.53.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/vmware/vmware-go-kcl v1.5.1 h1:1rJLfAX4sDnCyatNoD/WJzVafkwST6u/cgY/Uf2VgHk= -github.com/vmware/vmware-go-kcl v1.5.1/go.mod h1:kXJmQ6h0dRMRrp1uWU9XbIXvwelDpTxSPquvQUBdpbo= +github.com/vmware/vmware-go-kcl-v2 v1.0.0 h1:HPT5vu+khRmGspBSc/+AilEWbRGoTZhjlYqdrBbRMZs= +github.com/vmware/vmware-go-kcl-v2 v1.0.0/go.mod h1:GBDu+P4Neo0vwZAk0ZUCEC8GYsUOWvi3XhFwAZR3SjA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -1756,13 +1798,11 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= -github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= -github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zouyx/agollo/v3 v3.4.5 h1:7YCxzY9ZYaH9TuVUBvmI6Tk0mwMggikah+cfbYogcHQ= github.com/zouyx/agollo/v3 v3.4.5/go.mod h1:LJr3kDmm23QSW+F1Ol4TMHDa7HvJvscMdVxJ2IpUTVc= go.einride.tech/aip v0.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ= @@ -1796,18 +1836,19 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.6-0.20201102222123-380f4078db9f/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -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/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/contrib/instrumentation/runtime v0.32.0/go.mod h1:qtaLlIO4HC4DfedkYTOrvS2u7nA3N/v8w9mehrBD4O8= go.opentelemetry.io/contrib/propagators/b3 v1.7.0/go.mod h1:gXx7AhL4xXCF42gpm9dQvdohoDa2qeyEx4eIIxqK+h4= go.opentelemetry.io/contrib/propagators/jaeger v1.7.0/go.mod h1:kt2lNImfxV6dETRsDCENd6jU6G0mPRS+P0qlNuvtkTE= @@ -1815,41 +1856,41 @@ go.opentelemetry.io/contrib/propagators/opencensus v0.32.0/go.mod h1:rgmffkE6ivb go.opentelemetry.io/contrib/propagators/ot v1.7.0/go.mod h1:5qxBZR730yb71uXc3bazxt2Si8o8LQK3iJTnSLca/BU= go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdTiUde4= go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/bridge/opencensus v0.30.0/go.mod h1:jyERBSEU6EX7oR+LytaatX1UxNphEIRXj1q3n/6hIk0= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0/go.mod h1:M1hVZHNxcbkAlcvrOMlpQ4YOO3Awf+4N2dxkZL3xm04= go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.30.0/go.mod h1:8Lz1GGcrx1kPGE3zqDrK7ZcPzABEfIQqBjq7roQa5ZA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.30.0/go.mod h1:RejW0QAFotPIixlFZKZka4/70S5UaFOqDO9DYOgScIs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0/go.mod h1:ceUgdyfNv4h4gLxHR0WNfDiiVmZFodZhZSbOLhpxqXE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0/go.mod h1:E+/KKhwOSw8yoPxSSuUHG6vKppkvhN+S1Jc7Nib3k3o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.4.1/go.mod h1:BFiGsTMZdqtxufux8ANXuMeRz9dMPVFdJZadUWDFD7o= go.opentelemetry.io/otel/metric v0.30.0/go.mod h1:/ShZ7+TS4dHzDFmfi1kSXMhMVubNoP0oIaBp70J6UXU= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.4.1/go.mod h1:NBwHDgDIBYjwK2WNu1OPgsIc2IJzmBXNnvIJxJc8BpE= go.opentelemetry.io/otel/sdk v1.7.0/go.mod h1:uTEOTwaqIVuTGiJN7ii13Ibp75wJmYUDe374q6cZwUU= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v0.30.0/go.mod h1:8AKFRi5HyvTR0RRty3paN1aMC9HMT+NzcEhw/BLkLX8= -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/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.4.1/go.mod h1:iYEVbroFCNut9QkwEczV9vMRPHNKSSwYZjulEtsmhFc= go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.16.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1858,13 +1899,12 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -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/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 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/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1891,9 +1931,8 @@ goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/arch v0.0.0-20220722155209-00200b7164a7/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= -golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1918,10 +1957,14 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +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-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1929,7 +1972,6 @@ golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= @@ -1942,16 +1984,6 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5N golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1972,16 +2004,17 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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= @@ -2023,8 +2056,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= @@ -2032,7 +2068,6 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -2043,7 +2078,6 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= @@ -2053,19 +2087,30 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -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.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2081,8 +2126,9 @@ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/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.1.0/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.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180828065106-d99a578cf41b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2093,7 +2139,6 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2138,32 +2183,38 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2184,7 +2235,6 @@ golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2197,12 +2247,14 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2213,16 +2265,20 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -2236,8 +2292,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2270,7 +2326,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -2307,23 +2362,27 @@ golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -2331,12 +2390,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -2354,6 +2411,12 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -2363,6 +2426,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -2398,17 +2462,28 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210106152847-07624b53cd92/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 h1:0DnDgelxbooHLt0nyiPeCP0zrH/RL+UG558i1oNU1xE= google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:IuQRZAKkz+Mhos3ZZ0+hcGaTmLuuTuGw344uzwztGl8= -google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc= -google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= -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/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2429,11 +2504,15 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= @@ -2441,10 +2520,10 @@ google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -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/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= -google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= 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= @@ -2461,8 +2540,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= @@ -2471,7 +2550,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/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= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= @@ -2486,6 +2564,8 @@ gopkg.in/couchbaselabs/gojcbmock.v1 v1.0.4/go.mod h1:jl/gd/aQ2S8whKVSTnsPs6n7BPe gopkg.in/couchbaselabs/jsonx.v1 v1.0.1 h1:giDAdTGcyXUuY+uFCWeJ2foukiqMTYl4ORSxCi/ybcc= gopkg.in/couchbaselabs/jsonx.v1 v1.0.1/go.mod h1:oR201IRovxvLW/eISevH12/+MiKHtNQAKfcX8iWZvJY= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fatih/pool.v2 v2.0.0 h1:xIFeWtxifuQJGk/IEPKsTduEKcKvPmhoiVDGpC40nKg= gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -2509,7 +2589,6 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= @@ -2526,9 +2605,13 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2537,15 +2620,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= -k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= -k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/lock/redis/metadata.yaml b/lock/redis/metadata.yaml index 0d633a0f3c..cdaf95756a 100644 --- a/lock/redis/metadata.yaml +++ b/lock/redis/metadata.yaml @@ -58,8 +58,21 @@ authenticationProfiles: metadata: - name: redisHost required: true - description: "The Redis host address" + description: | + Connection-string for the redis host. If "redisType" is "cluster" it + can be multiple hosts separated by commas or just a single host. + The port can be included in the host string (e.g. "host:6379") or + provided separately via "redisPort". example: '"localhost:6379"' + type: string + - name: redisPort + required: false + description: | + The Redis port. Optional: if "redisHost" already contains a port, this + field must either match or be omitted. When "redisHost" does not include + a port and this field is not set, the default Redis port 6379 is used. + example: "6379" + type: string - name: redisType required: false description: "The Redis type" @@ -158,20 +171,20 @@ metadata: required: false type: bool description: "Whether to enable TLS encryption" - example: false - default: false + example: "false" + default: "false" - name: useEntraID required: false type: bool description: "Whether to use Entra ID for authentication" - example: false - default: false + example: "false" + default: "false" - name: failover required: false type: bool description: "Whether to enable failover mode (for Sentinel)" - example: false - default: false + example: "false" + default: "false" - name: sentinelMasterName required: false description: "The Sentinel master name (used if redisType is 'sentinel')" diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 2211caa58b..b2095e3568 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -75,10 +75,16 @@ func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadat } forceHTTPS := kitstrings.IsTruthy(meta.ForceHTTPS) + + var scopes []string + if meta.Scopes != "" { + scopes = strings.Split(meta.Scopes, ",") + } + conf := &oauth2.Config{ ClientID: meta.ClientID, ClientSecret: meta.ClientSecret, - Scopes: strings.Split(meta.Scopes, ","), + Scopes: scopes, RedirectURL: meta.RedirectURL, Endpoint: oauth2.Endpoint{ AuthURL: meta.AuthURL, diff --git a/middleware/http/oauth2/oauth2_middleware_test.go b/middleware/http/oauth2/oauth2_middleware_test.go index e8f67e4183..9b26d90abb 100644 --- a/middleware/http/oauth2/oauth2_middleware_test.go +++ b/middleware/http/oauth2/oauth2_middleware_test.go @@ -16,6 +16,7 @@ package oauth2 import ( "net/http" "net/http/httptest" + "net/url" "testing" "github.com/fasthttp-contrib/sessions" @@ -62,6 +63,64 @@ func TestOAuth2CreatesAuthorizationHeaderWhenInSessionState(t *testing.T) { assert.Equal(t, "Bearer abcd", r.Header.Get("someHeader")) } +func TestOAuth2EmptyScopesProducesNilSlice(t *testing.T) { + log := logger.NewLogger("oauth2.test") + + tests := []struct { + name string + scopes string + expectScope bool + expectedScope string // expected value of the scope query parameter (space-delimited per RFC 6749) + }{ + {name: "empty scopes omits scope param", scopes: "", expectScope: false, expectedScope: ""}, + {name: "single scope", scopes: "api", expectScope: true, expectedScope: "api"}, + {name: "multiple scopes", scopes: "api,refresh_token", expectScope: true, expectedScope: "api refresh_token"}, + {name: "salesforce no scopes", scopes: "", expectScope: false, expectedScope: ""}, + {name: "three scopes", scopes: "openid,profile,email", expectScope: true, expectedScope: "openid profile email"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var metadata middleware.Metadata + metadata.Properties = map[string]string{ + "clientID": "testId", + "clientSecret": "testSecret", + "scopes": tt.scopes, + "authURL": "https://idp:9999/authorize", + "tokenURL": "https://idp:9999/token", + "redirectUrl": "https://localhost:9999", + "authHeaderName": "Authorization", + } + + handler, err := NewOAuth2Middleware(log).GetHandler(t.Context(), metadata) + require.NoError(t, err) + + // Issue an unauthenticated request — the middleware should redirect to the auth URL + r := httptest.NewRequest(http.MethodGet, "http://dapr.io", nil) + w := httptest.NewRecorder() + + handler( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + ).ServeHTTP(w, r) + + require.Equal(t, http.StatusFound, w.Code) + location := w.Header().Get("Location") + redirectURL, err := url.Parse(location) + require.NoError(t, err) + + scopeValues := redirectURL.Query()["scope"] + if !tt.expectScope { + assert.Empty(t, scopeValues, "scope parameter should not be present when scopes metadata is empty") + } else { + require.Len(t, scopeValues, 1, "scope parameter should appear exactly once") + assert.Equal(t, tt.expectedScope, scopeValues[0], "scope value should match expected space-delimited scopes") + } + }) + } +} + func TestOAuth2CreatesAuthorizationHeaderGetNativeMetadata(t *testing.T) { var metadata middleware.Metadata metadata.Properties = map[string]string{ diff --git a/middleware/http/oauth2clientcredentials/metadata.yaml b/middleware/http/oauth2clientcredentials/metadata.yaml index 7420d1a8d0..bd4a5b401e 100644 --- a/middleware/http/oauth2clientcredentials/metadata.yaml +++ b/middleware/http/oauth2clientcredentials/metadata.yaml @@ -53,12 +53,12 @@ metadata: description: "Specifies additional parameters for requests to the token endpoint" example: "param1=value1¶m2=value2" - name: authStyle - type: integer + type: number required: false description: "Optionally specifies how the endpoint wants the client ID & client secret sent. 0: Auto-detect (tries both ways and caches the successful way), 1: Sends client_id and client_secret in POST body as application/x-www-form-urlencoded parameters, 2: Sends client_id and client_secret using HTTP Basic Authorization" - example: 0 - default: 0 + example: "0" + default: "0" allowedValues: - - 0 - - 1 - - 2 + - "0" + - "1" + - "2" diff --git a/middleware/http/opa/metadata.yaml b/middleware/http/opa/metadata.yaml index 30747ca0bf..8963b77e4c 100644 --- a/middleware/http/opa/metadata.yaml +++ b/middleware/http/opa/metadata.yaml @@ -20,8 +20,8 @@ metadata: type: number required: false description: "The status code to return for denied responses" - example: 403 - default: 403 + example: "403" + default: "403" - name: includedHeaders type: string required: false @@ -31,4 +31,4 @@ metadata: type: string required: false description: "Controls whether the middleware reads the entire request body in-memory and make it available for policy decisions" - example: false + example: "false" diff --git a/middleware/http/opa/middleware.go b/middleware/http/opa/middleware.go index 5de765da68..16d8e96be5 100644 --- a/middleware/http/opa/middleware.go +++ b/middleware/http/opa/middleware.go @@ -29,6 +29,15 @@ import ( "strings" "time" + // TODO: + // "github.com/open-policy-agent/opa/rego" is deprecated: This package is intended + // for older projects transitioning from OPA v0.x and will remain for the lifetime + // of OPA v1.x, but its use is not recommended. For newer features and behaviours, + // such as defaulting to the Rego v1 syntax, use the corresponding components in + // the [github.com/open-policy-agent/opa/v1] package instead. See + // https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more + // information. + //nolint:staticcheck "github.com/open-policy-agent/opa/rego" "github.com/dapr/components-contrib/common/httputils" diff --git a/middleware/http/ratelimit/metadata.yaml b/middleware/http/ratelimit/metadata.yaml index 95498eeb95..353ee2511e 100644 --- a/middleware/http/ratelimit/metadata.yaml +++ b/middleware/http/ratelimit/metadata.yaml @@ -13,7 +13,7 @@ urls: url: https://docs.dapr.io/reference/components-reference/supported-middleware/middleware-rate-limit/ metadata: - name: maxRequestsPerSecond - type: integer + type: number required: true description: "Maximum number of requests allowed per second" - example: 100 + example: "100" diff --git a/middleware/http/wasm/internal/e2e_test.go b/middleware/http/wasm/internal/e2e_test.go index a63a007b6e..b427b9420b 100644 --- a/middleware/http/wasm/internal/e2e_test.go +++ b/middleware/http/wasm/internal/e2e_test.go @@ -80,15 +80,23 @@ func Test_EndToEnd(t *testing.T) { // init (main) and the request[0-9] funcs to info level. // // Then, we expect to see stdout and stderr from both scopes - // at debug level. + // at debug level. Allow duplicates from multi-module pools by + // checking substrings instead of exact combined lines. for _, s := range []string{ `level=info msg="main ConsoleLog"`, `level=info msg="request[0] ConsoleLog"`, - `level=debug msg="wasm stdout: main Stdout\nrequest[0] Stdout\n"`, - `level=debug msg="wasm stderr: main Stderr\nrequest[0] Stderr\n"`, } { require.Contains(t, log.String(), s) } + + // stdout + require.Contains(t, log.String(), `level=debug msg="wasm stdout:`) + require.Contains(t, log.String(), "main Stdout") + require.Contains(t, log.String(), "request[0] Stdout") + // stderr + require.Contains(t, log.String(), `level=debug msg="wasm stderr:`) + require.Contains(t, log.String(), "main Stderr") + require.Contains(t, log.String(), "request[0] Stderr") }, }, { @@ -108,14 +116,20 @@ func Test_EndToEnd(t *testing.T) { for _, s := range []string{ `level=info msg="main ConsoleLog"`, `level=info msg="request[0] ConsoleLog"`, - `level=debug msg="wasm stdout: main Stdout\nrequest[0] Stdout\n"`, - `level=debug msg="wasm stderr: main Stderr\nrequest[0] Stderr\n"`, `level=info msg="request[1] ConsoleLog"`, - `level=debug msg="wasm stdout: request[1] Stdout\n"`, - `level=debug msg="wasm stderr: request[1] Stderr\n"`, } { require.Contains(t, log.String(), s) } + // Allow duplicates for main/request[0] stdout/stderr across modules. + require.Contains(t, log.String(), `level=debug msg="wasm stdout:`) + require.Contains(t, log.String(), "main Stdout") + require.Contains(t, log.String(), "request[0] Stdout") + require.Contains(t, log.String(), `level=debug msg="wasm stderr:`) + require.Contains(t, log.String(), "main Stderr") + require.Contains(t, log.String(), "request[0] Stderr") + // And ensure request[1] appears in stdout/stderr logs too. + require.Contains(t, log.String(), "request[1] Stdout") + require.Contains(t, log.String(), "request[1] Stderr") }, }, { diff --git a/nameresolution/aws/cloudmap/README.md b/nameresolution/aws/cloudmap/README.md new file mode 100644 index 0000000000..d5c11732ee --- /dev/null +++ b/nameresolution/aws/cloudmap/README.md @@ -0,0 +1,140 @@ +# AWS CloudMap Name Resolution + +This component uses [AWS Cloud Map](https://aws.amazon.com/cloud-map/) for service discovery in Dapr. It supports both HTTP and DNS namespaces, allowing services to discover and connect to other services using AWS Cloud Map's service discovery capabilities. + +## Component Format + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: appconfig +spec: + nameResolution: + component: "aws.cloudmap" + configuration: + # Required: AWS CloudMap namespace configuration (one of these is required) + namespaceName: "my-namespace" # The name of your CloudMap namespace + # namespaceId: "ns-xxxxxx" # Alternative: Use namespace ID instead of name + + # Optional: AWS authentication (choose one authentication method) + # Option 1: Environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY + # Option 2: IAM roles for Amazon EKS + # Option 3: Explicit credentials (not recommended for production) + accessKey: "****" + secretKey: "****" + sessionToken: "****" # Optional + + # Optional: AWS region and endpoint configuration + region: "us-west-2" + endpoint: "http://localhost:4566" # Optional: Custom endpoint for testing + + # Optional: Dapr configuration + defaultDaprPort: 3500 # Default port for Dapr sidecar if not specified in instance attributes +``` + +## Specification + +### AWS Authentication + +The component supports multiple authentication methods: + +1. Environment Variables: + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN (optional) + +2. IAM Roles: + - When running on AWS (EKS, EC2, etc.), the component can use IAM roles + +3. Explicit Credentials: + - Provided in the component metadata (not recommended for production) + +### Required Permissions + +The AWS credentials must have the following permissions: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "servicediscovery:DiscoverInstances", + "servicediscovery:GetNamespace", + "servicediscovery:ListNamespaces" + ], + "Resource": "*" + } + ] +} +``` + +### Configuration Options + +| Property | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| namespaceName | string | One of namespaceName or namespaceId | "" | The name of your AWS CloudMap namespace | +| namespaceId | string | One of namespaceName or namespaceId | "" | The ID of your AWS CloudMap namespace | +| region | string | N | "" | AWS region. If not provided, will be determined from environment or instance metadata | +| endpoint | string | N | "" | Custom endpoint for AWS CloudMap API. Useful for testing with LocalStack | +| defaultDaprPort | number | N | 3500 | Default port for Dapr sidecar if not specified in instance attributes | + +### Service Registration + +To use this name resolver, your services must be registered in AWS CloudMap. When registering instances, ensure they have the following attributes: + +1. Required: One of these address attributes: + - `AWS_INSTANCE_IPV4`: IPv4 address of the instance + - `AWS_INSTANCE_IPV6`: IPv6 address of the instance + - `AWS_INSTANCE_CNAME`: Hostname of the instance + +2. Optional: Dapr sidecar port attribute: + - `DAPR_PORT`: The port that the Dapr sidecar is listening on + - If not specified, the component will use the `defaultDaprPort` from configuration (defaults to 3500) + +The resolver will only return healthy instances (those with `HEALTHY` status) to ensure reliable service communication. + +Example instance attributes: +```json +{ + "AWS_INSTANCE_IPV4": "10.0.0.1", + "DAPR_PORT": "50002" +} +``` + + +## Example Usage + +### Minimal Configuration + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: appconfig +spec: + nameResolution: + component: "aws.cloudmap" + configuration: + namespaceName: "mynamespace.dev" + defaultDaprPort: 50002 +``` + +### Local Development with LocalStack + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: appconfig +spec: + nameResolution: + component: "aws.cloudmap" + configuration: + namespaceName: "my-namespace" + region: "us-east-1" + endpoint: "http://localhost:4566" + accessKey: "test" + secretKey: "test" +``` \ No newline at end of file diff --git a/nameresolution/aws/cloudmap/cloudmap.go b/nameresolution/aws/cloudmap/cloudmap.go new file mode 100644 index 0000000000..c338652bd5 --- /dev/null +++ b/nameresolution/aws/cloudmap/cloudmap.go @@ -0,0 +1,260 @@ +/* +Copyright 2025 The Dapr Authors +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 cloudmap + +import ( + "context" + "errors" + "fmt" + "math/rand" + "strconv" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/servicediscovery" + "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" + + awsCommon "github.com/dapr/components-contrib/common/aws" + awsAuth "github.com/dapr/components-contrib/common/aws/auth" + "github.com/dapr/components-contrib/nameresolution" + "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" +) + +// convertConfigurationToStringMap converts the generic Configuration field into a map[string]string for aws auth fields +func convertConfigurationToStringMap(cfg any) map[string]string { + if cfg == nil { + return nil + } + + switch v := cfg.(type) { + case map[string]string: + return v + case map[string]any: + out := make(map[string]string, len(v)) + for k, val := range v { + switch t := val.(type) { + case string: + out[k] = t + default: + out[k] = fmt.Sprint(t) + } + } + return out + default: + return nil + } +} + +// ServiceDiscoveryClient interface for mocking +type ServiceDiscoveryClient interface { + GetNamespace(ctx context.Context, input *servicediscovery.GetNamespaceInput, opts ...func(*servicediscovery.Options)) (*servicediscovery.GetNamespaceOutput, error) + ListNamespaces(ctx context.Context, input *servicediscovery.ListNamespacesInput, opts ...func(*servicediscovery.Options)) (*servicediscovery.ListNamespacesOutput, error) + DiscoverInstances(ctx context.Context, input *servicediscovery.DiscoverInstancesInput, opts ...func(*servicediscovery.Options)) (*servicediscovery.DiscoverInstancesOutput, error) +} + +// Resolver is the AWS CloudMap name resolver. +type Resolver struct { + client ServiceDiscoveryClient + logger logger.Logger + namespaceID string + namespaceName string + defaultDaprPort int +} + +// NewResolver creates a new AWS CloudMap name resolver. +func NewResolver(logger logger.Logger) nameresolution.Resolver { + return &Resolver{ + logger: logger, + defaultDaprPort: defaultDaprPort, + } +} + +// Init initializes the AWS CloudMap name resolver. +func (r *Resolver) Init(ctx context.Context, metadata nameresolution.Metadata) error { + var meta cloudMapMetadata + err := kitmd.DecodeMetadata(metadata.Configuration, &meta) + if err != nil { + return fmt.Errorf("failed to decode metadata: %w", err) + } + + if err = meta.Validate(); err != nil { + return fmt.Errorf("invalid metadata: %w", err) + } + + // Set default Dapr port if specified + if meta.DefaultDaprPort > 0 { + r.defaultDaprPort = meta.DefaultDaprPort + } + + authOpts := awsAuth.Options{ + Logger: r.logger, + Properties: convertConfigurationToStringMap(metadata.Configuration), + Region: meta.Region, + Endpoint: meta.Endpoint, + AccessKey: meta.AccessKey, + SecretKey: meta.SecretKey, + SessionToken: meta.SessionToken, + } + awsCfg, err := awsCommon.NewConfig(ctx, authOpts) + if err != nil { + return fmt.Errorf("failed to create AWS config: %w", err) + } + + // Create CloudMap client if not already set (for testing) + if r.client == nil { + r.client = servicediscovery.NewFromConfig(awsCfg) + } + + // Set namespace info + r.namespaceID = meta.NamespaceID + r.namespaceName = meta.NamespaceName + + // Validate access to CloudMap and resolve namespace if needed + if err := r.validateAccess(ctx); err != nil { + return fmt.Errorf("failed to validate CloudMap access: %w", err) + } + + return nil +} + +// ResolveID resolves a service ID to an address using AWS CloudMap. +func (r *Resolver) ResolveID(ctx context.Context, req nameresolution.ResolveRequest) (string, error) { + addresses, err := r.resolveIDMulti(ctx, req) + if err != nil { + return "", err + } + if len(addresses) == 0 { + return "", errors.New("no healthy instances found for service " + req.ID) + } + + // Pick a random address for load balancing + // gosec is complaining that we are using a non-crypto-safe PRNG. This is fine in this scenario since we are using it only for selecting a random address for load-balancing. + //nolint:gosec + return addresses[rand.Intn(len(addresses))], nil +} + +// ResolveIDMulti resolves a service ID to multiple addresses using AWS CloudMap. +func (r *Resolver) ResolveIDMulti(ctx context.Context, req nameresolution.ResolveRequest) (nameresolution.AddressList, error) { + return r.resolveIDMulti(ctx, req) +} + +func (r *Resolver) resolveIDMulti(ctx context.Context, req nameresolution.ResolveRequest) ([]string, error) { + // Prepare discovery input + input := &servicediscovery.DiscoverInstancesInput{ + NamespaceName: aws.String(r.namespaceName), + ServiceName: aws.String(req.ID), + HealthStatus: types.HealthStatusFilterHealthy, + } + + r.logger.Debugf("Discovering instances in CloudMap: namespace=%s service=%s", *input.NamespaceName, *input.ServiceName) + + // Call CloudMap API + result, err := r.client.DiscoverInstances(ctx, input) + if err != nil { + return nil, fmt.Errorf("failed to discover CloudMap instances: %w", err) + } + + r.logger.Debugf("Found %d instances for service %s", len(result.Instances), req.ID) + + // Extract addresses from instances + addresses := make([]string, 0, len(result.Instances)) + for _, instance := range result.Instances { + if instance.InstanceId == nil || instance.Attributes == nil { + r.logger.Warnf("Skipping instance with nil ID or attributes") + continue + } + + // Get IP/hostname from attributes + var addr string + if ipv4, ok := instance.Attributes["AWS_INSTANCE_IPV4"]; ok && ipv4 != "" { + addr = ipv4 + } else if ipv6, ok := instance.Attributes["AWS_INSTANCE_IPV6"]; ok && ipv6 != "" { + addr = ipv6 + } else if cname, ok := instance.Attributes["AWS_INSTANCE_CNAME"]; ok && cname != "" { + addr = cname + } else { + r.logger.Warnf("Instance %s has no valid address attributes", *instance.InstanceId) + continue + } + + // Get port from DAPR_PORT attribute or use default + port := r.defaultDaprPort + if daprPort, ok := instance.Attributes["DAPR_PORT"]; ok && daprPort != "" { + if p, parseErr := strconv.Atoi(daprPort); parseErr == nil { + port = p + } else { + r.logger.Warnf("Invalid DAPR_PORT value for instance %s: %s, using default port %d", *instance.InstanceId, daprPort, r.defaultDaprPort) + } + } + + addr = fmt.Sprintf("%s:%d", addr, port) + addresses = append(addresses, addr) + } + + if len(addresses) == 0 { + r.logger.Warnf("No healthy instances found for service %s", req.ID) + } else { + r.logger.Debugf("Resolved addresses for service %s: %v", req.ID, addresses) + } + + return addresses, nil +} + +// Close implements io.Closer. +func (r *Resolver) Close() error { + return nil +} + +// validateAccess validates access to AWS CloudMap and resolves namespace if needed. +func (r *Resolver) validateAccess(ctx context.Context) error { + if r.namespaceID != "" { + return r.validateAccessByID(ctx) + } + if r.namespaceName == "" { + return errors.New("either namespaceName or namespaceId must be provided") + } + return r.validateAccessByName(ctx) +} + +func (r *Resolver) validateAccessByID(ctx context.Context) error { + input := &servicediscovery.GetNamespaceInput{ + Id: aws.String(r.namespaceID), + } + result, err := r.client.GetNamespace(ctx, input) + if err != nil { + return fmt.Errorf("failed to get namespace with ID %s: %w", r.namespaceID, err) + } + if result.Namespace != nil && result.Namespace.Name != nil { + r.namespaceName = *result.Namespace.Name + return nil + } + return fmt.Errorf("namespace ID %s exists but has no name", r.namespaceID) +} + +func (r *Resolver) validateAccessByName(ctx context.Context) error { + input := &servicediscovery.ListNamespacesInput{} + result, err := r.client.ListNamespaces(ctx, input) + if err != nil { + return fmt.Errorf("failed to list namespaces: %w", err) + } + for _, ns := range result.Namespaces { + if ns.Name != nil && *ns.Name == r.namespaceName { + if ns.Id != nil { + r.namespaceID = *ns.Id + } + return nil + } + } + return fmt.Errorf("namespace not found: %s", r.namespaceName) +} diff --git a/nameresolution/aws/cloudmap/cloudmap_test.go b/nameresolution/aws/cloudmap/cloudmap_test.go new file mode 100644 index 0000000000..b37ab307fa --- /dev/null +++ b/nameresolution/aws/cloudmap/cloudmap_test.go @@ -0,0 +1,388 @@ +/* +Copyright 2025 The Dapr Authors +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 cloudmap + +import ( + "context" + "errors" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/servicediscovery" + "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/nameresolution" + "github.com/dapr/kit/logger" +) + +type mockServiceDiscoveryAPI struct { + getNamespaceResp *servicediscovery.GetNamespaceOutput + getNamespaceErr error + listNamespacesResp *servicediscovery.ListNamespacesOutput + listNamespacesErr error + discoverInstancesResp *servicediscovery.DiscoverInstancesOutput + discoverInstancesErr error +} + +func (m *mockServiceDiscoveryAPI) GetNamespace(ctx context.Context, input *servicediscovery.GetNamespaceInput, opts ...func(*servicediscovery.Options)) (*servicediscovery.GetNamespaceOutput, error) { + return m.getNamespaceResp, m.getNamespaceErr +} + +func (m *mockServiceDiscoveryAPI) ListNamespaces(ctx context.Context, input *servicediscovery.ListNamespacesInput, opts ...func(*servicediscovery.Options)) (*servicediscovery.ListNamespacesOutput, error) { + return m.listNamespacesResp, m.listNamespacesErr +} + +func (m *mockServiceDiscoveryAPI) DiscoverInstances(ctx context.Context, input *servicediscovery.DiscoverInstancesInput, opts ...func(*servicediscovery.Options)) (*servicediscovery.DiscoverInstancesOutput, error) { + return m.discoverInstancesResp, m.discoverInstancesErr +} + +func TestCloudMapResolver(t *testing.T) { + t.Run("init with valid namespace ID", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + mockClient := &mockServiceDiscoveryAPI{ + getNamespaceResp: &servicediscovery.GetNamespaceOutput{ + Namespace: &types.Namespace{ + Name: aws.String("test-namespace"), + }, + }, + } + r.client = mockClient + + err := r.Init(t.Context(), nameresolution.Metadata{ + Configuration: map[string]interface{}{ + "namespaceId": "ns-test", + "region": "us-west-2", + }, + }) + + require.NoError(t, err) + assert.Equal(t, "ns-test", r.namespaceID) + assert.Equal(t, "test-namespace", r.namespaceName) + assert.Equal(t, defaultDaprPort, r.defaultDaprPort) + }) + + t.Run("init with custom default port", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + mockClient := &mockServiceDiscoveryAPI{ + getNamespaceResp: &servicediscovery.GetNamespaceOutput{ + Namespace: &types.Namespace{ + Name: aws.String("test-namespace"), + }, + }, + } + r.client = mockClient + + err := r.Init(t.Context(), nameresolution.Metadata{ + Configuration: map[string]interface{}{ + "namespaceId": "ns-test", + "region": "us-west-2", + "defaultDaprPort": 5000, + }, + }) + + require.NoError(t, err) + assert.Equal(t, 5000, r.defaultDaprPort) + }) + + t.Run("init with valid namespace name", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + mockClient := &mockServiceDiscoveryAPI{ + listNamespacesResp: &servicediscovery.ListNamespacesOutput{ + Namespaces: []types.NamespaceSummary{ + { + Name: aws.String("test-namespace"), + Id: aws.String("ns-test"), + }, + }, + }, + } + r.client = mockClient + + err := r.Init(t.Context(), nameresolution.Metadata{ + Configuration: map[string]interface{}{ + "namespaceName": "test-namespace", + "region": "us-west-2", + }, + }) + + require.NoError(t, err) + assert.Equal(t, "ns-test", r.namespaceID) + assert.Equal(t, "test-namespace", r.namespaceName) + }) + + t.Run("init with missing namespace", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + err := r.Init(t.Context(), nameresolution.Metadata{ + Configuration: map[string]interface{}{ + "region": "us-west-2", + }, + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "either namespaceName or namespaceId must be provided") + }) + + t.Run("resolve service with healthy instances", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + mockClient := &mockServiceDiscoveryAPI{ + discoverInstancesResp: &servicediscovery.DiscoverInstancesOutput{ + Instances: []types.HttpInstanceSummary{ + { + InstanceId: aws.String("i-1234"), + Attributes: map[string]string{ + "AWS_INSTANCE_IPV4": "10.0.0.1", + "DAPR_PORT": "8080", + }, + }, + }, + }, + } + r.client = mockClient + r.namespaceName = "test-namespace" + + addr, err := r.ResolveID(t.Context(), nameresolution.ResolveRequest{ + ID: "test-service", + }) + + require.NoError(t, err) + assert.Equal(t, "10.0.0.1:8080", addr) + }) + + t.Run("resolve service with no instances", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + mockClient := &mockServiceDiscoveryAPI{ + discoverInstancesResp: &servicediscovery.DiscoverInstancesOutput{ + Instances: []types.HttpInstanceSummary{}, + }, + } + r.client = mockClient + r.namespaceName = "test-namespace" + + _, err := r.ResolveID(t.Context(), nameresolution.ResolveRequest{ + ID: "test-service", + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "no healthy instances found for service test-service") + }) + + t.Run("resolve service with discovery error", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + mockClient := &mockServiceDiscoveryAPI{ + discoverInstancesErr: assert.AnError, + } + r.client = mockClient + r.namespaceName = "test-namespace" + + _, err := r.ResolveID(t.Context(), nameresolution.ResolveRequest{ + ID: "test-service", + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to discover CloudMap instances") + }) + + t.Run("resolve service with DAPR_PORT", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + mockClient := &mockServiceDiscoveryAPI{ + discoverInstancesResp: &servicediscovery.DiscoverInstancesOutput{ + Instances: []types.HttpInstanceSummary{ + { + InstanceId: aws.String("i-1234"), + Attributes: map[string]string{ + "AWS_INSTANCE_IPV4": "10.0.0.1", + "DAPR_PORT": "5000", + }, + }, + }, + }, + } + r.client = mockClient + r.namespaceName = "test-namespace" + + addr, err := r.ResolveID(t.Context(), nameresolution.ResolveRequest{ + ID: "test-service", + }) + + require.NoError(t, err) + assert.Equal(t, "10.0.0.1:5000", addr) + }) + + t.Run("resolve service with default port", func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")).(*Resolver) + mockClient := &mockServiceDiscoveryAPI{ + discoverInstancesResp: &servicediscovery.DiscoverInstancesOutput{ + Instances: []types.HttpInstanceSummary{ + { + InstanceId: aws.String("i-1234"), + Attributes: map[string]string{ + "AWS_INSTANCE_IPV4": "10.0.0.1", + }, + }, + }, + }, + } + r.client = mockClient + r.namespaceName = "test-namespace" + r.defaultDaprPort = 3500 + + addr, err := r.ResolveID(t.Context(), nameresolution.ResolveRequest{ + ID: "test-service", + }) + + require.NoError(t, err) + assert.Equal(t, "10.0.0.1:3500", addr) + }) +} + +func TestResolve(t *testing.T) { + testCases := []struct { + name string + req nameresolution.ResolveRequest + mockResponse *servicediscovery.DiscoverInstancesOutput + mockError error + defaultPort int + expectedAddresses []string + expectedError bool + }{ + { + name: "successful resolution with DAPR_PORT", + req: nameresolution.ResolveRequest{ + ID: "test-service", + }, + mockResponse: &servicediscovery.DiscoverInstancesOutput{ + Instances: []types.HttpInstanceSummary{ + { + InstanceId: aws.String("i-1234"), + Attributes: map[string]string{ + "AWS_INSTANCE_IPV4": "192.0.2.1", + "DAPR_PORT": "5000", + }, + }, + { + InstanceId: aws.String("i-5678"), + Attributes: map[string]string{ + "AWS_INSTANCE_IPV4": "192.0.2.2", + "DAPR_PORT": "5000", + }, + }, + }, + }, + expectedAddresses: []string{"192.0.2.1:5000", "192.0.2.2:5000"}, + expectedError: false, + }, + { + name: "successful resolution with default port", + req: nameresolution.ResolveRequest{ + ID: "test-service", + }, + defaultPort: 3500, + mockResponse: &servicediscovery.DiscoverInstancesOutput{ + Instances: []types.HttpInstanceSummary{ + { + InstanceId: aws.String("i-1234"), + Attributes: map[string]string{ + "AWS_INSTANCE_IPV4": "192.0.2.1", + }, + }, + { + InstanceId: aws.String("i-5678"), + Attributes: map[string]string{ + "AWS_INSTANCE_IPV4": "192.0.2.2", + }, + }, + }, + }, + expectedAddresses: []string{"192.0.2.1:3500", "192.0.2.2:3500"}, + expectedError: false, + }, + { + name: "error from AWS", + req: nameresolution.ResolveRequest{ + ID: "test-service", + }, + mockError: errors.New("AWS error"), + expectedAddresses: nil, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockClient := &mockServiceDiscoveryAPI{ + discoverInstancesResp: tc.mockResponse, + discoverInstancesErr: tc.mockError, + } + + resolver := &Resolver{ + client: mockClient, + logger: logger.NewLogger("test"), + namespaceName: "test-namespace", + defaultDaprPort: tc.defaultPort, + } + + addresses, err := resolver.ResolveID(t.Context(), tc.req) + + if tc.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + if len(tc.expectedAddresses) == 1 { + assert.Equal(t, tc.expectedAddresses[0], addresses) + } else { + assert.Contains(t, tc.expectedAddresses, addresses) + } + } + }) + } +} + +func TestConvertConfigurationToStringMap(t *testing.T) { + t.Run("nil configuration", func(t *testing.T) { + got := convertConfigurationToStringMap(nil) + assert.Nil(t, got) + }) + + t.Run("map string to string", func(t *testing.T) { + in := map[string]string{ + "region": "us-west-2", + "foo": "bar", + } + got := convertConfigurationToStringMap(in) + assert.Equal(t, len(in), len(got)) + for k, v := range in { + assert.Equal(t, v, got[k]) + } + }) + + t.Run("map string to any with mixed types", func(t *testing.T) { + in := map[string]any{ + "string": "value", + "int": 42, + "bool": true, + } + got := convertConfigurationToStringMap(in) + assert.Equal(t, len(in), len(got)) + assert.Equal(t, "value", got["string"]) + assert.Equal(t, "42", got["int"]) + assert.Equal(t, "true", got["bool"]) + }) + + t.Run("unsupported type returns nil", func(t *testing.T) { + got := convertConfigurationToStringMap([]string{"not", "a", "map"}) + assert.Nil(t, got) + }) +} diff --git a/nameresolution/aws/cloudmap/metadata.go b/nameresolution/aws/cloudmap/metadata.go new file mode 100644 index 0000000000..6b2467effe --- /dev/null +++ b/nameresolution/aws/cloudmap/metadata.go @@ -0,0 +1,46 @@ +/* +Copyright 2025 The Dapr Authors +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 cloudmap + +import ( + "errors" +) + +type cloudMapMetadata struct { + // AWS Auth (handled by common auth) + AccessKey string `json:"accessKey" mapstructure:"accessKey" mdignore:"true"` + SecretKey string `json:"secretKey" mapstructure:"secretKey" mdignore:"true"` + SessionToken string `json:"sessionToken" mapstructure:"sessionToken" mdignore:"true"` + Region string `json:"region" mapstructure:"region" mapstructurealiases:"awsRegion" mdignore:"true"` + + // CloudMap Specific + Endpoint string `json:"endpoint"` // Optional endpoint for testing + NamespaceName string `json:"namespaceName"` // CloudMap namespace name + NamespaceID string `json:"namespaceId"` // Optional: CloudMap namespace ID (if name not provided) + + // Dapr Configuration + DefaultDaprPort int `json:"defaultDaprPort"` // Default Dapr sidecar port if not specified in instance attributes +} + +const ( + defaultDaprPort = 50002 +) + +func (m *cloudMapMetadata) Validate() error { + if m.NamespaceName == "" && m.NamespaceID == "" { + return errors.New("either namespaceName or namespaceId must be provided") + } + + return nil +} diff --git a/nameresolution/aws/cloudmap/metadata.yaml b/nameresolution/aws/cloudmap/metadata.yaml new file mode 100644 index 0000000000..f2f826ff46 --- /dev/null +++ b/nameresolution/aws/cloudmap/metadata.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: nameresolution +name: aws.cloudmap +version: v1 +status: alpha +title: "AWS CloudMap" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-name-resolution/nr-awscloudmap/ +builtinAuthenticationProfiles: + - name: "aws" +metadata: + - name: namespaceName + type: string + required: false + description: Name of the AWS CloudMap namespace. One of namespaceName or namespaceId is required. + example: "my-namespace" + - name: namespaceId + type: string + required: false + description: ID of the AWS CloudMap namespace. One of namespaceName or namespaceId is required. + example: "ns-xxxxxx" + - name: defaultDaprPort + type: number + required: false + description: Default port for the Dapr sidecar when DAPR_PORT is not set on the instance. + example: "3500" diff --git a/nameresolution/consul/README.md b/nameresolution/hashicorp/consul/README.md similarity index 100% rename from nameresolution/consul/README.md rename to nameresolution/hashicorp/consul/README.md diff --git a/nameresolution/consul/configuration.go b/nameresolution/hashicorp/consul/configuration.go similarity index 99% rename from nameresolution/consul/configuration.go rename to nameresolution/hashicorp/consul/configuration.go index ccf4e58010..15e66ca756 100644 --- a/nameresolution/consul/configuration.go +++ b/nameresolution/hashicorp/consul/configuration.go @@ -60,7 +60,7 @@ func newIntermediateConfig() intermediateConfig { } } -func parseConfig(rawConfig interface{}) (configSpec, error) { +func parseConfig(rawConfig any) (configSpec, error) { var result configSpec rawConfig, err := config.Normalize(rawConfig) if err != nil { diff --git a/nameresolution/consul/consul.go b/nameresolution/hashicorp/consul/consul.go similarity index 100% rename from nameresolution/consul/consul.go rename to nameresolution/hashicorp/consul/consul.go diff --git a/nameresolution/consul/consul_test.go b/nameresolution/hashicorp/consul/consul_test.go similarity index 100% rename from nameresolution/consul/consul_test.go rename to nameresolution/hashicorp/consul/consul_test.go diff --git a/nameresolution/hashicorp/consul/metadata.yaml b/nameresolution/hashicorp/consul/metadata.yaml new file mode 100644 index 0000000000..f0a79cb5a6 --- /dev/null +++ b/nameresolution/hashicorp/consul/metadata.yaml @@ -0,0 +1,157 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: nameresolution +name: hashicorp.consul +version: v1 +status: alpha +title: "HashiCorp Consul" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-name-resolution/setup-nr-consul/ +authenticationProfiles: + - title: "Token Authentication" + description: "Connect to Consul using a token for authentication." + metadata: + - name: token + type: string + required: true + description: The Consul ACL token for authentication. + sensitive: true + example: "consul-token" + - title: "Username/Password Authentication" + description: "Connect to Consul using a username and password." + metadata: + - name: username + type: string + required: false + description: Username for HTTP basic authentication. + example: "username" + - name: password + type: string + required: false + description: Password for HTTP basic authentication. + sensitive: true + example: "password" + - title: "TLS Authentication" + description: "Connect to Consul using TLS encryption with certificates." + metadata: + - name: tlsAddress + type: string + required: true + description: The TLS address of the Consul server. + example: "localhost:8501" + - name: caFile + type: string + required: true + description: Path to the CA certificate file. + example: "/path/to/ca.crt" + - name: certFile + type: string + required: true + description: Path to the client certificate file. + example: "/path/to/client.crt" + - name: keyFile + type: string + required: true + description: Path to the client private key file. + example: "/path/to/client.key" + - name: insecureSkipVerify + type: bool + required: false + description: Skip TLS certificate verification. + example: "false" + default: "false" +metadata: + - name: address + type: string + required: true + description: The address of the Consul server. + example: "localhost:8500" + - name: scheme + type: string + required: false + description: The scheme to use for connecting to Consul (http or https). + example: "https" + default: "http" + - name: datacenter + type: string + required: false + description: The Consul datacenter to use. + example: "dc1" + - name: waitTime + type: string + required: false + description: The wait time for Consul operations. + example: "10s" + - name: tokenFile + type: string + required: false + description: Path to a file containing the Consul ACL token. + example: "/path/to/token" + - name: daprPortMetaKey + type: string + required: false + description: The metadata key used to store the Dapr port. + example: "DAPR_PORT" + default: "DAPR_PORT" + - name: selfRegister + type: bool + required: false + description: Whether to register this service with Consul. + example: "true" + default: "false" + - name: selfDeregister + type: bool + required: false + description: Whether to deregister this service from Consul on shutdown. + example: "true" + default: "false" + - name: useCache + type: bool + required: false + description: Whether to use caching for service lookups. + example: "true" + default: "false" + - name: tags + type: string + required: false + description: Tags to associate with the service registration. + example: "dapr,v1" + - name: namespace + type: string + required: false + description: The Consul namespace to use for queries. + example: "default" + - name: partition + type: string + required: false + description: The Consul partition to use for queries. + example: "default" + - name: queryDatacenter + type: string + required: false + description: The Consul datacenter to use for queries. + example: "dc1" + - name: queryToken + type: string + required: false + description: The Consul ACL token to use for queries. + sensitive: true + example: "query-token" + - name: queryWaitTime + type: string + required: false + description: The wait time for Consul queries. + example: "5s" + - name: allowStale + type: bool + required: false + description: Whether to allow stale results in queries. + example: "true" + default: "false" + - name: requireConsistent + type: bool + required: false + description: Whether to require consistent reads in queries. + example: "false" + default: "false" \ No newline at end of file diff --git a/nameresolution/consul/watcher.go b/nameresolution/hashicorp/consul/watcher.go similarity index 100% rename from nameresolution/consul/watcher.go rename to nameresolution/hashicorp/consul/watcher.go diff --git a/nameresolution/kubernetes/metadata.yaml b/nameresolution/kubernetes/metadata.yaml new file mode 100644 index 0000000000..3f7ae02834 --- /dev/null +++ b/nameresolution/kubernetes/metadata.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: nameresolution +name: kubernetes +version: v1 +status: stable +title: "Kubernetes" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-name-resolution/nr-kubernetes/ +metadata: [] \ No newline at end of file diff --git a/nameresolution/mdns/metadata.yaml b/nameresolution/mdns/metadata.yaml new file mode 100644 index 0000000000..54374bbf38 --- /dev/null +++ b/nameresolution/mdns/metadata.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: nameresolution +name: mdns +version: v1 +status: stable +title: "mDNS" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-name-resolution/nr-mdns/ +metadata: [] \ No newline at end of file diff --git a/nameresolution/nameformat/README.md b/nameresolution/nameformat/README.md new file mode 100644 index 0000000000..edf0ee024a --- /dev/null +++ b/nameresolution/nameformat/README.md @@ -0,0 +1,39 @@ +# Name Format Name Resolution + +The Name Format name resolver provides a flexible way to resolve service names using a configurable format string with placeholders. This is useful in scenarios where you want to map service names to predictable DNS names following a specific pattern. + +## Configuration Format + +To use the Name Format name resolver, create a configuration in your Dapr environment: + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: appconfig +spec: + nameResolution: + component: "nameformat" + configuration: + format: "service-{appid}.default.svc.cluster.local" # Replace with your desired format pattern +``` + +## Configuration Fields + +| Field | Required | Details | Example | +|---------|----------|---------|---------| +| format | Y | The format string to use for name resolution. Must contain the `{appid}` placeholder which will be replaced with the actual service name. | `"service-{appid}.default.svc.cluster.local"` | + +## Examples + +When configured with `format: "service-{appid}.default.svc.cluster.local"`, the resolver will transform service names as follows: + +- Service ID "myapp" → "service-myapp.default.svc.cluster.local" +- Service ID "frontend" → "service-frontend.default.svc.cluster.local" + + +## Notes + +- Empty service IDs are not allowed and will result in an error +- The format string must be provided in the configuration +- The format string must contain at least one `{appid}` placeholder \ No newline at end of file diff --git a/nameresolution/nameformat/metadata.yaml b/nameresolution/nameformat/metadata.yaml new file mode 100644 index 0000000000..e6ba13eabf --- /dev/null +++ b/nameresolution/nameformat/metadata.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: nameresolution +name: nameformat +version: v1 +status: alpha +title: "Name Format" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-name-resolution/nr-nameformat/ +metadata: + - name: format + type: string + required: true + description: Format string for name resolution. Must contain {appid} placeholder. + example: "service-{appid}.default.svc.cluster.local" + diff --git a/nameresolution/nameformat/nameformat.go b/nameresolution/nameformat/nameformat.go new file mode 100644 index 0000000000..d11df0e622 --- /dev/null +++ b/nameresolution/nameformat/nameformat.go @@ -0,0 +1,94 @@ +/* +Copyright 2025 The Dapr Authors +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 nameformat + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/dapr/components-contrib/metadata" + nr "github.com/dapr/components-contrib/nameresolution" + "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" +) + +// NameFormatResolver implements a name resolution component that formats service names +// according to a configurable pattern. +type NameFormatResolver struct { + format string + logger logger.Logger +} + +// nameFormatMetadata defines the metadata properties for the name format resolver. +type nameFormatMetadata struct { + Format string `mapstructure:"format" description:"Format string for name resolution. Must contain {appid} placeholder."` +} + +// NewResolver creates a new Name Format resolver. +func NewResolver(logger logger.Logger) nr.Resolver { + return &NameFormatResolver{ + logger: logger, + } +} + +// Init initializes the name format resolver with the given metadata. +func (r *NameFormatResolver) Init(ctx context.Context, metadata nr.Metadata) error { + var meta nameFormatMetadata + err := kitmd.DecodeMetadata(metadata.Configuration, &meta) + if err != nil { + return fmt.Errorf("failed to decode metadata: %w", err) + } + + if meta.Format == "" { + return errors.New("format is required in metadata") + } + + // Validate that the format contains the appid placeholder + if !strings.Contains(meta.Format, "{appid}") { + return errors.New("format must contain {appid} placeholder") + } + + // Store the format string + r.format = meta.Format + r.logger.Debugf("Initialized with format: %s", r.format) + + return nil +} + +// ResolveID resolves a service ID to an address using the configured format. +func (r *NameFormatResolver) ResolveID(ctx context.Context, req nr.ResolveRequest) (string, error) { + if req.ID == "" { + return "", errors.New("empty ID not allowed") + } + + // Replace {appid} with the actual ID + resolvedAddress := strings.ReplaceAll(r.format, "{appid}", req.ID) + r.logger.Debugf("Resolved app ID '%s' to address: %s", req.ID, resolvedAddress) + return resolvedAddress, nil +} + +// Close implements io.Closer +func (r *NameFormatResolver) Close() error { + return nil +} + +// GetComponentMetadata returns the metadata information for the component. +func (r *NameFormatResolver) GetComponentMetadata() metadata.MetadataMap { + metadataInfo := metadata.MetadataMap{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(nameFormatMetadata{}), &metadataInfo, metadata.NameResolutionType) + return metadataInfo +} diff --git a/nameresolution/nameformat/nameformat_test.go b/nameresolution/nameformat/nameformat_test.go new file mode 100644 index 0000000000..bd3cab7955 --- /dev/null +++ b/nameresolution/nameformat/nameformat_test.go @@ -0,0 +1,142 @@ +/* +Copyright 2025 The Dapr Authors +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 nameformat + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + nr "github.com/dapr/components-contrib/nameresolution" + "github.com/dapr/kit/logger" +) + +func TestInit(t *testing.T) { + tests := []struct { + name string + metadata nr.Metadata + expectedError string + }{ + { + name: "valid metadata with format", + metadata: nr.Metadata{ + Configuration: map[string]string{ + "format": "service-{appid}.default.svc.cluster.local", + }, + }, + }, + { + name: "valid metadata with multiple placeholders", + metadata: nr.Metadata{ + Configuration: map[string]string{ + "format": "{appid}-service-{appid}.example.com", + }, + }, + }, + { + name: "missing format", + metadata: nr.Metadata{ + Configuration: map[string]string{}, + }, + expectedError: "format is required in metadata", + }, + { + name: "invalid format without placeholder", + metadata: nr.Metadata{ + Configuration: map[string]string{ + "format": "service.example.com", + }, + }, + expectedError: "must contain {appid} placeholder", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")) + err := r.Init(t.Context(), tt.metadata) + + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestResolveID(t *testing.T) { + tests := []struct { + name string + format string + request nr.ResolveRequest + expectedResult string + expectedError string + }{ + { + name: "valid app name with single placeholder", + format: "service-{appid}.default.svc.cluster.local", + request: nr.ResolveRequest{ + ID: "myapp", + }, + expectedResult: "service-myapp.default.svc.cluster.local", + }, + { + name: "valid app name with multiple placeholders", + format: "{appid}-service-{appid}.example.com", + request: nr.ResolveRequest{ + ID: "frontend", + }, + expectedResult: "frontend-service-frontend.example.com", + }, + { + name: "empty app name", + format: "service-{appid}.example.com", + request: nr.ResolveRequest{ + ID: "", + }, + expectedError: "empty ID not allowed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewResolver(logger.NewLogger("test")) + err := r.Init(t.Context(), nr.Metadata{ + Configuration: map[string]string{ + "format": tt.format, + }, + }) + require.NoError(t, err) + + result, err := r.ResolveID(t.Context(), tt.request) + + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} + +func TestClose(t *testing.T) { + r := NewResolver(logger.NewLogger("test")) + err := r.Close() + require.NoError(t, err) +} diff --git a/nameresolution/sqlite/metadata.yaml b/nameresolution/sqlite/metadata.yaml new file mode 100644 index 0000000000..a13241fb94 --- /dev/null +++ b/nameresolution/sqlite/metadata.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: nameresolution +name: sqlite +version: v1 +status: stable +title: "SQLite" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-name-resolution/nr-sqlite/ +metadata: [] \ No newline at end of file diff --git a/pubsub/aws/snssqs/metadata.go b/pubsub/aws/snssqs/metadata.go index 2972172e7c..581259f5b3 100644 --- a/pubsub/aws/snssqs/metadata.go +++ b/pubsub/aws/snssqs/metadata.go @@ -3,11 +3,10 @@ package snssqs import ( "errors" "fmt" + "strings" "github.com/dapr/components-contrib/pubsub" "github.com/dapr/kit/metadata" - - "github.com/aws/aws-sdk-go/aws/endpoints" ) type snsSqsMetadata struct { @@ -25,7 +24,7 @@ type snsSqsMetadata struct { // TODO: rm the alias on region in Dapr 1.17. Region string `json:"region" mapstructure:"region" mapstructurealiases:"awsRegion" mdignore:"true"` // aws partition in which SNS/SQS should create resources. - internalPartition string `mapstructure:"-"` + Partition string `mapstructure:"partition" mdignore:"true"` // name of the queue for this application. The is provided by the runtime as "consumerID". SqsQueueName string `mapstructure:"consumerID" mdignore:"true"` // name of the dead letter queue for this application. @@ -70,6 +69,29 @@ func maskLeft(s string) string { return string(rs) } +// getPartitionFromRegion returns the AWS partition for a given region. +// TODO: @mikeee - remove this partition acquisition. +func getPartitionFromRegion(region string) string { + switch { + case strings.HasPrefix(region, "cn-"): + return "aws-cn" + case strings.HasPrefix(region, "eusc-"): + return "aws-eusc" + case strings.HasPrefix(region, "us-iso-"): + return "aws-iso" + case strings.HasPrefix(region, "us-isob-"): + return "aws-iso-b" + case strings.HasPrefix(region, "eu-isoe-"): + return "aws-iso-e" + case strings.HasPrefix(region, "us-isof-"): + return "aws-iso-f" + case strings.HasPrefix(region, "us-gov-"): + return "aws-us-gov" + default: + return "aws" + } +} + func (s *snsSqs) getSnsSqsMetadata(meta pubsub.Metadata) (*snsSqsMetadata, error) { md := &snsSqsMetadata{ AssetsManagementTimeoutSeconds: assetsManagementDefaultTimeoutSeconds, @@ -85,10 +107,9 @@ func (s *snsSqs) getSnsSqsMetadata(meta pubsub.Metadata) (*snsSqsMetadata, error } if md.Region != "" { - if partition, ok := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), md.Region); ok { - md.internalPartition = partition.ID() - } else { - md.internalPartition = "aws" + // Use an explicitly provided partition if available. + if md.Partition == "" { + md.Partition = getPartitionFromRegion(md.Region) } } diff --git a/pubsub/aws/snssqs/snssqs.go b/pubsub/aws/snssqs/snssqs.go index 374919761d..e358e969d5 100644 --- a/pubsub/aws/snssqs/snssqs.go +++ b/pubsub/aws/snssqs/snssqs.go @@ -212,7 +212,7 @@ func (s *snsSqs) setAwsAccountIDIfNotProvided(parentCtx context.Context) error { } func (s *snsSqs) buildARN(serviceName, entityName string) string { - return fmt.Sprintf("arn:%s:%s:%s:%s:%s", s.metadata.internalPartition, serviceName, s.metadata.Region, s.metadata.AccountID, entityName) + return fmt.Sprintf("arn:%s:%s:%s:%s:%s", s.metadata.Partition, serviceName, s.metadata.Region, s.metadata.AccountID, entityName) } func (s *snsSqs) createTopic(parentCtx context.Context, topic string) (string, error) { diff --git a/pubsub/aws/snssqs/subscription_mgmt.go b/pubsub/aws/snssqs/subscription_mgmt.go index c868d9dccc..480708d867 100644 --- a/pubsub/aws/snssqs/subscription_mgmt.go +++ b/pubsub/aws/snssqs/subscription_mgmt.go @@ -118,7 +118,11 @@ func (sm *SubscriptionManager) queueConsumerController(queueConsumerCbk func(con sm.lock.Unlock() case <-sm.closeCh: - return + if sm.topicsHandlers.Size() == 0 { + return + } else { + sm.logger.Info("Shutdown initiated, waiting for all subscriptions to be cleaned up") + } } } } diff --git a/pubsub/azure/servicebus/topics/servicebus_test.go b/pubsub/azure/servicebus/topics/servicebus_test.go new file mode 100644 index 0000000000..c59b115ce7 --- /dev/null +++ b/pubsub/azure/servicebus/topics/servicebus_test.go @@ -0,0 +1,376 @@ +/* +Copyright 2025 The Dapr Authors +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 topics + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + azservicebus "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + impl "github.com/dapr/components-contrib/common/component/azure/servicebus" + "github.com/dapr/kit/logger" + "github.com/dapr/kit/ptr" +) + +type mockReceiver struct { + messages []*azservicebus.ReceivedMessage + messageIndex int + sessionID string + mu sync.Mutex + closed bool +} + +func newMockReceiver(sessionID string, messages []*azservicebus.ReceivedMessage) *mockReceiver { + return &mockReceiver{ + sessionID: sessionID, + messages: messages, + } +} + +func (m *mockReceiver) ReceiveMessages(ctx context.Context, count int, options *azservicebus.ReceiveMessagesOptions) ([]*azservicebus.ReceivedMessage, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.closed { + return nil, errors.New("receiver closed") + } + + if ctx.Err() != nil { + return nil, ctx.Err() + } + + if m.messageIndex >= len(m.messages) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(100 * time.Millisecond): + return nil, errors.New("no more messages") + } + } + + end := m.messageIndex + count + if end > len(m.messages) { + end = len(m.messages) + } + + result := m.messages[m.messageIndex:end] + m.messageIndex = end + return result, nil +} + +func (m *mockReceiver) CompleteMessage(ctx context.Context, message *azservicebus.ReceivedMessage, options *azservicebus.CompleteMessageOptions) error { + return nil +} + +func (m *mockReceiver) AbandonMessage(ctx context.Context, message *azservicebus.ReceivedMessage, options *azservicebus.AbandonMessageOptions) error { + return nil +} + +func (m *mockReceiver) Close(ctx context.Context) error { + m.mu.Lock() + defer m.mu.Unlock() + m.closed = true + return nil +} + +func TestSessionOrderingWithSingleHandler(t *testing.T) { + const numMessages = 10 + sessionID := "test-session-1" + + messages := make([]*azservicebus.ReceivedMessage, numMessages) + for i := range numMessages { + seqNum := int64(i + 1) + messages[i] = &azservicebus.ReceivedMessage{ + MessageID: fmt.Sprintf("msg-%d", i), + SessionID: &sessionID, + SequenceNumber: &seqNum, + Body: []byte(fmt.Sprintf("message-%d", i)), + } + } + + sub := impl.NewSubscription( + impl.SubscriptionOptions{ + MaxActiveMessages: 100, + TimeoutInSec: 5, + MaxBulkSubCount: ptr.Of(1), + MaxConcurrentHandlers: 1, + Entity: "test-topic", + LockRenewalInSec: 30, + RequireSessions: true, + SessionIdleTimeout: time.Second * 5, + }, + logger.NewLogger("test"), + ) + + var ( + processedOrder []int + orderMu sync.Mutex + ) + + handlerFunc := func(ctx context.Context, msgs []*azservicebus.ReceivedMessage) ([]impl.HandlerResponseItem, error) { + var msgIndex int + _, err := fmt.Sscanf(string(msgs[0].Body), "message-%d", &msgIndex) + require.NoError(t, err) + + time.Sleep(10 * time.Millisecond) + + orderMu.Lock() + processedOrder = append(processedOrder, msgIndex) + orderMu.Unlock() + + return nil, nil + } + + receiver := newMockReceiver(sessionID, messages) + + ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) + defer cancel() + + done := make(chan struct{}) + go func() { + defer close(done) + _ = sub.ReceiveBlocking(ctx, handlerFunc, receiver, func() {}, "test-session") + }() + + <-done + + expectedOrder := make([]int, numMessages) + for i := range expectedOrder { + expectedOrder[i] = i + } + + assert.Equal(t, expectedOrder, processedOrder, "messages must be processed in order") +} + +func TestMultipleSessionsConcurrentHandler(t *testing.T) { + const ( + numSessions = 5 + messagesPerSession = 10 + maxConcurrentLimit = 3 + ) + + sessionIDs := make([]string, numSessions) + for i := range numSessions { + sessionIDs[i] = fmt.Sprintf("session-%d", i) + } + + allMessages := make(map[string][]*azservicebus.ReceivedMessage) + for _, sessionID := range sessionIDs { + messages := make([]*azservicebus.ReceivedMessage, messagesPerSession) + for i := range messagesPerSession { + seqNum := int64(i + 1) + sessID := sessionID + messages[i] = &azservicebus.ReceivedMessage{ + MessageID: fmt.Sprintf("%s-msg-%d", sessionID, i), + SessionID: &sessID, + SequenceNumber: &seqNum, + Body: []byte(fmt.Sprintf("%s:%d", sessionID, i)), + } + } + allMessages[sessionID] = messages + } + + sub := impl.NewSubscription( + impl.SubscriptionOptions{ + MaxActiveMessages: 100, + TimeoutInSec: 5, + MaxBulkSubCount: ptr.Of(1), + MaxConcurrentHandlers: maxConcurrentLimit, + Entity: "test-topic", + LockRenewalInSec: 30, + RequireSessions: true, + SessionIdleTimeout: time.Second * 5, + }, + logger.NewLogger("test"), + ) + + // Track processing times and active messages per session + type messageProcessing struct { + sessionID string + messageID string + seqNum int64 + startTime time.Time + endTime time.Time + msgIndex int + } + + var ( + mu sync.Mutex + globalOrder []string + sessionOrders = make(map[string][]int) + concurrentHandlers atomic.Int32 + maxConcurrentHandlers atomic.Int32 + processingLog []messageProcessing + activePerSession = make(map[string]int32) + parallelViolations atomic.Int32 + ) + + handlerFunc := func(ctx context.Context, msgs []*azservicebus.ReceivedMessage) ([]impl.HandlerResponseItem, error) { + msg := msgs[0] + sessionID := *msg.SessionID + startTime := time.Now() + + // Track concurrent processing within the same session + mu.Lock() + activeCount := activePerSession[sessionID] + if activeCount > 0 { + // Another message from this session is already being processed + parallelViolations.Add(1) + t.Errorf("Session %s has %d messages processing in parallel at %v", + sessionID, activeCount+1, startTime) + } + activePerSession[sessionID]++ + mu.Unlock() + + current := concurrentHandlers.Add(1) + defer func() { + concurrentHandlers.Add(-1) + mu.Lock() + activePerSession[sessionID]-- + mu.Unlock() + }() + + for { + max := maxConcurrentHandlers.Load() + if current <= max || maxConcurrentHandlers.CompareAndSwap(max, current) { + break + } + } + + var msgIndex int + parts := strings.Split(string(msg.Body), ":") + require.Len(t, parts, 2) + _, err := fmt.Sscanf(parts[1], "%d", &msgIndex) + require.NoError(t, err) + + time.Sleep(50 * time.Millisecond) + + endTime := time.Now() + + mu.Lock() + globalOrder = append(globalOrder, sessionID) + sessionOrders[sessionID] = append(sessionOrders[sessionID], msgIndex) + processingLog = append(processingLog, messageProcessing{ + sessionID: sessionID, + messageID: msg.MessageID, + seqNum: *msg.SequenceNumber, + startTime: startTime, + endTime: endTime, + msgIndex: msgIndex, + }) + mu.Unlock() + + return nil, nil + } + + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + + var wg sync.WaitGroup + for _, sessionID := range sessionIDs { + wg.Add(1) + go func() { + defer wg.Done() + receiver := newMockReceiver(sessionID, allMessages[sessionID]) + done := make(chan struct{}) + go func() { + defer close(done) + _ = sub.ReceiveBlocking(ctx, handlerFunc, receiver, func() {}, "session-"+sessionID) + }() + <-done + }() + } + + wg.Wait() + + // Verify no parallel session processing was detected + assert.Equal(t, int32(0), parallelViolations.Load(), + "N messages from the same session should process in parallel") + + // Verify FIFO ordering per session + for _, sessionID := range sessionIDs { + order := sessionOrders[sessionID] + require.Len(t, order, messagesPerSession, "session %s should process all messages", sessionID) + + for i := range messagesPerSession { + assert.Equal(t, i, order[i], "session %s message %d out of order", sessionID, i) + } + } + + // Verify strict ordering, message N+1 must start after message N ends + sessionProcessingTimes := make(map[string][]messageProcessing) + for _, proc := range processingLog { + sessionProcessingTimes[proc.sessionID] = append(sessionProcessingTimes[proc.sessionID], proc) + } + + for sessionID, procs := range sessionProcessingTimes { + require.Len(t, procs, messagesPerSession, "session %s should have all processing records", sessionID) + + // Sort by message index to ensure we check in FIFO order + sortedProcs := make([]messageProcessing, len(procs)) + for _, proc := range procs { + sortedProcs[proc.msgIndex] = proc + } + + // Verify each message starts after the previous one completes + for i := 1; i < len(sortedProcs); i++ { + prev := sortedProcs[i-1] + curr := sortedProcs[i] + + assert.False(t, curr.startTime.Before(prev.endTime), + "Session %s: message %d started at %v before message %d ended at %v (overlap detected)", + sessionID, curr.msgIndex, curr.startTime, prev.msgIndex, prev.endTime) + + // Also verify sequence numbers are strictly increasing + assert.Equal(t, prev.seqNum+1, curr.seqNum, + "Session %s: sequence numbers should be consecutive", sessionID) + } + } + + // Verify concurrent handler limits + assert.LessOrEqual(t, maxConcurrentHandlers.Load(), int32(maxConcurrentLimit), + "concurrent handlers should not exceed configured maximum") + + assert.Greater(t, maxConcurrentHandlers.Load(), int32(1), + "multiple handlers should run concurrently across sessions") + + // Check global order to prove concurrent processing + // If processed sequentially, all messages from one session would come before the next + // If processed concurrently, session IDs will be interleaved + hasInterleaving := false + seenSessions := make(map[string]bool) + lastSession := "" + + for _, sessionID := range globalOrder { + if sessionID != lastSession && seenSessions[sessionID] { + // We've seen this session before but with a different session in between + hasInterleaving = true + break + } + seenSessions[sessionID] = true + lastSession = sessionID + } + + assert.True(t, hasInterleaving, + "global order must show session interleaving, proving concurrent processing across sessions") +} diff --git a/pubsub/in-memory/in-memory.go b/pubsub/in-memory/in-memory.go index bbd4db6244..5ffb8a2573 100644 --- a/pubsub/in-memory/in-memory.go +++ b/pubsub/in-memory/in-memory.go @@ -18,7 +18,6 @@ import ( "errors" "sync" "sync/atomic" - "time" "github.com/dapr/components-contrib/common/eventbus" "github.com/dapr/components-contrib/metadata" @@ -64,7 +63,7 @@ func (a *bus) Publish(_ context.Context, req *pubsub.PublishRequest) error { return errors.New("component is closed") } - a.bus.Publish(req.Topic, req.Data) + a.bus.Publish(req.Topic, req.Data, req.Metadata) return nil } @@ -74,23 +73,14 @@ func (a *bus) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, handle return errors.New("component is closed") } - // For this component we allow built-in retries because it is backed by memory - retryHandler := func(data []byte) { - for range 10 { - handleErr := handler(ctx, &pubsub.NewMessage{Data: data, Topic: req.Topic, Metadata: req.Metadata}) - if handleErr == nil { - break - } - a.log.Error(handleErr) - select { - case <-time.After(100 * time.Millisecond): - // Nop - case <-ctx.Done(): - return - } + loghandler := func(data []byte, md map[string]string) { + err := handler(ctx, &pubsub.NewMessage{Data: data, Topic: req.Topic, Metadata: md}) + if err != nil { + a.log.Error(err) } } - err := a.bus.SubscribeAsync(req.Topic, retryHandler, true) + + err := a.bus.SubscribeAsync(req.Topic, loghandler, true) if err != nil { return err } @@ -103,7 +93,7 @@ func (a *bus) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, handle case <-ctx.Done(): case <-a.closeCh: } - err := a.bus.Unsubscribe(req.Topic, retryHandler) + err := a.bus.Unsubscribe(req.Topic, loghandler) if err != nil { a.log.Errorf("error while unsubscribing from topic %s: %v", req.Topic, err) } diff --git a/pubsub/in-memory/in-memory_test.go b/pubsub/in-memory/in-memory_test.go index 597c6cb459..ca7cf79f83 100644 --- a/pubsub/in-memory/in-memory_test.go +++ b/pubsub/in-memory/in-memory_test.go @@ -15,7 +15,6 @@ package inmemory import ( "context" - "errors" "testing" "github.com/stretchr/testify/assert" @@ -81,25 +80,24 @@ func TestWildcards(t *testing.T) { assert.Equal(t, "3", string(<-ch2)) } -func TestRetry(t *testing.T) { +func TestMessageMetadataPropagation(t *testing.T) { bus := New(logger.NewLogger("test")) bus.Init(t.Context(), pubsub.Metadata{}) ch := make(chan []byte) - i := -1 - + metadataCh := make(chan map[string]string) bus.Subscribe(t.Context(), pubsub.SubscribeRequest{Topic: "demo"}, func(ctx context.Context, msg *pubsub.NewMessage) error { - i++ - if i < 5 { - return errors.New("if at first you don't succeed") - } - - return publish(ch, msg) + return publishWithMetadata(ch, metadataCh, msg) }) - bus.Publish(t.Context(), &pubsub.PublishRequest{Data: []byte("ABCD"), Topic: "demo"}) + bus.Publish(t.Context(), &pubsub.PublishRequest{Data: []byte("ABCD"), Metadata: map[string]string{ + "test": "test", + }, Topic: "demo"}) + assert.Equal(t, "ABCD", string(<-ch)) - assert.Equal(t, 5, i) + assert.Equal(t, map[string]string{ + "test": "test", + }, <-metadataCh) } func publish(ch chan []byte, msg *pubsub.NewMessage) error { @@ -107,3 +105,14 @@ func publish(ch chan []byte, msg *pubsub.NewMessage) error { return nil } + +func publishWithMetadata(ch chan []byte, mdCh chan map[string]string, msg *pubsub.NewMessage) error { + err := publish(ch, msg) + if err != nil { + return err + } + + go func() { mdCh <- msg.Metadata }() + + return nil +} diff --git a/pubsub/jetstream/metadata.yaml b/pubsub/jetstream/metadata.yaml index 0124f6a525..bfbc3b66af 100644 --- a/pubsub/jetstream/metadata.yaml +++ b/pubsub/jetstream/metadata.yaml @@ -95,53 +95,53 @@ metadata: description: The queue group name for load balancing. example: "my-queue-group" - name: startSequence - type: integer + type: number required: false description: The starting sequence number for message delivery. - example: 1 + example: "1" - name: startTime - type: integer + type: number required: false description: The starting time (Unix timestamp) for message delivery. - example: 1640995200 - default: 0 + example: "1640995200" + default: "0" - name: flowControl type: bool required: false description: Enable flow control for the consumer. - example: false - default: false + example: "false" + default: "false" - name: ackWait type: string required: false description: The acknowledgment wait time. example: "30s" - name: maxDeliver - type: integer + type: number required: false description: The maximum number of message deliveries. - example: 5 + example: "5" - name: maxAckPending - type: integer + type: number required: false description: The maximum number of unacknowledged messages. - example: 100 + example: "100" - name: replicas - type: integer + type: number required: false description: The number of stream replicas. - example: 3 + example: "3" - name: memoryStorage type: bool required: false description: Use memory storage for the stream. - example: false - default: false + example: "false" + default: "false" - name: rateLimit - type: integer + type: number required: false description: The rate limit for message consumption. - example: 1000 + example: "1000" - name: heartbeat type: string required: false @@ -185,12 +185,7 @@ metadata: example: "single" default: "single" - name: backOff - type: array + type: string required: false description: The backoff configuration for message delivery for the consumer. example: "[1s, 2s, 4s]" - - name: maxAckPending - type: integer - required: false - description: The maximum number of unacknowledged messages for the consumer. - example: 100 diff --git a/pubsub/kafka/metadata.yaml b/pubsub/kafka/metadata.yaml index 04d9955012..1f1d298e48 100644 --- a/pubsub/kafka/metadata.yaml +++ b/pubsub/kafka/metadata.yaml @@ -27,7 +27,7 @@ builtinAuthenticationProfiles: type: string required: false description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'accessKey' instead. If both fields are set, then 'accessKey' value will be used. AWS access key associated with an IAM account. @@ -37,7 +37,7 @@ builtinAuthenticationProfiles: required: false sensitive: true description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'secretKey' instead. If both fields are set, then 'secretKey' value will be used. The secret key associated with the access key. @@ -46,7 +46,7 @@ builtinAuthenticationProfiles: type: string sensitive: true description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'sessionToken' instead. If both fields are set, then 'sessionToken' value will be used. AWS session token to use. A session token is only required if you are using temporary security credentials. @@ -55,7 +55,7 @@ builtinAuthenticationProfiles: type: string required: false description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'assumeRoleArn' instead. If both fields are set, then 'assumeRoleArn' value will be used. IAM role that has access to MSK. This is another option to authenticate with MSK aside from the AWS Credentials. @@ -63,7 +63,7 @@ builtinAuthenticationProfiles: - name: awsStsSessionName type: string description: | - This maintains backwards compatibility with existing fields. + This maintains backwards compatibility with existing fields. It will be deprecated as of Dapr 1.17. Use 'sessionName' instead. If both fields are set, then 'sessionName' value will be used. Represents the session name for assuming a role. @@ -72,7 +72,7 @@ builtinAuthenticationProfiles: authenticationProfiles: - title: "OIDC Authentication" description: | - Authenticate using OpenID Connect. + Authenticate using OpenID Connect providing a client secret. metadata: - name: authType type: string @@ -115,6 +115,78 @@ authenticationProfiles: example: | {"cluster":"kafka","poolid":"kafkapool"} type: string + - title: "OIDC Private Key JWT Authentication" + description: | + Authenticate using OpenID Connect providing a client certificate and private key. + metadata: + - name: authType + type: string + required: true + description: | + Authentication type. + This must be set to "oidc_private_key_jwt" for this authentication profile. + example: '"oidc_private_key_jwt"' + allowedValues: + - "oidc_private_key_jwt" + - name: oidcTokenEndpoint + type: string + required: true + description: | + URL of the OAuth2 identity provider access token endpoint. + example: '"https://identity.example.com/v1/token"' + - name: oidcClientID + description: | + The OAuth2 client ID that has been provisioned in the identity provider. + example: '"my-client-id"' + type: string + required: true + - name: oidcClientAssertionCert + type: string + required: true + description: | + PEM-encoded X.509 certificate used to advertise the client certificate in the x5c header. + example: | + -----BEGIN CERTIFICATE-----\n... + - name: oidcClientAssertionKey + type: string + required: true + sensitive: true + description: | + PEM-encoded private key used to sign the client certificate. + example: | + -----BEGIN PRIVATE KEY-----\n... + - name: oidcResource + type: string + required: false + description: | + Optional OAuth2 resource (audience) parameter to include in the token request when required by the identity provider. + example: '"api://kafka"' + - name: oidcAudience + type: string + required: false + description: | + Overrides the JWT client assertion audience (aud). If not set, the component uses the + issuer derived from the token endpoint URL when available; otherwise, it falls back to the token URL. + example: '"http:///realms/local"' + - name: oidcScopes + type: string + description: | + Comma-delimited list of OAuth2/OIDC scopes to request with the access token. + Although not required, this field is recommended. + example: '"openid,kafka-prod"' + default: '"openid"' + - name: oidcExtensions + description: | + String containing a JSON-encoded dictionary of OAuth2/OIDC extensions to request with the access token. + example: | + {"cluster":"kafka","poolid":"kafkapool"} + type: string + - name: oidcKid + type: string + required: false + description: | + The JWT key ID (kid) to use for the client assertion. + example: '"1234567890"' - title: "SASL Authentication" description: | Authenticate using SASL. @@ -339,10 +411,17 @@ metadata: type: bool required: false description: | - Enables URL escaping of the message header values. + Enables URL escaping of the message header values. It allows sending headers with special characters that are usually not allowed in HTTP headers. example: "true" default: "false" + - name: useAvroJSON + type: bool + required: false + description: | + Enables Avro JSON schema for serialization. Only applicable when the subscription uses valueSchemaType=Avro + example: "true" + default: "false" - name: compression type: string required: false diff --git a/pubsub/kubemq/metadata.yaml b/pubsub/kubemq/metadata.yaml index b4acab0ff7..c37a35803f 100644 --- a/pubsub/kubemq/metadata.yaml +++ b/pubsub/kubemq/metadata.yaml @@ -37,11 +37,11 @@ metadata: type: bool required: false description: Whether to use KubeMQ Event Store (true) or Events (false). - example: true - default: true + example: "true" + default: "true" - name: disableReDelivery type: bool required: false description: Disable message re-delivery on error. - example: false - default: false + example: "false" + default: "false" diff --git a/pubsub/pulsar/metadata.go b/pubsub/pulsar/metadata.go index c373005425..5f1f4b5632 100644 --- a/pubsub/pulsar/metadata.go +++ b/pubsub/pulsar/metadata.go @@ -16,6 +16,8 @@ package pulsar import ( "time" + goavro "github.com/linkedin/goavro/v2" + "github.com/dapr/components-contrib/common/authentication/oauth2" ) @@ -42,10 +44,13 @@ type pulsarMetadata struct { ReplicateSubscriptionState bool `mapstructure:"replicateSubscriptionState"` SubscriptionMode string `mapstructure:"subscribeMode"` Token string `mapstructure:"token"` + CompressionType string `mapstructure:"compressionType"` + CompressionLevel string `mapstructure:"compressionLevel"` oauth2.ClientCredentialsMetadata `mapstructure:",squash"` } type schemaMetadata struct { protocol string value string + codec *goavro.Codec // cached Avro codec, compiled once at init } diff --git a/pubsub/pulsar/metadata.yaml b/pubsub/pulsar/metadata.yaml index 72c5fd5a30..e496df39f5 100644 --- a/pubsub/pulsar/metadata.yaml +++ b/pubsub/pulsar/metadata.yaml @@ -31,6 +31,16 @@ authenticationProfiles: sensitive: true description: | The OAuth Client Secret. + - name: oauth2ClientSecretPath + type: string + description: | + The path to a plain text file containing the OAuth Client Secret. + example: "/path/to/oauth2/client_secret.txt" + - name: oauth2CredentialsFile + type: string + description: | + The path to a JSON file containing both client_id and client_secret. + example: "/path/to/oauth2/credentials.json" - name: oauth2TokenCAPEM type: string description: | @@ -108,6 +118,32 @@ metadata: type: number default: '"131072" (128 KB)' example: '"131072"' + - name: compressionType + type: string + description: | + Sets the compression type for messages sent by the producer. + Compression can help reduce message size and improve throughput for large messages. + default: '"none"' + example: '"lz4"' + allowedValues: + - none + - lz4 + - zlib + - zstd + url: + title: "Pulsar Message Compression" + url: "https://pulsar.apache.org/docs/3.0.x/concepts-messaging/#compression" + - name: compressionLevel + type: string + description: | + Sets the compression level when compressionType is enabled. + Higher compression levels provide better compression ratios but require more CPU resources. + default: '"default"' + example: '"faster"' + allowedValues: + - default + - faster + - better - name: publicKey type: string description: | @@ -221,4 +257,4 @@ metadata: example: '"durable"' url: title: "Pulsar SubscriptionMode" - url: "https://pkg.go.dev/github.com/apache/pulsar-client-go/pulsar#SubscriptionMode" + url: "https://pkg.go.dev/github.com/apache/pulsar-client-go/pulsar#SubscriptionMode" \ No newline at end of file diff --git a/pubsub/pulsar/pulsar.go b/pubsub/pulsar/pulsar.go index 1ce44dc209..9709b4a379 100644 --- a/pubsub/pulsar/pulsar.go +++ b/pubsub/pulsar/pulsar.go @@ -27,8 +27,8 @@ import ( "github.com/apache/pulsar-client-go/pulsar" "github.com/apache/pulsar-client-go/pulsar/crypto" - "github.com/hamba/avro/v2" lru "github.com/hashicorp/golang-lru/v2" + goavro "github.com/linkedin/goavro/v2" "github.com/dapr/components-contrib/common/authentication/oauth2" "github.com/dapr/components-contrib/metadata" @@ -104,24 +104,40 @@ const ( subscribeModeDurable = "durable" subscribeModeNonDurable = "non_durable" + + compressionTypeKey = "compressionType" + compressionLevelKey = "compressionLevel" + + compressionTypeNone = "none" + compressionTypeLZ4 = "lz4" + compressionTypeZLib = "zlib" + compressionTypeZSTD = "zstd" + + compressionLevelDefault = "default" + compressionLevelFaster = "faster" + compressionLevelBetter = "better" ) type ProcessMode string type Pulsar struct { - logger logger.Logger - client pulsar.Client - metadata pulsarMetadata - cache *lru.Cache[string, pulsar.Producer] - closed atomic.Bool - closeCh chan struct{} - wg sync.WaitGroup + logger logger.Logger + client pulsar.Client + metadata pulsarMetadata + cache *lru.Cache[string, pulsar.Producer] + closed atomic.Bool + closeCh chan struct{} + wg sync.WaitGroup + newClientFn pulsarClientFactory } +type pulsarClientFactory func(pulsar.ClientOptions) (pulsar.Client, error) + func NewPulsar(l logger.Logger) pubsub.PubSub { return &Pulsar{ - logger: l, - closeCh: make(chan struct{}), + logger: l, + closeCh: make(chan struct{}), + newClientFn: pulsar.NewClient, } } @@ -164,6 +180,16 @@ func parsePulsarMetadata(meta pubsub.Metadata) (*pulsarMetadata, error) { return nil, errors.New("invalid subscription mode") } + m.CompressionType, err = parseCompressionType(meta.Properties[compressionTypeKey]) + if err != nil { + return nil, errors.New("invalid compression type. Accepted values are `none`, `lz4`, `zlib` and `zstd`") + } + + m.CompressionLevel, err = parseCompressionLevel(meta.Properties[compressionLevelKey]) + if err != nil { + return nil, errors.New("invalid compression level. Accepted values are `default`, `faster` and `better`") + } + for k, v := range meta.Properties { switch { case strings.HasSuffix(k, topicJSONSchemaIdentifier): @@ -174,9 +200,14 @@ func parsePulsarMetadata(meta pubsub.Metadata) (*pulsarMetadata, error) { } case strings.HasSuffix(k, topicAvroSchemaIdentifier): topic := k[:len(k)-len(topicAvroSchemaIdentifier)] + codec, codecErr := goavro.NewCodecForStandardJSONFull(v) + if codecErr != nil { + return nil, fmt.Errorf("failed to parse avro schema for topic %q: %w", topic, codecErr) + } m.internalTopicSchemas[topic] = schemaMetadata{ protocol: avroProtocol, value: v, + codec: codec, } case strings.HasSuffix(k, topicProtoSchemaIdentifier): topic := k[:len(k)-len(topicProtoSchemaIdentifier)] @@ -187,6 +218,11 @@ func parsePulsarMetadata(meta pubsub.Metadata) (*pulsarMetadata, error) { } } + // Resolve credentials from file if ClientSecretPath is set + if err := m.ClientCredentialsMetadata.ResolveCredentials(); err != nil { + return nil, err + } + return &m, nil } @@ -209,24 +245,16 @@ func (p *Pulsar) Init(ctx context.Context, metadata pubsub.Metadata) error { case len(m.Token) > 0: options.Authentication = pulsar.NewAuthenticationToken(m.Token) case len(m.ClientCredentialsMetadata.TokenURL) > 0: - var cc *oauth2.ClientCredentials - cc, err = oauth2.NewClientCredentials(ctx, oauth2.ClientCredentialsOptions{ - Logger: p.logger, - TokenURL: m.ClientCredentialsMetadata.TokenURL, - CAPEM: []byte(m.ClientCredentialsMetadata.TokenCAPEM), - ClientID: m.ClientCredentialsMetadata.ClientID, - ClientSecret: m.ClientCredentialsMetadata.ClientSecret, - Scopes: m.ClientCredentialsMetadata.Scopes, - Audiences: m.ClientCredentialsMetadata.Audiences, - }) + credsOpts := m.ClientCredentialsMetadata.ToOptions(p.logger) + var cliCreds *oauth2.ClientCredentials + cliCreds, err = oauth2.NewClientCredentials(ctx, credsOpts) if err != nil { return fmt.Errorf("could not instantiate oauth2 token provider: %w", err) } - - options.Authentication = pulsar.NewAuthenticationTokenFromSupplier(cc.Token) + options.Authentication = pulsar.NewAuthenticationTokenFromSupplier(cliCreds.Token) } - client, err := pulsar.NewClient(options) + client, err := p.newClientFn(options) if err != nil { return fmt.Errorf("could not instantiate pulsar client: %v", err) } @@ -297,6 +325,8 @@ func (p *Pulsar) Publish(ctx context.Context, req *pubsub.PublishRequest) error BatchingMaxPublishDelay: p.metadata.BatchingMaxPublishDelay, BatchingMaxMessages: p.metadata.BatchingMaxMessages, BatchingMaxSize: p.metadata.BatchingMaxSize, + CompressionType: getCompressionType(p.metadata.CompressionType), + CompressionLevel: getCompressionLevel(p.metadata.CompressionLevel), } if hasSchema { @@ -368,18 +398,15 @@ func parsePublishMetadata(req *pubsub.PublishRequest, schema schemaMetadata) ( msg.Value = obj case avroProtocol: - var obj interface{} - avroSchema, parseErr := avro.Parse(schema.value) - if parseErr != nil { - return nil, parseErr - } - - err = avro.Unmarshal(avroSchema, req.Data, &obj) - if err != nil { - return nil, err + // Use the cached goavro codec (compiled once at init) to validate JSON + // against the Avro schema. NativeFromTextual parses JSON and validates it + // in one step — if the data doesn't conform, it returns an error. + native, _, nativeErr := schema.codec.NativeFromTextual(req.Data) + if nativeErr != nil { + return nil, fmt.Errorf("avro schema validation failed: %w", nativeErr) } - msg.Value = obj + msg.Value = native } for name, value := range req.Metadata { @@ -486,6 +513,54 @@ func getSubscriptionMode(subsModeStr string) pulsar.SubscriptionMode { } } +func parseCompressionType(in string) (string, error) { + compType := strings.ToLower(in) + switch compType { + case compressionTypeNone, compressionTypeLZ4, compressionTypeZLib, compressionTypeZSTD: + return compType, nil + case "": + return compressionTypeNone, nil + default: + return "", fmt.Errorf("invalid compression type: %s", compType) + } +} + +func getCompressionType(compTypeStr string) pulsar.CompressionType { + switch compTypeStr { + case compressionTypeLZ4: + return pulsar.LZ4 + case compressionTypeZLib: + return pulsar.ZLib + case compressionTypeZSTD: + return pulsar.ZSTD + default: + return pulsar.NoCompression + } +} + +func parseCompressionLevel(in string) (string, error) { + compLevel := strings.ToLower(in) + switch compLevel { + case compressionLevelDefault, compressionLevelFaster, compressionLevelBetter: + return compLevel, nil + case "": + return compressionLevelDefault, nil + default: + return "", fmt.Errorf("invalid compression level: %s", compLevel) + } +} + +func getCompressionLevel(compLevelStr string) pulsar.CompressionLevel { + switch compLevelStr { + case compressionLevelFaster: + return pulsar.Faster + case compressionLevelBetter: + return pulsar.Better + default: + return pulsar.Default + } +} + func (p *Pulsar) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, handler pubsub.Handler) error { if p.closed.Load() { return errors.New("component is closed") @@ -504,8 +579,8 @@ func (p *Pulsar) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, han Topic: topic, SubscriptionName: p.metadata.ConsumerID, Type: getSubscribeType(subscribeType), - SubscriptionInitialPosition: getSubscribePosition(subscribeInitialPosition), - SubscriptionMode: getSubscriptionMode(subscribeMode), + SubscriptionInitialPosition: getSubscribePosition(p.metadata.SubscriptionInitialPosition), + SubscriptionMode: getSubscriptionMode(p.metadata.SubscriptionMode), MessageChannel: channel, NackRedeliveryDelay: p.metadata.RedeliveryDelay, ReceiverQueueSize: p.metadata.ReceiverQueueSize, diff --git a/pubsub/pulsar/pulsar_test.go b/pubsub/pulsar/pulsar_test.go index eff822515b..0add0e5668 100644 --- a/pubsub/pulsar/pulsar_test.go +++ b/pubsub/pulsar/pulsar_test.go @@ -14,16 +14,37 @@ limitations under the License. package pulsar import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" "testing" "time" "github.com/apache/pulsar-client-go/pulsar" + goavro "github.com/linkedin/goavro/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/pubsub" + "github.com/dapr/kit/logger" ) +// newAvroSchemaMetadata creates a schemaMetadata with a pre-compiled goavro codec, +// matching the production path where codecs are compiled once at init. +func newAvroSchemaMetadata(t *testing.T, avroSchemaJSON string) schemaMetadata { + t.Helper() + codec, err := goavro.NewCodecForStandardJSONFull(avroSchemaJSON) + require.NoError(t, err, "failed to compile test avro schema") + return schemaMetadata{ + protocol: avroProtocol, + value: avroSchemaJSON, + codec: codec, + } +} + func TestParsePulsarMetadata(t *testing.T) { m := pubsub.Metadata{} m.Properties = map[string]string{ @@ -326,6 +347,13 @@ func TestParsePulsarMetadataSubscriptionCombination(t *testing.T) { } } +// Simple valid Avro schemas for metadata parsing tests. +const ( + testAvroSchema1 = `{"type":"record","name":"S1","fields":[{"name":"id","type":"int"}]}` + testAvroSchema2 = `{"type":"record","name":"S2","fields":[{"name":"id","type":"int"}]}` + testAvroSchema3 = `{"type":"record","name":"S3","fields":[{"name":"id","type":"int"}]}` +) + func TestParsePulsarSchemaMetadata(t *testing.T) { t.Run("test json", func(t *testing.T) { m := pubsub.Metadata{} @@ -347,23 +375,25 @@ func TestParsePulsarSchemaMetadata(t *testing.T) { m := pubsub.Metadata{} m.Properties = map[string]string{ "host": "a", - "obiwan.avroschema": "1", - "kenobi.avroschema.avroschema": "2", + "obiwan.avroschema": testAvroSchema1, + "kenobi.avroschema.avroschema": testAvroSchema2, } meta, err := parsePulsarMetadata(m) require.NoError(t, err) assert.Equal(t, "a", meta.Host) assert.Len(t, meta.internalTopicSchemas, 2) - assert.Equal(t, "1", meta.internalTopicSchemas["obiwan"].value) - assert.Equal(t, "2", meta.internalTopicSchemas["kenobi.avroschema"].value) + assert.JSONEq(t, testAvroSchema1, meta.internalTopicSchemas["obiwan"].value) + assert.NotNil(t, meta.internalTopicSchemas["obiwan"].codec) + assert.JSONEq(t, testAvroSchema2, meta.internalTopicSchemas["kenobi.avroschema"].value) + assert.NotNil(t, meta.internalTopicSchemas["kenobi.avroschema"].codec) }) t.Run("test proto", func(t *testing.T) { m := pubsub.Metadata{} m.Properties = map[string]string{ "host": "a", - "obiwan.avroschema": "1", + "obiwan.avroschema": testAvroSchema1, "kenobi.protoschema.protoschema": "2", } meta, err := parsePulsarMetadata(m) @@ -371,7 +401,7 @@ func TestParsePulsarSchemaMetadata(t *testing.T) { require.NoError(t, err) assert.Equal(t, "a", meta.Host) assert.Len(t, meta.internalTopicSchemas, 2) - assert.Equal(t, "1", meta.internalTopicSchemas["obiwan"].value) + assert.JSONEq(t, testAvroSchema1, meta.internalTopicSchemas["obiwan"].value) assert.Equal(t, "2", meta.internalTopicSchemas["kenobi.protoschema"].value) }) @@ -379,7 +409,7 @@ func TestParsePulsarSchemaMetadata(t *testing.T) { m := pubsub.Metadata{} m.Properties = map[string]string{ "host": "a", - "obiwan.avroschema": "1", + "obiwan.avroschema": testAvroSchema1, "kenobi.jsonschema": "2", } meta, err := parsePulsarMetadata(m) @@ -387,7 +417,7 @@ func TestParsePulsarSchemaMetadata(t *testing.T) { require.NoError(t, err) assert.Equal(t, "a", meta.Host) assert.Len(t, meta.internalTopicSchemas, 2) - assert.Equal(t, "1", meta.internalTopicSchemas["obiwan"].value) + assert.JSONEq(t, testAvroSchema1, meta.internalTopicSchemas["obiwan"].value) assert.Equal(t, "2", meta.internalTopicSchemas["kenobi"].value) assert.Equal(t, avroProtocol, meta.internalTopicSchemas["obiwan"].protocol) assert.Equal(t, jsonProtocol, meta.internalTopicSchemas["kenobi"].protocol) //nolint:testifylint @@ -397,7 +427,7 @@ func TestParsePulsarSchemaMetadata(t *testing.T) { m := pubsub.Metadata{} m.Properties = map[string]string{ "host": "a", - "obiwan.avroschema": "1", + "obiwan.avroschema": testAvroSchema1, "kenobi.jsonschema": "2", "darth.protoschema": "3", } @@ -406,7 +436,7 @@ func TestParsePulsarSchemaMetadata(t *testing.T) { require.NoError(t, err) assert.Equal(t, "a", meta.Host) assert.Len(t, meta.internalTopicSchemas, 3) - assert.Equal(t, "1", meta.internalTopicSchemas["obiwan"].value) + assert.JSONEq(t, testAvroSchema1, meta.internalTopicSchemas["obiwan"].value) assert.Equal(t, "2", meta.internalTopicSchemas["kenobi"].value) assert.Equal(t, "3", meta.internalTopicSchemas["darth"].value) assert.Equal(t, avroProtocol, meta.internalTopicSchemas["obiwan"].protocol) @@ -418,14 +448,15 @@ func TestParsePulsarSchemaMetadata(t *testing.T) { m := pubsub.Metadata{} m.Properties = map[string]string{ "host": "a", - "obiwan.jsonschema.avroschema": "1", + "obiwan.jsonschema.avroschema": testAvroSchema1, } meta, err := parsePulsarMetadata(m) require.NoError(t, err) assert.Equal(t, "a", meta.Host) assert.Len(t, meta.internalTopicSchemas, 1) - assert.Equal(t, "1", meta.internalTopicSchemas["obiwan.jsonschema"].value) + assert.JSONEq(t, testAvroSchema1, meta.internalTopicSchemas["obiwan.jsonschema"].value) + assert.NotNil(t, meta.internalTopicSchemas["obiwan.jsonschema"].codec) }) } @@ -464,6 +495,642 @@ func TestParsePublishMetadata(t *testing.T) { msg.DeliverAt.Format(time.RFC3339)) } +func TestParsePublishMetadataAvroSchemaValidation(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Student", + "namespace": "test", + "fields": [ + {"name": "studentId", "type": "int"}, + {"name": "studentName", "type": "string"}, + {"name": "age", "type": "int"} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("valid message", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"studentId": 1, "studentName": "John", "age": 25}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + assert.NotNil(t, msg.Value) + }) + + t.Run("invalid type for age field", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"studentId": 1, "studentName": "John", "age": "not_a_number"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + assert.Contains(t, err.Error(), "age") + }) + + t.Run("missing required field", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"studentId": 1, "studentName": "John"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("wrong type for studentName field", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"studentId": 1, "studentName": 123, "age": 25}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + assert.Contains(t, err.Error(), "studentName") + }) + + t.Run("invalid JSON payload", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`not valid json`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("floating-point value for int field", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"studentId": 1, "studentName": "John", "age": 25.5}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaWithNullableFields(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Person", + "namespace": "test", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "nickname", "type": ["null", "string"], "default": null} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("nullable field with null value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "John", "nickname": null}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("nullable field with string value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "John", "nickname": "Johnny"}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("nullable field omitted with default", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "John"}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("nullable field with wrong type", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "John", "nickname": 123}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaWithNestedRecord(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Enrollment", + "namespace": "test", + "fields": [ + {"name": "id", "type": "int"}, + {"name": "student", "type": { + "type": "record", + "name": "Student", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "age", "type": "int"} + ] + }} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("valid nested record", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"id": 1, "student": {"name": "John", "age": 25}}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("invalid nested record type", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"id": 1, "student": {"name": "John", "age": "twenty"}}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "age") + }) + + t.Run("nested record not an object", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"id": 1, "student": "not_an_object"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaWithArrays(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Classroom", + "namespace": "test", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "scores", "type": {"type": "array", "items": "int"}} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("valid array", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "Math", "scores": [90, 85, 95]}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("invalid array element type", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "Math", "scores": [90, "eighty-five", 95]}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaWithMap(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Config", + "namespace": "test", + "fields": [ + {"name": "settings", "type": {"type": "map", "values": "string"}} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("valid map", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"settings": {"key1": "value1", "key2": "value2"}}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("invalid map value type", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"settings": {"key1": 123}}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("map field is not an object", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"settings": "not_a_map"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaWithEnum(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Shirt", + "namespace": "test", + "fields": [ + {"name": "color", "type": {"type": "enum", "name": "Color", "symbols": ["RED", "GREEN", "BLUE"]}} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("valid enum value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"color": "RED"}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("invalid enum value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"color": "YELLOW"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("enum field wrong type", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"color": 42}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaWithBoolean(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Feature", + "namespace": "test", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "enabled", "type": "boolean"} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("valid boolean true", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "dark_mode", "enabled": true}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("valid boolean false", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "dark_mode", "enabled": false}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("boolean field wrong type", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "dark_mode", "enabled": "yes"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaWithFixed(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Hash", + "namespace": "test", + "fields": [ + {"name": "md5", "type": {"type": "fixed", "name": "MD5", "size": 16}} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("valid fixed raw bytes length", func(t *testing.T) { + // goavro expects fixed values as strings with exact byte length matching schema size. + req := &pubsub.PublishRequest{ + Data: []byte(`{"md5": "abcdefghijklmnop"}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("invalid fixed wrong size", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"md5": "tooshort"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("fixed field wrong type", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"md5": 12345}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaIntOverflow(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Numbers", + "namespace": "test", + "fields": [ + {"name": "small", "type": "int"}, + {"name": "big", "type": "long"} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("int within range", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"small": 2147483647, "big": 100}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("int overflow positive", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"small": 2147483648, "big": 100}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("int overflow negative", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"small": -2147483649, "big": 100}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("long accepts large value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"small": 1, "big": 2147483648}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) +} + +func TestParsePublishMetadataAvroSchemaUnknownFields(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Person", + "namespace": "test", + "fields": [ + {"name": "name", "type": "string"} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("extra field rejected", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"name": "John", "extra": "field"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaFloatDoubleBytes(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Measurement", + "namespace": "test", + "fields": [ + {"name": "temperature", "type": "float"}, + {"name": "precise", "type": "double"}, + {"name": "payload", "type": "bytes"} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("valid float value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"temperature": 36.6, "precise": 3.14159, "payload": "aGVsbG8="}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("float field wrong type string", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"temperature": "hot", "precise": 3.14159, "payload": "aGVsbG8="}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("double field wrong type string", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"temperature": 36.6, "precise": "not_a_number", "payload": "aGVsbG8="}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("bytes field wrong type number", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"temperature": 36.6, "precise": 3.14159, "payload": 12345}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaFloatOverflow(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Measurement", + "namespace": "test", + "fields": [ + {"name": "temperature", "type": "float"}, + {"name": "precise", "type": "double"} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("float field rejects overflow value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"temperature": 1e300, "precise": 1.0}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) + + t.Run("double field accepts large value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"temperature": 36.6, "precise": 1e300}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) +} + +func TestParsePublishMetadataAvroSchemaLongRejectsFloat(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Counter", + "namespace": "test", + "fields": [ + {"name": "count", "type": "long"} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("long field rejects float value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"count": 1.5}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaFixedLengthValidation(t *testing.T) { + // goavro validates fixed values by raw string byte length matching schema size. + avroSchemaJSON := `{ + "type": "record", + "name": "Token", + "namespace": "test", + "fields": [ + {"name": "id", "type": {"type": "fixed", "name": "FixedID", "size": 3}} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("fixed value with correct raw byte length", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"id": "abc"}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("fixed value with wrong raw byte length", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"id": "AQID"}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaMultiTypeUnion(t *testing.T) { + avroSchemaJSON := `{ + "type": "record", + "name": "Flexible", + "namespace": "test", + "fields": [ + {"name": "value", "type": ["null", "string", "int"]} + ] + }` + + sm := newAvroSchemaMetadata(t, avroSchemaJSON) + + t.Run("union with null value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"value": null}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("union with string value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"value": "hello"}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("union with int value", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"value": 42}`), + } + msg, err := parsePublishMetadata(req, sm) + require.NoError(t, err) + assert.NotNil(t, msg) + }) + + t.Run("union rejects boolean not in union types", func(t *testing.T) { + req := &pubsub.PublishRequest{ + Data: []byte(`{"value": true}`), + } + _, err := parsePublishMetadata(req, sm) + require.Error(t, err) + assert.Contains(t, err.Error(), "avro schema validation failed") + }) +} + +func TestParsePublishMetadataAvroSchemaInvalidSchemaDefinition(t *testing.T) { + // Invalid Avro schemas are now rejected at init time (parsePulsarMetadata), + // not at publish time, since the codec is compiled once and cached. + m := pubsub.Metadata{} + m.Properties = map[string]string{ + "host": "a", + "mytopic" + topicAvroSchemaIdentifier: `{this is not valid json or avro schema`, + } + _, err := parsePulsarMetadata(m) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse avro schema") +} + func TestMissingHost(t *testing.T) { m := pubsub.Metadata{} m.Properties = map[string]string{"host": ""} @@ -647,6 +1314,226 @@ func TestParsePulsarMetadataReplicateSubscriptionState(t *testing.T) { } } +func TestParsePulsarMetadataCompressionType(t *testing.T) { + tt := []struct { + name string + compressionType string + expected string + expectedPulsar pulsar.CompressionType + err bool + }{ + { + name: "test valid compression type - none", + compressionType: "none", + expected: "none", + expectedPulsar: pulsar.NoCompression, + err: false, + }, + { + name: "test valid compression type - lz4", + compressionType: "lz4", + expected: "lz4", + expectedPulsar: pulsar.LZ4, + err: false, + }, + { + name: "test valid compression type - zlib", + compressionType: "zlib", + expected: "zlib", + expectedPulsar: pulsar.ZLib, + err: false, + }, + { + name: "test valid compression type - zstd", + compressionType: "zstd", + expected: "zstd", + expectedPulsar: pulsar.ZSTD, + err: false, + }, + { + name: "test valid compression type - empty defaults to none", + compressionType: "", + expected: "none", + expectedPulsar: pulsar.NoCompression, + err: false, + }, + { + name: "test valid compression type - case insensitive", + compressionType: "LZ4", + expected: "lz4", + expectedPulsar: pulsar.LZ4, + err: false, + }, + { + name: "test invalid compression type", + compressionType: "invalid", + err: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + m := pubsub.Metadata{} + m.Properties = map[string]string{ + "host": "a", + "compressionType": tc.compressionType, + } + meta, err := parsePulsarMetadata(m) + + if tc.err { + require.Error(t, err) + assert.Nil(t, meta) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.expected, meta.CompressionType) + assert.Equal(t, tc.expectedPulsar, getCompressionType(meta.CompressionType)) + }) + } +} + +func TestParsePulsarMetadataCompressionLevel(t *testing.T) { + tt := []struct { + name string + compressionLevel string + expected string + expectedPulsar pulsar.CompressionLevel + err bool + }{ + { + name: "test valid compression level - default", + compressionLevel: "default", + expected: "default", + expectedPulsar: pulsar.Default, + err: false, + }, + { + name: "test valid compression level - faster", + compressionLevel: "faster", + expected: "faster", + expectedPulsar: pulsar.Faster, + err: false, + }, + { + name: "test valid compression level - better", + compressionLevel: "better", + expected: "better", + expectedPulsar: pulsar.Better, + err: false, + }, + { + name: "test valid compression level - empty defaults to default", + compressionLevel: "", + expected: "default", + expectedPulsar: pulsar.Default, + err: false, + }, + { + name: "test valid compression level - case insensitive", + compressionLevel: "FASTER", + expected: "faster", + expectedPulsar: pulsar.Faster, + err: false, + }, + { + name: "test invalid compression level", + compressionLevel: "invalid", + err: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + m := pubsub.Metadata{} + m.Properties = map[string]string{ + "host": "a", + "compressionLevel": tc.compressionLevel, + } + meta, err := parsePulsarMetadata(m) + + if tc.err { + require.Error(t, err) + assert.Nil(t, meta) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.expected, meta.CompressionLevel) + assert.Equal(t, tc.expectedPulsar, getCompressionLevel(meta.CompressionLevel)) + }) + } +} + +func TestParsePulsarMetadataCompressionCombination(t *testing.T) { + tt := []struct { + name string + compressionType string + compressionLevel string + expectedType string + expectedLevel string + err bool + }{ + { + name: "test default compression settings", + compressionType: "", + compressionLevel: "", + expectedType: "none", + expectedLevel: "default", + err: false, + }, + { + name: "test lz4 with faster compression", + compressionType: "lz4", + compressionLevel: "faster", + expectedType: "lz4", + expectedLevel: "faster", + err: false, + }, + { + name: "test zstd with better compression", + compressionType: "zstd", + compressionLevel: "better", + expectedType: "zstd", + expectedLevel: "better", + err: false, + }, + { + name: "test invalid compression type", + compressionType: "invalid", + err: true, + }, + { + name: "test invalid compression level", + compressionType: "lz4", + compressionLevel: "invalid", + err: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + m := pubsub.Metadata{} + m.Properties = map[string]string{ + "host": "a", + "compressionType": tc.compressionType, + "compressionLevel": tc.compressionLevel, + } + meta, err := parsePulsarMetadata(m) + + if tc.err { + require.Error(t, err) + assert.Nil(t, meta) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.expectedType, meta.CompressionType) + assert.Equal(t, tc.expectedLevel, meta.CompressionLevel) + }) + } +} + func TestSanitiseURL(t *testing.T) { tests := []struct { name string @@ -670,3 +1557,318 @@ func TestSanitiseURL(t *testing.T) { }) } } + +func TestInitUsesTokenSupplierWhenClientSecretPathProvided(t *testing.T) { + server := newOAuthTestServer(t) + secretPath := writeTempFile(t, "rotating-secret") + + var capturedOpts pulsar.ClientOptions + p := NewPulsar(logger.NewLogger("test")).(*Pulsar) + t.Cleanup(func() { + p.newClientFn = pulsar.NewClient + }) + p.newClientFn = func(opts pulsar.ClientOptions) (pulsar.Client, error) { + capturedOpts = opts + return nil, nil + } + + md := pubsub.Metadata{} + md.Properties = map[string]string{ + "host": "localhost:6650", + "oauth2TokenURL": server.URL, + "oauth2ClientID": "client-id", + "oauth2ClientSecretPath": secretPath, + "oauth2Scopes": "scope1", + "oauth2Audiences": "aud1", + } + err := p.Init(t.Context(), md) + + require.NoError(t, err) + require.NotNil(t, capturedOpts.Authentication) + // Should use TokenSupplier, not TokenFromFile + expected := pulsar.NewAuthenticationTokenFromSupplier(func() (string, error) { + return "", nil + }) + assert.IsType(t, expected, capturedOpts.Authentication) +} + +func TestInitUsesTokenSupplierWithPlainTextSecretFile(t *testing.T) { + server := newOAuthTestServer(t) + secretPath := writeTempFile(t, "plain-text-secret-12345") + + var capturedOpts pulsar.ClientOptions + p := NewPulsar(logger.NewLogger("test")).(*Pulsar) + t.Cleanup(func() { + p.newClientFn = pulsar.NewClient + }) + p.newClientFn = func(opts pulsar.ClientOptions) (pulsar.Client, error) { + capturedOpts = opts + return nil, nil + } + + md := pubsub.Metadata{} + md.Properties = map[string]string{ + "host": "localhost:6650", + "oauth2TokenURL": server.URL, + "oauth2ClientID": "client-id", + "oauth2ClientSecretPath": secretPath, + "oauth2Scopes": "scope1", + "oauth2Audiences": "aud1", + } + err := p.Init(t.Context(), md) + + require.NoError(t, err) + require.NotNil(t, capturedOpts.Authentication) + expected := pulsar.NewAuthenticationTokenFromSupplier(func() (string, error) { + return "", nil + }) + assert.IsType(t, expected, capturedOpts.Authentication) +} + +func TestInitUsesTokenSupplierWithJSONSecretFile(t *testing.T) { + server := newOAuthTestServer(t) + credentialsPath := writeTempFile(t, fmt.Sprintf(`{ + "client_id": "json-id-from-file", + "client_secret": "json-secret-from-file", + "issuer_url": "%s" + }`, server.URL)) + + var capturedOpts pulsar.ClientOptions + p := NewPulsar(logger.NewLogger("test")).(*Pulsar) + t.Cleanup(func() { + p.newClientFn = pulsar.NewClient + }) + p.newClientFn = func(opts pulsar.ClientOptions) (pulsar.Client, error) { + capturedOpts = opts + return nil, nil + } + + md := pubsub.Metadata{} + md.Properties = map[string]string{ + "host": "localhost:6650", + "oauth2CredentialsFile": credentialsPath, + "oauth2Scopes": "scope1", + "oauth2Audiences": "aud1", + } + err := p.Init(t.Context(), md) + + require.NoError(t, err) + require.NotNil(t, capturedOpts.Authentication) + expected := pulsar.NewAuthenticationTokenFromSupplier(func() (string, error) { + return "", nil + }) + assert.IsType(t, expected, capturedOpts.Authentication) +} + +func TestInitUsesClientIDFromMetadataWhenFileHasOnlySecret(t *testing.T) { + server := newOAuthTestServer(t) + // Test that oauth2ClientSecretPath works with plain text (client_id comes from metadata) + //nolint:gosec + plainTextSecret := "plain-text-secret-12345" + secretPath := writeTempFile(t, plainTextSecret) + + var capturedOpts pulsar.ClientOptions + p := NewPulsar(logger.NewLogger("test")).(*Pulsar) + t.Cleanup(func() { + p.newClientFn = pulsar.NewClient + }) + p.newClientFn = func(opts pulsar.ClientOptions) (pulsar.Client, error) { + capturedOpts = opts + return nil, nil + } + + md := pubsub.Metadata{} + md.Properties = map[string]string{ + "host": "localhost:6650", + "oauth2TokenURL": server.URL, + "oauth2ClientID": "metadata-client-id", // client_id from metadata + "oauth2ClientSecretPath": secretPath, // plain text secret in file + "oauth2Scopes": "scope1", + "oauth2Audiences": "aud1", + } + err := p.Init(t.Context(), md) + + require.NoError(t, err) + require.NotNil(t, capturedOpts.Authentication) + expected := pulsar.NewAuthenticationTokenFromSupplier(func() (string, error) { + return "", nil + }) + assert.IsType(t, expected, capturedOpts.Authentication) +} + +func TestInitFailsWhenClientCredentialsTypeMissingClientSecret(t *testing.T) { + // Test that credentials file requires client_secret + //nolint:gosec + credentialsJSON := `{ + "client_id": "test-id", + "issuer_url": "https://oauth.example.com/token" + }` + secretPath := writeTempFile(t, credentialsJSON) + + md := pubsub.Metadata{} + md.Properties = map[string]string{ + "host": "localhost:6650", + "oauth2CredentialsFile": secretPath, + "oauth2Scopes": "scope1", + "oauth2Audiences": "aud1", + } + p := NewPulsar(logger.NewLogger("test")) + err := p.Init(t.Context(), md) + + require.Error(t, err) + assert.Contains(t, err.Error(), "must contain client_id and client_secret") +} + +func TestInitUsesTokenSupplierWhenClientSecretPathMissing(t *testing.T) { + server := newOAuthTestServer(t) + + var capturedOpts pulsar.ClientOptions + p := NewPulsar(logger.NewLogger("test")).(*Pulsar) + t.Cleanup(func() { + p.newClientFn = pulsar.NewClient + }) + p.newClientFn = func(opts pulsar.ClientOptions) (pulsar.Client, error) { + capturedOpts = opts + return nil, nil + } + + md := pubsub.Metadata{} + md.Properties = map[string]string{ + "host": "localhost:6650", + "oauth2TokenURL": server.URL, + "oauth2ClientID": "client-id", + "oauth2ClientSecret": "client-secret", + "oauth2Scopes": "scope1", + "oauth2Audiences": "aud1", + } + err := p.Init(t.Context(), md) + + require.NoError(t, err) + require.NotNil(t, capturedOpts.Authentication) + expected := pulsar.NewAuthenticationTokenFromSupplier(func() (string, error) { + return "", nil + }) + assert.IsType(t, expected, capturedOpts.Authentication) +} + +func TestInitUsesTokenWhenProvided(t *testing.T) { + var capturedOpts pulsar.ClientOptions + p := NewPulsar(logger.NewLogger("test")).(*Pulsar) + t.Cleanup(func() { + p.newClientFn = pulsar.NewClient + }) + p.newClientFn = func(opts pulsar.ClientOptions) (pulsar.Client, error) { + capturedOpts = opts + return nil, nil + } + + md := pubsub.Metadata{} + md.Properties = map[string]string{ + "host": "localhost:6650", + "token": "my-token", + } + err := p.Init(t.Context(), md) + + require.NoError(t, err) + require.NotNil(t, capturedOpts.Authentication) + expected := pulsar.NewAuthenticationToken("my-token") + assert.IsType(t, expected, capturedOpts.Authentication) +} + +func newOAuthTestServer(t *testing.T) *httptest.Server { + t.Helper() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"access_token":"token","token_type":"bearer","expires_in":3600}`)) + })) + t.Cleanup(server.Close) + + return server +} + +func writeTempFile(t *testing.T, content string) string { + t.Helper() + + f, err := os.CreateTemp(t.TempDir(), "pulsar-secret-*") + require.NoError(t, err) + _, err = f.WriteString(content) + require.NoError(t, err) + require.NoError(t, f.Close()) + return f.Name() +} + +func TestSubscribe_AppliesMetadataOptions(t *testing.T) { + p := NewPulsar(logger.NewLogger("test")).(*Pulsar) + + md := pubsub.Metadata{ + Base: metadata.Base{Properties: map[string]string{ + "host": "localhost:6650", + "consumerID": "my-test-consumer", + "topic": "my-topic", + "subscribeInitialPosition": "earliest", + "subscribeMode": "non_durable", + }}, + } + + var capturedOptions pulsar.ConsumerOptions + mockClient := &MockPulsarClient{ + SubscribeFn: func(options pulsar.ConsumerOptions) (pulsar.Consumer, error) { + capturedOptions = options + return &MockPulsarConsumer{ + Ch: make(chan pulsar.ConsumerMessage), + }, nil + }, + } + p.client = mockClient + + parsedMeta, err := parsePulsarMetadata(md) + require.NoError(t, err) + p.metadata = *parsedMeta + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + req := pubsub.SubscribeRequest{ + Topic: "my-topic", + Metadata: md.Properties, + } + + err = p.Subscribe(ctx, req, func(ctx context.Context, msg *pubsub.NewMessage) error { + return nil + }) + require.NoError(t, err) + + // Verify Initial Position: Should be Earliest (1), NOT Latest (0) + assert.Equal(t, pulsar.SubscriptionPositionEarliest, capturedOptions.SubscriptionInitialPosition, + "Bug: SubscriptionInitialPosition defaulted to 'Latest' instead of 'Earliest'") + + // Verify Subscription Mode: Should be NonDurable (1), NOT Durable (0) + assert.Equal(t, pulsar.NonDurable, capturedOptions.SubscriptionMode, + "Bug: SubscriptionMode defaulted to 'Durable' instead of 'NonDurable'") +} + +type MockPulsarClient struct { + pulsar.Client + SubscribeFn func(pulsar.ConsumerOptions) (pulsar.Consumer, error) +} + +func (m *MockPulsarClient) Subscribe(options pulsar.ConsumerOptions) (pulsar.Consumer, error) { + if m.SubscribeFn != nil { + return m.SubscribeFn(options) + } + return nil, nil +} + +func (m *MockPulsarClient) Close() {} + +type MockPulsarConsumer struct { + pulsar.Consumer + Ch chan pulsar.ConsumerMessage +} + +func (m *MockPulsarConsumer) Chan() <-chan pulsar.ConsumerMessage { + return m.Ch +} + +func (m *MockPulsarConsumer) Close() {} diff --git a/pubsub/rabbitmq/rabbitmq.go b/pubsub/rabbitmq/rabbitmq.go index a62105a6db..bab4d52540 100644 --- a/pubsub/rabbitmq/rabbitmq.go +++ b/pubsub/rabbitmq/rabbitmq.go @@ -499,8 +499,8 @@ func (r *rabbitMQ) prepareSubscription(channel rabbitMQChannelBroker, req pubsub } func (r *rabbitMQ) ensureSubscription(req pubsub.SubscribeRequest, queueName string) (rabbitMQChannelBroker, int, *amqp.Queue, error) { - r.channelMutex.RLock() - defer r.channelMutex.RUnlock() + r.channelMutex.Lock() + defer r.channelMutex.Unlock() if r.channel == nil { return nil, r.connectionCount, nil, errors.New(errorChannelNotInitialized) diff --git a/pubsub/redis/metadata.yaml b/pubsub/redis/metadata.yaml index 5e0684d1e1..9ccd81083c 100644 --- a/pubsub/redis/metadata.yaml +++ b/pubsub/redis/metadata.yaml @@ -59,9 +59,22 @@ metadata: - name: redisHost required: true description: | - Connection-string for the redis host. If "redisType" is "cluster" it can be multiple hosts separated by commas or just a single host + Connection-string for the redis host. If "redisType" is "cluster" it + can be multiple hosts separated by commas or just a single host. + The port can be included in the host string (e.g. "host:6379") or + provided separately via "redisPort". example: '"redis-master.default.svc.cluster.local:6379"' type: string + - name: redisPort + required: false + description: | + The Redis port. Optional: if "redisHost" already contains a port, this + field must either match or be omitted. When "redisHost" does not include + a port and this field is not set, the default Redis port 6379 is used. + In cluster mode, this port is applied to every host in the + comma-separated list. + example: "6379" + type: string - name: consumerID required: false description: The consumer group ID diff --git a/pubsub/redis/redis.go b/pubsub/redis/redis.go index 5f88468fe5..9cd8629d47 100644 --- a/pubsub/redis/redis.go +++ b/pubsub/redis/redis.go @@ -245,8 +245,16 @@ func (r *redisStreams) processMessage(msg redisMessageWrapper) error { } if err := msg.handler(ctx, &msg.message); err != nil { r.logger.Errorf("Error processing Redis message %s: %v", msg.messageID, err) + if ctx.Err() != nil { + // If the subscription context is cancelled (shutdown/timeout), skip ACK so Redis can redeliver after restart. + return err + } + if err := r.client.XAck(ctx, msg.message.Topic, r.clientSettings.ConsumerID, msg.messageID); err != nil { + r.logger.Errorf("Error acknowledging Redis message %s: %v", msg.messageID, err) - return err + return err + } + return nil } // Use the background context in case subscriptionCtx is already closed. @@ -272,7 +280,7 @@ func (r *redisStreams) pollNewMessagesLoop(ctx context.Context, stream string, h //nolint:gosec streams, err := r.client.XReadGroupResult(ctx, r.clientSettings.ConsumerID, r.clientSettings.ConsumerID, []string{stream, ">"}, int64(r.clientSettings.QueueDepth), time.Duration(r.clientSettings.ReadTimeout)) if err != nil { - if !errors.Is(err, r.client.GetNilValueError()) && err != context.Canceled { + if !r.client.IsNilValueError(err) && err != context.Canceled { if strings.Contains(err.Error(), "NOGROUP") { r.logger.Warnf("redis streams: consumer group %s does not exist for stream %s. This could mean the server experienced data loss, or the group/stream was deleted.", r.clientSettings.ConsumerID, stream) r.logger.Warnf("redis streams: recreating group %s for stream %s", r.clientSettings.ConsumerID, stream) @@ -327,7 +335,7 @@ func (r *redisStreams) reclaimPendingMessages(ctx context.Context, stream string "+", int64(r.clientSettings.QueueDepth), //nolint:gosec ) - if err != nil && !errors.Is(err, r.client.GetNilValueError()) { + if err != nil && !r.client.IsNilValueError(err) { r.logger.Errorf("error retrieving pending Redis messages: %v", err) break @@ -354,7 +362,7 @@ func (r *redisStreams) reclaimPendingMessages(ctx context.Context, stream string r.clientSettings.ProcessingTimeout, msgIDs, ) - if err != nil && !errors.Is(err, r.client.GetNilValueError()) { + if err != nil && !r.client.IsNilValueError(err) { r.logger.Errorf("error claiming pending Redis messages: %v", err) break @@ -366,7 +374,7 @@ func (r *redisStreams) reclaimPendingMessages(ctx context.Context, stream string // If the Redis nil error is returned, it means somes message in the pending // state no longer exist. We need to acknowledge these messages to // remove them from the pending list. - if errors.Is(err, r.client.GetNilValueError()) { + if r.client.IsNilValueError(err) { // Build a set of message IDs that were not returned // that potentially no longer exist. expectedMsgIDs := make(map[string]struct{}, len(msgIDs)) @@ -394,14 +402,14 @@ func (r *redisStreams) removeMessagesThatNoLongerExistFromPending(ctx context.Co 0, []string{pendingID}, ) - if err != nil && !errors.Is(err, r.client.GetNilValueError()) { + if err != nil && !r.client.IsNilValueError(err) { r.logger.Errorf("error claiming pending Redis message %s: %v", pendingID, err) continue } // Ack the message to remove it from the pending list. - if errors.Is(err, r.client.GetNilValueError()) { + if r.client.IsNilValueError(err) { // Use the background context in case subscriptionCtx is already closed. if err = r.client.XAck(context.Background(), stream, r.clientSettings.ConsumerID, pendingID); err != nil { r.logger.Errorf("error acknowledging Redis message %s after failed claim for %s: %v", pendingID, stream, err) diff --git a/pubsub/redis/redis_test.go b/pubsub/redis/redis_test.go index c2e442c1dc..f6a241a121 100644 --- a/pubsub/redis/redis_test.go +++ b/pubsub/redis/redis_test.go @@ -102,7 +102,8 @@ func TestProcessStreams(t *testing.T) { // act testRedisStream := &redisStreams{ logger: logger.NewLogger("test"), - clientSettings: &commonredis.Settings{}, + client: &stubRedisClient{}, + clientSettings: &commonredis.Settings{ConsumerID: "group"}, } testRedisStream.queue = make(chan redisMessageWrapper, 10) go testRedisStream.worker() @@ -140,7 +141,8 @@ func TestProcessStreamsWithoutEventMetadata(t *testing.T) { // act testRedisStream := &redisStreams{ logger: logger.NewLogger("test"), - clientSettings: &commonredis.Settings{}, + client: &stubRedisClient{}, + clientSettings: &commonredis.Settings{ConsumerID: "group"}, } testRedisStream.queue = make(chan redisMessageWrapper, 10) go testRedisStream.worker() @@ -153,6 +155,152 @@ func TestProcessStreamsWithoutEventMetadata(t *testing.T) { assert.Equal(t, 3, messageCount) } +func TestProcessMessageAcksOnError(t *testing.T) { + client := &stubRedisClient{} + rs := &redisStreams{ + logger: logger.NewLogger("test"), + client: client, + clientSettings: &commonredis.Settings{ + ConsumerID: "group", + }, + } + + msg := redisMessageWrapper{ + ctx: t.Context(), + messageID: "1-0", + message: pubsub.NewMessage{ + Topic: "topic", + }, + handler: func(context.Context, *pubsub.NewMessage) error { + return errors.New("retry") + }, + } + + err := rs.processMessage(msg) + + require.NoError(t, err) + assert.Equal(t, 1, client.ackCount) + assert.Equal(t, "topic", client.ackStream) + assert.Equal(t, "group", client.ackGroup) + assert.Equal(t, "1-0", client.ackMessageID) +} + +func TestProcessMessageAckFailureOnError(t *testing.T) { + client := &stubRedisClient{ + ackErr: errors.New("ack-failed"), + } + rs := &redisStreams{ + logger: logger.NewLogger("test"), + client: client, + clientSettings: &commonredis.Settings{ + ConsumerID: "group", + }, + } + + msg := redisMessageWrapper{ + ctx: t.Context(), + messageID: "1-0", + message: pubsub.NewMessage{ + Topic: "topic", + }, + handler: func(context.Context, *pubsub.NewMessage) error { + return errors.New("retry") + }, + } + + err := rs.processMessage(msg) + + require.Error(t, err) + assert.Equal(t, client.ackErr, err) + assert.Equal(t, 1, client.ackCount) +} + +// TestReclaimPendingMessagesWhenXClaimReturnsRedisNil ensures that when XClaimResult returns +// redis "key does not exist" (redis: nil), we treat it as message no longer exists and call XAck +// instead of propagating the error to users. +func TestReclaimPendingMessagesWhenXClaimReturnsRedisNil(t *testing.T) { + client := &stubRedisClient{ + nilValueErrStr: "redis: nil", + pendingResult: []commonredis.RedisXPendingExt{ + {ID: "1-0", Consumer: "c", Idle: time.Minute, RetryCount: 0}, + }, + claimErr: commonredis.RedisError("redis: nil"), + } + rs := &redisStreams{ + logger: logger.NewLogger("test"), + client: client, + clientSettings: &commonredis.Settings{ + ConsumerID: "group", + ProcessingTimeout: time.Second, + RedeliverInterval: time.Hour, + QueueDepth: 10, + }, + } + + rs.reclaimPendingMessages(t.Context(), "stream", func(ctx context.Context, msg *pubsub.NewMessage) error { + return nil + }) + + // When XClaim returns redis nil, we ack the message to remove it from pending + assert.GreaterOrEqual(t, client.ackCount, 1, "expected XAck to be called for message that no longer exists") +} + +// TestReclaimPendingMessagesWhenXPendingExtReturnsRedisNil ensures that when XPendingExtResult +// returns redis "key does not exist" (redis: nil), we do not log it as an error and the loop exits +func TestReclaimPendingMessagesWhenXPendingExtReturnsRedisNil(t *testing.T) { + client := &stubRedisClient{ + nilValueErrStr: "redis: nil", + xPendingErr: commonredis.RedisError("redis: nil"), + } + rs := &redisStreams{ + logger: logger.NewLogger("test"), + client: client, + clientSettings: &commonredis.Settings{ + ConsumerID: "group", + ProcessingTimeout: time.Second, + RedeliverInterval: time.Hour, + QueueDepth: 10, + }, + } + + // treat as not pending and break when XPending returns redis nil + rs.reclaimPendingMessages(t.Context(), "stream", func(ctx context.Context, msg *pubsub.NewMessage) error { + return nil + }) +} + +// TestPollNewMessagesLoopWhenXReadGroupResultReturnsRedisNil ensures that when XReadGroupResult +// returns redis "key does not exist" (redis: nil), we do not log it and the loop continues until ctx is canceled +func TestPollNewMessagesLoopWhenXReadGroupResultReturnsRedisNil(t *testing.T) { + client := &stubRedisClient{ + nilValueErrStr: "redis: nil", + readGroupErr: commonredis.RedisError("redis: nil"), + } + rs := &redisStreams{ + logger: logger.NewLogger("test"), + client: client, + clientSettings: &commonredis.Settings{ + ConsumerID: "group", + QueueDepth: 10, + ReadTimeout: commonredis.Duration(time.Second), + }, + } + + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan struct{}) + go func() { + rs.pollNewMessagesLoop(ctx, "stream", func(context.Context, *pubsub.NewMessage) error { return nil }) + close(done) + }() + time.Sleep(2 * time.Millisecond) + cancel() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("pollNewMessagesLoop did not exit after context cancel") + } +} + func generateRedisStreamTestData(messageCount int, data string, metadata string) []commonredis.RedisXMessage { generateXMessage := func(id int) commonredis.RedisXMessage { values := map[string]interface{}{ @@ -176,3 +324,126 @@ func generateRedisStreamTestData(messageCount int, data string, metadata string) return xmessageArray } + +type stubRedisClient struct { + ackCount int + ackErr error + ackStream string + ackGroup string + ackMessageID string + nilValueErrStr string // default "nil", used "redis: nil" to test nil key handling + pendingResult []commonredis.RedisXPendingExt + claimErr error // if set, XClaimResult returns (nil, claimErr) + xPendingErr error // if set, XPendingExtResult returns (nil, xPendingErr) + readGroupErr error // if set, XReadGroupResult returns (nil, readGroupErr) +} + +func (s *stubRedisClient) IsNilValueError(err error) bool { + return err != nil && err.Error() == s.nilValueErrStr +} + +func (s *stubRedisClient) Context() context.Context { + return context.Background() +} + +func (s *stubRedisClient) DoRead(context.Context, ...interface{}) (interface{}, error) { + return nil, nil +} + +func (s *stubRedisClient) DoWrite(context.Context, ...interface{}) error { + return nil +} + +func (s *stubRedisClient) Del(context.Context, ...string) error { + return nil +} + +func (s *stubRedisClient) Get(context.Context, string) (string, error) { + return "", nil +} + +func (s *stubRedisClient) GetDel(context.Context, string) (string, error) { + return "", nil +} + +func (s *stubRedisClient) Close() error { + return nil +} + +func (s *stubRedisClient) PingResult(context.Context) (string, error) { + return "", nil +} + +func (s *stubRedisClient) ConfigurationSubscribe(context.Context, *commonredis.ConfigurationSubscribeArgs) { +} + +func (s *stubRedisClient) SetNX(context.Context, string, interface{}, time.Duration) (*bool, error) { + return nil, nil +} + +func (s *stubRedisClient) EvalInt(context.Context, string, []string, ...interface{}) (*int, error, error) { + i := 0 + return &i, nil, nil +} + +func (s *stubRedisClient) XAdd(context.Context, string, int64, string, map[string]interface{}) (string, error) { + return "", nil +} + +func (s *stubRedisClient) XGroupCreateMkStream(context.Context, string, string, string) error { + return nil +} + +func (s *stubRedisClient) XAck(ctx context.Context, stream string, group string, messageID string) error { + s.ackCount++ + s.ackStream = stream + s.ackGroup = group + s.ackMessageID = messageID + return s.ackErr +} + +func (s *stubRedisClient) XReadGroupResult(context.Context, string, string, []string, int64, time.Duration) ([]commonredis.RedisXStream, error) { + if s.readGroupErr != nil { + return nil, s.readGroupErr + } + return nil, nil +} + +func (s *stubRedisClient) XPendingExtResult(context.Context, string, string, string, string, int64) ([]commonredis.RedisXPendingExt, error) { + if s.xPendingErr != nil { + return nil, s.xPendingErr + } + if s.pendingResult != nil { + result := s.pendingResult + s.pendingResult = nil // so next call returns empty and reclaimPendingMessages loop exits + return result, nil + } + return nil, nil +} + +func (s *stubRedisClient) XClaimResult(context.Context, string, string, string, time.Duration, []string) ([]commonredis.RedisXMessage, error) { + if s.claimErr != nil { + return nil, s.claimErr + } + return nil, nil +} + +func (s *stubRedisClient) TxPipeline() commonredis.RedisPipeliner { + return &stubRedisPipeliner{} +} + +func (s *stubRedisClient) TTLResult(context.Context, string) (time.Duration, error) { + return 0, nil +} + +func (s *stubRedisClient) AuthACL(context.Context, string, string) error { + return nil +} + +type stubRedisPipeliner struct{} + +func (p *stubRedisPipeliner) Exec(context.Context) error { + return nil +} + +func (p *stubRedisPipeliner) Do(context.Context, ...interface{}) {} diff --git a/pubsub/rocketmq/metadata.yaml b/pubsub/rocketmq/metadata.yaml index 3bf37eb6bd..dbb863a8e2 100644 --- a/pubsub/rocketmq/metadata.yaml +++ b/pubsub/rocketmq/metadata.yaml @@ -62,8 +62,8 @@ metadata: type: number required: false description: The number of retry attempts for sending messages. - example: 3 - default: 3 + example: "3" + default: "3" - name: producerQueueSelector type: string required: false @@ -99,76 +99,76 @@ metadata: type: bool required: false description: Enable orderly message consumption. - example: false - default: false + example: "false" + default: "false" - name: consumeMessageBatchMaxSize type: number required: false description: The maximum batch size for message consumption. - example: 10 + example: "10" - name: consumeConcurrentlyMaxSpan type: number required: false description: The maximum span for concurrent consumption. - example: 10 + example: "10" - name: maxReconsumeTimes type: number required: false description: The maximum number of reconsume times. -1 means 16 times. - example: 10000 + example: "10000" - name: autoCommit type: bool required: false description: Enable automatic commit. - example: true - default: false + example: "true" + default: "false" - name: consumeTimeout type: number required: false description: The consume timeout in minutes. - example: 10 + example: "10" - name: consumerPullTimeout type: number required: false description: The consumer pull timeout in milliseconds. - example: 30 - default: 30 + example: "30" + default: "30" - name: pullInterval type: number required: false description: The pull interval in minutes. - example: 100 - default: 100 + example: "100" + default: "100" - name: consumerBatchSize type: number required: false description: The consumer batch size. - example: 10 + example: "10" - name: pullBatchSize type: number required: false description: The pull batch size. - example: 10 + example: "10" - name: pullThresholdForQueue type: number required: false description: The pull threshold for queue. - example: 100 + example: "100" - name: pullThresholdForTopic type: number required: false description: The pull threshold for topic. - example: 100 + example: "100" - name: pullThresholdSizeForQueue type: number required: false description: The pull threshold size for queue. - example: 10 + example: "10" - name: pullThresholdSizeForTopic type: number required: false description: The pull threshold size for topic. - example: 10 + example: "10" - name: content-type type: string required: false @@ -178,7 +178,7 @@ metadata: type: number required: false description: The send timeout in seconds. - example: 10 + example: "10" - name: logLevel type: string required: false @@ -199,4 +199,4 @@ metadata: type: number required: false description: The send timeout in nanoseconds (deprecated, use sendTimeOutSec instead). - example: 10000000000 + example: "10000000000" diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md new file mode 100644 index 0000000000..ad0466cdcb --- /dev/null +++ b/secretstores/akeyless/README.md @@ -0,0 +1,231 @@ + + +# Akeyless Secret Store + +This component provides a Dapr secret store implementation for [Akeyless](https://www.akeyless.io/), a cloud-native secrets management platform. + +## Configuration + +The Akeyless Dapr Secret Store component only supports the following [Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods): + +- [API Key](https://docs.akeyless.io/docs/api-key) +- [OAuth2.0/JWT](https://docs.akeyless.io/docs/oauth20jwt) +- [AWS IAM](https://docs.akeyless.io/docs/aws-iam) +- [Kubernetes](https://docs.akeyless.io/docs/kubernetes-auth) + +### Authentication + +The Akeyless secret store component supports the following configuration options: + +| Field | Required | Description | Example | +|-------|----------|-------------|---------| +| `gatewayUrl` | No | The Akeyless Gateway API URL. Default is https://api.akeyless.io. | `https://gw.akeyless.svc.cluster.local:8000/api/v2` | +| `gatewayTlsCa` | No | The `base64`-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway with a self-signed or custom CA certificate. The Akeyless client will be set to a 30 second timeout. | `LS0tLS1CRUdJTi...` | +| `accessId` | Yes | The Akeyless authentication access ID. | `p-123456780wm` | +| `jwt` | No | If using an OAuth2.0/JWT access ID, specify the JSON Web Token | `eyJ...` | +| `accessKey` | No | If using an API Key access ID, specify the API key | `ABCD123...=` | +| `k8sAuthConfigName` | No | If using the k8s auth method, specify the name of the k8s auth config. | `k8s-auth-config` | +| `k8sGatewayUrl` | No | The gateway URL that where the k8s auth config is located. | `http://gw.akeyless.svc.cluster.local:8000` | +| `k8sServiceAccountToken` | No | If using the k8s auth method, specify the service account token. If not specified, + we will try to read it from the default service account token file `/var/run/secrets/kubernetes.io/serviceaccount/token`. | `eyJ...` | + + + +## Examples + +We currently support the following [Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods): + + +## Examples + +## Example Configuration: API Key + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless-secretstore +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "https://your-gateway.akeyless.io" + - name: gatewayTlsCa + value: "LS0tLS1CRUdJTi...." + - name: accessId + value: "p-1234Abcdam" + - name: accessKey + value: "ABCD1233...=" +``` + + +## Example Configuration: JWT + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless-secretstore +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" + - name: accessId + value: "p-1234Abcdom" + - name: jwt + value: "eyJ....." +``` + +## Example Configuration: AWS IAM + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" + - name: accessId + value: "p-1234Abcdwm" +``` + +## Example Configuration: Kubernetes + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" + - name: accessId + value: "p-1234Abcdkm" + - name: k8sAuthConfigName + value: "us-east-1-prod-akeyless-k8s-conf" + - name: k8sGatewayUrl + value: https://gw.akeyless.svc.cluster.local +``` + +## Usage + +Once configured, you can retrieve secrets using the Dapr secrets API: + +```bash +# Get a single secret +curl http://localhost:3500/v1.0/secrets/akeyless/my-secret + +# Get all secrets (static, dynamic, rotated) from root (/) path +curl http://localhost:3500/v1.0/secrets/akeyless/bulk + +# Get all secrets static secrets +curl http://localhost:3500/v1.0/secrets/akeyless/bulk?metadata.secrets_type=static + +# Get all static and dynamic secrets from a specific path (/my/org) +curl http://localhost:3500/v1.0/secrets/akeyless/bulk?metadata.secrets_type=static,dynamic&metadata.path=/my/org +``` + +Or using the Dapr SDK. The example below retrieves all static secrets from path `/path/to/department`. +```go +log.Println("Starting test application") +client, err := dapr.NewClient() +if err != nil { + log.Printf("Error creating Dapr client: %v\n", err) + panic(err) +} +log.Println("Dapr client created successfully") +const daprSecretStore = "akeyless" + +defer client.Close() +ctx := context.Background() +akeylessBulkMetadata := map[string]string{ + "path": "/path/to/department", + "secrets_type": "static", +} +secrets, err := client.GetBulkSecret(ctx, daprSecretStore, akeylessBulkMetadata) +if err != nil { + log.Printf("Error fetching secrets: %v\n", err) + panic(err) +} +log.Printf("Found %d secrets: ", len(secrets)) +for secretName, secretValue := range secrets { + log.Printf("Secret: %s, Value: %s", secretName, secretValue) +} +``` + +## Features + +- Supports static, dynamic and rotated secrets. +- **GetSecret**: Retrieve an individual value secret by path. +- **BulkGetSecret**: Retrieve all secrets from a specified path (or `/` by default) recursively. + +## Response Formats + +The Akeyless secret store returns different response formats depending on the secret type: + +### Static Secrets +Static secrets return their value directly as a string: + +```json +{ + "my-static-secret": "secret-value" +} +``` + +### Dynamic Secrets +Dynamic secrets return a JSON string containing the credentials. The exact structure depends on the target system: + +**MySQL Dynamic Secret:** +```json +{ + "my-mysql-secret": "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}" +} +``` + +**Azure AD Dynamic Secret:** +```json +{ + "my-azure-secret": "{\"user\":{\"id\":\"user_id\",\"displayName\":\"user_name\",\"mail\":\"email@domain.com\"},\"secret\":{\"keyId\":\"secret_key_id\",\"displayName\":\"secret_name\",\"tenantId\":\"tenant_id\"},\"ttl_in_minutes\":\"60\",\"id\":\"user_id\",\"msg\":\"User has been added successfully...\"}" +} +``` + +**GCP Dynamic Secret:** +```json +{ + "my-gcp-secret": "{\"encoded_key\":\"base64_encoded_service_account_key\",\"ttl_in_minutes\":\"60\",\"id\":\"service_account_name\"}" +} +``` + +### Rotated Secrets +Rotated secrets return a JSON object containing all available fields: + +```json +{ + "my-rotated-secret": "{\"value\":{\"username\":\"rotated_user\",\"password\":\"rotated_password\",\"application_id\":\"1234567890\"}}" +} +``` + +**Note:** The exact fields in dynamic and rotated secret responses vary by target system and configuration. Applications should parse the JSON string to extract the specific credentials they need. diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go new file mode 100644 index 0000000000..9e956bd7b1 --- /dev/null +++ b/secretstores/akeyless/akeyless.go @@ -0,0 +1,988 @@ +/* +Copyright 2026 The Dapr Authors +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 akeyless + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "strings" + "sync" + "time" + + aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" + akeylesssdk "github.com/akeylesslabs/akeyless-go/v5" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" +) + +var _ secretstores.SecretStore = (*akeylessSecretStore)(nil) + +// akeylessSecretStore is a secret store implementation for Akeyless. +type akeylessSecretStore struct { + v2 *akeylesssdk.V2ApiService + token string + tokenExpiry time.Time + metadata *akeylessMetadata + mu sync.RWMutex + logger logger.Logger + closeCh chan struct{} + wg sync.WaitGroup +} + +// NewAkeylessSecretStore returns a new Akeyless secret store. +func NewAkeylessSecretStore(logger logger.Logger) secretstores.SecretStore { + return &akeylessSecretStore{ + logger: logger, + } +} + +// akeylessMetadata contains the metadata for the Akeyless secret store. +type akeylessMetadata struct { + GatewayURL string `json:"gatewayUrl" mapstructure:"gatewayUrl"` + GatewayTLSCa string `json:"gatewayTlsCa" mapstructure:"gatewayTlsCa"` + JWT string `json:"jwt" mapstructure:"jwt"` + AccessID string `json:"accessId" mapstructure:"accessId"` + AccessKey string `json:"accessKey" mapstructure:"accessKey"` + K8SGatewayURL string `json:"k8sGatewayUrl" mapstructure:"k8sGatewayUrl"` + K8SAuthConfigName string `json:"k8sAuthConfigName" mapstructure:"k8sAuthConfigName"` + K8sServiceAccountToken string `json:"k8sServiceAccountToken" mapstructure:"k8sServiceAccountToken"` +} + +// Init creates a new Akeyless secret store client and sets up the Akeyless API client +// with authentication method based on the accessId. +func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metadata) error { + a.logger.Info("Initializing Akeyless secret store...") + m, err := a.parseMetadata(meta) + if err != nil { + return errors.New("failed to parse metadata: " + err.Error()) + } + + a.metadata = m + a.closeCh = make(chan struct{}) + + err = a.authenticate(ctx, m) + if err != nil { + return errors.New("failed to authenticate with Akeyless: " + err.Error()) + } + + // Start background token refresh routine if we have expiration time + if !a.tokenExpiry.IsZero() { + a.startTokenRefreshRoutine(ctx, m) + } + + return nil +} + +// Authenticate authenticates with Akeyless using the provided metadata. +// It returns an error if the authentication fails. +func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeylessMetadata) error { + a.logger.Debug("Creating authentication request to Akeyless...") + authRequest := akeylesssdk.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + + // Get the authentication method + a.logger.Debug("extracting access type from accessId...") + accessTypeChar, err := extractAccessTypeChar(metadata.AccessID) + if err != nil { + return errors.New("unable to extract access type character from accessId, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + } + + a.logger.Debugf("getting access type display name for character '%s'...", accessTypeChar) + accessType, err := getAccessTypeDisplayName(accessTypeChar) + if err != nil { + return errors.New("unable to get access type from character '" + accessTypeChar + "': " + err.Error()) + } + + a.logger.Debugf("authenticating using access type '%s'", accessType) + + // Depending on the access type we set the appropriate authentication method + switch accessType { + case AuthDefault: + if metadata.AccessKey == "" { + return errors.New("accessKey is required for API key authentication") + } + authRequest.SetAccessKey(metadata.AccessKey) + case AuthIAM: + authRequest.SetAccessType(AuthIAM) + cloudID, cloudErr := aws.GetCloudId() + if cloudErr != nil { + return errors.New("unable to get cloud ID: " + cloudErr.Error()) + } + authRequest.SetCloudId(cloudID) + case AuthJWT: + authRequest.SetAccessType(AuthJWT) + if metadata.JWT == "" { + return errors.New("jwt is required for JWT authentication") + } + authRequest.SetJwt(metadata.JWT) + case AuthK8S: + authRequest.SetAccessType(AuthK8S) + if k8sErr := setK8SAuthConfiguration(*metadata, authRequest, a); k8sErr != nil { + return errors.New("failed to set k8s auth configuration: " + k8sErr.Error()) + } + } + + // Create Akeyless API client configuration + a.logger.Debug("creating Akeyless API client configuration...") + config := akeylesssdk.NewConfiguration() + config.Servers = []akeylesssdk.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = UserAgent + config.AddDefaultHeader(ClientSource, UserAgent) + + // Configure TLS if gatewayTlsCa is provided + if metadata.GatewayTLSCa != "" { + a.logger.Debug("configuring TLS for Akeyless client...") + tlsConfig, tlsErr := createTLSConfig(metadata.GatewayTLSCa) + if tlsErr != nil { + return errors.New("failed to create TLS configuration: " + tlsErr.Error()) + } + + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + config.HTTPClient = httpClient + } + + a.v2 = akeylesssdk.NewAPIClient(config).V2Api + + a.logger.Debug("authenticating with Akeyless...") + out, httpResponse, err := a.v2.Auth(ctx).Body(*authRequest).Execute() + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + if err != nil { + if httpResponse != nil { + return fmt.Errorf("failed to authenticate with Akeyless (HTTP status code: %d): %w", httpResponse.StatusCode, err) + } + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + if httpResponse == nil || httpResponse.StatusCode != http.StatusOK { + statusCode := 0 + status := "unknown" + if httpResponse != nil { + statusCode = httpResponse.StatusCode + status = httpResponse.Status + } + return fmt.Errorf("failed to authenticate with Akeyless (HTTP status code: %d): %s", statusCode, status) + } + if out != nil && out.GetToken() == "" { + return errors.New("authentication failed, no token returned") + } + if out != nil && out.GetExpiration() == "" { + return errors.New("authentication failed, no expiration time returned") + } + + a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) + + // Store token and expiration with mutex protection + a.mu.Lock() + a.token = out.GetToken() + expirationStr := out.GetExpiration() + a.mu.Unlock() + + // Parse and store expiration time + if expirationStr != "" { + expiration, err := parseTokenExpirationDate(expirationStr) + if err != nil { + a.logger.Warnf("failed to parse token expiration '%s': %v", expirationStr, err) + } else { + a.mu.Lock() + a.tokenExpiry = expiration + a.mu.Unlock() + a.logger.Debugf("token expiration parsed and set successfully: %s", expiration.Format(time.RFC3339)) + } + } + + return nil +} + +// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values. +func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) { + if a.v2 == nil { + return secretstores.GetSecretResponse{}, errors.New("akeyless client not initialized") + } + + a.logger.Debugf("getting secret type for '%s'...", req.Name) + secretType, err := a.getSecretType(ctx, req.Name) + if err != nil { + return secretstores.GetSecretResponse{}, errors.New("failed to get secret type: " + err.Error()) + } + + a.logger.Debugf("getting secret value for '%s' (type %s)...", req.Name, secretType) + + secretValue, err := a.getSingleSecretValue(ctx, req.Name, secretType) + if err != nil { + return secretstores.GetSecretResponse{}, errors.New(err.Error()) + } + a.logger.Debugf("successfully retrieved secret '%s'", req.Name) + + return getDaprSingleSecretResponse(req.Name, secretValue) +} + +// BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values. +// The method performs the following steps: +// 1. Recursively list all items in Akeyless +// 2. Filter out inactive/failing secrets +// 3. Separate items by type since only static secrets are supported for bulk get +// 4. Get secret values concurrently, each item type in a separate goroutine +func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstores.BulkGetSecretRequest) (secretstores.BulkGetSecretResponse, error) { + if a.v2 == nil { + return secretstores.BulkGetSecretResponse{}, errors.New("akeyless client not initialized") + } + + // initialize response + response := secretstores.BulkGetSecretResponse{ + Data: make(map[string]map[string]string), + } + + // get secrets path to retrieve secrets from + // use root path if not specified + var secretsPath string + if value, ok := req.Metadata[MetadataPathKey]; ok { + // normalize path + if !strings.HasPrefix(value, "/") { + secretsPath = "/" + value + } + + a.logger.Debugf("using path '%s' from metadata...", secretsPath) + } else { + a.logger.Debugf("no path found in metadata, using default path '%s'", PathDefault) + secretsPath = PathDefault + } + + // get secrets type to retrieve secrets from + // use all types if not specified + var requestedTypes []string + if value, ok := req.Metadata[MetadataSecretsTypeKey]; ok { + parsedTypes, err := parseSecretTypes(value) + if err != nil { + return response, fmt.Errorf("invalid secrets_type metadata: %w", err) + } + requestedTypes = parsedTypes + a.logger.Debugf("using secrets types '%v' from metadata...", requestedTypes) + } else { + a.logger.Debugf("no '%s' found in metadata, using all supported secret types '%v'", MetadataSecretsTypeKey, supportedSecretTypes) + requestedTypes = supportedSecretTypes + } + + // For bulk get, we need to list all secrets first + a.logger.Debugf("listing items from '%s' path with types '%v'...", secretsPath, requestedTypes) + listItems, err := a.listItemsRecursively(ctx, secretsPath, requestedTypes) + if err != nil { + return response, fmt.Errorf("failed to list items from Akeyless: %w", err) + } + + // if no items returned, return empty response + if len(listItems) == 0 { + a.logger.Debug("no items returned from / path") + return response, nil + } + + // filter out inactive secrets + a.logger.Debugf("%d items before filtering out inactive secrets", len(listItems)) + listItems = a.filterInactiveSecrets(listItems) + a.logger.Debugf("%d items remaining after filtering out inactive secrets", len(listItems)) + + // separate items by type since only static secrets are supported for bulk get + staticItemNames, dynamicItemNames, rotatedItemNames := a.separateItemsByType(listItems) + a.logger.Infof("%d items returned (static: %d, dynamic: %d, rotated: %d)", len(listItems), len(staticItemNames), len(dynamicItemNames), len(rotatedItemNames)) + + haveStaticItems := len(staticItemNames) > 0 + haveDynamicItems := len(dynamicItemNames) > 0 + haveRotatedItems := len(rotatedItemNames) > 0 + + secretResultChannels := make(chan secretResultCollection, len(listItems)) + + // get secret values concurrently, each item type in a separate goroutine + wg := sync.WaitGroup{} + if haveStaticItems { + wg.Add(1) + go func() { + defer wg.Done() + if len(staticItemNames) == 1 { + staticSecretName := staticItemNames[0] + value, err := a.getSingleSecretValue(ctx, staticSecretName, StaticSecretResponse) + if err != nil { + secretResultChannels <- secretResultCollection{name: staticSecretName, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: nil} + } + } else { + secretResponse := a.getBulkStaticSecretValues(ctx, staticItemNames) + if len(secretResponse) > 0 { + for _, result := range secretResponse { + secretResultChannels <- result + } + } + } + }() + } + if haveDynamicItems { + wg.Add(1) + go func() { + defer wg.Done() + for _, item := range dynamicItemNames { + value, err := a.getSingleSecretValue(ctx, item, DynamicSecretResponse) + if err != nil { + secretResultChannels <- secretResultCollection{name: item, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: item, value: value, err: nil} + } + } + }() + } + if haveRotatedItems { + wg.Add(1) + go func() { + defer wg.Done() + for _, item := range rotatedItemNames { + value, err := a.getSingleSecretValue(ctx, item, RotatedSecretResponse) + if err != nil { + secretResultChannels <- secretResultCollection{name: item, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: item, value: value, err: nil} + } + } + }() + } + + // close the channel when all goroutines are done + go func() { + wg.Wait() + close(secretResultChannels) + }() + + // collect results and populate response + for result := range secretResultChannels { + if result.err != nil { + a.logger.Errorf("error getting secret '%s': %s. Skipping...", result.name, result.err.Error()) + continue + } + + response.Data[result.name] = map[string]string{result.name: result.value} + } + + // Use the new BulkGetSecretResponse function to handle all secret types properly + // return BulkGetSecretResponse(ctx, itemsList.Items, a) + return response, nil +} + +// Features returns the features available in this secret store. +func (a *akeylessSecretStore) Features() []secretstores.Feature { + return []secretstores.Feature{} +} + +// Close closes the secret store. +func (a *akeylessSecretStore) Close() error { + if a.closeCh != nil { + close(a.closeCh) + a.wg.Wait() + } + return nil +} + +// parseMetadata parses the metadata from the component configuration. +func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeylessMetadata, error) { + a.logger.Debug("Parsing metadata...") + var m akeylessMetadata + err := kitmd.DecodeMetadata(meta.Properties, &m) + if err != nil { + return nil, err + } + + // Validate access ID + if m.AccessID == "" { + return nil, errors.New("accessId is required") + } + + if !isValidAccessIDFormat(m.AccessID) { + return nil, errors.New("invalid accessId format, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + } + + // Set default gateway URL if not specified + if m.GatewayURL == "" { + a.logger.Infof("Gateway URL is not set, using default value %s...", PublicGatewayURL) + m.GatewayURL = PublicGatewayURL + } else { + _, err = url.ParseRequestURI(m.GatewayURL) + if err != nil { + return nil, fmt.Errorf("invalid gateway URL '%s': %w", m.GatewayURL, err) + } + } + + // Trim trailing slash from gateway URL + m.GatewayURL = strings.TrimSuffix(m.GatewayURL, "/") + + return &m, nil +} + +func (a *akeylessSecretStore) getSecretType(ctx context.Context, secretName string) (string, error) { + if err := a.ensureValidToken(ctx); err != nil { + return "", fmt.Errorf("failed to ensure valid token: %w", err) + } + + describeItem := akeylesssdk.NewDescribeItem(secretName) + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + describeItem.SetToken(token) + + result, httpResponse, err := a.executeWithRetryOn401( + ctx, + "DescribeItem", + describeItem, + func(newToken string) { + describeItem.SetToken(newToken) + }, + ) + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + + if err != nil { + return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) + } + + describeItemResp, ok := result.(*akeylesssdk.Item) + if !ok { + return "", fmt.Errorf("unexpected result type from DescribeItem: %T", result) + } + + if describeItemResp.ItemType == nil { + return "", errors.New("unable to retrieve secret type, missing type in describe item response") + } + + return *describeItemResp.ItemType, nil +} + +// executeWithRetryOn401 executes an API call using reflection and retries once if it receives a 401 Unauthorized response. +// It takes the method name (e.g., "GetSecretValue"), the body object, and a function to update the token in the body. +// Returns the result, httpResponse, and error using reflection. +func (a *akeylessSecretStore) executeWithRetryOn401( + ctx context.Context, + methodName string, + body interface{}, + updateToken func(string), +) (interface{}, *http.Response, error) { + // Helper to get current token (with mutex protection) + getToken := func() string { + a.mu.RLock() + defer a.mu.RUnlock() + return a.token + } + + // Helper function to execute the API call using reflection + executeCall := func() (interface{}, *http.Response, error) { + // Use reflection to call the method dynamically + v2Value := reflect.ValueOf(a.v2) + method := v2Value.MethodByName(methodName) + if !method.IsValid() { + return nil, nil, errors.New("method " + methodName + " not found on V2ApiService") + } + + // Call the method with context: a.v2.MethodName(ctx) + ctxValue := reflect.ValueOf(ctx) + callResult := method.Call([]reflect.Value{ctxValue}) + if len(callResult) == 0 { + return nil, nil, errors.New("method " + methodName + " returned no values") + } + + // Get the Body() method from the result: result.Body() + bodyMethod := callResult[0].MethodByName("Body") + if !bodyMethod.IsValid() { + return nil, nil, errors.New("Body method not found on result of " + methodName) + } + + // Call Body(*body): result.Body(*body) + // Body() expects a value (not a pointer), so we need to dereference if it's a pointer + bodyValue := reflect.ValueOf(body) + if bodyValue.Kind() == reflect.Ptr { + // Dereference the pointer to get the value + bodyValue = bodyValue.Elem() + } + // Pass the value to Body() + bodyCallResult := bodyMethod.Call([]reflect.Value{bodyValue}) + if len(bodyCallResult) == 0 { + return nil, nil, errors.New("body method returned no values") + } + + // Get the Execute() method: result.Body(*body).Execute() + executeMethod := bodyCallResult[0].MethodByName("Execute") + if !executeMethod.IsValid() { + return nil, nil, errors.New("execute method not found on Body result") + } + + // Execute the API call: result.Body(*body).Execute() + executeResult := executeMethod.Call([]reflect.Value{}) + if len(executeResult) < 3 { + return nil, nil, errors.New("execute method did not return 3 values (result, response, error)") + } + + // Extract results + var result interface{} + var httpResponse *http.Response + var apiErr error + + if !executeResult[0].IsNil() { + result = executeResult[0].Interface() + } + if !executeResult[1].IsNil() { + httpResponse = executeResult[1].Interface().(*http.Response) + } + if !executeResult[2].IsNil() { + apiErr = executeResult[2].Interface().(error) + } + + return result, httpResponse, apiErr + } + + // Execute the API call + result, httpResponse, apiErr := executeCall() + + // Check for 401 Unauthorized using the actual HTTP status code + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + a.logger.Debugf("received 401 unauthorized in %s, re-authenticating...", methodName) + if reauthErr := a.ensureValidToken(ctx); reauthErr != nil { + return nil, httpResponse, fmt.Errorf("failed to re-authenticate after 401: %w", reauthErr) + } + // Update token in the request object before retry + newToken := getToken() + updateToken(newToken) + + // Retry the API call once + return executeCall() + } + + return result, httpResponse, apiErr +} + +// getSingleSecretValue gets the value of a single secret from Akeyless. +// It returns the value of the secret or an error if the secret is not found. +func (a *akeylessSecretStore) getSingleSecretValue(ctx context.Context, secretName string, secretType string) (string, error) { + if err := a.ensureValidToken(ctx); err != nil { + return "", fmt.Errorf("failed to ensure valid token: %w", err) + } + + var secretValue string + var err error + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + switch secretType { + case StaticSecretResponse: + getSecretValue := akeylesssdk.NewGetSecretValue([]string{secretName}) + getSecretValue.SetToken(token) + + result, httpResponse, apiErr := a.executeWithRetryOn401( + ctx, + "GetSecretValue", + getSecretValue, + func(newToken string) { + getSecretValue.SetToken(newToken) + }, + ) + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + + if apiErr != nil { + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) + break + } + + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetSecretValue: %T", result) + break + } + + // check if secret key is in response + value, ok := secretRespMap[secretName] + if !ok { + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: key not found", secretName) + break + } + + // single static secrets can be of type string, or map[string]string + // if it's a map[string]string, we need to transform it to a string + secretValue, err = stringifyStaticSecret(value, secretName) + if err != nil { + err = fmt.Errorf("failed to stringify static secret '%s': %w", secretName, err) + break + } + + case DynamicSecretResponse: + getDynamicSecretValue := akeylesssdk.NewGetDynamicSecretValue(secretName) + getDynamicSecretValue.SetToken(token) + + result, httpResponse, apiErr := a.executeWithRetryOn401( + ctx, + "GetDynamicSecretValue", + getDynamicSecretValue, + func(newToken string) { + getDynamicSecretValue.SetToken(newToken) + }, + ) + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + + if apiErr != nil { + err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) + break + } + + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetDynamicSecretValue: %T", result) + break + } + + // Parse response to extract value and check for errors + var dynamicSecretResp struct { + Value string `json:"value"` + Error string `json:"error"` + } + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + if unmarshalErr := json.Unmarshal(jsonBytes, &dynamicSecretResp); unmarshalErr != nil { + err = fmt.Errorf("failed to unmarshal secret response: %w", unmarshalErr) + break + } + + // Check if the response contains an error + if dynamicSecretResp.Error != "" { + err = fmt.Errorf("dynamic secret retrieval error: %s", dynamicSecretResp.Error) + break + } + + // Return the value field directly (already a JSON string with credentials) + secretValue = dynamicSecretResp.Value + + case RotatedSecretResponse: + getRotatedSecretValue := akeylesssdk.NewGetRotatedSecretValue(secretName) + getRotatedSecretValue.SetToken(token) + + result, httpResponse, apiErr := a.executeWithRetryOn401( + ctx, + "GetRotatedSecretValue", + getRotatedSecretValue, + func(newToken string) { + getRotatedSecretValue.SetToken(newToken) + }, + ) + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + + if apiErr != nil { + err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) + break + } + + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetRotatedSecretValue: %T", result) + break + } + + // Marshal the entire response value object + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal rotated secret response to JSON: %w", marshalErr) + break + } + secretValue = string(jsonBytes) + } + + return secretValue, err +} + +// getBulkStaticSecretValues gets the values of multiple static secrets from Akeyless. +// It returns a map of secret names and their values. +func (a *akeylessSecretStore) getBulkStaticSecretValues(ctx context.Context, secretNames []string) []secretResultCollection { + if err := a.ensureValidToken(ctx); err != nil { + return []secretResultCollection{ + {name: "", value: "", err: fmt.Errorf("failed to ensure valid token: %w", err)}, + } + } + + var secretResponse []secretResultCollection + + getSecretsValues := akeylesssdk.NewGetSecretValue(secretNames) + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + getSecretsValues.SetToken(token) + + secretRespMap, httpResponse, apiErr := a.v2.GetSecretValue(ctx).Body(*getSecretsValues).Execute() + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + + // Handle 401 Unauthorized by re-authenticating and retrying once + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + a.logger.Debug("received 401 Unauthorized in bulk get, re-authenticating...") + if err := a.ensureValidToken(ctx); err != nil { + secretResponse = append(secretResponse, secretResultCollection{ + name: "", value: "", err: fmt.Errorf("failed to re-authenticate after 401: %w", err), + }) + return secretResponse + } + + a.mu.RLock() + token = a.token + a.mu.RUnlock() + + getSecretsValues.SetToken(token) + secretRespMap, httpResponse, apiErr = a.v2.GetSecretValue(ctx).Body(*getSecretsValues).Execute() + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + } + + if apiErr != nil { + secretResponse = append(secretResponse, secretResultCollection{ + name: "", value: "", err: fmt.Errorf("failed to get static secrets' '%s' value from Akeyless API: %w", secretNames, apiErr), + }) + } else { + for secretName, secretValue := range secretRespMap { + value, err := stringifyStaticSecret(secretValue, secretName) + secretResponse = append(secretResponse, secretResultCollection{name: secretName, value: value, err: err}) + } + } + + return secretResponse +} + +// listItemsRecursively lists all items in a given path recursively. +// It returns a list of items and an error if the list items request fails. +func (a *akeylessSecretStore) listItemsRecursively(ctx context.Context, path string, types []string) ([]akeylesssdk.Item, error) { + if err := a.ensureValidToken(ctx); err != nil { + return nil, fmt.Errorf("failed to ensure valid token: %w", err) + } + + var allItems []akeylesssdk.Item + + // Create the list items request + listItems := akeylesssdk.NewListItems() + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + listItems.SetToken(token) + listItems.SetPath(path) + listItems.SetAutoPagination("enabled") + listItems.SetType(types) + + // Execute the list items request + a.logger.Debugf("listing items from path '%s'...", path) + result, httpResponse, err := a.executeWithRetryOn401( + ctx, + "ListItems", + listItems, + func(newToken string) { + listItems.SetToken(newToken) + }, + ) + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + + if err != nil { + return nil, err + } + + itemsList, ok := result.(*akeylesssdk.ListItemsInPathOutput) + if !ok { + return nil, fmt.Errorf("unexpected result type from ListItems: %T", result) + } + + // Add items from current path + if itemsList.Items != nil { + allItems = append(allItems, itemsList.Items...) + } + + // Recursively process each subfolder + if itemsList.Folders != nil { + for _, folder := range itemsList.Folders { + subItems, err := a.listItemsRecursively(ctx, folder, types) + if err != nil { + return nil, err + } + allItems = append(allItems, subItems...) + } + } + + return allItems, nil +} + +func (a *akeylessSecretStore) separateItemsByType(items []akeylesssdk.Item) ([]string, []string, []string) { + var staticItems []akeylesssdk.Item + var dynamicItems []akeylesssdk.Item + var rotatedItems []akeylesssdk.Item + for _, item := range items { + itemType := *item.ItemType + + switch itemType { + case StaticSecretResponse: + staticItems = append(staticItems, item) + case DynamicSecretResponse: + dynamicItems = append(dynamicItems, item) + case RotatedSecretResponse: + rotatedItems = append(rotatedItems, item) + } + } + + // listItems can get quite large, so we don't need all item details, we can use the item names instead + // and free memory + staticItemNames := getItemNames(staticItems) + dynamicItemNames := getItemNames(dynamicItems) + rotatedItemNames := getItemNames(rotatedItems) + a.logger.Debugf("static items: %v", staticItemNames) + a.logger.Debugf("dynamic items: %v", dynamicItemNames) + a.logger.Debugf("rotated items: %v", rotatedItemNames) + + return staticItemNames, dynamicItemNames, rotatedItemNames +} + +func (a *akeylessSecretStore) filterInactiveSecrets(secrets []akeylesssdk.Item) []akeylesssdk.Item { + filteredSecrets := []akeylesssdk.Item{} + + for _, secret := range secrets { + if isSecretActive(secret, a.logger) { + filteredSecrets = append(filteredSecrets, secret) + } + } + + return filteredSecrets +} + +// ensureValidToken checks if the token is valid and refreshes it if needed (5 minutes before expiration) +// It returns an error if the token refresh fails. +func (a *akeylessSecretStore) ensureValidToken(ctx context.Context) error { + a.mu.RLock() + expiry := a.tokenExpiry + metadata := a.metadata + a.mu.RUnlock() + + // If token expiry is zero, we can't validate it, so skip validation + // This can happen if expiration parsing failed or wasn't provided + if expiry.IsZero() { + a.logger.Debug("token expiration not set, skipping validation") + return nil + } + + tokenValid := time.Now().Before(expiry.Add(-TokenRefreshGracePeriod)) + if tokenValid { + return nil + } + + // Token expired or about to expire, need to refresh/reauthenticate + a.logger.Debug("token expired or about to expire, reauthenticating...") + a.mu.Lock() + defer a.mu.Unlock() + + // Double-check after acquiring lock (another goroutine might have refreshed) + expiry = a.tokenExpiry + if expiry.IsZero() || time.Now().Before(expiry.Add(-TokenRefreshGracePeriod)) { + return nil + } + + return a.authenticate(ctx, metadata) +} + +// startTokenRefreshRoutine starts a bg goroutine that refreshes the token +func (a *akeylessSecretStore) startTokenRefreshRoutine(ctx context.Context, metadata *akeylessMetadata) { + a.wg.Add(1) + go func() { + defer a.wg.Done() + // Use background context for the refresh routine, not the init context + refreshCtx := context.Background() + + for { + // Check if we should stop first, before acquiring any locks + select { + case <-a.closeCh: + a.logger.Debug("token refresh routine stopped") + return + default: + } + + a.mu.RLock() + expiry := a.tokenExpiry + a.mu.RUnlock() + + if expiry.IsZero() { + a.logger.Warn("token expiration is zero, stopping refresh routine...") + return + } + + refreshDuration := time.Until(expiry.Add(-TokenRefreshGracePeriod)) + if refreshDuration <= 0 { + refreshDuration = time.Minute // Refresh immediately if less than 1 minute left + } + + a.logger.Debugf("next token refresh scheduled in %v", refreshDuration) + + select { + case <-time.After(refreshDuration): + a.logger.Debug("refreshing token...") + if err := a.authenticate(refreshCtx, metadata); err != nil { + a.logger.Errorf("failed to refresh token: %v", err) + // Retry after 1 minute on failure + time.Sleep(time.Minute) + continue + } + a.logger.Debug("token refreshed successfully") + case <-a.closeCh: + a.logger.Debug("token refresh routine stopped") + return + } + } + }() +} + +func (a *akeylessSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + metadataStruct := akeylessMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) + return +} diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go new file mode 100644 index 0000000000..6231ac5322 --- /dev/null +++ b/secretstores/akeyless/akeyless_test.go @@ -0,0 +1,1325 @@ +/* +Copyright 2026 The Dapr Authors +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 akeyless + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + akeylesssdk "github.com/akeylesslabs/akeyless-go/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" +) + +const ( + testAccessIDIAM = "p-xt3sT2nah7gpwm" + testAccessIDJwt = "p-xt3sT2nah7gpom" + testAccessIDKey = "p-xt3sT2nah7gpam" + testAccessKey = "ABCD1233xxx=" + // { + // "sub": "1234567890", + // "name": "John Doe", + // "iat": 1516239022 + // } + testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QeJkP5vWKT_yUZJgIeUAnYw2brk" + testSecretValue = "r3vE4L3D" +) + +var ( + mockStaticSecretItem = "/static-secret-test" + mockStaticSecretJSONItemName = "/static-secret-json-test" + mockStaticSecretPasswordItemName = "/static-secret-password-test" + mockDynamicSecretItemName = "/dynamic-secret-test" + mockRotatedSecretItemName = "/rotated-secret-test" + mockDescribeStaticSecretName = "/path/to/akeyless" + mockStaticSecretItem + mockDescribeStaticSecretType = StaticSecretResponse + mockDescribeStaticSecretItemResponse = akeylesssdk.Item{ + ItemName: &mockDescribeStaticSecretName, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mockStaticSecretJSONName = "/path/to/akeyless" + mockStaticSecretJSONItemName + mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ + mockStaticSecretJSONName: { + "some": "json", + }, + } + mockStaticSecretJSONItemResponse = akeylesssdk.Item{ + ItemName: &mockStaticSecretJSONName, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mockStaticSecretPasswordName = "/path/to/akeyless" + mockStaticSecretPasswordItemName + mockGetSingleSecretPasswordValueResponse = map[string]map[string]string{ + mockStaticSecretPasswordName: { + "password": testSecretValue, + "username": "akeyless", + }, + } + mockDescribeDynamicSecretName = "/path/to/akeyless" + mockDynamicSecretItemName + mockDescribeDynamicSecretType = DynamicSecretResponse + mockDescribeDynamicSecretItemResponse = akeylesssdk.Item{ + ItemName: &mockDescribeDynamicSecretName, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeylesssdk.ItemGeneralInfo{ + DynamicSecretProducerDetails: &akeylesssdk.DynamicSecretProducerInfo{ + ProducerStatus: func(s string) *string { return &s }("ProducerConnected"), + }, + }, + } + mockGetSingleDynamicSecretValueResponse = map[string]interface{}{ + "value": "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}", + "error": "", + } + mockDescribeRotatedSecretName = "/path/to/akeyless" + mockRotatedSecretItemName + mockDescribeRotatedSecretType = RotatedSecretResponse + mockDescribeRotatedSecretItemResponse = akeylesssdk.Item{ + ItemName: &mockDescribeRotatedSecretName, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeylesssdk.ItemGeneralInfo{ + RotatedSecretDetails: &akeylesssdk.RotatedSecretDetailsInfo{ + RotatorStatus: func(s string) *string { return &s }("RotationSucceeded"), + }, + }, + } + mockGetSingleRotatedSecretValueResponse = map[string]interface{}{ + "value": map[string]interface{}{ + "username": "abcdefghijklmnopqrstuvwxyz", + "password": testSecretValue, + "application_id": "1234567890", + }, + } +) + +var mockGetSingleSecretValueResponse = map[string]string{ + mockDescribeStaticSecretName: testSecretValue, +} + +// Global mock server for all tests +var mockGateway *httptest.Server + +// mockAuthenticate is a test version of the Authenticate function that uses a mock cloud ID +func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { + // Initialize closeCh if not already set + if akeylessSecretStore.closeCh == nil { + akeylessSecretStore.closeCh = make(chan struct{}) + } + + authRequest := akeylesssdk.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + + authRequest.SetAccessKey(metadata.AccessKey) + + config := akeylesssdk.NewConfiguration() + config.Servers = []akeylesssdk.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = UserAgent + config.AddDefaultHeader("akeylessclienttype", UserAgent) + + akeylessSecretStore.v2 = akeylesssdk.NewAPIClient(config).V2Api + + out, httpResponse, err := akeylessSecretStore.v2.Auth(context.TODO()).Body(*authRequest).Execute() + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + if err != nil { + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + + akeylessSecretStore.mu.Lock() + akeylessSecretStore.token = out.GetToken() + expirationStr := out.GetExpiration() + akeylessSecretStore.mu.Unlock() + + // Parse and store expiration time (same as in authenticate) + if expirationStr != "" { + expiration, err := parseTokenExpirationDate(expirationStr) + if err != nil { + // Log warning but don't fail - expiration parsing is optional + akeylessSecretStore.logger.Debugf("failed to parse token expiration '%s': %v", expirationStr, err) + } else { + akeylessSecretStore.mu.Lock() + akeylessSecretStore.tokenExpiry = expiration + akeylessSecretStore.mu.Unlock() + } + } + + return nil +} + +// TestMain sets up and tears down the mock server for all tests +func TestMain(m *testing.M) { + // Setup mock server that returns an *akeylesssdk.AuthOutput + mockGateway = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeylesssdk.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single static secret value + case "/get-secret-value": + jsonResponse, _ := json.Marshal(mockGetSingleSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/list-items": + listItemsResponse := akeylesssdk.NewListItemsInPathOutput() + listItemsResponse.SetItems( + []akeylesssdk.Item{mockDescribeStaticSecretItemResponse}, + ) + jsonResponse, _ := json.Marshal(listItemsResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(mockDescribeStaticSecretItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + // Run tests + code := m.Run() + + // Exit with the same code as the tests + os.Exit(code) +} + +func TestNewAkeylessSecretStore(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + assert.NotNil(t, store) +} + +func TestInit(t *testing.T) { + tests := []struct { + name string + metadata secretstores.Metadata + expectError bool + }{ + { + name: "gw, access id and key", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "gw, access id and jwt", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDJwt, + "jwt": testJWT, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "gw, access id (aws_iam)", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDIAM, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "missing access id", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + defer store.Close() // Clean up background goroutine + + tt.metadata.Properties["gatewayUrl"] = mockGateway.URL + + // For AWS IAM test, use mock authentication to avoid AWS dependency + if tt.name == "gw, access id (aws_iam)" { + // Parse metadata first + m, err := store.parseMetadata(tt.metadata) + require.NoError(t, err) + + // Use mock authentication instead of the real one + err = mockAuthenticate(m, store) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + } + } else { + // Use normal Init for other test cases + err := store.Init(t.Context(), tt.metadata) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + } + } + }) + } +} + +func TestGetSecretWithoutInit(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + req := secretstores.GetSecretRequest{ + Name: "test-secret", + } + + _, err := store.GetSecret(t.Context(), req) + require.Error(t, err) + assert.Contains(t, err.Error(), "not initialized") +} + +func TestBulkGetSecretWithoutInit(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + req := secretstores.BulkGetSecretRequest{} + + _, err := store.BulkGetSecret(t.Context(), req) + require.Error(t, err) + assert.Contains(t, err.Error(), "not initialized") +} + +func TestFeatures(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + + features := store.Features() + assert.Empty(t, features) +} + +func TestClose(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + + err := store.Close() + assert.NoError(t, err) +} + +func TestParseMetadata(t *testing.T) { + tests := []struct { + name string + properties map[string]string + expectError bool + expected *akeylessMetadata + }{ + { + name: "valid metadata with access id and key", + properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIDKey, + AccessKey: testAccessKey, + GatewayURL: "https://api.akeyless.io", // Default gateway URL + }, + }, + { + name: "valid metadata with access id and jwt", + properties: map[string]string{ + "accessId": testAccessIDJwt, + "jwt": testJWT, + "gatewayUrl": mockGateway.URL, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIDJwt, + JWT: testJWT, + GatewayURL: mockGateway.URL, + }, + }, + { + name: "valid metadata with access id aws_iam", + properties: map[string]string{ + "accessId": testAccessIDIAM, + "gatewayUrl": mockGateway.URL, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIDIAM, + GatewayURL: mockGateway.URL, + }, + }, + { + name: "missing access id", + properties: map[string]string{ + "gatewayUrl": mockGateway.URL, + }, + expectError: true, + }, + { + name: "invalid gateway url", + properties: map[string]string{ + "gatewayUrl": "http:/invalidaddress", + }, + expectError: true, + }, + { + name: "invalid access id format", + properties: map[string]string{ + "accessId": "invalid", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: tt.properties, + }, + } + + result, err := store.parseMetadata(meta) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestMockServerReturnsAuthOutput(t *testing.T) { + // Test that the mock server properly returns an AuthOutput response + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + + // Test with access key authentication + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(t.Context(), meta) + require.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + assert.Equal(t, "t-1234567890", store.token) + defer store.Close() // Clean up background goroutine +} + +func TestMockAWSCloudID(t *testing.T) { + // Test that the mock AWS cloud ID works correctly + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + + // Test with AWS IAM authentication using mock cloud ID + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDIAM, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + // Parse metadata first + m, err := store.parseMetadata(meta) + require.NoError(t, err) + + // Use mock authentication with mock cloud ID + err = mockAuthenticate(m, store) + require.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + assert.Equal(t, "t-1234567890", store.token) +} + +func TestGetSecret(t *testing.T) { + // Setup a properly initialized store + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(t.Context(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + tests := []struct { + name string + request secretstores.GetSecretRequest + expectError bool + expectedSecret string + }{ + { + name: "test text single static secret", + request: secretstores.GetSecretRequest{ + Name: mockDescribeStaticSecretName, + }, + expectError: false, + expectedSecret: testSecretValue, + }, + // { + // name: "get non-existing secret", + // request: secretstores.GetSecretRequest{ + // Name: mockDescribeStaticSecretName, + // }, + // expectError: true, + // expectedSecret: "", + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := store.GetSecret(t.Context(), tt.request) + if tt.expectError { + require.Error(t, err) + assert.Empty(t, response.Data) + } else { + require.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, tt.request.Name) + assert.Equal(t, tt.expectedSecret, response.Data[tt.request.Name]) + } + }) + } +} + +func TestGetSingleSecretJSON(t *testing.T) { + gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth", "/v2/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeylesssdk.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single static secret value + case "/get-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleSecretJSONValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + mockDescribeItemResponse := akeylesssdk.Item{ + ItemName: &mockStaticSecretJSONName, + ItemType: &mockDescribeStaticSecretType, + } + jsonResponse, _ := json.Marshal(&mockDescribeItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": gateway.URL, + }, + }, + } + + err := store.Init(t.Context(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + response, err := store.GetSecret(t.Context(), secretstores.GetSecretRequest{ + Name: mockStaticSecretJSONName, + }) + require.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, mockStaticSecretJSONName) + assert.JSONEq(t, "{\"some\":\"json\"}", response.Data[mockStaticSecretJSONName]) + + gateway.Close() +} + +func TestGetSingleSecretPassword(t *testing.T) { + gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth", "/v2/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeylesssdk.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single static secret value + case "/get-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleSecretPasswordValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + mockDescribeItemResponse := akeylesssdk.Item{ + ItemName: &mockStaticSecretPasswordName, + ItemType: &mockDescribeStaticSecretType, + } + jsonResponse, _ := json.Marshal(&mockDescribeItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": gateway.URL, + }, + }, + } + + err := store.Init(t.Context(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + response, err := store.GetSecret(t.Context(), secretstores.GetSecretRequest{ + Name: mockStaticSecretPasswordName, + }) + require.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, mockStaticSecretPasswordName) + assert.JSONEq(t, "{\"password\":\"r3vE4L3D\",\"username\":\"akeyless\"}", response.Data[mockStaticSecretPasswordName]) + + gateway.Close() +} + +// Test GetSecretType functions +func TestGetSecretType(t *testing.T) { + // Test GetSecretType + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + ctx := t.Context() + err := store.Init(ctx, meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + secretType, err := store.getSecretType(ctx, mockDescribeStaticSecretName) + require.NoError(t, err) + assert.Equal(t, StaticSecretResponse, secretType) +} + +func TestGetSingleDynamicSecret(t *testing.T) { + gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeylesssdk.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-dynamic-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleDynamicSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(&mockDescribeDynamicSecretItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + // Test GetSingleDynamicSecret + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": gateway.URL, + }, + }, + } + + ctx := t.Context() + err := store.Init(ctx, meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + secretValue, err := store.getSingleSecretValue(ctx, mockDescribeDynamicSecretName, DynamicSecretResponse) + require.NoError(t, err) + assert.JSONEq(t, "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}", secretValue) + gateway.Close() +} + +func TestGetSingleRotatedSecret(t *testing.T) { + gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeylesssdk.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(&mockDescribeRotatedSecretItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + // Test GetSingleRotatedSecret + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": gateway.URL, + }, + }, + } + + ctx := t.Context() + err := store.Init(ctx, meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + secretValue, err := store.getSingleSecretValue(ctx, mockDescribeRotatedSecretName, RotatedSecretResponse) + require.NoError(t, err) + assert.JSONEq(t, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", secretValue) + + gateway.Close() +} + +func TestGetBulkSecretValues(t *testing.T) { + gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeylesssdk.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-secret-value": + secretValue := map[string]string{ + mockStaticSecretItem: testSecretValue, + mockStaticSecretJSONItemName: "{\"some\":\"json\"}", + } + jsonResponse, _ := json.Marshal(&secretValue) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/list-items": + items := akeylesssdk.NewListItemsInPathOutput() + items.SetItems( + []akeylesssdk.Item{ + mockDescribeStaticSecretItemResponse, + mockStaticSecretJSONItemResponse, + mockDescribeDynamicSecretItemResponse, + mockDescribeRotatedSecretItemResponse, + }, + ) + jsonResponse, _ := json.Marshal(&items) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-dynamic-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleDynamicSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": gateway.URL, + }, + }, + } + + err := store.Init(t.Context(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + response, err := store.BulkGetSecret(t.Context(), secretstores.BulkGetSecretRequest{}) + require.NoError(t, err) + assert.NotNil(t, response.Data) + + // Check that we got all 4 secrets (excluding any empty keys) + nonEmptySecrets := 0 + for key, value := range response.Data { + if key != "" && len(value) > 0 { + nonEmptySecrets++ + } + } + assert.Equal(t, 4, nonEmptySecrets) + + // Check static secret (text) - using the actual key from the response + staticSecretKey := "/static-secret-test" + assert.Contains(t, response.Data, staticSecretKey) + assert.Equal(t, testSecretValue, response.Data[staticSecretKey][staticSecretKey]) + + // Check static secret (JSON) + jsonSecretKey := "/static-secret-json-test" + assert.Contains(t, response.Data, jsonSecretKey) + assert.JSONEq(t, "{\"some\":\"json\"}", response.Data[jsonSecretKey][jsonSecretKey]) + + // Check dynamic secret + dynamicSecretKey := "/path/to/akeyless/dynamic-secret-test" //nolint:gosec // G101: test data only + assert.Contains(t, response.Data, dynamicSecretKey) + expectedDynamicValue := "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}" + assert.JSONEq(t, expectedDynamicValue, response.Data[dynamicSecretKey][dynamicSecretKey]) + + // Check rotated secret + rotatedSecretKey := "/path/to/akeyless/rotated-secret-test" //nolint:gosec // G101: test data only + assert.Contains(t, response.Data, rotatedSecretKey) + assert.JSONEq(t, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", response.Data[rotatedSecretKey][rotatedSecretKey]) + + gateway.Close() +} + +func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { + // Test recursive secret retrieval from different hierarchical paths + // This test simulates a folder structure where: + // - Root "/" contains 4 subfolders + // - Each subfolder contains different types of secrets + // - The listItemsRecursively method should traverse all folders + + // Define mock secrets for different paths + staticSecret1 := "/path/to/static/secrets/secret1" + staticSecret2 := "/path/to/static/secrets/secret2" + staticSecret3 := "/path/to/static/secrets/secret3" + dynamicSecret1 := "/path/to/dynamic/secrets/dynamic1" + dynamicSecret2 := "/path/to/dynamic/secrets/dynamic2" + rotatedSecret1 := "/path/to/rotated/secrets/rotated1" + mixedStaticSecret := "/path/to/mixed/secrets/mixed-static" //nolint:gosec // G101: test data only + mixedDynamicSecret := "/path/to/mixed/secrets/mixed-dynamic" //nolint:gosec // G101: test data only + mixedRotatedSecret := "/path/to/mixed/secrets/mixed-rotated" //nolint:gosec // G101: test data only + + // Create mock items for different paths + staticItem1 := akeylesssdk.Item{ + ItemName: &staticSecret1, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + staticItem2 := akeylesssdk.Item{ + ItemName: &staticSecret2, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + staticItem3 := akeylesssdk.Item{ + ItemName: &staticSecret3, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + dynamicItem1 := akeylesssdk.Item{ + ItemName: &dynamicSecret1, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + dynamicItem2 := akeylesssdk.Item{ + ItemName: &dynamicSecret2, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + rotatedItem1 := akeylesssdk.Item{ + ItemName: &rotatedSecret1, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedStaticItem := akeylesssdk.Item{ + ItemName: &mixedStaticSecret, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedDynamicItem := akeylesssdk.Item{ + ItemName: &mixedDynamicSecret, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedRotatedItem := akeylesssdk.Item{ + ItemName: &mixedRotatedSecret, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + + gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeylesssdk.NewAuthOutput() + authOutput.SetToken("t-1234567890") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-secret-value": + secretValue := map[string]string{ + staticSecret1: testSecretValue, + staticSecret2: "static-secret-2-value", + staticSecret3: "static-secret-3-value", + mixedStaticSecret: "mixed-static-secret-value", + } + jsonResponse, _ := json.Marshal(&secretValue) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/list-items": + // Parse the path from request body to determine what to return + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to read request body"}`)) + return + } + + var listItemsRequest akeylesssdk.ListItems + if err := json.Unmarshal(body, &listItemsRequest); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to parse request body"}`)) + return + } + + path := "" + if listItemsRequest.Path != nil { + path = *listItemsRequest.Path + } + // Debug: Uncomment to see recursive calls + // fmt.Printf("DEBUG: list-items called for path: '%s'\n", path) + + var items akeylesssdk.ListItemsInPathOutput + + switch path { + case "/": + // Root path returns only folders, no items + folders := []string{ + "/path/to/static/secrets", + "/path/to/dynamic/secrets", + "/path/to/rotated/secrets", + "/path/to/mixed/secrets", + } + items.SetFolders(folders) + items.SetItems([]akeylesssdk.Item{}) + + case "/path/to/static/secrets": + // Static secrets folder + items.SetItems([]akeylesssdk.Item{staticItem1, staticItem2, staticItem3}) + items.SetFolders([]string{}) + + case "/path/to/dynamic/secrets": + // Dynamic secrets folder + items.SetItems([]akeylesssdk.Item{dynamicItem1, dynamicItem2}) + items.SetFolders([]string{}) + + case "/path/to/rotated/secrets": + // Rotated secrets folder + items.SetItems([]akeylesssdk.Item{rotatedItem1}) + items.SetFolders([]string{}) + + case "/path/to/mixed/secrets": + // Mixed secrets folder + items.SetItems([]akeylesssdk.Item{mixedStaticItem, mixedDynamicItem, mixedRotatedItem}) + items.SetFolders([]string{}) + + default: + // Unknown path + items.SetItems([]akeylesssdk.Item{}) + items.SetFolders([]string{}) + } + + jsonResponse, _ := json.Marshal(&items) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-dynamic-secret-value": + // Create dynamic secret responses for each secret + dynamicSecretResponse := map[string]interface{}{ + "value": "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}", + "error": "", + } + jsonResponse, _ := json.Marshal(&dynamicSecretResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-rotated-secret-value": + // Create rotated secret response + rotatedSecretResponse := map[string]interface{}{ + "value": map[string]interface{}{ + "username": "rotated-user", + "password": "rotated-secret-1-value", + "application_id": "1234567890", + }, + } + jsonResponse, _ := json.Marshal(&rotatedSecretResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/describe-item": + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to read request body"}`)) + return + } + + var describeItemRequest akeylesssdk.DescribeItem + if err := json.Unmarshal(body, &describeItemRequest); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to parse request body"}`)) + return + } + + var itemResponse akeylesssdk.Item + switch describeItemRequest.Name { + case staticSecret1, staticSecret2, staticSecret3, mixedStaticSecret: + itemResponse = akeylesssdk.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + case dynamicSecret1, dynamicSecret2, mixedDynamicSecret: + itemResponse = akeylesssdk.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeylesssdk.ItemGeneralInfo{ + DynamicSecretProducerDetails: &akeylesssdk.DynamicSecretProducerInfo{ + ProducerStatus: func(s string) *string { return &s }("ProducerConnected"), + }, + }, + } + case rotatedSecret1, mixedRotatedSecret: + itemResponse = akeylesssdk.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeylesssdk.ItemGeneralInfo{ + RotatedSecretDetails: &akeylesssdk.RotatedSecretDetailsInfo{ + RotatorStatus: func(s string) *string { return &s }("RotationSucceeded"), + }, + }, + } + default: + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "invalid item name"}`)) + return + } + + jsonResponse, _ := json.Marshal(&itemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIDKey, + "accessKey": testAccessKey, + "gatewayUrl": gateway.URL, + }, + }, + } + + err := store.Init(t.Context(), meta) + require.NoError(t, err) + defer store.Close() // Clean up background goroutine + + response, err := store.BulkGetSecret(t.Context(), secretstores.BulkGetSecretRequest{}) + require.NoError(t, err) + assert.NotNil(t, response.Data) + + // Check that we got all 9 secrets (4 static, 3 dynamic, 2 rotated) + nonEmptySecrets := 0 + for key, value := range response.Data { + if key != "" && len(value) > 0 { + nonEmptySecrets++ + } + } + assert.Equal(t, 9, nonEmptySecrets) + + // Check static secrets from /path/to/static/secrets + assert.Contains(t, response.Data, staticSecret1) + assert.Equal(t, testSecretValue, response.Data[staticSecret1][staticSecret1]) + assert.Contains(t, response.Data, staticSecret2) + assert.Equal(t, "static-secret-2-value", response.Data[staticSecret2][staticSecret2]) + assert.Contains(t, response.Data, staticSecret3) + assert.Equal(t, "static-secret-3-value", response.Data[staticSecret3][staticSecret3]) + + // Check dynamic secrets from /path/to/dynamic/secrets + assert.Contains(t, response.Data, dynamicSecret1) + expectedDynamicValue1 := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" + assert.JSONEq(t, expectedDynamicValue1, response.Data[dynamicSecret1][dynamicSecret1]) + assert.Contains(t, response.Data, dynamicSecret2) + expectedDynamicValue2 := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" + assert.JSONEq(t, expectedDynamicValue2, response.Data[dynamicSecret2][dynamicSecret2]) + + // Check rotated secret from /path/to/rotated/secrets + assert.Contains(t, response.Data, rotatedSecret1) + expectedRotatedValue1 := "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"rotated-secret-1-value\",\"username\":\"rotated-user\"}}" + assert.JSONEq(t, expectedRotatedValue1, response.Data[rotatedSecret1][rotatedSecret1]) + + // Check mixed secrets from /path/to/mixed/secrets + assert.Contains(t, response.Data, mixedStaticSecret) + assert.Equal(t, "mixed-static-secret-value", response.Data[mixedStaticSecret][mixedStaticSecret]) + assert.Contains(t, response.Data, mixedDynamicSecret) + expectedMixedDynamicValue := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" + assert.JSONEq(t, expectedMixedDynamicValue, response.Data[mixedDynamicSecret][mixedDynamicSecret]) + assert.Contains(t, response.Data, mixedRotatedSecret) + expectedMixedRotatedValue := "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"rotated-secret-1-value\",\"username\":\"rotated-user\"}}" + assert.JSONEq(t, expectedMixedRotatedValue, response.Data[mixedRotatedSecret][mixedRotatedSecret]) + + gateway.Close() +} + +func TestParseSecretTypes(t *testing.T) { + tests := []struct { + name string + input string + expected []string + expectError bool + }{ + { + name: "all", + input: "all", + expected: []string{StaticSecretType, DynamicSecretType, RotatedSecretType}, + }, + { + name: "static", + input: "static", + expected: []string{StaticSecretType}, + }, + { + name: "dynamic", + input: "dynamic", + expected: []string{DynamicSecretType}, + }, + { + name: "rotated", + input: "rotated", + expected: []string{RotatedSecretType}, + }, + { + name: "static,dynamic", + input: "static,dynamic", + expected: []string{StaticSecretType, DynamicSecretType}, + }, + { + name: "static,dynamic,rotated", + input: "static,dynamic,rotated", + expected: []string{StaticSecretType, DynamicSecretType, RotatedSecretType}, + }, + { + name: "invalid", + input: "invalid", + expectError: true, + }, + { + name: "empty", + input: "", + expectError: false, + expected: supportedSecretTypes, + }, + { + name: "mixed case", + input: "Static,Dynamic,ROTATED", + expectError: false, + expected: []string{StaticSecretType, DynamicSecretType, RotatedSecretType}, + }, + { + name: "duplicates", + input: "static-secret,dynamic-secret,static-secret", + expectError: false, + expected: []string{StaticSecretType, DynamicSecretType}, + }, + { + name: "mixed sdk format and direct format", + input: "static-secret,dynamic-secret,rotated-secret,static", + expectError: false, + expected: []string{StaticSecretType, DynamicSecretType, RotatedSecretType}, + }, + { + name: "invalid type", + input: "invalid", + expectError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsedTypes, err := parseSecretTypes(tt.input) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, parsedTypes) + } + }) + } +} diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml new file mode 100644 index 0000000000..9137c1fdf1 --- /dev/null +++ b/secretstores/akeyless/metadata.yaml @@ -0,0 +1,101 @@ + +# Copyright 2026 The Dapr Authors +# 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. + + +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: secretstores +name: akeyless +version: v1 +status: beta +title: "Akeyless Secret Store" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-secret-stores/akeyless/ +metadata: + - name: gatewayUrl + required: false + description: | + The URL to the Akeyless Gateway API. Default is https://api.akeyless.io. + default: "https://api.akeyless.io" + example: "https://your.akeyless.gw" + type: string + - name: gatewayTlsCa + required: false + description: | + base64-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway + with a self-signed or custom CA certificate. + example: "LS0tLS1CRUdJTi..." + type: string + sensitive: true + - name: accessId + required: true + description: | + The Akeyless Access ID. Currently supported authentication methods are: API keys (`access_key`, default), JWT (`jwt`) and AWS IAM (`aws_iam`). + example: "p-123456780wm" + type: string + - name: jwt + required: false + description: | + If using the JWT authentication method, specify it here. + example: "eyJ..." + type: string + sensitive: true + - name: accessKey + required: false + description: | + If using the API key (access_key) authentication method, specify it here. + example: "ABCD1233...=" + type: string + sensitive: true + - name: k8sAuthConfigName + required: false + description: | + If using the k8s auth method, specify the name of the k8s auth config. + example: "k8s-auth-config" + type: string + - name: k8sGatewayUrl + required: false + description: | + The gateway URL that where the k8s auth config is located. + example: "http://gw.akeyless.svc.cluster.local:8000" + type: string + - name: k8sServiceAccountToken + required: false + description: | + If using the k8s auth method, specify the service account token. If not specified, + we will try to read it from the default service account token file. + example: "eyJ..." + type: string + sensitive: true + - name: k8sAuthConfigName + required: false + description: | + If using the k8s auth method, specify the name of the k8s auth config. + example: "k8s-auth-config" + type: string + - name: k8sGatewayUrl + required: false + description: | + The gateway URL that where the k8s auth config is located. + example: "https://gw.akeyless.svc.cluster.local" + type: string + - name: k8sServiceAccountToken + required: false + description: | + If using the k8s auth method, specify the service account token. If not specified, + we will try to read it from the default service account token file. + example: "eyJ..." + type: string + sensitive: true diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go new file mode 100644 index 0000000000..5e3b37fbc2 --- /dev/null +++ b/secretstores/akeyless/utils.go @@ -0,0 +1,344 @@ +/* +Copyright 2026 The Dapr Authors +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 akeyless + +import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "os" + "regexp" + "strings" + "time" + + akeylesssdk "github.com/akeylesslabs/akeyless-go/v5" + + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" +) + +const ( + AuthJWT = "jwt" + AuthDefault = "access_key" + AuthIAM = "aws_iam" + AuthK8S = "k8s" + PublicGatewayURL = "https://api.akeyless.io" + UserAgent = "dapr.io/akeyless-secret-store" + StaticSecretResponse = "STATIC_SECRET" + DynamicSecretResponse = "DYNAMIC_SECRET" + RotatedSecretResponse = "ROTATED_SECRET" + StaticSecretType = "static-secret" + DynamicSecretType = "dynamic-secret" + RotatedSecretType = "rotated-secret" + AllSecretTypes = "all" + ClientSource = "akeylessclienttype" + PathDefault = "/" + MetadataPathKey = "path" + MetadataSecretsTypeKey = "secrets_type" + TokenRefreshGracePeriod = 5 * time.Minute +) + +var supportedSecretTypes = []string{StaticSecretType, DynamicSecretType, RotatedSecretType} + +// AccessTypeCharMap maps single-character access types to their display names. +var accessTypeCharMap = map[string]string{ + "a": AuthDefault, + "o": AuthJWT, + "w": AuthIAM, + "k": AuthK8S, +} + +// AccessIDRegex is the compiled regular expression for validating Akeyless Access IDs. +var accessIDRegex = regexp.MustCompile(`^p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})$`) + +// isValidAccessIDFormat validates the format of an Akeyless Access ID. +// The format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12}). +// It returns true if the format is valid, and false otherwise. +func isValidAccessIDFormat(accessID string) bool { + return accessIDRegex.MatchString(accessID) +} + +// extractAccessTypeChar extracts the Akeyless Access Type character from a valid Access ID. +// The access type character is the second to last character of the ID part. +// It returns the single-character access type (e.g., 'a', 'o') or an empty string and an error if the format is invalid. +func extractAccessTypeChar(accessID string) (string, error) { + if !isValidAccessIDFormat(accessID) { + return "", errors.New("invalid access ID format") + } + parts := strings.Split(accessID, "-") + idPart := parts[1] // Get the part after "p-" + // The access type char is the second-to-last character + return string(idPart[len(idPart)-2]), nil +} + +// getAccessTypeDisplayName gets the full display name of the access type from the character. +// It returns the display name (e.g., 'api_key') or an error if the type character is unknown. +func getAccessTypeDisplayName(typeChar string) (string, error) { + if typeChar == "" { + return "", errors.New("unable to retrieve access type, missing type char") + } + displayName, ok := accessTypeCharMap[typeChar] + if !ok { + return "Unknown", errors.New("access type character not found in map") + } + return displayName, nil +} + +func getDaprSingleSecretResponse(secretName string, secretValue string) (secretstores.GetSecretResponse, error) { + return secretstores.GetSecretResponse{ + Data: map[string]string{ + secretName: secretValue, + }, + }, nil +} + +func getItemNames(items []akeylesssdk.Item) []string { + itemNames := []string{} + for _, item := range items { + itemNames = append(itemNames, *item.ItemName) + } + return itemNames +} + +func stringifyStaticSecret(secretValue any, secretName string) (string, error) { + var err error + + switch valueType := secretValue.(type) { + case string: + // valueType is already a string, no conversion needed + case map[string]string: + encoded, marshalErr := json.Marshal(valueType) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response for secret '%s': %w", secretName, marshalErr) + } else { + secretValue = string(encoded) + } + case any: + encoded, marshalErr := json.Marshal(valueType) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response for secret '%s': %w", secretName, marshalErr) + break + } else { + secretValue = string(encoded) + break + } + + default: + err = fmt.Errorf("failed to assert type of secret response to string for secret '%s'", secretName) + } + + // At this point, secretValue should be a string (either from case string or from marshaling) + if err != nil { + return "", err + } + return secretValue.(string), nil +} + +type secretResultCollection struct { + name string + value string + err error +} + +func isSecretActive(secret akeylesssdk.Item, logger logger.Logger) bool { + var isActive bool + + // check if secret has isEnabled field + if secret.IsEnabled == nil { + logger.Debugf("secret '%s' is missing isEnabled field, skipping...", *secret.ItemName) + return false + } + + if !*secret.IsEnabled { + logger.Debugf("secret '%s' is not enabled, skipping...", *secret.ItemName) + return false + } + + switch *secret.ItemType { + case StaticSecretResponse: + logger.Debugf("static secret '%s' is active", *secret.ItemName) + isActive = true + case DynamicSecretResponse: + // Check if ItemGeneralInfo is available, if not, include the secret + if secret.ItemGeneralInfo != nil && + secret.ItemGeneralInfo.DynamicSecretProducerDetails != nil && + secret.ItemGeneralInfo.DynamicSecretProducerDetails.ProducerStatus != nil { + status := *secret.ItemGeneralInfo.DynamicSecretProducerDetails.ProducerStatus + if status == "ProducerConnected" { + logger.Debugf("dynamic secret '%s' is active, adding to filtered secrets...", *secret.ItemName) + isActive = true + } else { + logger.Debugf("dynamic secret '%s' producer status is '%s', skipping...", *secret.ItemName, status) + } + } else { + // If detailed info is not available, include the secret + logger.Debugf("dynamic secret '%s' is missing detailed info. adding to filtered secrets...", *secret.ItemName) + isActive = true + } + case RotatedSecretResponse: + // Check if ItemGeneralInfo is available, if not, include the secret + if secret.ItemGeneralInfo != nil && + secret.ItemGeneralInfo.RotatedSecretDetails != nil && + secret.ItemGeneralInfo.RotatedSecretDetails.RotatorStatus != nil { + status := *secret.ItemGeneralInfo.RotatedSecretDetails.RotatorStatus + if status == "RotationSucceeded" || status == "RotationInitialStatus" { + isActive = true + } else { + logger.Debugf("rotated secret '%s' rotation status is '%s', skipping...", *secret.ItemName, status) + } + } else { + // If detailed info is not available, include the secret + logger.Debugf("rotated secret '%s' is missing detailed info. adding to filtered secrets...", *secret.ItemName) + isActive = true + } + default: + logger.Debugf("secret '%s' is of unsupported type '%s', skipping...", *secret.ItemName, *secret.ItemType) + isActive = false + } + + return isActive +} + +func setK8SAuthConfiguration(metadata akeylessMetadata, authRequest *akeylesssdk.Auth, a *akeylessSecretStore) error { + if metadata.K8SAuthConfigName == "" { + return errors.New("k8s auth config name is required") + } + authRequest.SetK8sAuthConfigName(metadata.K8SAuthConfigName) + if metadata.K8sServiceAccountToken == "" { + a.logger.Debug("k8s service account token is missing, attempting to read from default service account token file") + token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + return fmt.Errorf("failed to read default service account token file: %w", err) + } + metadata.K8sServiceAccountToken = string(token) + } + + // base64 encode the token if it's not already encoded + if _, err := base64.StdEncoding.DecodeString(metadata.K8sServiceAccountToken); err != nil { + a.logger.Info("k8sServiceAccountToken is not base64 encoded, encoding it...") + metadata.K8sServiceAccountToken = base64.StdEncoding.EncodeToString([]byte(metadata.K8sServiceAccountToken)) + } + authRequest.SetK8sServiceAccountToken(metadata.K8sServiceAccountToken) + + if metadata.K8SGatewayURL == "" { + a.logger.Debug("k8s gateway url is missing, using gatewayUrl") + metadata.K8SGatewayURL = metadata.GatewayURL + } + metadata.K8SGatewayURL = strings.TrimSuffix(metadata.K8SGatewayURL, "/api/v2") + authRequest.SetGatewayUrl(metadata.K8SGatewayURL) + return nil +} + +// `parseSecretTypes` parses the `secret_types` metadata parameter +// and returns a slice of supported secret types in the format expected +// by the Akeyless `POST /list-items` API. +// It accepts a comma-separated string of secret types and returns a slice of supported secret types. +func parseSecretTypes(secretTypes string) ([]string, error) { + // Handle "all" or empty string which returns all supported secret types + if secretTypes == AllSecretTypes || secretTypes == "" { + return supportedSecretTypes, nil + } + + // Parse comma-separated values + types := strings.Split(secretTypes, ",") + if len(types) == 0 { + return nil, errors.New("no secret types provided") + } + result := make([]string, 0, len(types)) + + // Map metadata.secret_types to supportedSecretTypes + typeMap := map[string]string{ + "static": StaticSecretType, + "dynamic": DynamicSecretType, + "rotated": RotatedSecretType, + } + + for _, t := range types { + t = strings.ToLower(strings.TrimSpace(t)) + if mappedType, ok := typeMap[t]; ok { + result = append(result, mappedType) + } else { + // Allow direct SDK format + if t == StaticSecretType || t == DynamicSecretType || t == RotatedSecretType { + result = append(result, t) + } else { + return nil, fmt.Errorf("invalid secret type '%s', supported types: static[-secret], dynamic[-secret], rotated[-secret]", t) + } + } + } + + // Dedup + seen := make(map[string]bool) + unique := []string{} + for _, t := range result { + if !seen[t] { + seen[t] = true + unique = append(unique, t) + } + } + + return unique, nil +} + +func createTLSConfig(gatewayTLSCa string) (*tls.Config, error) { + // Decode base64 to PEM + certBytes, err := base64.StdEncoding.DecodeString(gatewayTLSCa) + if err != nil { + return nil, fmt.Errorf("failed to decode base64-encoded gateway TLS CA: %w", err) + } + + // Validate PEM format + block, _ := pem.Decode(certBytes) + if block == nil { + return nil, errors.New("failed to decode PEM certificate: invalid PEM format") + } + + // Cereate cert pool and add certificate + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(certBytes) { + return nil, errors.New("failed to add certificate to cert pool") + } + + return &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: caCertPool, + }, nil +} + +func parseTokenExpirationDate(expirationStr string) (time.Time, error) { + // Try multiple formats to handle different expiration date formats + // Format 1: ISO 8601 format "2025-01-01T00:00:00Z" (used in tests) + layouts := []string{ + time.RFC3339, // "2006-01-02T15:04:05Z07:00" + time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00" + "2006-01-02T15:04:05Z", // "2006-01-02T15:04:05Z" + "2006-01-02 15:04:05 -0700 MST", // "2025-12-09 21:35:00 +0000 UTC" (custom format) + "2006-01-02 15:04:05 -0700", // "2025-12-09 21:35:00 +0000" (without MST) + } + + for _, layout := range layouts { + parsedTime, err := time.Parse(layout, expirationStr) + if err == nil { + return parsedTime, nil + } + } + + return time.Time{}, fmt.Errorf("failed to parse token expiration date '%s' with any supported format", expirationStr) +} diff --git a/secretstores/aws/parameterstore/parameterstore.go b/secretstores/aws/parameterstore/parameterstore.go index 038399b30c..0640260937 100644 --- a/secretstores/aws/parameterstore/parameterstore.go +++ b/secretstores/aws/parameterstore/parameterstore.go @@ -18,10 +18,13 @@ import ( "fmt" "reflect" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ssm" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsCommonAuth "github.com/dapr/components-contrib/common/aws/auth" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/types" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/secretstores" "github.com/dapr/kit/logger" @@ -52,9 +55,10 @@ type ParameterStoreMetaData struct { } type ssmSecretStore struct { - authProvider awsAuth.Provider - prefix string - logger logger.Logger + prefix string + logger logger.Logger + + ssmClient awsCommon.ParameterStoreClient } // Init creates an AWS secret manager client. @@ -64,20 +68,21 @@ func (s *ssmSecretStore) Init(ctx context.Context, metadata secretstores.Metadat return err } - opts := awsAuth.Options{ + configOpts := awsCommonAuth.Options{ Logger: s.logger, Properties: metadata.Properties, Region: m.Region, AccessKey: m.AccessKey, SecretKey: m.SecretKey, - SessionToken: "", + SessionToken: m.SessionToken, } - // extra configs needed per component type - provider, err := awsAuth.NewProvider(ctx, opts, awsAuth.GetConfig(opts)) + + config, err := awsCommon.NewConfig(ctx, configOpts) if err != nil { - return err + return fmt.Errorf("error creating AWS config: %w", err) } - s.authProvider = provider + + s.ssmClient = ssm.NewFromConfig(config) s.prefix = m.Prefix return nil @@ -93,7 +98,7 @@ func (s *ssmSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecr name = fmt.Sprintf("%s:%s", req.Name, versionID) } - output, err := s.authProvider.ParameterStore().Store.GetParameterWithContext(ctx, &ssm.GetParameterInput{ + output, err := s.ssmClient.GetParameter(ctx, &ssm.GetParameterInput{ Name: ptr.Of(s.prefix + name), WithDecryption: ptr.Of(true), }) @@ -121,19 +126,19 @@ func (s *ssmSecretStore) BulkGetSecret(ctx context.Context, req secretstores.Bul search := true var nextToken *string = nil - var filters []*ssm.ParameterStringFilter + var filters []types.ParameterStringFilter if s.prefix != "" { - filters = []*ssm.ParameterStringFilter{ + filters = []types.ParameterStringFilter{ { - Key: aws.String(ssm.ParametersFilterKeyName), + Key: aws.String(string(types.ParametersFilterKeyName)), Option: aws.String("BeginsWith"), - Values: aws.StringSlice([]string{s.prefix}), + Values: []string{s.prefix}, }, } } for search { - output, err := s.authProvider.ParameterStore().Store.DescribeParametersWithContext(ctx, &ssm.DescribeParametersInput{ + output, err := s.ssmClient.DescribeParameters(ctx, &ssm.DescribeParametersInput{ MaxResults: nil, NextToken: nextToken, ParameterFilters: filters, @@ -143,7 +148,7 @@ func (s *ssmSecretStore) BulkGetSecret(ctx context.Context, req secretstores.Bul } for _, entry := range output.Parameters { - params, err := s.authProvider.ParameterStore().Store.GetParameterWithContext(ctx, &ssm.GetParameterInput{ + params, err := s.ssmClient.GetParameter(ctx, &ssm.GetParameterInput{ Name: entry.Name, WithDecryption: aws.Bool(true), }) @@ -182,8 +187,6 @@ func (s *ssmSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataM } func (s *ssmSecretStore) Close() error { - if s.authProvider != nil { - return s.authProvider.Close() - } + // removed auth provider closers return nil } diff --git a/secretstores/aws/parameterstore/parameterstore_test.go b/secretstores/aws/parameterstore/parameterstore_test.go index c62eddbc2a..d751b6165d 100644 --- a/secretstores/aws/parameterstore/parameterstore_test.go +++ b/secretstores/aws/parameterstore/parameterstore_test.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +Copyright 2025 The Dapr Authors 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 @@ -21,12 +21,11 @@ import ( "strings" "testing" - "github.com/aws/aws-sdk-go/aws" + awsMock "github.com/dapr/components-contrib/common/aws/mock" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" + ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/ssm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -56,11 +55,11 @@ func TestInit(t *testing.T) { func TestGetSecret(t *testing.T) { t.Run("successfully retrieve secret", func(t *testing.T) { t.Run("with valid path", func(t *testing.T) { - mockSSM := &awsAuth.MockParameterStore{ - GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, option ...request.Option) (*ssm.GetParameterOutput, error) { + mockSSM := &awsMock.ParameterStoreClient{ + GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { secret := secretValue return &ssm.GetParameterOutput{ - Parameter: &ssm.Parameter{ + Parameter: &ssmTypes.Parameter{ Name: input.Name, Value: &secret, }, @@ -68,18 +67,8 @@ func TestGetSecret(t *testing.T) { }, } - paramStore := awsAuth.ParameterStoreClients{ - Store: mockSSM, - } - - mockedClients := awsAuth.Clients{ - ParameterStore: ¶mStore, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) - s := ssmSecretStore{ - authProvider: mockAuthProvider, + ssmClient: mockSSM, } req := secretstores.GetSecretRequest{ @@ -92,8 +81,8 @@ func TestGetSecret(t *testing.T) { }) t.Run("with version id", func(t *testing.T) { - mockSSM := &awsAuth.MockParameterStore{ - GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, option ...request.Option) (*ssm.GetParameterOutput, error) { + mockSSM := &awsMock.ParameterStoreClient{ + GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { secret := secretValue keys := strings.Split(*input.Name, ":") assert.NotNil(t, keys) @@ -101,7 +90,7 @@ func TestGetSecret(t *testing.T) { assert.Equalf(t, "1", keys[1], "Version IDs are same") return &ssm.GetParameterOutput{ - Parameter: &ssm.Parameter{ + Parameter: &ssmTypes.Parameter{ Name: &keys[0], Value: &secret, }, @@ -109,17 +98,8 @@ func TestGetSecret(t *testing.T) { }, } - paramStore := awsAuth.ParameterStoreClients{ - Store: mockSSM, - } - - mockedClients := awsAuth.Clients{ - ParameterStore: ¶mStore, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := ssmSecretStore{ - authProvider: mockAuthProvider, + ssmClient: mockSSM, } req := secretstores.GetSecretRequest{ @@ -134,13 +114,13 @@ func TestGetSecret(t *testing.T) { }) t.Run("with prefix", func(t *testing.T) { - mockSSM := &awsAuth.MockParameterStore{ - GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, option ...request.Option) (*ssm.GetParameterOutput, error) { + mockSSM := &awsMock.ParameterStoreClient{ + GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { assert.Equal(t, "/prefix/aws/dev/secret", *input.Name) secret := secretValue return &ssm.GetParameterOutput{ - Parameter: &ssm.Parameter{ + Parameter: &ssmTypes.Parameter{ Name: input.Name, Value: &secret, }, @@ -148,19 +128,9 @@ func TestGetSecret(t *testing.T) { }, } - paramStore := awsAuth.ParameterStoreClients{ - Store: mockSSM, - } - - mockedClients := awsAuth.Clients{ - ParameterStore: ¶mStore, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) - s := ssmSecretStore{ - authProvider: mockAuthProvider, - prefix: "/prefix", + ssmClient: mockSSM, + prefix: "/prefix", } req := secretstores.GetSecretRequest{ @@ -174,25 +144,15 @@ func TestGetSecret(t *testing.T) { }) t.Run("unsuccessfully retrieve secret", func(t *testing.T) { - mockSSM := &awsAuth.MockParameterStore{ - GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, option ...request.Option) (*ssm.GetParameterOutput, error) { + mockSSM := &awsMock.ParameterStoreClient{ + GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { return nil, errors.New("failed due to any reason") }, } - paramStore := awsAuth.ParameterStoreClients{ - Store: mockSSM, - } - - mockedClients := awsAuth.Clients{ - ParameterStore: ¶mStore, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) - s := ssmSecretStore{ - authProvider: mockAuthProvider, - prefix: "/prefix", + ssmClient: mockSSM, + prefix: "/prefix", } req := secretstores.GetSecretRequest{ @@ -206,22 +166,24 @@ func TestGetSecret(t *testing.T) { func TestGetBulkSecrets(t *testing.T) { t.Run("successfully retrieve bulk secrets", func(t *testing.T) { - mockSSM := &awsAuth.MockParameterStore{ - DescribeParametersFn: func(context.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error) { - return &ssm.DescribeParametersOutput{NextToken: nil, Parameters: []*ssm.ParameterMetadata{ + mockSSM := &awsMock.ParameterStoreClient{ + DescribeParametersFn: func(ctx context.Context, input *ssm.DescribeParametersInput, optFns ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) { + secret1 := "/aws/dev/secret1" //nolint:gosec + secret2 := "/aws/dev/secret2" //nolint:gosec + return &ssm.DescribeParametersOutput{NextToken: nil, Parameters: []ssmTypes.ParameterMetadata{ { - Name: aws.String("/aws/dev/secret1"), + Name: &secret1, }, { - Name: aws.String("/aws/dev/secret2"), + Name: &secret2, }, }}, nil }, - GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, option ...request.Option) (*ssm.GetParameterOutput, error) { + GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { secret := fmt.Sprintf("%s-%s", *input.Name, secretValue) return &ssm.GetParameterOutput{ - Parameter: &ssm.Parameter{ + Parameter: &ssmTypes.Parameter{ Name: input.Name, Value: &secret, }, @@ -229,17 +191,8 @@ func TestGetBulkSecrets(t *testing.T) { }, } - paramStore := awsAuth.ParameterStoreClients{ - Store: mockSSM, - } - - mockedClients := awsAuth.Clients{ - ParameterStore: ¶mStore, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := ssmSecretStore{ - authProvider: mockAuthProvider, + ssmClient: mockSSM, } req := secretstores.BulkGetSecretRequest{ @@ -252,22 +205,24 @@ func TestGetBulkSecrets(t *testing.T) { }) t.Run("successfully retrieve bulk secrets with prefix", func(t *testing.T) { - mockSSM := &awsAuth.MockParameterStore{ - DescribeParametersFn: func(context.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error) { - return &ssm.DescribeParametersOutput{NextToken: nil, Parameters: []*ssm.ParameterMetadata{ + mockSSM := &awsMock.ParameterStoreClient{ + DescribeParametersFn: func(ctx context.Context, input *ssm.DescribeParametersInput, optFns ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) { + secret1 := "/prefix/aws/dev/secret1" //nolint:gosec + secret2 := "/prefix/aws/dev/secret2" //nolint:gosec + return &ssm.DescribeParametersOutput{NextToken: nil, Parameters: []ssmTypes.ParameterMetadata{ { - Name: aws.String("/prefix/aws/dev/secret1"), + Name: &secret1, }, { - Name: aws.String("/prefix/aws/dev/secret2"), + Name: &secret2, }, }}, nil }, - GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, option ...request.Option) (*ssm.GetParameterOutput, error) { + GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { secret := fmt.Sprintf("%s-%s", *input.Name, secretValue) return &ssm.GetParameterOutput{ - Parameter: &ssm.Parameter{ + Parameter: &ssmTypes.Parameter{ Name: input.Name, Value: &secret, }, @@ -275,18 +230,9 @@ func TestGetBulkSecrets(t *testing.T) { }, } - paramStore := awsAuth.ParameterStoreClients{ - Store: mockSSM, - } - - mockedClients := awsAuth.Clients{ - ParameterStore: ¶mStore, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := ssmSecretStore{ - authProvider: mockAuthProvider, - prefix: "/prefix", + ssmClient: mockSSM, + prefix: "/prefix", } req := secretstores.BulkGetSecretRequest{ @@ -299,33 +245,26 @@ func TestGetBulkSecrets(t *testing.T) { }) t.Run("unsuccessfully retrieve bulk secrets on get parameter", func(t *testing.T) { - mockSSM := &awsAuth.MockParameterStore{ - DescribeParametersFn: func(context.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error) { - return &ssm.DescribeParametersOutput{NextToken: nil, Parameters: []*ssm.ParameterMetadata{ + mockSSM := &awsMock.ParameterStoreClient{ + DescribeParametersFn: func(ctx context.Context, input *ssm.DescribeParametersInput, optFns ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) { + secret1 := "/aws/dev/secret1" //nolint:gosec + secret2 := "/aws/dev/secret2" //nolint:gosec + return &ssm.DescribeParametersOutput{NextToken: nil, Parameters: []ssmTypes.ParameterMetadata{ { - Name: aws.String("/aws/dev/secret1"), + Name: &secret1, }, { - Name: aws.String("/aws/dev/secret2"), + Name: &secret2, }, }}, nil }, - GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, option ...request.Option) (*ssm.GetParameterOutput, error) { + GetParameterFn: func(ctx context.Context, input *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { return nil, errors.New("failed due to any reason") }, } - paramStore := awsAuth.ParameterStoreClients{ - Store: mockSSM, - } - - mockedClients := awsAuth.Clients{ - ParameterStore: ¶mStore, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := ssmSecretStore{ - authProvider: mockAuthProvider, + ssmClient: mockSSM, } req := secretstores.BulkGetSecretRequest{ @@ -336,23 +275,14 @@ func TestGetBulkSecrets(t *testing.T) { }) t.Run("unsuccessfully retrieve bulk secrets on describe parameter", func(t *testing.T) { - mockSSM := &awsAuth.MockParameterStore{ - DescribeParametersFn: func(context.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error) { + mockSSM := &awsMock.ParameterStoreClient{ + DescribeParametersFn: func(ctx context.Context, input *ssm.DescribeParametersInput, optFns ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) { return nil, errors.New("failed due to any reason") }, } - paramStore := awsAuth.ParameterStoreClients{ - Store: mockSSM, - } - - mockedClients := awsAuth.Clients{ - ParameterStore: ¶mStore, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := ssmSecretStore{ - authProvider: mockAuthProvider, + ssmClient: mockSSM, } req := secretstores.BulkGetSecretRequest{ diff --git a/secretstores/aws/secretmanager/metadata.yaml b/secretstores/aws/secretmanager/metadata.yaml index 21bfbd5b2c..5f7769cb76 100644 --- a/secretstores/aws/secretmanager/metadata.yaml +++ b/secretstores/aws/secretmanager/metadata.yaml @@ -16,4 +16,10 @@ metadata: description: | The Secrets manager endpoint. The AWS SDK will generate a default endpoint if not specified. Useful for local testing with AWS LocalStack example: '"http://localhost:4566"' - type: string \ No newline at end of file + type: string + - name: multipleKeyValuesPerSecret + required: false + description: | + A boolean value to indicate if the secrets with multiple key/values should break keys out. + example: "true" + type: bool \ No newline at end of file diff --git a/secretstores/aws/secretmanager/secretmanager.go b/secretstores/aws/secretmanager/secretmanager.go index 979739be5b..fb5aaf901f 100644 --- a/secretstores/aws/secretmanager/secretmanager.go +++ b/secretstores/aws/secretmanager/secretmanager.go @@ -19,12 +19,15 @@ import ( "fmt" "reflect" - "github.com/aws/aws-sdk-go/service/secretsmanager" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsCommonAuth "github.com/dapr/components-contrib/common/aws/auth" + + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/secretstores" "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" ) const ( @@ -40,16 +43,19 @@ func NewSecretManager(logger logger.Logger) secretstores.SecretStore { } type SecretManagerMetaData struct { - Region string `json:"region" mapstructure:"region" mdignore:"true"` - AccessKey string `json:"accessKey" mapstructure:"accessKey" mdignore:"true"` - SecretKey string `json:"secretKey" mapstructure:"secretKey" mdignore:"true"` - SessionToken string `json:"sessionToken" mapstructure:"sessionToken" mdignore:"true"` - Endpoint string `json:"endpoint" mapstructure:"endpoint"` + Region string `json:"region" mapstructure:"region" mdignore:"true"` + AccessKey string `json:"accessKey" mapstructure:"accessKey" mdignore:"true"` + SecretKey string `json:"secretKey" mapstructure:"secretKey" mdignore:"true"` + SessionToken string `json:"sessionToken" mapstructure:"sessionToken" mdignore:"true"` + Endpoint string `json:"endpoint" mapstructure:"endpoint"` + MultipleKeyValuesPerSecret bool `json:"multipleKeyValuesPerSecret" mapstructure:"multipleKeyValuesPerSecret"` } type smSecretStore struct { - authProvider awsAuth.Provider - logger logger.Logger + logger logger.Logger + + secretsManagerClient awsCommon.SecretsManagerClient + multipleKeyValuesPerSecret bool } // Init creates an AWS secret manager client. @@ -59,23 +65,63 @@ func (s *smSecretStore) Init(ctx context.Context, metadata secretstores.Metadata return err } - opts := awsAuth.Options{ - Logger: s.logger, + configOpts := awsCommonAuth.Options{ + Logger: s.logger, + + Properties: metadata.Properties, + Region: meta.Region, + Endpoint: meta.Endpoint, AccessKey: meta.AccessKey, SecretKey: meta.SecretKey, SessionToken: meta.SessionToken, - Endpoint: meta.Endpoint, } + s.multipleKeyValuesPerSecret = meta.MultipleKeyValuesPerSecret - provider, err := awsAuth.NewProvider(ctx, opts, awsAuth.GetConfig(opts)) + awsConfig, err := awsCommon.NewConfig(ctx, configOpts) if err != nil { return err } - s.authProvider = provider + + s.secretsManagerClient = secretsmanager.NewFromConfig(awsConfig) + return nil } +func convertMapAnyToString(m map[string]any) map[string]string { + result := make(map[string]string, len(m)) + for k, v := range m { + switch v := v.(type) { + case string: + result[k] = v + default: + jVal, _ := json.Marshal(v) + result[k] = string(jVal) + } + } + return result +} + +func (s *smSecretStore) formatSecret(output *secretsmanager.GetSecretValueOutput) map[string]string { + result := map[string]string{} + + if output.Name != nil && output.SecretString != nil { + if s.multipleKeyValuesPerSecret { + data := map[string]any{} + if err := json.Unmarshal([]byte(*output.SecretString), &data); err != nil { + result[*output.Name] = *output.SecretString + } else { + // In case of a nested JSON value, we need to stringify it + result = convertMapAnyToString(data) + } + } else { + result[*output.Name] = *output.SecretString + } + } + + return result +} + // GetSecret retrieves a secret using a key and returns a map of decrypted string/string values. func (s *smSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) { var versionID *string @@ -86,7 +132,7 @@ func (s *smSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecre if value, ok := req.Metadata[VersionStage]; ok { versionStage = &value } - output, err := s.authProvider.SecretManager().Manager.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{ + output, err := s.secretsManagerClient.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ SecretId: &req.Name, VersionId: versionID, VersionStage: versionStage, @@ -98,9 +144,7 @@ func (s *smSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecre resp := secretstores.GetSecretResponse{ Data: map[string]string{}, } - if output.Name != nil && output.SecretString != nil { - resp.Data[*output.Name] = *output.SecretString - } + resp.Data = s.formatSecret(output) return resp, nil } @@ -115,7 +159,7 @@ func (s *smSecretStore) BulkGetSecret(ctx context.Context, req secretstores.Bulk var nextToken *string = nil for search { - output, err := s.authProvider.SecretManager().Manager.ListSecretsWithContext(ctx, &secretsmanager.ListSecretsInput{ + output, err := s.secretsManagerClient.ListSecrets(ctx, &secretsmanager.ListSecretsInput{ MaxResults: nil, NextToken: nextToken, }) @@ -124,16 +168,14 @@ func (s *smSecretStore) BulkGetSecret(ctx context.Context, req secretstores.Bulk } for _, entry := range output.SecretList { - secrets, err := s.authProvider.SecretManager().Manager.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{ + secrets, err := s.secretsManagerClient.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ SecretId: entry.Name, }) if err != nil { return secretstores.BulkGetSecretResponse{Data: nil}, fmt.Errorf("couldn't get secret: %s", *entry.Name) } - if entry.Name != nil && secrets.SecretString != nil { - resp.Data[*entry.Name] = map[string]string{*entry.Name: *secrets.SecretString} - } + resp.Data[*entry.Name] = s.formatSecret(secrets) } nextToken = output.NextToken @@ -144,23 +186,21 @@ func (s *smSecretStore) BulkGetSecret(ctx context.Context, req secretstores.Bulk } func (s *smSecretStore) getSecretManagerMetadata(spec secretstores.Metadata) (*SecretManagerMetaData, error) { - b, err := json.Marshal(spec.Properties) - if err != nil { - return nil, err - } - var meta SecretManagerMetaData - err = json.Unmarshal(b, &meta) + err := kitmd.DecodeMetadata(spec.Properties, &meta) if err != nil { return nil, err } - return &meta, nil } // Features returns the features available in this secret store. func (s *smSecretStore) Features() []secretstores.Feature { - return []secretstores.Feature{} // No Feature supported. + if s.multipleKeyValuesPerSecret { + return []secretstores.Feature{secretstores.FeatureMultipleKeyValuesPerSecret} + } + + return []secretstores.Feature{} } func (s *smSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { @@ -170,8 +210,6 @@ func (s *smSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMa } func (s *smSecretStore) Close() error { - if s.authProvider != nil { - return s.authProvider.Close() - } + // Removed auth provider return nil } diff --git a/secretstores/aws/secretmanager/secretmanager_test.go b/secretstores/aws/secretmanager/secretmanager_test.go index fce8169f62..bde120a2a1 100644 --- a/secretstores/aws/secretmanager/secretmanager_test.go +++ b/secretstores/aws/secretmanager/secretmanager_test.go @@ -19,13 +19,14 @@ import ( "errors" "testing" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/secretsmanager" + awsMock "github.com/dapr/components-contrib/common/aws/mock" + + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + secretsmanagerTypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" - "github.com/dapr/components-contrib/secretstores" "github.com/dapr/kit/logger" ) @@ -52,30 +53,19 @@ func TestInit(t *testing.T) { func TestGetSecret(t *testing.T) { t.Run("successfully retrieve secret", func(t *testing.T) { t.Run("without version id and version stage", func(t *testing.T) { - mockSSM := &awsAuth.MockSecretManager{ - GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, option ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - assert.Nil(t, input.VersionId) - assert.Nil(t, input.VersionStage) - secret := secretValue - - return &secretsmanager.GetSecretValueOutput{ - Name: input.SecretId, - SecretString: &secret, - }, nil - }, - } - - secret := awsAuth.SecretManagerClients{ - Manager: mockSSM, - } - - mockedClients := awsAuth.Clients{ - Secret: &secret, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := smSecretStore{ - authProvider: mockAuthProvider, + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.Nil(t, input.VersionId) + assert.Nil(t, input.VersionStage) + secret := secretValue + + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secret, + }, nil + }, + }, } req := secretstores.GetSecretRequest{ @@ -88,36 +78,50 @@ func TestGetSecret(t *testing.T) { }) t.Run("with version id", func(t *testing.T) { - mockSSM := &awsAuth.MockSecretManager{ - GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, option ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - assert.NotNil(t, input.VersionId) - secret := secretValue - - return &secretsmanager.GetSecretValueOutput{ - Name: input.SecretId, - SecretString: &secret, - }, nil + s := smSecretStore{ + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.NotNil(t, input.VersionId) + secret := secretValue + + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secret, + }, nil + }, }, } - secret := awsAuth.SecretManagerClients{ - Manager: mockSSM, - } - - mockedClients := awsAuth.Clients{ - Secret: &secret, + req := secretstores.GetSecretRequest{ + Name: "/aws/secret/testing", + Metadata: map[string]string{ + VersionID: "1", + }, } + output, e := s.GetSecret(t.Context(), req) + require.NoError(t, e) + assert.Equal(t, secretValue, output.Data[req.Name]) + }) - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) + t.Run("with version stage", func(t *testing.T) { s := smSecretStore{ - authProvider: mockAuthProvider, + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.NotNil(t, input.VersionStage) + secret := secretValue + + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secret, + }, nil + }, + }, } req := secretstores.GetSecretRequest{ Name: "/aws/secret/testing", Metadata: map[string]string{ - VersionID: "1", + VersionStage: "dev", }, } output, e := s.GetSecret(t.Context(), req) @@ -125,79 +129,281 @@ func TestGetSecret(t *testing.T) { assert.Equal(t, secretValue, output.Data[req.Name]) }) - t.Run("with version stage", func(t *testing.T) { - mockSSM := &awsAuth.MockSecretManager{ - GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, option ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - assert.NotNil(t, input.VersionStage) - secret := secretValue - - return &secretsmanager.GetSecretValueOutput{ - Name: input.SecretId, - SecretString: &secret, - }, nil + t.Run("with multiple keys per secret", func(t *testing.T) { + s := smSecretStore{ + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.Nil(t, input.VersionId) + assert.Nil(t, input.VersionStage) + // #nosec G101: This is a mock secret used for testing purposes. + secret := `{"key1":"value1","key2":"value2","key3":{"nested":"value3"}}` + + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secret, + }, nil + }, }, + multipleKeyValuesPerSecret: true, } - secret := awsAuth.SecretManagerClients{ - Manager: mockSSM, + req := secretstores.GetSecretRequest{ + Name: "/aws/secret/testing", + Metadata: map[string]string{}, } + output, e := s.GetSecret(t.Context(), req) + require.NoError(t, e) + assert.Len(t, output.Data, 3) + assert.Equal(t, "value1", output.Data["key1"]) + assert.Equal(t, "value2", output.Data["key2"]) + assert.JSONEq(t, `{"nested":"value3"}`, output.Data["key3"]) + }) - mockedClients := awsAuth.Clients{ - Secret: &secret, + t.Run("with multiple keys per secret and option disabled", func(t *testing.T) { + s := smSecretStore{ + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.Nil(t, input.VersionId) + assert.Nil(t, input.VersionStage) + // #nosec G101: This is a mock secret used for testing purposes. + secret := `{"key1":"value1","key2":"value2"}` + + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secret, + }, nil + }, + }, + } + + req := secretstores.GetSecretRequest{ + Name: "/aws/secret/testing", + Metadata: map[string]string{}, } + output, e := s.GetSecret(t.Context(), req) + require.NoError(t, e) + assert.Len(t, output.Data, 1) + assert.JSONEq(t, `{"key1":"value1","key2":"value2"}`, output.Data["/aws/secret/testing"]) + }) - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) + t.Run("with multiple keys per secret and secret is NOT json", func(t *testing.T) { s := smSecretStore{ - authProvider: mockAuthProvider, + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.Nil(t, input.VersionId) + assert.Nil(t, input.VersionStage) + secret := "not json" + + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secret, + }, nil + }, + }, + multipleKeyValuesPerSecret: true, } req := secretstores.GetSecretRequest{ - Name: "/aws/secret/testing", - Metadata: map[string]string{ - VersionStage: "dev", + Name: "/aws/secret/testing", + Metadata: map[string]string{}, + } + output, e := s.GetSecret(t.Context(), req) + require.NoError(t, e) + assert.Len(t, output.Data, 1) + assert.Equal(t, "not json", output.Data["/aws/secret/testing"]) + }) + + t.Run("with multiple keys per secret and secret is json collection", func(t *testing.T) { + s := smSecretStore{ + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.Nil(t, input.VersionId) + assert.Nil(t, input.VersionStage) + secret := `[{"key1":"value1"},{"key2":"value2"}]` // #nosec G101: This is a mock secret used for testing purposes. + + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secret, + }, nil + }, }, + multipleKeyValuesPerSecret: true, + } + + req := secretstores.GetSecretRequest{ + Name: "/aws/secret/testing", + Metadata: map[string]string{}, } output, e := s.GetSecret(t.Context(), req) require.NoError(t, e) - assert.Equal(t, secretValue, output.Data[req.Name]) + assert.Len(t, output.Data, 1) + assert.JSONEq(t, `[{"key1":"value1"},{"key2":"value2"}]`, output.Data["/aws/secret/testing"]) }) }) t.Run("unsuccessfully retrieve secret", func(t *testing.T) { - mockSSM := &awsAuth.MockSecretManager{ - GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, option ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - return nil, errors.New("failed due to any reason") + s := smSecretStore{ + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + return nil, errors.New("failed due to any reason") + }, }, } - secret := awsAuth.SecretManagerClients{ - Manager: mockSSM, + req := secretstores.GetSecretRequest{ + Name: "/aws/secret/testing", + Metadata: map[string]string{}, + } + _, err := s.GetSecret(t.Context(), req) + require.Error(t, err) + }) +} + +func TestBulkGetSecret(t *testing.T) { + t.Run("returns all secrets in store", func(t *testing.T) { + secret1 := "/aws/secret/testing1" + secretValue1 := "secret1" + secret2 := "/aws/secret/testing2" + secretValue2 := "secret2" + + s := smSecretStore{ + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.Nil(t, input.VersionId) + assert.Nil(t, input.VersionStage) + + if *input.SecretId == secret1 { + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secretValue1, + }, nil + } else { + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secretValue2, + }, nil + } + }, + + ListSecretsFn: func(ctx context.Context, input *secretsmanager.ListSecretsInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) { + return &secretsmanager.ListSecretsOutput{ + SecretList: []secretsmanagerTypes.SecretListEntry{ + {Name: &secret1}, + {Name: &secret2}, + }, + }, nil + }, + }, } - mockedClients := awsAuth.Clients{ - Secret: &secret, + req := secretstores.BulkGetSecretRequest{ + Metadata: map[string]string{}, } + output, e := s.BulkGetSecret(t.Context(), req) + require.NoError(t, e) + assert.Equal(t, map[string]map[string]string{ + secret1: { + secret1: secretValue1, + }, + secret2: { + secret2: secretValue2, + }, + }, output.Data) + }) + + t.Run("when multipleKeyValuesPerSecret = true, returns all secrets in store broken out by key", func(t *testing.T) { + secret1 := "/aws/secret/testing1" + // #nosec G101: This is a mock secret used for testing purposes. + secretValue1 := `{"key1":"value1","key2":"value2"}` + secret2 := "/aws/secret/testing2" + // #nosec G101: This is a mock secret used for testing purposes. + secretValue2 := `{"key3":"value3","key4":{"nested":"value4"}}` - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := smSecretStore{ - authProvider: mockAuthProvider, + secretsManagerClient: &awsMock.SecretsManagerClient{ + GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + assert.Nil(t, input.VersionId) + assert.Nil(t, input.VersionStage) + + if *input.SecretId == secret1 { + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secretValue1, + }, nil + } else { + return &secretsmanager.GetSecretValueOutput{ + Name: input.SecretId, + SecretString: &secretValue2, + }, nil + } + }, + + ListSecretsFn: func(ctx context.Context, input *secretsmanager.ListSecretsInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) { + return &secretsmanager.ListSecretsOutput{ + SecretList: []secretsmanagerTypes.SecretListEntry{ + {Name: &secret1}, + {Name: &secret2}, + }, + }, nil + }, + }, + multipleKeyValuesPerSecret: true, } - req := secretstores.GetSecretRequest{ - Name: "/aws/secret/testing", + req := secretstores.BulkGetSecretRequest{ Metadata: map[string]string{}, } - _, err := s.GetSecret(t.Context(), req) - require.Error(t, err) + output, e := s.BulkGetSecret(t.Context(), req) + require.NoError(t, e) + assert.Equal(t, map[string]map[string]string{ + secret1: { + "key1": "value1", + "key2": "value2", + }, + secret2: { + "key3": "value3", + "key4": `{"nested":"value4"}`, + }, + }, output.Data) }) } func TestGetFeatures(t *testing.T) { s := smSecretStore{} - t.Run("no features are advertised", func(t *testing.T) { + t.Run("when multipleKeyValuesPerSecret = true, return feature", func(t *testing.T) { + s.multipleKeyValuesPerSecret = true + f := s.Features() + assert.True(t, secretstores.FeatureMultipleKeyValuesPerSecret.IsPresent(f)) + }) + + t.Run("when multipleKeyValuesPerSecret = false, no feature advertised", func(t *testing.T) { + s.multipleKeyValuesPerSecret = false f := s.Features() assert.Empty(t, f) }) + + t.Run("by default, no feature advertised", func(t *testing.T) { + f := s.Features() + assert.Empty(t, f) + }) +} + +func TestGetSecretManagerMetadata(t *testing.T) { + s := &smSecretStore{ + logger: logger.NewLogger("test"), + } + + t.Run("parse multipleKeyValuesPerSecret as string", func(t *testing.T) { + metadata := secretstores.Metadata{} + metadata.Properties = map[string]string{ + "region": "us-east-1", + "accessKey": "test", + "secretKey": "test", + "multipleKeyValuesPerSecret": "true", + } + + meta, err := s.getSecretManagerMetadata(metadata) + require.NoError(t, err) + assert.True(t, meta.MultipleKeyValuesPerSecret) + }) } diff --git a/secretstores/local/file/metadata.yaml b/secretstores/local/file/metadata.yaml index 04f364551b..6773cd3177 100644 --- a/secretstores/local/file/metadata.yaml +++ b/secretstores/local/file/metadata.yaml @@ -25,5 +25,5 @@ metadata: type: bool required: false description: If true, enables multiple key-values per secret feature. - example: false - default: false + example: "false" + default: "false" diff --git a/state/aws/dynamodb/dynamodb.go b/state/aws/dynamodb/dynamodb.go index af620165e3..5179247395 100644 --- a/state/aws/dynamodb/dynamodb.go +++ b/state/aws/dynamodb/dynamodb.go @@ -49,7 +49,7 @@ type StateStore struct { ttlAttributeName string partitionKey string - dynamodbClient *dynamodb.Client + dynamodbClient awsCommon.DynamoDBClient } type dynamoDBMetadata struct { @@ -66,6 +66,13 @@ type dynamoDBMetadata struct { PartitionKey string `json:"partitionKey"` } +type putData struct { + ConditionExpression *string + ExpressionAttributeValues map[string]types.AttributeValue + Item map[string]types.AttributeValue + TableName *string +} + const ( defaultPartitionKeyName = "key" metadataPartitionKey = "partitionKey" @@ -81,8 +88,13 @@ func NewDynamoDBStateStore(logger logger.Logger) state.Store { return s } -// Init does metadata and connection parsing. +// Init does metadata and connection parsing func (d *StateStore) Init(ctx context.Context, metadata state.Metadata) error { + return d.InitWithOptions(ctx, metadata) +} + +// InitWithOptions does metadata and connection parsing and extra aws options +func (d *StateStore) InitWithOptions(ctx context.Context, metadata state.Metadata, opts ...awsCommon.ConfigOption) error { meta, err := d.getDynamoDBMetadata(metadata) if err != nil { return err @@ -100,13 +112,14 @@ func (d *StateStore) Init(ctx context.Context, metadata state.Metadata) error { SessionToken: meta.SessionToken, } - awsConfig, err := awsCommon.NewConfig(ctx, configOpts) - if err != nil { - return err - } - - d.dynamodbClient = dynamodb.NewFromConfig(awsConfig) + if d.dynamodbClient == nil { + awsConfig, err := awsCommon.NewConfig(ctx, configOpts, opts...) + if err != nil { + return err + } + d.dynamodbClient = dynamodb.NewFromConfig(awsConfig) + } d.table = meta.Table d.ttlAttributeName = meta.TTLAttributeName d.partitionKey = meta.PartitionKey @@ -171,9 +184,9 @@ func (d *StateStore) Get(ctx context.Context, req *state.GetRequest) (*state.Get return &state.GetResponse{}, nil } - var output string - if err = attributevalue.Unmarshal(result.Item["value"], &output); err != nil { - return nil, err + data, err := unmarshalValue(result.Item["value"]) + if err != nil { + return nil, fmt.Errorf("dynamodb error: failed to unmarshal value for key %s: %w", req.Key, err) } var metadata map[string]string @@ -194,7 +207,7 @@ func (d *StateStore) Get(ctx context.Context, req *state.GetRequest) (*state.Get } resp := &state.GetResponse{ - Data: []byte(output), + Data: data, Metadata: metadata, } @@ -212,29 +225,12 @@ func (d *StateStore) Get(ctx context.Context, req *state.GetRequest) (*state.Get // Set saves a dynamoDB item. func (d *StateStore) Set(ctx context.Context, req *state.SetRequest) error { - item, err := d.getItemFromReq(req) + pd, err := d.createPutData(req) if err != nil { return err } - input := &dynamodb.PutItemInput{ - Item: item, - TableName: &d.table, - } - - if req.HasETag() { - condExpr := "etag = :etag" - input.ConditionExpression = &condExpr - exprAttrValues := make(map[string]types.AttributeValue) - exprAttrValues[":etag"] = &types.AttributeValueMemberS{ - Value: *req.ETag, - } - input.ExpressionAttributeValues = exprAttrValues - } else if req.Options.Concurrency == state.FirstWrite { - condExpr := "attribute_not_exists(etag)" - input.ConditionExpression = &condExpr - } - _, err = d.dynamodbClient.PutItem(ctx, input) + _, err = d.dynamodbClient.PutItem(ctx, pd.ToPutItemInput()) if err != nil && req.HasETag() { var cErr *types.ConditionalCheckFailedException switch { @@ -298,9 +294,55 @@ func (d *StateStore) getDynamoDBMetadata(meta state.Metadata) (*dynamoDBMetadata return &m, err } -// getItemFromReq converts a dapr state.SetRequest into an dynamodb item -func (d *StateStore) getItemFromReq(req *state.SetRequest) (map[string]types.AttributeValue, error) { - value, err := d.marshalToString(req.Value) +// createPutData creates a DynamoDB put request data from a SetRequest. +func (d *StateStore) createPutData(req *state.SetRequest) (putData, error) { + item, err := d.createItem(req) + if err != nil { + return putData{}, err + } + + pd := putData{ + Item: item, + TableName: ptr.Of(d.table), + } + + if req.HasETag() { + condExpr := "etag = :etag" + pd.ConditionExpression = &condExpr + exprAttrValues := make(map[string]types.AttributeValue) + exprAttrValues[":etag"] = &types.AttributeValueMemberS{ + Value: *req.ETag, + } + pd.ExpressionAttributeValues = exprAttrValues + } else if req.Options.Concurrency == state.FirstWrite { + condExpr := "attribute_not_exists(etag)" + pd.ConditionExpression = &condExpr + } + + return pd, nil +} + +func (d putData) ToPutItemInput() *dynamodb.PutItemInput { + return &dynamodb.PutItemInput{ + ConditionExpression: d.ConditionExpression, + ExpressionAttributeValues: d.ExpressionAttributeValues, + Item: d.Item, + TableName: d.TableName, + } +} + +func (d putData) ToPut() *types.Put { + return &types.Put{ + ConditionExpression: d.ConditionExpression, + ExpressionAttributeValues: d.ExpressionAttributeValues, + Item: d.Item, + TableName: d.TableName, + } +} + +// createItem creates a DynamoDB item from a SetRequest. +func (d *StateStore) createItem(req *state.SetRequest) (map[string]types.AttributeValue, error) { + value, err := marshalValue(req.Value) if err != nil { return nil, fmt.Errorf("dynamodb error: failed to marshal value for key %s: %w", req.Key, err) } @@ -319,9 +361,7 @@ func (d *StateStore) getItemFromReq(req *state.SetRequest) (map[string]types.Att d.partitionKey: &types.AttributeValueMemberS{ Value: req.Key, }, - "value": &types.AttributeValueMemberS{ - Value: value, - }, + "value": value, "etag": &types.AttributeValueMemberS{ Value: strconv.FormatUint(newEtag, 16), }, @@ -346,12 +386,35 @@ func getRand64() (uint64, error) { return binary.LittleEndian.Uint64(randBuf), nil } -func (d *StateStore) marshalToString(v interface{}) (string, error) { - if buf, ok := v.([]byte); ok { - return string(buf), nil +func marshalValue(v interface{}) (types.AttributeValue, error) { + if bt, ok := v.([]byte); ok { + return &types.AttributeValueMemberB{Value: bt}, nil + } + + str, err := jsoniterator.ConfigFastest.MarshalToString(v) + if err != nil { + return nil, err + } + + return &types.AttributeValueMemberS{Value: str}, nil +} + +func unmarshalValue(value types.AttributeValue) ([]byte, error) { + if value == nil { + return []byte(nil), nil + } + + var bytes []byte + if err := attributevalue.Unmarshal(value, &bytes); err == nil { + return bytes, nil } - return jsoniterator.ConfigFastest.MarshalToString(v) + var str string + if err := attributevalue.Unmarshal(value, &str); err == nil { + return []byte(str), nil + } + + return nil, fmt.Errorf("unsupported attribute value type %T", value) } // Parse and process ttlInSeconds. @@ -410,21 +473,11 @@ func (d *StateStore) Multi(ctx context.Context, request *state.TransactionalStat twi := types.TransactWriteItem{} switch req := o.(type) { case state.SetRequest: - value, err := d.marshalToString(req.Value) + pd, err := d.createPutData(&req) if err != nil { return fmt.Errorf("dynamodb error: failed to marshal value for key %s: %w", req.Key, err) } - twi.Put = &types.Put{ - TableName: ptr.Of(d.table), - Item: map[string]types.AttributeValue{ - d.partitionKey: &types.AttributeValueMemberS{ - Value: req.Key, - }, - "value": &types.AttributeValueMemberS{ - Value: value, - }, - }, - } + twi.Put = pd.ToPut() case state.DeleteRequest: twi.Delete = &types.Delete{ diff --git a/state/aws/dynamodb/dynamodb_test.go b/state/aws/dynamodb/dynamodb_test.go index abd2448004..5fced6d29f 100644 --- a/state/aws/dynamodb/dynamodb_test.go +++ b/state/aws/dynamodb/dynamodb_test.go @@ -1,6 +1,3 @@ -// TODO: Migrate mocks -//go:build legacy - /* Copyright 2021 The Dapr Authors Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,12 +21,13 @@ import ( "testing" "time" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + + awsMock "github.com/dapr/components-contrib/common/aws/mock" + "github.com/dapr/kit/logger" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -47,27 +45,23 @@ const ( pkey = "partitionKey" ) -func TestInit(t *testing.T) { - m := state.Metadata{} - mockedDB := &awsAuth.MockDynamoDB{ - // We're adding this so we can pass the connection check on Init - GetItemWithContextFn: func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (*dynamodb.GetItemOutput, error) { - return nil, nil - }, - } +var log logger.Logger - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } +func init() { + log = logger.NewLogger("dynamodb-state-store") + log.SetOutputLevel(logger.DebugLevel) +} - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } +func TestInit(t *testing.T) { + m := state.Metadata{} - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + logger: log, + dynamodbClient: &awsMock.DynamoDBClient{ + GetItemFn: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + return nil, nil + }, + }, partitionKey: defaultPartitionKeyName, } @@ -85,6 +79,7 @@ func TestInit(t *testing.T) { "Table": "a", "TtlAttributeName": "a", } + err := s.Init(t.Context(), m) require.NoError(t, err) }) @@ -124,21 +119,13 @@ func TestInit(t *testing.T) { "Table": table, } - mockedDB := &awsAuth.MockDynamoDB{ - GetItemWithContextFn: func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (*dynamodb.GetItemOutput, error) { - return nil, errors.New("Requested resource not found") - }, - } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + logger: log, + dynamodbClient: &awsMock.DynamoDBClient{ + GetItemFn: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + return nil, errors.New("Requested resource not found") + }, + }, partitionKey: defaultPartitionKeyName, table: table, } @@ -151,38 +138,30 @@ func TestInit(t *testing.T) { func TestGet(t *testing.T) { t.Run("Successfully retrieve item", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - GetItemWithContextFn: func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (output *dynamodb.GetItemOutput, err error) { + mockedDB := &awsMock.DynamoDBClient{ + GetItemFn: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { return &dynamodb.GetItemOutput{ - Item: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("someKey"), + Item: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "someKey", }, - "value": { - S: aws.String("some value"), + "value": &types.AttributeValueMemberS{ + Value: "some value", }, - "etag": { - S: aws.String("1bdead4badc0ffee"), + "etag": &types.AttributeValueMemberS{ + Value: "1bdead4badc0ffee", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } + req := &state.GetRequest{ Key: "someKey", Metadata: nil, @@ -197,39 +176,30 @@ func TestGet(t *testing.T) { assert.NotContains(t, out.Metadata, "ttlExpireTime") }) t.Run("Successfully retrieve item (with unexpired ttl)", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - GetItemWithContextFn: func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (output *dynamodb.GetItemOutput, err error) { + mockedDB := &awsMock.DynamoDBClient{ + GetItemFn: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { return &dynamodb.GetItemOutput{ - Item: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("someKey"), + Item: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "someKey", }, - "value": { - S: aws.String("some value"), + "value": &types.AttributeValueMemberS{ + Value: "some value", }, - "testAttributeName": { - N: aws.String("4074862051"), + "testAttributeName": &types.AttributeValueMemberN{ + Value: "4074862051", }, - "etag": { - S: aws.String("1bdead4badc0ffee"), + "etag": &types.AttributeValueMemberS{ + Value: "1bdead4badc0ffee", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + logger: log, + dynamodbClient: mockedDB, ttlAttributeName: "testAttributeName", } req := &state.GetRequest{ @@ -248,40 +218,32 @@ func TestGet(t *testing.T) { require.NoError(t, err) assert.Equal(t, int64(4074862051), expireTime.Unix()) }) + t.Run("Successfully retrieve item (with expired ttl)", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - GetItemWithContextFn: func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (output *dynamodb.GetItemOutput, err error) { + mockedDB := &awsMock.DynamoDBClient{ + GetItemFn: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { return &dynamodb.GetItemOutput{ - Item: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("someKey"), + Item: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "someKey", }, - "value": { - S: aws.String("some value"), + "value": &types.AttributeValueMemberS{ + Value: "some value", }, - "testAttributeName": { - N: aws.String("35489251"), + "testAttributeName": &types.AttributeValueMemberN{ + Value: "35489251", }, - "etag": { - S: aws.String("1bdead4badc0ffee"), + "etag": &types.AttributeValueMemberS{ + Value: "1bdead4badc0ffee", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + logger: log, + dynamodbClient: mockedDB, ttlAttributeName: "testAttributeName", } req := &state.GetRequest{ @@ -298,24 +260,13 @@ func TestGet(t *testing.T) { assert.Nil(t, out.Metadata) }) t.Run("Unsuccessfully get item", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - GetItemWithContextFn: func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (output *dynamodb.GetItemOutput, err error) { - return nil, errors.New("failed to retrieve data") - }, - } - - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + dynamodbClient: &awsMock.DynamoDBClient{ + GetItemFn: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + return nil, errors.New("failed to retrieve data") + }, + }, + logger: log, } req := &state.GetRequest{ @@ -330,26 +281,15 @@ func TestGet(t *testing.T) { assert.Nil(t, out) }) t.Run("Unsuccessfully with empty response", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - GetItemWithContextFn: func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (output *dynamodb.GetItemOutput, err error) { - return &dynamodb.GetItemOutput{ - Item: map[string]*dynamodb.AttributeValue{}, - }, nil - }, - } - - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + dynamodbClient: &awsMock.DynamoDBClient{ + GetItemFn: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + return &dynamodb.GetItemOutput{ + Item: map[string]types.AttributeValue{}, + }, nil + }, + }, + logger: log, } req := &state.GetRequest{ Key: "key", @@ -365,30 +305,19 @@ func TestGet(t *testing.T) { assert.Nil(t, out.Metadata) }) t.Run("Unsuccessfully with no required key", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - GetItemWithContextFn: func(ctx context.Context, input *dynamodb.GetItemInput, op ...request.Option) (output *dynamodb.GetItemOutput, err error) { - return &dynamodb.GetItemOutput{ - Item: map[string]*dynamodb.AttributeValue{ - "value2": { - S: aws.String("value"), + s := StateStore{ + dynamodbClient: &awsMock.DynamoDBClient{ + GetItemFn: func(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) { + return &dynamodb.GetItemOutput{ + Item: map[string]types.AttributeValue{ + "value2": &types.AttributeValueMemberS{ + Value: "value", + }, }, - }, - }, nil + }, nil + }, }, - } - - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) - s := StateStore{ - authProvider: mockAuthProvider, + logger: log, } req := &state.GetRequest{ Key: "key", @@ -410,39 +339,32 @@ func TestSet(t *testing.T) { } t.Run("Successfully set item", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String("key"), - }, *input.Item["key"]) - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String(`{"Value":"value"}`), - }, *input.Item["value"]) - assert.Len(t, input.Item, 3) + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "key", + }, params.Item["key"]) + + assert.Equal(t, &types.AttributeValueMemberS{ + Value: `{"Value":"value"}`, + }, params.Item["value"]) + + assert.Len(t, params.Item, 3) return &dynamodb.PutItemOutput{ - Attributes: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("value"), + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } req := &state.SetRequest{ @@ -456,39 +378,69 @@ func TestSet(t *testing.T) { }) t.Run("Successfully set item with binary value", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String("key"), - }, *input.Item["key"]) - assert.Equal(t, dynamodb.AttributeValue{ - B: []byte("value"), - }, *input.Item["value"]) - assert.Len(t, input.Item, 3) + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "key", + }, params.Item["key"]) + + assert.Equal(t, &types.AttributeValueMemberB{ + Value: []byte("value"), + }, params.Item["value"]) + + assert.Len(t, params.Item, 3) return &dynamodb.PutItemOutput{ - Attributes: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("value"), + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, + s := StateStore{ + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, + req := &state.SetRequest{ + Key: "key", + Value: []byte("value"), + } + err := s.Set(t.Context(), req) + require.NoError(t, err) + }) + + t.Run("Successfully set item with binary value", func(t *testing.T) { + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "key", + }, params.Item["key"]) + + assert.Equal(t, &types.AttributeValueMemberB{ + Value: []byte("value"), + }, params.Item["value"]) + + assert.Len(t, params.Item, 3) + + return &dynamodb.PutItemOutput{ + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", + }, + }, + }, nil + }, } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } req := &state.SetRequest{ @@ -496,47 +448,43 @@ func TestSet(t *testing.T) { Value: []byte("value"), } err := s.Set(t.Context(), req) + require.NoError(t, err) }) t.Run("Successfully set item with matching etag", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String("key"), - }, *input.Item["key"]) - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String(`{"Value":"value"}`), - }, *input.Item["value"]) - assert.Equal(t, "etag = :etag", *input.ConditionExpression) - assert.Equal(t, &dynamodb.AttributeValue{ - S: aws.String("1bdead4badc0ffee"), - }, input.ExpressionAttributeValues[":etag"]) - assert.Len(t, input.Item, 3) + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "key", + }, params.Item["key"]) + + assert.Equal(t, &types.AttributeValueMemberS{ + Value: `{"Value":"value"}`, + }, params.Item["value"]) + + assert.Equal(t, "etag = :etag", *params.ConditionExpression) + + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "1bdead4badc0ffee", + }, params.ExpressionAttributeValues[":etag"]) + + assert.Len(t, params.Item, 3) return &dynamodb.PutItemOutput{ - Attributes: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("value"), + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } etag := "1bdead4badc0ffee" req := &state.SetRequest{ @@ -551,38 +499,33 @@ func TestSet(t *testing.T) { }) t.Run("Unsuccessfully set item with mismatched etag", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String("key"), - }, *input.Item["key"]) - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String(`{"Value":"value"}`), - }, *input.Item["value"]) - assert.Equal(t, "etag = :etag", *input.ConditionExpression) - assert.Equal(t, &dynamodb.AttributeValue{ - S: aws.String("bogusetag"), - }, input.ExpressionAttributeValues[":etag"]) - assert.Len(t, input.Item, 3) - - var checkErr dynamodb.ConditionalCheckFailedException - return nil, &checkErr - }, - } + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "key", + }, params.Item["key"]) - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } + assert.Equal(t, &types.AttributeValueMemberS{ + Value: `{"Value":"value"}`, + }, params.Item["value"]) - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, + assert.Equal(t, "etag = :etag", *params.ConditionExpression) + + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "bogusetag", + }, params.ExpressionAttributeValues[":etag"]) + + assert.Len(t, params.Item, 3) + + var checkErr types.ConditionalCheckFailedException + return nil, &checkErr + }, } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } etag := "bogusetag" req := &state.SetRequest{ @@ -604,40 +547,34 @@ func TestSet(t *testing.T) { }) t.Run("Successfully set item with first-write-concurrency", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String("key"), - }, *input.Item["key"]) - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String(`{"Value":"value"}`), - }, *input.Item["value"]) - assert.Equal(t, "attribute_not_exists(etag)", *input.ConditionExpression) - assert.Len(t, input.Item, 3) + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "key", + }, params.Item["key"]) + + assert.Equal(t, &types.AttributeValueMemberS{ + Value: `{"Value":"value"}`, + }, params.Item["value"]) + + assert.Equal(t, "attribute_not_exists(etag)", *params.ConditionExpression) + + assert.Len(t, params.Item, 3) return &dynamodb.PutItemOutput{ - Attributes: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("value"), + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } req := &state.SetRequest{ Key: "key", @@ -653,35 +590,29 @@ func TestSet(t *testing.T) { }) t.Run("Unsuccessfully set item with first-write-concurrency", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String("key"), - }, *input.Item["key"]) - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String(`{"Value":"value"}`), - }, *input.Item["value"]) - assert.Equal(t, "attribute_not_exists(etag)", *input.ConditionExpression) - assert.Len(t, input.Item, 3) - - var checkErr dynamodb.ConditionalCheckFailedException - return nil, &checkErr - }, - } + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "key", + }, params.Item["key"]) - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } + assert.Equal(t, &types.AttributeValueMemberS{ + Value: `{"Value":"value"}`, + }, params.Item["value"]) - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, + assert.Equal(t, "attribute_not_exists(etag)", *params.ConditionExpression) + + assert.Len(t, params.Item, 3) + + var checkErr types.ConditionalCheckFailedException + return nil, &checkErr + }, } - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } req := &state.SetRequest{ Key: "key", @@ -702,38 +633,30 @@ func TestSet(t *testing.T) { }) t.Run("Successfully set item with ttl = -1", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Len(t, input.Item, 4) + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Len(t, params.Item, 4) + result := DynamoDBItem{} - dynamodbattribute.UnmarshalMap(input.Item, &result) + require.NoError(t, attributevalue.UnmarshalMap(params.Item, &result)) + assert.Equal(t, "someKey", result.Key) assert.JSONEq(t, "{\"Value\":\"someValue\"}", result.Value) assert.Greater(t, result.TestAttributeName, time.Now().Unix()-2) assert.Less(t, result.TestAttributeName, time.Now().Unix()) return &dynamodb.PutItemOutput{ - Attributes: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("value"), + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", }, }, }, nil }, } - - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + logger: log, + dynamodbClient: mockedDB, ttlAttributeName: "testAttributeName", partitionKey: defaultPartitionKeyName, } @@ -750,39 +673,33 @@ func TestSet(t *testing.T) { err := s.Set(t.Context(), req) require.NoError(t, err) }) + t.Run("Successfully set item with 'correct' ttl", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Len(t, input.Item, 4) + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Len(t, params.Item, 4) + result := DynamoDBItem{} - dynamodbattribute.UnmarshalMap(input.Item, &result) + require.NoError(t, attributevalue.UnmarshalMap(params.Item, &result)) + assert.Equal(t, "someKey", result.Key) assert.JSONEq(t, "{\"Value\":\"someValue\"}", result.Value) assert.Greater(t, result.TestAttributeName, time.Now().Unix()+180-1) assert.Less(t, result.TestAttributeName, time.Now().Unix()+180+1) return &dynamodb.PutItemOutput{ - Attributes: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("value"), + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + logger: log, + dynamodbClient: mockedDB, partitionKey: defaultPartitionKeyName, ttlAttributeName: "testAttributeName", } @@ -801,25 +718,16 @@ func TestSet(t *testing.T) { }) t.Run("Unsuccessfully set item", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { return nil, errors.New("unable to put item") }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } req := &state.SetRequest{ Key: "key", @@ -830,40 +738,34 @@ func TestSet(t *testing.T) { err := s.Set(t.Context(), req) require.Error(t, err) }) + t.Run("Successfully set item with correct ttl but without component metadata", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String("someKey"), - }, *input.Item["key"]) - assert.Equal(t, dynamodb.AttributeValue{ - S: aws.String(`{"Value":"someValue"}`), - }, *input.Item["value"]) - assert.Len(t, input.Item, 3) + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "someKey", + }, params.Item["key"]) + + assert.Equal(t, &types.AttributeValueMemberS{ + Value: `{"Value":"someValue"}`, + }, params.Item["value"]) + + assert.Len(t, params.Item, 3) return &dynamodb.PutItemOutput{ - Attributes: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("value"), + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } req := &state.SetRequest{ Key: "someKey", @@ -877,43 +779,35 @@ func TestSet(t *testing.T) { err := s.Set(t.Context(), req) require.NoError(t, err) }) + t.Run("Unsuccessfully set item with ttl (invalid value)", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - PutItemWithContextFn: func(ctx context.Context, input *dynamodb.PutItemInput, op ...request.Option) (output *dynamodb.PutItemOutput, err error) { - assert.Equal(t, map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("somekey"), + mockedDB := &awsMock.DynamoDBClient{ + PutItemFn: func(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) { + assert.Equal(t, map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "somekey", }, - "value": { - S: aws.String(`{"Value":"somevalue"}`), + "value": &types.AttributeValueMemberS{ + Value: "somevalue", }, - "ttlInSeconds": { - N: aws.String("180"), + "ttlInSeconds": &types.AttributeValueMemberN{ + Value: "100", }, - }, input.Item) + }, params.Item) return &dynamodb.PutItemOutput{ - Attributes: map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String("value"), + Attributes: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: "value", }, }, }, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + logger: log, + dynamodbClient: mockedDB, ttlAttributeName: "testAttributeName", } req := &state.SetRequest{ @@ -937,31 +831,22 @@ func TestDelete(t *testing.T) { Key: "key", } - mockedDB := &awsAuth.MockDynamoDB{ - DeleteItemWithContextFn: func(ctx context.Context, input *dynamodb.DeleteItemInput, op ...request.Option) (output *dynamodb.DeleteItemOutput, err error) { - assert.Equal(t, map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String(req.Key), + mockedDB := &awsMock.DynamoDBClient{ + DeleteItemFn: func(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) { + assert.Equal(t, map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: req.Key, }, - }, input.Key) + }, params.Key) return nil, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } err := s.Delete(t.Context(), req) @@ -975,35 +860,25 @@ func TestDelete(t *testing.T) { Key: "key", } - mockedDB := &awsAuth.MockDynamoDB{ - DeleteItemWithContextFn: func(ctx context.Context, input *dynamodb.DeleteItemInput, op ...request.Option) (output *dynamodb.DeleteItemOutput, err error) { - assert.Equal(t, map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String(req.Key), + mockedDB := &awsMock.DynamoDBClient{ + DeleteItemFn: func(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) { + assert.Equal(t, map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: req.Key, }, - }, input.Key) - assert.Equal(t, "etag = :etag", *input.ConditionExpression) - assert.Equal(t, &dynamodb.AttributeValue{ - S: aws.String("1bdead4badc0ffee"), - }, input.ExpressionAttributeValues[":etag"]) + }, params.Key) + assert.Equal(t, "etag = :etag", *params.ConditionExpression) + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "1bdead4badc0ffee", + }, params.ExpressionAttributeValues[":etag"]) return nil, nil }, } - - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } err := s.Delete(t.Context(), req) @@ -1017,36 +892,27 @@ func TestDelete(t *testing.T) { Key: "key", } - mockedDB := &awsAuth.MockDynamoDB{ - DeleteItemWithContextFn: func(ctx context.Context, input *dynamodb.DeleteItemInput, op ...request.Option) (output *dynamodb.DeleteItemOutput, err error) { - assert.Equal(t, map[string]*dynamodb.AttributeValue{ - "key": { - S: aws.String(req.Key), + mockedDB := &awsMock.DynamoDBClient{ + DeleteItemFn: func(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) { + assert.Equal(t, map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{ + Value: req.Key, }, - }, input.Key) - assert.Equal(t, "etag = :etag", *input.ConditionExpression) - assert.Equal(t, &dynamodb.AttributeValue{ - S: aws.String("bogusetag"), - }, input.ExpressionAttributeValues[":etag"]) + }, params.Key) + assert.Equal(t, "etag = :etag", *params.ConditionExpression) + assert.Equal(t, &types.AttributeValueMemberS{ + Value: "bogusetag", + }, params.ExpressionAttributeValues[":etag"]) - var checkErr dynamodb.ConditionalCheckFailedException + var checkErr types.ConditionalCheckFailedException return nil, &checkErr }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + partitionKey: defaultPartitionKeyName, } err := s.Delete(t.Context(), req) require.Error(t, err) @@ -1059,24 +925,15 @@ func TestDelete(t *testing.T) { }) t.Run("Unsuccessfully delete item", func(t *testing.T) { - mockedDB := &awsAuth.MockDynamoDB{ - DeleteItemWithContextFn: func(ctx context.Context, input *dynamodb.DeleteItemInput, op ...request.Option) (output *dynamodb.DeleteItemOutput, err error) { + mockedDB := &awsMock.DynamoDBClient{ + DeleteItemFn: func(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) { return nil, errors.New("unable to delete item") }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, + dynamodbClient: mockedDB, + logger: log, } req := &state.DeleteRequest{ @@ -1112,17 +969,17 @@ func TestMultiTx(t *testing.T) { }, } - mockedDB := &awsAuth.MockDynamoDB{ - TransactWriteItemsWithContextFn: func(ctx context.Context, input *dynamodb.TransactWriteItemsInput, op ...request.Option) (*dynamodb.TransactWriteItemsOutput, error) { + mockedDB := &awsMock.DynamoDBClient{ + TransactWriteItemsFn: func(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) { // ops - duplicates exOps := len(ops) - 1 - assert.Len(t, input.TransactItems, exOps, "unexpected number of operations") + assert.Len(t, params.TransactItems, exOps, "unexpected number of operations") txs := map[string]int{ "P": 0, "D": 0, } - for _, input := range input.TransactItems { + for _, input := range params.TransactItems { switch { case input.Put != nil: txs["P"] += 1 @@ -1132,25 +989,15 @@ func TestMultiTx(t *testing.T) { } assert.Equal(t, 1, txs["P"], "unexpected number of Put Operations") assert.Equal(t, 2, txs["D"], "unexpected number of Delete Operations") - return &dynamodb.TransactWriteItemsOutput{}, nil }, } - dynamo := awsAuth.DynamoDBClients{ - DynamoDB: mockedDB, - } - - mockedClients := awsAuth.Clients{ - Dynamo: &dynamo, - } - - mockAuthProvider := &awsAuth.StaticAuth{} - mockAuthProvider.WithMockClients(&mockedClients) s := StateStore{ - authProvider: mockAuthProvider, - table: tableName, - partitionKey: defaultPartitionKeyName, + logger: log, + dynamodbClient: mockedDB, + table: tableName, + partitionKey: defaultPartitionKeyName, } req := &state.TransactionalStateRequest{ diff --git a/state/azure/blobstorage/v2/metadata.yaml b/state/azure/blobstorage/v2/metadata.yaml index ba52673110..10a128b20a 100644 --- a/state/azure/blobstorage/v2/metadata.yaml +++ b/state/azure/blobstorage/v2/metadata.yaml @@ -20,6 +20,24 @@ builtinAuthenticationProfiles: description: "The storage account name" example: '"mystorageaccount"' authenticationProfiles: + - title: "SPIFFE Authentication" + description: "Authenticate using SPIFFE with Azure AD." + metadata: + - name: accountName + required: true + sensitive: false + description: "The storage account name" + example: '"mystorageaccount"' + - name: tenantId + required: true + sensitive: false + description: "The Azure AD tenant ID" + example: '"00000000-0000-0000-0000-000000000000"' + - name: clientId + required: true + sensitive: false + description: "The Azure AD client ID (application ID)" + example: '"00000000-0000-0000-0000-000000000000"' - title: "Connection string" description: "Authenticate using a connection string." metadata: diff --git a/state/cockroachdb/cockroachdb.go b/state/cockroachdb/cockroachdb.go index 8e8bbdc910..069b5ff48f 100644 --- a/state/cockroachdb/cockroachdb.go +++ b/state/cockroachdb/cockroachdb.go @@ -70,6 +70,7 @@ WHERE } func ensureTables(ctx context.Context, db pginterfaces.PGXPoolConn, opts postgresql.MigrateOptions) error { + // Create state table if missing, with row_id ready for pagination exists, err := tableExists(ctx, db, opts.StateTableName) if err != nil { return err @@ -78,42 +79,88 @@ func ensureTables(ctx context.Context, db pginterfaces.PGXPoolConn, opts postgre if !exists { opts.Logger.Info("Creating CockroachDB state table") _, err = db.Exec(ctx, fmt.Sprintf(`CREATE TABLE %s ( - key text NOT NULL PRIMARY KEY, - value jsonb NOT NULL, - isbinary boolean NOT NULL, - etag INT, - insertdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - updatedate TIMESTAMP WITH TIME ZONE NULL, - expiredate TIMESTAMP WITH TIME ZONE NULL, - INDEX expiredate_idx (expiredate) + key text NOT NULL PRIMARY KEY, + value jsonb NOT NULL, + isbinary boolean NOT NULL, + etag INT, + insertdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updatedate TIMESTAMP WITH TIME ZONE NULL, + expiredate TIMESTAMP WITH TIME ZONE NULL, + row_id INT8 NOT NULL DEFAULT unique_rowid(), + UNIQUE (row_id) );`, opts.StateTableName)) if err != nil { return err } - } + // Indexes created after table create for idempotency + if _, err = db.Exec(ctx, fmt.Sprintf( + `CREATE INDEX IF NOT EXISTS %s_expiredate_idx ON %s (expiredate);`, + opts.StateTableName, opts.StateTableName)); err != nil { + return err + } + } else { + // Existing table: make sure columns + indexes exist + // 1) expiredate (idempotent) + if _, err = db.Exec(ctx, fmt.Sprintf( + `ALTER TABLE %s ADD COLUMN IF NOT EXISTS expiredate TIMESTAMPTZ NULL;`, + opts.StateTableName)); err != nil { + return err + } + if _, err = db.Exec(ctx, fmt.Sprintf( + `CREATE INDEX IF NOT EXISTS %s_expiredate_idx ON %s (expiredate);`, + opts.StateTableName, opts.StateTableName)); err != nil { + return err + } - // If table was created before v1.11. - _, err = db.Exec(ctx, fmt.Sprintf( - `ALTER TABLE %s ADD COLUMN IF NOT EXISTS expiredate TIMESTAMP WITH TIME ZONE NULL;`, opts.StateTableName)) - if err != nil { - return err - } - _, err = db.Exec(ctx, fmt.Sprintf( - `CREATE INDEX IF NOT EXISTS expiredate_idx ON %s (expiredate);`, opts.StateTableName)) - if err != nil { - return err + // 2) row_id for keyset pagination + opts.Logger.Infof("Ensuring row_id exists on '%s'", opts.StateTableName) + + // Add column if missing (nullable initially) + if _, err = db.Exec(ctx, fmt.Sprintf( + `ALTER TABLE %s ADD COLUMN IF NOT EXISTS row_id INT8;`, + opts.StateTableName)); err != nil { + return err + } + + // Ensure it has a default generator + if _, err = db.Exec(ctx, fmt.Sprintf( + `ALTER TABLE %s ALTER COLUMN row_id SET DEFAULT unique_rowid();`, + opts.StateTableName)); err != nil { + return err + } + + // Backfill NULLs (older rows) with generated values + if _, err = db.Exec(ctx, fmt.Sprintf( + `UPDATE %s SET row_id = unique_rowid() WHERE row_id IS NULL;`, + opts.StateTableName)); err != nil { + return err + } + + // Enforce NOT NULL + if _, err = db.Exec(ctx, fmt.Sprintf( + `ALTER TABLE %s ALTER COLUMN row_id SET NOT NULL;`, + opts.StateTableName)); err != nil { + return err + } + + // Unique index to guarantee ordering without changing PK + if _, err = db.Exec(ctx, fmt.Sprintf( + `CREATE UNIQUE INDEX IF NOT EXISTS %s_row_id_uidx ON %s (row_id);`, + opts.StateTableName, opts.StateTableName)); err != nil { + return err + } } + // Metadata table exists, err = tableExists(ctx, db, opts.MetadataTableName) if err != nil { return err } - if !exists { opts.Logger.Info("Creating CockroachDB metadata table") _, err = db.Exec(ctx, fmt.Sprintf(`CREATE TABLE %s ( - key text NOT NULL PRIMARY KEY, - value text NOT NULL +key text NOT NULL PRIMARY KEY, +value text NOT NULL );`, opts.MetadataTableName)) if err != nil { return err @@ -124,7 +171,7 @@ func ensureTables(ctx context.Context, db pginterfaces.PGXPoolConn, opts postgre } func tableExists(ctx context.Context, db pginterfaces.PGXPoolConn, tableName string) (bool, error) { - exists := false - err := db.QueryRow(ctx, "SELECT EXISTS (SELECT * FROM pg_tables where tablename = $1)", tableName).Scan(&exists) + var exists bool + err := db.QueryRow(ctx, "SELECT EXISTS (SELECT * FROM pg_tables WHERE tablename = $1)", tableName).Scan(&exists) return exists, err } diff --git a/state/couchbase/metadata.yaml b/state/couchbase/metadata.yaml index 6b30d80448..08d4def372 100644 --- a/state/couchbase/metadata.yaml +++ b/state/couchbase/metadata.yaml @@ -30,14 +30,14 @@ authenticationProfiles: example: "default" metadata: - name: numReplicasDurableReplication - type: integer + type: number required: false description: The number of replicas for durable replication. - example: 1 - default: 0 + example: "1" + default: "0" - name: numReplicasDurablePersistence - type: integer + type: number required: false description: The number of replicas for durable persistence. - example: 1 - default: 0 \ No newline at end of file + example: "1" + default: "0" \ No newline at end of file diff --git a/state/errors.go b/state/errors.go index 6f7b293dd4..3d0847e0b8 100644 --- a/state/errors.go +++ b/state/errors.go @@ -28,6 +28,8 @@ const ( ETagMismatch ETagErrorKind = "mismatch" ) +var ErrKeysLikeEmptyPattern = errors.New("keys like pattern cannot be empty") + // ETagError is a custom error type for etag exceptions. type ETagError struct { err error diff --git a/state/etcd/etcd.go b/state/etcd/etcd.go index b7a1db3a69..2a6c9a8b8b 100644 --- a/state/etcd/etcd.go +++ b/state/etcd/etcd.go @@ -77,6 +77,7 @@ func newETCD(logger logger.Logger, schema schemaMarshaller) state.Store { state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, + state.FeatureKeysLike, }, } s.BulkStore = state.NewDefaultBulkStore(s) @@ -448,3 +449,218 @@ func NewTLSConfig(clientCert, clientKey, caCert string) (*tls.Config, error) { return config, nil } + +func (e *Etcd) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + userPrefix := likeLiteralPrefix(req.Pattern) + etcdPrefix := strings.TrimSuffix(e.keyPrefixPath, "/") + "/" + userPrefix + base := strings.TrimSuffix(e.keyPrefixPath, "/") + "/" + + // Continue token format: : + var snapRev, afterCreate int64 + if tok := req.ContinuationToken; tok != nil && *tok != "" { + parts := strings.SplitN(*tok, ":", 2) + if len(parts) != 2 { + return nil, errors.New("invalid continue token") + } + var err error + if snapRev, err = strconv.ParseInt(parts[0], 10, 64); err != nil { + return nil, fmt.Errorf("invalid continue token: %w", err) + } + if afterCreate, err = strconv.ParseInt(parts[1], 10, 64); err != nil { + return nil, fmt.Errorf("invalid continue token: %w", err) + } + } + + // Page size handling + want := 0 // 0 = unlimited + fetch := 1024 // initial server-side limit + if req.PageSize != nil && *req.PageSize > 0 { + want = int(*req.PageSize) + fetch = int(*req.PageSize) * 4 // over-fetch to reduce round-trips + } + + keys := make([]string, 0, 1024) + var lastCreate int64 + + for { + opts := []clientv3.OpOption{ + clientv3.WithPrefix(), + clientv3.WithSort(clientv3.SortByCreateRevision, clientv3.SortAscend), + clientv3.WithLimit(int64(fetch)), + clientv3.WithKeysOnly(), + } + if snapRev > 0 { + opts = append(opts, clientv3.WithRev(snapRev)) + } + + cctx, cancel := context.WithTimeout(ctx, 5*time.Second) + resp, err := e.client.Get(cctx, etcdPrefix, opts...) + cancel() + if err != nil { + return nil, err + } + if snapRev == 0 { + // Freeze snapshot to be stable across internal retries + snapRev = resp.Header.Revision + } + + // Track the max CreateRevision we saw in THIS batch, regardless of LIKE match, + // so we can advance the cursor and not re-scan the same window. + maxSeenCreate := afterCreate + + for _, kv := range resp.Kvs { + if kv.CreateRevision > maxSeenCreate { + maxSeenCreate = kv.CreateRevision + } + + // Extract user key + fullKey := string(kv.Key) + if !strings.HasPrefix(fullKey, base) { + continue + } + userKey := fullKey[len(base):] + + // Skip anything at or before our current cursor + if kv.CreateRevision <= afterCreate { + continue + } + + // LIKE filter + if !likeMatch(userKey, req.Pattern) { + continue + } + + keys = append(keys, userKey) + lastCreate = kv.CreateRevision + + // If we have a page size and it's filled, return with next token + if want > 0 && len(keys) >= want { + tok := fmt.Sprintf("%d:%d", snapRev, lastCreate) + return &state.KeysLikeResponse{ + Keys: keys, + ContinuationToken: &tok, + }, nil + } + } + + // Advance the creation-revision cursor so next loop does NOT re-scan same items + afterCreate = maxSeenCreate + + // If server returned fewer than we asked for, we're at end-of-range at this snapshot + if int64(len(resp.Kvs)) < int64(fetch) { + return &state.KeysLikeResponse{ + Keys: keys, + ContinuationToken: nil, + }, nil + } + + // Otherwise, keep going until page fills or range ends. + // (We can keep fetch constant; doubling is optional. Keep a safety cap.) + if fetch < 8192 { + fetch *= 2 + } else if want == 0 { + // Unlimited page but we've hit our internal cap; return what we have + return &state.KeysLikeResponse{Keys: keys}, nil + } + } +} + +// likeLiteralPrefix returns the literal prefix before the first unescaped % or _. +func likeLiteralPrefix(p string) string { + var b strings.Builder + for i := 0; i < len(p); i++ { + c := p[i] + switch c { + case '\\': + if i+1 < len(p) { + b.WriteByte(p[i+1]) + i++ + } else { + // Trailing backslash: treat it literally. + b.WriteByte('\\') + } + case '%', '_': + return b.String() + default: + b.WriteByte(c) + } + } + return b.String() +} + +// likeMatch implements SQL LIKE for ASCII with % (any) and _ (single char). +// Backslash escapes %, _, and \ (as used in the conformance tests). +func likeMatch(s, p string) bool { + i, j := 0, 0 + star := -1 // position of last % in pattern + match := 0 // index in s where we started to match after last % + for i < len(s) { + if j < len(p) { + switch p[j] { + case '\\': + // Escape next char, must match literally + if j+1 >= len(p) { + // dangling escape => treat as literal '\' + if s[i] != '\\' { + goto backtrack + } + i++ + j++ + continue + } + j++ + if s[i] == p[j] { + i++ + j++ + continue + } + goto backtrack + case '_': + // Match any single char + i++ + j++ + continue + case '%': + // Remember position of % and try to match zero chars first + star = j + match = i + j++ + continue + default: + if s[i] == p[j] { + i++ + j++ + continue + } + } + } + backtrack: + if star != -1 { + // Backtrack: extend % to cover one more char + j = star + 1 + match++ + i = match + continue + } + return false + } + // Consume trailing % (and escaped sequences like "\%" are not %) + for j < len(p) { + if p[j] == '%' { + j++ + continue + } + if p[j] == '\\' { + // Escaped literal remains unmatched since s ended + // If there's a char after '\', it cannot match empty + return false + } + // Any other char (including '_') cannot match empty + return false + } + return true +} diff --git a/state/etcd/metadata.yaml b/state/etcd/metadata.yaml index 671a91442f..9fc5e651c9 100644 --- a/state/etcd/metadata.yaml +++ b/state/etcd/metadata.yaml @@ -54,8 +54,8 @@ metadata: example: "my_key_prefix_path" default: "" - name: maxTxnOps - type: integer + type: number required: false description: Maximum number of operations allowed in a transaction. - example: 128 - default: 128 + example: "128" + default: "128" diff --git a/state/feature.go b/state/feature.go index fb346e2113..d50bac4446 100644 --- a/state/feature.go +++ b/state/feature.go @@ -30,6 +30,8 @@ const ( FeatureDeleteWithPrefix Feature = "DELETE_WITH_PREFIX" // FeaturePartitionKey is the feature that supports the partition FeaturePartitionKey Feature = "PARTITION_KEY" + // FeatureKeysLike is the feature that supports keys like list operation. + FeatureKeysLike Feature = "KEYS_LIKE" ) // Feature names a feature that can be implemented by state store components. diff --git a/state/gcp/firestore/metadata.yaml b/state/gcp/firestore/metadata.yaml index b489334518..275b08b74b 100644 --- a/state/gcp/firestore/metadata.yaml +++ b/state/gcp/firestore/metadata.yaml @@ -94,5 +94,5 @@ metadata: type: bool required: false description: Whether to disable indexing for the entity. - example: false - default: false + example: "false" + default: "false" diff --git a/state/in-memory/in_memory.go b/state/in-memory/in_memory.go index 7e8fbe8442..7373ed5ffe 100644 --- a/state/in-memory/in_memory.go +++ b/state/in-memory/in_memory.go @@ -18,6 +18,8 @@ import ( "encoding/json" "errors" "fmt" + "regexp" + "sort" "strconv" "strings" "sync" @@ -34,10 +36,12 @@ import ( "github.com/dapr/kit/ptr" ) -type inMemoryStore struct { +type InMemoryStore struct { state.BulkStore - items map[string]*inMemStateStoreItem + items map[string]*inMemStateStoreItem + idx uint64 + lock sync.RWMutex log logger.Logger clock clock.Clock @@ -50,8 +54,8 @@ func NewInMemoryStateStore(log logger.Logger) state.Store { return newStateStore(log) } -func newStateStore(log logger.Logger) *inMemoryStore { - s := &inMemoryStore{ +func newStateStore(log logger.Logger) *InMemoryStore { + s := &InMemoryStore{ items: map[string]*inMemStateStoreItem{}, log: log, closeCh: make(chan struct{}), @@ -61,7 +65,7 @@ func newStateStore(log logger.Logger) *inMemoryStore { return s } -func (store *inMemoryStore) Init(ctx context.Context, metadata state.Metadata) error { +func (store *InMemoryStore) Init(ctx context.Context, metadata state.Metadata) error { // start a background go routine to clean expired item store.wg.Add(1) go func() { @@ -71,7 +75,7 @@ func (store *inMemoryStore) Init(ctx context.Context, metadata state.Metadata) e return nil } -func (store *inMemoryStore) Close() error { +func (store *InMemoryStore) Close() error { if store.closed.CompareAndSwap(false, true) { close(store.closeCh) } @@ -88,16 +92,17 @@ func (store *inMemoryStore) Close() error { return nil } -func (store *inMemoryStore) Features() []state.Feature { +func (store *InMemoryStore) Features() []state.Feature { return []state.Feature{ state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, state.FeatureDeleteWithPrefix, + state.FeatureKeysLike, } } -func (store *inMemoryStore) Delete(ctx context.Context, req *state.DeleteRequest) error { +func (store *InMemoryStore) Delete(ctx context.Context, req *state.DeleteRequest) error { // step1: validate parameters if err := state.CheckRequestOptions(req.Options); err != nil { return err @@ -118,7 +123,7 @@ func (store *inMemoryStore) Delete(ctx context.Context, req *state.DeleteRequest return nil } -func (store *inMemoryStore) DeleteWithPrefix(ctx context.Context, req state.DeleteWithPrefixRequest) (state.DeleteWithPrefixResponse, error) { +func (store *InMemoryStore) DeleteWithPrefix(ctx context.Context, req state.DeleteWithPrefixRequest) (state.DeleteWithPrefixResponse, error) { // step1: validate parameters err := req.Validate() if err != nil { @@ -146,7 +151,7 @@ func (store *inMemoryStore) DeleteWithPrefix(ctx context.Context, req state.Dele return state.DeleteWithPrefixResponse{Count: count}, nil } -func (store *inMemoryStore) doValidateEtag(key string, etag *string, concurrency string) error { +func (store *InMemoryStore) doValidateEtag(key string, etag *string, concurrency string) error { hasEtag := etag != nil && *etag != "" if concurrency == state.FirstWrite && !hasEtag { @@ -173,11 +178,11 @@ func (store *inMemoryStore) doValidateEtag(key string, etag *string, concurrency return nil } -func (store *inMemoryStore) doDelete(ctx context.Context, key string) { +func (store *InMemoryStore) doDelete(ctx context.Context, key string) { delete(store.items, key) } -func (store *inMemoryStore) Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) { +func (store *InMemoryStore) Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) { store.lock.RLock() item := store.items[req.Key] store.lock.RUnlock() @@ -201,7 +206,7 @@ func (store *inMemoryStore) Get(ctx context.Context, req *state.GetRequest) (*st return &state.GetResponse{Data: item.data, ETag: item.etag, Metadata: metadata}, nil } -func (store *inMemoryStore) BulkGet(ctx context.Context, req []state.GetRequest, _ state.BulkGetOpts) ([]state.BulkGetResponse, error) { +func (store *InMemoryStore) BulkGet(ctx context.Context, req []state.GetRequest, _ state.BulkGetOpts) ([]state.BulkGetResponse, error) { res := make([]state.BulkGetResponse, len(req)) if len(req) == 0 { return res, nil @@ -235,7 +240,7 @@ func (store *inMemoryStore) BulkGet(ctx context.Context, req []state.GetRequest, return res, nil } -func (store *inMemoryStore) getAndExpire(key string) *inMemStateStoreItem { +func (store *InMemoryStore) getAndExpire(key string) *inMemStateStoreItem { // get item and check expired again to avoid if item changed between we got this write-lock item := store.items[key] if item == nil { @@ -248,7 +253,7 @@ func (store *inMemoryStore) getAndExpire(key string) *inMemStateStoreItem { return item } -func (store *inMemoryStore) marshal(v any) (bt []byte, err error) { +func (store *InMemoryStore) marshal(v any) (bt []byte, err error) { byteArray, isBinary := v.([]uint8) if isBinary { bt = byteArray @@ -261,7 +266,7 @@ func (store *inMemoryStore) marshal(v any) (bt []byte, err error) { return bt, nil } -func (store *inMemoryStore) Set(ctx context.Context, req *state.SetRequest) error { +func (store *InMemoryStore) Set(ctx context.Context, req *state.SetRequest) error { // step1: validate parameters ttlInSeconds, err := store.doSetValidateParameters(req) if err != nil { @@ -289,7 +294,7 @@ func (store *inMemoryStore) Set(ctx context.Context, req *state.SetRequest) erro return nil } -func (store *inMemoryStore) doSetValidateParameters(req *state.SetRequest) (int, error) { +func (store *InMemoryStore) doSetValidateParameters(req *state.SetRequest) (int, error) { err := state.CheckRequestOptions(req.Options) if err != nil { return 0, err @@ -321,12 +326,16 @@ func doParseTTLInSeconds(metadata map[string]string) (int, error) { return i, nil } -func (store *inMemoryStore) doSet(ctx context.Context, key string, data []byte, ttlInSeconds int) { +func (store *InMemoryStore) doSet(ctx context.Context, key string, data []byte, ttlInSeconds int) { etag := uuid.New().String() el := &inMemStateStoreItem{ data: data, etag: &etag, + idx: store.idx, } + + store.idx++ + if ttlInSeconds > 0 { el.expire = ptr.Of(store.clock.Now().Add(time.Duration(ttlInSeconds) * time.Second)) } @@ -355,7 +364,7 @@ func (r innerSetRequest) GetMetadata() map[string]string { return r.req.Metadata } -func (store *inMemoryStore) Multi(ctx context.Context, request *state.TransactionalStateRequest) error { +func (store *InMemoryStore) Multi(ctx context.Context, request *state.TransactionalStateRequest) error { if len(request.Operations) == 0 { return nil } @@ -420,7 +429,7 @@ func (store *inMemoryStore) Multi(ctx context.Context, request *state.Transactio return nil } -func (store *inMemoryStore) startCleanThread() { +func (store *InMemoryStore) startCleanThread() { for { select { case <-time.After(time.Second): @@ -431,7 +440,7 @@ func (store *inMemoryStore) startCleanThread() { } } -func (store *inMemoryStore) doCleanExpiredItems() { +func (store *InMemoryStore) doCleanExpiredItems() { store.lock.Lock() defer store.lock.Unlock() @@ -442,7 +451,7 @@ func (store *inMemoryStore) doCleanExpiredItems() { } } -func (store *inMemoryStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { +func (store *InMemoryStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { // no metadata, hence no metadata struct to convert here return } @@ -451,6 +460,7 @@ type inMemStateStoreItem struct { data []byte etag *string expire *time.Time + idx uint64 } func (item *inMemStateStoreItem) isExpired(now time.Time) bool { @@ -459,3 +469,107 @@ func (item *inMemStateStoreItem) isExpired(now time.Time) bool { } return now.After(*item.expire) } + +func (store *InMemoryStore) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + store.lock.RLock() + defer store.lock.RUnlock() + + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + re, err := likeToRegex(req.Pattern) + if err != nil { + return nil, fmt.Errorf("failed to convert like pattern to regex: %w", err) + } + + kk := &sortingKeys{ + keys: make([]string, 0, 1024), + items: make([]*inMemStateStoreItem, 0, 1024), + } + + for k, i := range store.items { + if re.MatchString(k) { + kk.keys = append(kk.keys, k) + kk.items = append(kk.items, i) + } + } + + if len(kk.items) == 0 { + return new(state.KeysLikeResponse), nil + } + + sort.Stable(kk) + + if ct := req.ContinuationToken; ct != nil { + ct, err := strconv.ParseUint(*req.ContinuationToken, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid continue token: %w", err) + } + cut := -1 + for i, item := range kk.items { + if item.idx >= ct { + cut = i + break + } + } + + if cut == -1 { + return new(state.KeysLikeResponse), nil + } + + kk.items = kk.items[cut:] + kk.keys = kk.keys[cut:] + } + + var continueToken *string + if ps := req.PageSize; ps != nil { + pageSize := int(*ps) + + if len(kk.keys) > pageSize { + nextIdx := pageSize + + continueToken = ptr.Of(strconv.FormatUint(kk.items[nextIdx].idx, 10)) + + kk.keys = kk.keys[:pageSize] + kk.items = kk.items[:pageSize] + } + } + + return &state.KeysLikeResponse{ + Keys: kk.keys, + ContinuationToken: continueToken, + }, nil +} + +func likeToRegex(pattern string) (*regexp.Regexp, error) { + var b strings.Builder + b.Grow(len(pattern) + 4) + b.WriteString("^") + + escaped := false + for _, r := range pattern { + if escaped { + b.WriteString(regexp.QuoteMeta(string(r))) + escaped = false + continue + } + switch r { + case '\\': + escaped = true + case '%': + b.WriteString(".*") + case '_': + b.WriteString(".") + default: + b.WriteString(regexp.QuoteMeta(string(r))) + } + } + + if escaped { + b.WriteString(regexp.QuoteMeta(`\`)) + } + + b.WriteString("$") + return regexp.Compile(b.String()) +} diff --git a/state/in-memory/in_memory_test.go b/state/in-memory/in_memory_test.go index 6cf46415b7..6ab434c9ea 100644 --- a/state/in-memory/in_memory_test.go +++ b/state/in-memory/in_memory_test.go @@ -32,7 +32,7 @@ func TestReadAndWrite(t *testing.T) { defer ctl.Finish() - store := NewInMemoryStateStore(logger.NewLogger("test")).(*inMemoryStore) + store := NewInMemoryStateStore(logger.NewLogger("test")).(*InMemoryStore) fakeClock := clocktesting.NewFakeClock(time.Now()) store.clock = fakeClock store.Init(t.Context(), state.Metadata{}) @@ -177,3 +177,7 @@ func TestReadAndWrite(t *testing.T) { require.NoError(t, err) }) } + +func Test_KeyLike(t *testing.T) { + var _ state.KeysLiker = NewInMemoryStateStore(nil).(*InMemoryStore) +} diff --git a/state/in-memory/keys.go b/state/in-memory/keys.go new file mode 100644 index 0000000000..9ac8498ef5 --- /dev/null +++ b/state/in-memory/keys.go @@ -0,0 +1,36 @@ +/* +Copyright 2025 The Dapr Authors +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 inmemory + +type sortingKeys struct { + keys []string + items []*inMemStateStoreItem +} + +func (s *sortingKeys) Len() int { + return len(s.keys) +} + +func (s *sortingKeys) Less(i, j int) bool { + return s.items[i].idx < s.items[j].idx +} + +func (s *sortingKeys) Swap(i, j int) { + tmpk := s.keys[i] + tmpi := s.items[i] + s.keys[i] = s.keys[j] + s.items[i] = s.items[j] + s.keys[j] = tmpk + s.items[j] = tmpi +} diff --git a/state/mongodb/mongodb.go b/state/mongodb/mongodb.go index 112043d637..173bebfd6b 100644 --- a/state/mongodb/mongodb.go +++ b/state/mongodb/mongodb.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "reflect" + "regexp" "strconv" "strings" "time" @@ -715,3 +716,104 @@ func (m *MongoDB) Close() error { defer cancel() return m.client.Disconnect(ctx) } + +func (m *MongoDB) KeysLike(ctx context.Context, req state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + re, err := likeToRegex(req.Pattern) + if err != nil { + return nil, fmt.Errorf("invalid pattern: %w", err) + } + + and := bson.A{ + bson.D{{Key: id, Value: bson.M{"$regex": re.String()}}}, + getFilterTTL(), + } + + if req.ContinuationToken != nil && *req.ContinuationToken != "" { + and = append(and, bson.D{{Key: id, Value: bson.M{"$gt": *req.ContinuationToken}}}) + } + + filter := bson.D{{Key: "$and", Value: and}} + + findOpts := options.Find().SetSort(bson.D{{Key: id, Value: 1}}) + + var pageSize uint32 + if req.PageSize != nil && *req.PageSize > 0 { + pageSize = *req.PageSize + findOpts.SetLimit(int64(pageSize + 1)) + } + + qctx, cancel := context.WithTimeout(ctx, m.operationTimeout) + defer cancel() + + cur, err := m.collection.Find(qctx, filter, findOpts) + if err != nil { + return nil, err + } + defer cur.Close(qctx) + + type rec struct { + Key string `bson:"_id"` + } + var recs []rec + for cur.Next(qctx) { + var r rec + if err := cur.Decode(&r); err != nil { + return nil, err + } + recs = append(recs, r) + } + if err := cur.Err(); err != nil { + return nil, err + } + + resp := &state.KeysLikeResponse{ + Keys: make([]string, 0, len(recs)), + } + + //nolint:gosec + if pageSize > 0 && uint32(len(recs)) > pageSize { + next := recs[pageSize].Key // first NOT returned + resp.ContinuationToken = &next + recs = recs[:pageSize] + } + + for _, r := range recs { + resp.Keys = append(resp.Keys, r.Key) + } + + return resp, nil +} + +func likeToRegex(pattern string) (*regexp.Regexp, error) { + var b strings.Builder + b.Grow(len(pattern) + 4) + b.WriteString("^") + + escaped := false + for _, r := range pattern { + if escaped { + b.WriteString(regexp.QuoteMeta(string(r))) + escaped = false + continue + } + switch r { + case '\\': + escaped = true + case '%': + b.WriteString(".*") + case '_': + b.WriteString(".") + default: + b.WriteString(regexp.QuoteMeta(string(r))) + } + } + if escaped { + b.WriteString(regexp.QuoteMeta(`\`)) + } + b.WriteString("$") + return regexp.Compile(b.String()) +} diff --git a/state/mysql/mysql.go b/state/mysql/mysql.go index d963d483ac..a0c043eabb 100644 --- a/state/mysql/mysql.go +++ b/state/mysql/mysql.go @@ -231,6 +231,7 @@ func (m *MySQL) Features() []state.Feature { state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, + state.FeatureKeysLike, } } @@ -371,12 +372,12 @@ func (m *MySQL) ensureStateTable(ctx context.Context, schemaName, stateTableName } // Check if expiredate column exists - to cater cases when table was created before v1.11. - columnExists, err := columnExists(ctx, m.db, schemaName, stateTableName, "expiredate", m.timeout) + ce, err := columnExists(ctx, m.db, schemaName, stateTableName, "expiredate", m.timeout) if err != nil { return err } - if !columnExists { + if !ce { m.logger.Infof("Adding expiredate column to MySql state table '%s'", stateTableName) _, err = m.db.ExecContext(ctx, fmt.Sprintf( `ALTER TABLE %s ADD COLUMN IF NOT EXISTS expiredate TIMESTAMP NULL;`, stateTableName)) @@ -390,6 +391,29 @@ func (m *MySQL) ensureStateTable(ctx context.Context, schemaName, stateTableName } } + // Check is row_id column exists - to cater cases when table was created before v1.17 + ce, err = columnExists(ctx, m.db, schemaName, stateTableName, "row_id", m.timeout) + if err != nil { + return err + } + + if !ce { + m.logger.Infof("Adding row_id column to MySql state table '%s'", stateTableName) + stmt := fmt.Sprintf(` + ALTER TABLE %s + ADD COLUMN row_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + ADD UNIQUE KEY row_id_uidx (row_id);`, stateTableName) + + if _, err = m.db.ExecContext(ctx, stmt); err != nil { + // If the unique index already exists (e.g., rerun), ignore duplicate + // key-name errors. + // MySQL errno 1061 / SQLSTATE 42000; MariaDB uses the same errno. + if !strings.Contains(err.Error(), "Error 1061") && !strings.Contains(strings.ToLower(err.Error()), "duplicate key name") { + return err + } + } + } + return nil } @@ -605,27 +629,20 @@ func (m *MySQL) setValue(parentCtx context.Context, querier querier, req *state. AND (expiredate IS NULL OR expiredate > CURRENT_TIMESTAMP)` params = []any{enc, eTag, isBinary, req.Key, *req.ETag} } else if req.Options.Concurrency == state.FirstWrite { + // Insert only if there's no non-expired row for this id. + // If a row exists but is expired, treat it as deleted and allow insert. // If the operation uses first-write concurrency, we need to handle the special case of a row that has expired but hasn't been garbage collected yet // In this case, the row should be considered as if it were deleted - query = `REPLACE INTO ` + m.tableName + ` - WITH a AS ( - SELECT - ? AS id, - ? AS value, - ? AS isbinary, - CURRENT_TIMESTAMP AS insertDate, - CURRENT_TIMESTAMP AS updateDate, - ? AS eTag, - ` + ttlQuery + ` AS expiredate - WHERE NOT EXISTS ( - SELECT 1 - FROM ` + m.tableName + ` - WHERE id = ? - AND (expiredate IS NULL OR expiredate > CURRENT_TIMESTAMP) - ) - ) - SELECT * FROM a` - params = []any{req.Key, enc, isBinary, eTag, req.Key} + query = `INSERT INTO ` + m.tableName + ` (id, value, eTag, isbinary, expiredate) +SELECT ?, ?, ?, ?, ` + ttlQuery + ` +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 + FROM ` + m.tableName + ` + WHERE id = ? + AND (expiredate IS NULL OR expiredate > CURRENT_TIMESTAMP) +)` + params = []any{req.Key, enc, eTag, isBinary, req.Key} } else { query = `REPLACE INTO ` + m.tableName + ` (id, value, eTag, isbinary, expiredate) VALUES (?, ?, ?, ?, ` + ttlQuery + `)` @@ -853,3 +870,92 @@ func (m *MySQL) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) return } + +func (m *MySQL) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + var ( + args []any + whereParts []string + ) + + whereParts = append(whereParts, + "id LIKE ?", + "(expiredate IS NULL OR expiredate > CURRENT_TIMESTAMP)", + ) + args = append(args, req.Pattern) + + // Continue strictly AFTER the last returned row_id from previous page + if req.ContinuationToken != nil && *req.ContinuationToken != "" { + // row_id is BIGINT UNSIGNED; parse for clarity (MySQL would coerce strings too) + rid, err := strconv.ParseUint(*req.ContinuationToken, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid continue token: %w", err) + } + whereParts = append(whereParts, "row_id > ?") + args = append(args, rid) + } + + orderClause := " ORDER BY row_id ASC" + + limitClause := "" + var pageSize uint32 + if req.PageSize != nil && *req.PageSize > 0 { + pageSize = *req.PageSize + // fetch one extra to detect "has next" + limitClause = " LIMIT ?" + args = append(args, pageSize+1) + } + + //nolint:gosec + query := ` + SELECT id, row_id + FROM ` + m.tableName + ` + WHERE ` + strings.Join(whereParts, " AND ") + ` + ` + orderClause + limitClause + + runCtx, cancel := context.WithTimeout(ctx, m.timeout) + defer cancel() + + rows, err := m.db.QueryContext(runCtx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + type rec struct { + id string + rowID uint64 + } + recs := make([]rec, 0, 256) + for rows.Next() { + var id string + var rid uint64 + if err := rows.Scan(&id, &rid); err != nil { + return nil, err + } + recs = append(recs, rec{id: id, rowID: rid}) + } + if err := rows.Err(); err != nil { + return nil, err + } + + resp := &state.KeysLikeResponse{Keys: make([]string, 0, len(recs))} + + // If we over-fetched, set token to the LAST returned record (index pageSize-1) + //nolint:gosec + if pageSize > 0 && uint32(len(recs)) > pageSize { + lastReturned := recs[pageSize-1] + tok := strconv.FormatUint(lastReturned.rowID, 10) + resp.ContinuationToken = &tok + recs = recs[:pageSize] + } + + for _, r := range recs { + resp.Keys = append(resp.Keys, r.id) + } + + return resp, nil +} diff --git a/state/mysql/mysql_test.go b/state/mysql/mysql_test.go index 885c72f7ce..9950fbe491 100644 --- a/state/mysql/mysql_test.go +++ b/state/mysql/mysql_test.go @@ -501,6 +501,8 @@ func TestEnsureStateTableCreatesTable(t *testing.T) { m.mock1.ExpectExec("CREATE TABLE").WillReturnResult(sqlmock.NewResult(1, 1)) rows = sqlmock.NewRows([]string{"exists"}).AddRow(1) m.mock1.ExpectQuery("SELECT count(/*)").WillReturnRows(rows) + rows = sqlmock.NewRows([]string{"exists"}).AddRow(1) + m.mock1.ExpectQuery("SELECT count(/*)").WillReturnRows(rows) m.mock1.ExpectExec("CREATE PROCEDURE").WillReturnResult(sqlmock.NewResult(1, 1)) // Act @@ -929,3 +931,8 @@ func TestValidIdentifier(t *testing.T) { }) } } + +func Test_KeysLike(t *testing.T) { + m, _ := mockDatabase(t) + var _ state.KeysLiker = m.mySQL +} diff --git a/state/oci/objectstorage/metadata.yaml b/state/oci/objectstorage/metadata.yaml index 896831a050..bc77405590 100644 --- a/state/oci/objectstorage/metadata.yaml +++ b/state/oci/objectstorage/metadata.yaml @@ -44,8 +44,8 @@ authenticationProfiles: type: bool required: true description: Set to true to use instance principal authentication. - example: true - default: false + example: "true" + default: "false" - title: "Configuration File Authentication" description: "Authenticate using OCI configuration file." metadata: @@ -53,8 +53,8 @@ authenticationProfiles: type: bool required: true description: Set to true to use configuration file authentication. - example: true - default: falsee + example: "true" + default: "false" - name: configFilePath type: string required: true diff --git a/state/oracledatabase/oracledatabaseaccess.go b/state/oracledatabase/oracledatabaseaccess.go index 42a9c77aa6..1ad83f5907 100644 --- a/state/oracledatabase/oracledatabaseaccess.go +++ b/state/oracledatabase/oracledatabaseaccess.go @@ -337,7 +337,17 @@ func (o *oracleDatabaseAccess) BulkGet(ctx context.Context, req []state.GetReque rows, err := o.db.QueryContext(ctx, query, params...) if err != nil { - return nil, err + // If the query fails, return per-key error entries instead of + // propagating the error, for consistency with other state stores + // (e.g. Redis, SQL Server) which return HTTP 200 with per-key errors. + res := make([]state.BulkGetResponse, len(req)) + for i, r := range req { + res[i] = state.BulkGetResponse{ + Key: r.Key, + Error: "bulk get query failed: " + err.Error(), + } + } + return res, nil } defer rows.Close() @@ -383,10 +393,22 @@ func (o *oracleDatabaseAccess) BulkGet(ctx context.Context, req []state.GetReque data []byte ) if err = json.Unmarshal([]byte(value), &s); err != nil { - return nil, err + res[n] = state.BulkGetResponse{ + Key: key, + Error: "failed to decode binary value: " + err.Error(), + } + foundKeys[key] = struct{}{} + n++ + continue } if data, err = base64.StdEncoding.DecodeString(s); err != nil { - return nil, err + res[n] = state.BulkGetResponse{ + Key: key, + Error: "failed to decode binary value: " + err.Error(), + } + foundKeys[key] = struct{}{} + n++ + continue } response.Data = data } else { @@ -400,8 +422,29 @@ func (o *oracleDatabaseAccess) BulkGet(ctx context.Context, req []state.GetReque n++ } + // rows.Err() reports errors from iteration; return what we have as per-key + // errors for any keys not yet found, for consistency with other stores. if err = rows.Err(); err != nil { - return nil, err + errMsg := err.Error() + anyUnfound := false + for _, r := range req { + if _, ok := foundKeys[r.Key]; !ok { + anyUnfound = true + if n >= len(req) { + break + } + res[n] = state.BulkGetResponse{ + Key: r.Key, + Error: "rows iteration failed: " + errMsg, + } + foundKeys[r.Key] = struct{}{} + n++ + } + } + if !anyUnfound { + o.logger.Warnf("Oracle BulkGet: rows iteration error after all rows processed: %v", err) + } + return res[:n], nil } // Populate missing keys with empty values @@ -527,3 +570,80 @@ func tableExists(db *sql.DB, tableName string) (bool, error) { } return true, nil } + +func (o *oracleDatabaseAccess) KeysLike(ctx context.Context, req state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + if o.db == nil { + return nil, errors.New("oracle db not initialized") + } + + table := o.metadata.TableName + + baseWhere := " WHERE key LIKE :pat ESCAPE '\\' AND (expiration_time IS NULL OR expiration_time > SYSTIMESTAMP) " + + args := []any{req.Pattern} + + seek := "" + if req.ContinuationToken != nil && *req.ContinuationToken != "" { + seek = " AND key > :token " + args = append(args, *req.ContinuationToken) + } + + orderBy := " ORDER BY key ASC " + + var query string + var pageSize uint32 + + if req.PageSize != nil && *req.PageSize > 0 { + pageSize = *req.PageSize + take := int64(pageSize + 1) + + query = fmt.Sprintf(` +SELECT key FROM ( + SELECT key + FROM %s + %s%s%s +) +WHERE ROWNUM <= :take +`, table, baseWhere, seek, orderBy) + + args = append(args, take) + } else { + query = fmt.Sprintf(` +SELECT key +FROM %s +%s%s%s +`, table, baseWhere, seek, orderBy) + } + + rows, err := o.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + keys := make([]string, 0, 256) + for rows.Next() { + var k string + if err := rows.Scan(&k); err != nil { + return nil, err + } + keys = append(keys, k) + } + if err := rows.Err(); err != nil { + return nil, err + } + + resp := &state.KeysLikeResponse{ + Keys: make([]string, 0, len(keys)), + } + + //nolint:gosec + if pageSize > 0 && uint32(len(keys)) > pageSize { + next := keys[pageSize] + resp.ContinuationToken = &next + keys = keys[:pageSize] + } + + resp.Keys = append(resp.Keys, keys...) + return resp, nil +} diff --git a/state/oracledatabase/oracledatabaseaccess_test.go b/state/oracledatabase/oracledatabaseaccess_test.go index 106d833096..6c44af16ed 100644 --- a/state/oracledatabase/oracledatabaseaccess_test.go +++ b/state/oracledatabase/oracledatabaseaccess_test.go @@ -16,14 +16,20 @@ limitations under the License. package oracledatabase import ( + "encoding/base64" + "encoding/json" + "errors" "net/url" "testing" + "time" - "github.com/dapr/components-contrib/metadata" - "github.com/dapr/components-contrib/state" - + "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" + "github.com/dapr/kit/logger" ) func TestConnectionString(t *testing.T) { @@ -153,3 +159,407 @@ func TestConnectionString(t *testing.T) { }) } } + +func TestBulkGetQueryFailure(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + db: db, + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + mock.ExpectQuery("SELECT").WillReturnError(errors.New("connection refused")) + + req := []state.GetRequest{ + {Key: "key1"}, + {Key: "key2"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.NoError(t, err) + require.Len(t, res, 2) + + for _, r := range res { + assert.NotEmpty(t, r.Error) + assert.Contains(t, r.Error, "bulk get query failed:") + assert.Contains(t, r.Error, "connection refused") + assert.Nil(t, r.Data) + assert.Nil(t, r.ETag) + } + assert.Equal(t, "key1", res[0].Key) + assert.Equal(t, "key2", res[1].Key) + + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestBulkGetBinaryDecodeFailure(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + db: db, + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + // First row: valid non-binary data + // Second row: binary flag "Y" but value is not valid JSON-encoded base64 + // Third row: valid binary data + validBinaryValue, _ := json.Marshal(base64.StdEncoding.EncodeToString([]byte("hello"))) + rows := sqlmock.NewRows([]string{"key", "value", "binary_yn", "etag", "expiration_time"}). + AddRow("key1", `"plain text"`, "N", "etag1", nil). + AddRow("key2", `not-valid-json`, "Y", "etag2", nil). + AddRow("key3", string(validBinaryValue), "Y", "etag3", nil) + + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + req := []state.GetRequest{ + {Key: "key1"}, + {Key: "key2"}, + {Key: "key3"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.NoError(t, err) + require.Len(t, res, 3) + + // key1 should succeed + assert.Equal(t, "key1", res[0].Key) + assert.Empty(t, res[0].Error) + assert.NotNil(t, res[0].Data) + + // key2 should have an error with clean struct (no ETag/Metadata leaking) + assert.Equal(t, "key2", res[1].Key) + assert.NotEmpty(t, res[1].Error) + assert.Nil(t, res[1].ETag, "error response should not leak ETag") + assert.Nil(t, res[1].Metadata, "error response should not leak Metadata") + + // key3 should succeed + assert.Equal(t, "key3", res[2].Key) + assert.Empty(t, res[2].Error) + assert.Equal(t, []byte("hello"), res[2].Data) + + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestBulkGetBinaryBase64DecodeFailure(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + db: db, + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + // Value is valid JSON string but not valid base64 + rows := sqlmock.NewRows([]string{"key", "value", "binary_yn", "etag", "expiration_time"}). + AddRow("key1", `"not-valid-base64!!!"`, "Y", "etag1", nil) + + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + req := []state.GetRequest{ + {Key: "key1"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.NoError(t, err) + require.Len(t, res, 1) + + assert.Equal(t, "key1", res[0].Key) + assert.NotEmpty(t, res[0].Error) + assert.Nil(t, res[0].ETag, "error response should not leak ETag") + assert.Nil(t, res[0].Metadata, "error response should not leak Metadata") + + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestBulkGetRowsErrFailure(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + db: db, + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + // sqlmock's RowError(N, err) causes rows.Next() to return false (with the + // error surfaced via rows.Err()) when attempting to read row at index N. + // We need at least N+1 rows added for RowError(N) to fire; the error + // prevents the Nth row from being read. So we add a dummy second row + // (key2) and set RowError(1, ...) -- key1 at index 0 is scanned + // successfully, then the attempt to advance to index 1 fails. + rows := sqlmock.NewRows([]string{"key", "value", "binary_yn", "etag", "expiration_time"}). + AddRow("key1", `"value1"`, "N", "etag1", nil). + AddRow("key2", `"value2"`, "N", "etag2", nil). + RowError(1, errors.New("network timeout")) + + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + req := []state.GetRequest{ + {Key: "key1"}, + {Key: "key2"}, + {Key: "key3"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.NoError(t, err) + require.Len(t, res, 3) + + // Build a map for easier assertion regardless of ordering. + byKey := make(map[string]state.BulkGetResponse, len(res)) + for _, r := range res { + byKey[r.Key] = r + } + + // key1 was scanned successfully before the error. + successfulResult := byKey["key1"] + assert.Empty(t, successfulResult.Error) + assert.NotNil(t, successfulResult.Data) + + // key2 and key3 were never returned by the DB; they should carry the + // rows.Err() message as per-key errors. + for _, k := range []string{"key2", "key3"} { + r := byKey[k] + assert.Equal(t, k, r.Key) + assert.NotEmpty(t, r.Error, "unfound key %q should have an error from rows.Err()", k) + assert.Contains(t, r.Error, "rows iteration failed:") + assert.Contains(t, r.Error, "network timeout") + assert.Nil(t, r.Data, "error entry should have nil data") + } + + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestBulkGetRowsErrAfterAllRowsProcessed(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + db: db, + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + // Return the one requested row successfully, then trigger rows.Err(). + // This tests the warning-log path where all requested keys are found but + // rows.Err() still reports an error (e.g. a late network hiccup). + // + // sqlmock limitation: CloseError only sets the error returned by + // rows.Close(), NOT rows.Err(). To make rows.Err() return an error we + // must use RowError. RowError(N) requires at least N+1 rows to be added + // so that sqlmock actually attempts to advance to index N. We add a + // dummy second row and set RowError(1, ...) -- row 0 (key1) is read + // successfully, then the attempt to advance to row 1 fails, and + // rows.Err() reports the error. + rows := sqlmock.NewRows([]string{"key", "value", "binary_yn", "etag", "expiration_time"}). + AddRow("key1", `"value1"`, "N", "etag1", nil). + AddRow("dummy", `"dummy"`, "N", "dummy", nil). + RowError(1, errors.New("late network error")) + + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + req := []state.GetRequest{ + {Key: "key1"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.NoError(t, err) + // key1 was found before the error, so the warning-log branch is taken + // (no unfound keys to attach the error to). The result should still + // contain the successfully scanned key. + require.Len(t, res, 1) + assert.Equal(t, "key1", res[0].Key) + assert.Empty(t, res[0].Error) + + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestBulkGetWithExpiration(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + db: db, + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + expTime := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) + rows := sqlmock.NewRows([]string{"key", "value", "binary_yn", "etag", "expiration_time"}). + AddRow("key1", `"value1"`, "N", "etag1", expTime) + + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + req := []state.GetRequest{ + {Key: "key1"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.NoError(t, err) + require.Len(t, res, 1) + assert.Equal(t, "key1", res[0].Key) + assert.Empty(t, res[0].Error) + assert.NotNil(t, res[0].Metadata) + assert.Contains(t, res[0].Metadata, state.GetRespMetaKeyTTLExpireTime) + + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestBulkGetEmpty(t *testing.T) { + t.Parallel() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + res, err := o.BulkGet(t.Context(), []state.GetRequest{}) + require.NoError(t, err) + assert.Empty(t, res) +} + +func TestBulkGetMissingKeys(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + db: db, + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + // DB only returns key2; key1 and key3 are not in the database. + rows := sqlmock.NewRows([]string{"key", "value", "binary_yn", "etag", "expiration_time"}). + AddRow("key2", `"value2"`, "N", "etag2", nil) + + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + req := []state.GetRequest{ + {Key: "key1"}, + {Key: "key2"}, + {Key: "key3"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.NoError(t, err) + require.Len(t, res, 3) + + // Build a map for easier assertion regardless of ordering. + byKey := make(map[string]state.BulkGetResponse, len(res)) + for _, r := range res { + byKey[r.Key] = r + } + + // key2 was returned by the DB — should have data and etag. + r2 := byKey["key2"] + assert.Empty(t, r2.Error) + assert.NotNil(t, r2.Data) + assert.NotNil(t, r2.ETag) + + // key1 and key3 were not in the DB — should be empty entries with no error. + for _, k := range []string{"key1", "key3"} { + r := byKey[k] + assert.Equal(t, k, r.Key) + assert.Empty(t, r.Error, "missing key should not have an error") + assert.Nil(t, r.Data, "missing key should have nil data") + assert.Nil(t, r.ETag, "missing key should have nil etag") + assert.Nil(t, r.Metadata, "missing key should have nil metadata") + } + + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestBulkGetRowsScanFailure(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + db: db, + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + // To trigger a Scan error, pass a string for the expiration_time column. + // sql.NullTime.Scan cannot convert a plain string to time.Time, + // so rows.Scan() will return an error for that row. + rows := sqlmock.NewRows([]string{"key", "value", "binary_yn", "etag", "expiration_time"}). + AddRow("key1", `"value1"`, "N", "etag1", nil). // valid row + AddRow("key2", `"value2"`, "N", "etag2", "not-a-time-value"). // expiration_time is a string — Scan will fail + AddRow("key3", `"value3"`, "N", "etag3", nil) // valid row + + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + req := []state.GetRequest{ + {Key: "key1"}, + {Key: "key2"}, + {Key: "key3"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.NoError(t, err) + require.Len(t, res, 3) + + // key1 should succeed. + assert.Equal(t, "key1", res[0].Key) + assert.Empty(t, res[0].Error) + assert.NotNil(t, res[0].Data) + + // key2 should have a per-key scan error. + assert.NotEmpty(t, res[1].Error, "scan failure should produce a per-key error") + assert.Nil(t, res[1].Data, "scan failure entry should have nil data") + + // key3 should succeed despite key2's failure. + assert.Equal(t, "key3", res[2].Key) + assert.Empty(t, res[2].Error) + assert.NotNil(t, res[2].Data) + + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestBulkGetEmptyKeyValidation(t *testing.T) { + t.Parallel() + + o := &oracleDatabaseAccess{ + logger: logger.NewLogger("test"), + metadata: oracleDatabaseMetadata{TableName: "state"}, + } + + req := []state.GetRequest{ + {Key: "key1"}, + {Key: ""}, + {Key: "key3"}, + } + + res, err := o.BulkGet(t.Context(), req) + require.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, "missing key in bulk get operation", err.Error()) +} diff --git a/state/postgresql/v1/migrations.go b/state/postgresql/v1/migrations.go index 990c43413c..43bc0d8318 100644 --- a/state/postgresql/v1/migrations.go +++ b/state/postgresql/v1/migrations.go @@ -15,8 +15,10 @@ package postgresql import ( "context" + "database/sql" "errors" "fmt" + "strings" "github.com/jackc/pgerrcode" "github.com/jackc/pgx/v5/pgconn" @@ -79,8 +81,119 @@ func performMigrations(ctx context.Context, db pginterfaces.PGXPoolConn, opts po if err != nil { return fmt.Errorf("failed to update state table: %w", err) } + + return nil + }, + + // Migration 2: add row_id (identity), backfill deterministically, enforce uniqueness + func(ctx context.Context) error { + opts.Logger.Infof("Ensuring row_id (identity) exists on '%s'", opts.StateTableName) + + // Resolve schema+table and quoted FQ table name + var schema, table, fqtnQI, regName string + if err := db.QueryRow(ctx, ` + SELECT n.nspname, + c.relname, + format('%I.%I', n.nspname, c.relname) AS fqtn_quoted, + n.nspname || '.' || c.relname AS reg_name + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.oid = to_regclass($1) + `, opts.StateTableName).Scan(&schema, &table, &fqtnQI, ®Name); err != nil || schema == "" || table == "" { + if err == nil { + err = fmt.Errorf("table %q not found", opts.StateTableName) + } + return fmt.Errorf("resolve table OID: %w", err) + } + + // 1) Add column if missing + var hasCol bool + if err := db.QueryRow(ctx, ` + SELECT EXISTS ( + SELECT 1 + FROM pg_attribute + WHERE attrelid = to_regclass($1) + AND attname = 'row_id' + AND NOT attisdropped + )`, opts.StateTableName).Scan(&hasCol); err != nil { + return fmt.Errorf("introspect row_id: %w", err) + } + if !hasCol { + if _, err := db.Exec(ctx, `ALTER TABLE `+fqtnQI+` ADD COLUMN row_id BIGINT`); err != nil { + return fmt.Errorf("add row_id column: %w", err) + } + } + + // 2) Backfill NULLs deterministically (insertdate, key) + var nulls int64 + if err := db.QueryRow(ctx, `SELECT COUNT(*) FROM `+fqtnQI+` WHERE row_id IS NULL`).Scan(&nulls); err != nil { + return fmt.Errorf("count NULL row_id: %w", err) + } + if nulls > 0 { + if _, err := db.Exec(ctx, ` +WITH ranked AS ( + SELECT key, ROW_NUMBER() OVER (ORDER BY insertdate ASC, key ASC) AS rn + FROM `+fqtnQI+` + WHERE row_id IS NULL +) +UPDATE `+fqtnQI+` AS t +SET row_id = r.rn +FROM ranked r +WHERE r.key = t.key +`); err != nil { + return fmt.Errorf("backfill row_id: %w", err) + } + } + + // 3) NOT NULL + if _, err := db.Exec(ctx, `ALTER TABLE `+fqtnQI+` ALTER COLUMN row_id SET NOT NULL`); err != nil { + return fmt.Errorf("set NOT NULL: %w", err) + } + + // 4) Identity if not present + var isIdentity bool + if err := db.QueryRow(ctx, ` + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = $1 + AND table_name = $2 + AND column_name = 'row_id' + AND is_identity = 'YES' + )`, schema, table).Scan(&isIdentity); err != nil { + return fmt.Errorf("check identity: %w", err) + } + if !isIdentity { + if _, err := db.Exec(ctx, `ALTER TABLE `+fqtnQI+` ALTER COLUMN row_id ADD GENERATED BY DEFAULT AS IDENTITY`); err != nil { + return fmt.Errorf("add identity: %w", err) + } + } + + // 5) Align identity sequence to MAX(row_id)+1 + var seqName sql.NullString + if err := db.QueryRow(ctx, `SELECT pg_get_serial_sequence($1, 'row_id')`, regName).Scan(&seqName); err != nil { + return fmt.Errorf("get identity sequence: %w", err) + } + if seqName.Valid && seqName.String != "" { + if _, err := db.Exec(ctx, ` + SELECT setval($1, COALESCE((SELECT MAX(row_id) FROM `+fqtnQI+`), 0) + 1, false) + `, seqName.String); err != nil { + return fmt.Errorf("set identity sequence value: %w", err) + } + } + + // 6) Unique index on row_id — schema-qualified & quoted + idxNameQI := quoteIdent(table + "_row_id_uidx") + if _, err := db.Exec(ctx, + `CREATE UNIQUE INDEX IF NOT EXISTS `+idxNameQI+` ON `+fqtnQI+` (row_id)`); err != nil { + return fmt.Errorf("create unique index on row_id: %w", err) + } + return nil }, - }, - ) + }) +} + +func quoteIdent(s string) string { + return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` } diff --git a/state/postgresql/v1/postgresql_integration_test.go b/state/postgresql/v1/postgresql_integration_test.go index 79afb06ad9..dd09d3686e 100644 --- a/state/postgresql/v1/postgresql_integration_test.go +++ b/state/postgresql/v1/postgresql_integration_test.go @@ -141,6 +141,12 @@ func TestPostgreSQLIntegration(t *testing.T) { }) } +func Test_KeysLiker(t *testing.T) { + pg := NewPostgreSQLStateStore(logger.NewLogger("test")) + _, ok := pg.(state.KeysLiker) + require.True(t, ok) +} + // setGetUpdateDeleteOneItem validates setting one item, getting it, and deleting it. func setGetUpdateDeleteOneItem(t *testing.T, pgs *postgresql.PostgreSQL) { key := randomKey() diff --git a/state/postgresql/v2/metadata.go b/state/postgresql/v2/metadata.go index 2268b3c9a3..7249b777c9 100644 --- a/state/postgresql/v2/metadata.go +++ b/state/postgresql/v2/metadata.go @@ -17,8 +17,8 @@ import ( "errors" "time" - "github.com/dapr/components-contrib/common/authentication/aws" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" + "github.com/dapr/components-contrib/common/aws" "github.com/dapr/components-contrib/state" "github.com/dapr/kit/metadata" "github.com/dapr/kit/ptr" @@ -60,7 +60,7 @@ func (m *pgMetadata) InitWithMetadata(meta state.Metadata, opts pgauth.InitWithM return err } - // Validate and sanitize inputq + // Validate and sanitize input err = m.PostgresAuthMetadata.InitWithMetadata(meta.Properties, opts) if err != nil { return err diff --git a/state/postgresql/v2/postgresql.go b/state/postgresql/v2/postgresql.go index 2b80567834..10aceeaeb8 100644 --- a/state/postgresql/v2/postgresql.go +++ b/state/postgresql/v2/postgresql.go @@ -15,11 +15,13 @@ package postgresql import ( "context" + "database/sql" "encoding/json" "errors" "fmt" "reflect" "strconv" + "strings" "time" "github.com/google/uuid" @@ -28,8 +30,9 @@ import ( "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" - awsAuth "github.com/dapr/components-contrib/common/authentication/aws" pgauth "github.com/dapr/components-contrib/common/authentication/postgresql" + awsCommon "github.com/dapr/components-contrib/common/aws" + awsAuth "github.com/dapr/components-contrib/common/aws/auth" pginterfaces "github.com/dapr/components-contrib/common/component/postgresql/interfaces" pgtransactions "github.com/dapr/components-contrib/common/component/postgresql/transactions" sqlinternal "github.com/dapr/components-contrib/common/component/sql" @@ -52,8 +55,6 @@ type PostgreSQL struct { enableAzureAD bool enableAWSIAM bool - - awsAuthProvider awsAuth.Provider } type Options struct { @@ -107,13 +108,20 @@ func (p *PostgreSQL) Init(ctx context.Context, meta state.Metadata) (err error) return fmt.Errorf("failed to validate AWS IAM authentication fields: %w", validateErr) } - var provider awsAuth.Provider - provider, err = awsAuth.NewProvider(ctx, *opts, awsAuth.GetConfig(*opts)) - if err != nil { + authOpts := awsAuth.Options{ + Logger: p.logger, + Properties: meta.Properties, + Region: opts.Region, + AccessKey: opts.AccessKey, + SecretKey: opts.SecretKey, + SessionToken: opts.SessionToken, + AssumeRoleArn: opts.AssumeRoleArn, + AssumeRoleSessionName: opts.AssumeRoleSessionName, + Endpoint: opts.Endpoint, + } + if err = awsCommon.ConfigurePostgresIAM(ctx, config, authOpts); err != nil { return err } - p.awsAuthProvider = provider - p.awsAuthProvider.UpdatePostgres(ctx, config) } connCtx, connCancel := context.WithTimeout(ctx, p.metadata.Timeout) @@ -209,6 +217,113 @@ CREATE INDEX ON %[1]s (expires_at); } return nil }, + + // Migration 2: add row_id (identity), backfill deterministically, enforce uniqueness (schema-safe) + func(ctx context.Context) error { + p.logger.Infof("Ensuring row_id (identity) exists on '%s'", stateTable) + + // Resolve schema + table and a safely quoted FQ table name + var schema, table, fqtnQI, regName string + if err := p.db.QueryRow(ctx, ` + SELECT n.nspname, + c.relname, + format('%I.%I', n.nspname, c.relname) AS fqtn_quoted, + n.nspname || '.' || c.relname AS reg_name + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.oid = to_regclass($1) + `, stateTable).Scan(&schema, &table, &fqtnQI, ®Name); err != nil || schema == "" || table == "" { + if err == nil { + err = fmt.Errorf("table %q not found", stateTable) + } + return fmt.Errorf("resolve table OID: %w", err) + } + + // 1) Add column if missing + var hasCol bool + if err := p.db.QueryRow(ctx, ` + SELECT EXISTS ( + SELECT 1 + FROM pg_attribute + WHERE attrelid = to_regclass($1) + AND attname = 'row_id' + AND NOT attisdropped + )`, stateTable).Scan(&hasCol); err != nil { + return fmt.Errorf("introspect row_id: %w", err) + } + if !hasCol { + if _, err := p.db.Exec(ctx, `ALTER TABLE `+fqtnQI+` ADD COLUMN row_id BIGINT`); err != nil { + return fmt.Errorf("add row_id column: %w", err) + } + } + + // 2) Backfill NULLs deterministically (created_at, key) + var nulls int64 + if err := p.db.QueryRow(ctx, `SELECT COUNT(*) FROM `+fqtnQI+` WHERE row_id IS NULL`).Scan(&nulls); err != nil { + return fmt.Errorf("count NULL row_id: %w", err) + } + if nulls > 0 { + if _, err := p.db.Exec(ctx, ` +WITH ranked AS ( + SELECT key, ROW_NUMBER() OVER (ORDER BY created_at ASC, key ASC) AS rn + FROM `+fqtnQI+` + WHERE row_id IS NULL +) +UPDATE `+fqtnQI+` AS t +SET row_id = r.rn +FROM ranked r +WHERE r.key = t.key +`); err != nil { + return fmt.Errorf("backfill row_id: %w", err) + } + } + + // 3) Enforce NOT NULL + if _, err := p.db.Exec(ctx, `ALTER TABLE `+fqtnQI+` ALTER COLUMN row_id SET NOT NULL`); err != nil { + return fmt.Errorf("set NOT NULL: %w", err) + } + + // 4) Turn row_id into an identity column if not already + var isIdentity bool + if err := p.db.QueryRow(ctx, ` + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = $1 + AND table_name = $2 + AND column_name = 'row_id' + AND is_identity = 'YES' + )`, schema, table).Scan(&isIdentity); err != nil { + return fmt.Errorf("check identity: %w", err) + } + if !isIdentity { + if _, err := p.db.Exec(ctx, `ALTER TABLE `+fqtnQI+` ALTER COLUMN row_id ADD GENERATED BY DEFAULT AS IDENTITY`); err != nil { + return fmt.Errorf("add identity: %w", err) + } + } + + // 5) Align the identity sequence to MAX(row_id)+1 + var seqName sql.NullString + if err := p.db.QueryRow(ctx, `SELECT pg_get_serial_sequence($1, 'row_id')`, regName).Scan(&seqName); err != nil { + return fmt.Errorf("get identity sequence: %w", err) + } + if seqName.Valid && seqName.String != "" { + if _, err := p.db.Exec(ctx, ` + SELECT setval($1, COALESCE((SELECT MAX(row_id) FROM `+fqtnQI+`), 0) + 1, false) + `, seqName.String); err != nil { + return fmt.Errorf("set identity sequence value: %w", err) + } + } + + // 6) Unique index on row_id — schema-qualified and quoted + idxNameQI := quoteIdent(table + "_row_id_uidx") + if _, err := p.db.Exec(ctx, + `CREATE UNIQUE INDEX IF NOT EXISTS `+idxNameQI+` ON `+fqtnQI+` (row_id)`); err != nil { + return fmt.Errorf("create unique index on row_id: %w", err) + } + + return nil + }, }) } @@ -218,6 +333,7 @@ func (p *PostgreSQL) Features() []state.Feature { state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, + state.FeatureKeysLike, } } @@ -545,22 +661,17 @@ func (p *PostgreSQL) CleanupExpired() error { return nil } -// Close implements io.Close. +// Close implements io.Closer. func (p *PostgreSQL) Close() error { if p.db != nil { p.db.Close() p.db = nil } - errs := make([]error, 2) if p.gc != nil { - errs[0] = p.gc.Close() + return p.gc.Close() } - - if p.awsAuthProvider != nil { - errs[1] = p.awsAuthProvider.Close() - } - return errors.Join(errs...) + return nil } // GetCleanupInterval returns the cleanupInterval property. @@ -574,3 +685,90 @@ func (p *PostgreSQL) GetComponentMetadata() (metadataInfo metadata.MetadataMap) metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) return } + +func (p *PostgreSQL) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + // 1) Validate pattern + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + where := []string{ + "key LIKE $1", + "(expires_at IS NULL OR expires_at > now())", + } + args := []any{req.Pattern} + + // 2) Continue strictly AFTER the last returned row_id of prev page + if req.ContinuationToken != nil && *req.ContinuationToken != "" { + rid, err := strconv.ParseInt(*req.ContinuationToken, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid continue token: %w", err) + } + where = append(where, fmt.Sprintf("row_id > $%d", len(args)+1)) + args = append(args, rid) + } + + orderClause := " ORDER BY row_id ASC" + + limitClause := "" + var pageSize uint32 + if req.PageSize != nil && *req.PageSize > 0 { + pageSize = *req.PageSize + // fetch one extra to detect "has next" + limitClause = fmt.Sprintf(" LIMIT $%d", len(args)+1) + args = append(args, pageSize+1) + } + + query := fmt.Sprintf(` + SELECT key, row_id + FROM %s + WHERE %s%s%s + `, p.metadata.TableName(pgTableState), strings.Join(where, " AND "), orderClause, limitClause) + + rows, err := p.db.Query(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + type rec struct { + key string + rowID int64 + } + recs := make([]rec, 0, 256) + + for rows.Next() { + var k string + var rid int64 + if err := rows.Scan(&k, &rid); err != nil { + return nil, err + } + recs = append(recs, rec{key: k, rowID: rid}) + } + if err := rows.Err(); err != nil { + return nil, err + } + + resp := &state.KeysLikeResponse{ + Keys: make([]string, 0, len(recs)), + } + + // 3) If we over-fetched, token must be LAST returned record (index pageSize-1) + //nolint:gosec + if pageSize > 0 && uint32(len(recs)) > pageSize { + lastReturned := recs[pageSize-1] + tok := strconv.FormatInt(lastReturned.rowID, 10) + resp.ContinuationToken = &tok + recs = recs[:pageSize] + } + + for _, r := range recs { + resp.Keys = append(resp.Keys, r.key) + } + + return resp, nil +} + +func quoteIdent(s string) string { + return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` +} diff --git a/state/postgresql/v2/postgresql_integration_test.go b/state/postgresql/v2/postgresql_integration_test.go index 251486ca71..ccddc60042 100644 --- a/state/postgresql/v2/postgresql_integration_test.go +++ b/state/postgresql/v2/postgresql_integration_test.go @@ -734,6 +734,13 @@ func TestMultiOperationOrder(t *testing.T) { require.NoError(t, err) } +func Test_KeysLiker(t *testing.T) { + m, _ := mockDatabase(t) + t.Cleanup(m.db.Close) + + var _ state.KeysLiker = m.pg +} + func createSetRequest() state.SetRequest { return state.SetRequest{ Key: randomKey(), diff --git a/state/ravendb/metadata.yaml b/state/ravendb/metadata.yaml new file mode 100644 index 0000000000..4dfcfa0e38 --- /dev/null +++ b/state/ravendb/metadata.yaml @@ -0,0 +1,51 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: state +name: ravendb +version: v1 +status: alpha +title: "RavenDB" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-ravendb/ +capabilities: + - crud +authenticationProfiles: + - title: "No authentication" + description: | + No authentication. Connect via connection string + metadata: + - name : connectionString + required: true + description: | + Connection string to ravenDB cluster + example: '"http://live-test.ravendb.net"' +metadata: + - name: DatabaseName + description: | + The name of the database to use. + default: '"daprStore"' + example: '"daprStore"' + - name: ServerURL + description: | + Url to ravendb cluster + default: '"127.0.0.1"' + example: '"http://live-test.ravendb.net"' + - name: CertPath + description: | + Path to the certificate for secure connection + example: "/path/to/cert" + - name: KeyPath + description: | + Path to the key for secure connection + example: "/path/to/key" + - name: EnableTTL + description: | + Boolean value that enables or disables RaveDB TTL functionality + example: "true" + default: "true" + - name: TTLFrequency + description: | + Sets RavenDB frequency on running the background expiration task and deleting records + example: "15" + default: "60" \ No newline at end of file diff --git a/state/ravendb/ravendb.go b/state/ravendb/ravendb.go new file mode 100644 index 0000000000..0b5368e955 --- /dev/null +++ b/state/ravendb/ravendb.go @@ -0,0 +1,520 @@ +/* +Copyright 2021 The Dapr Authors +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 ravendb is an implementation of StateStore interface to perform operations on store + +package ravendb + +import ( + "context" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "net/http" + "reflect" + "strings" + "time" + + jsoniterator "github.com/json-iterator/go" + ravendb "github.com/ravendb/ravendb-go-client" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" + stateutils "github.com/dapr/components-contrib/state/utils" + "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" +) + +const ( + defaultDatabaseName = "daprStore" + databaseName = "databaseName" + serverURL = "serverUrl" + httpsPrefix = "https" + certPath = "certPath" + keyPath = "keyPath" + enableTTL = "enableTTL" + ttlFrequency = "ttlFrequency" + changeVector = "@change-vector" + expires = "@expires" + defaultEnableTTL = true + defaultTTLFrequency = int64(60) +) + +type RavenDB struct { + state.BulkStore + + documentStore *ravendb.DocumentStore + metadata RavenDBMetadata + + features []state.Feature + logger logger.Logger +} + +type RavenDBMetadata struct { + DatabaseName string + ServerURL string + CertPath string + KeyPath string + EnableTTL bool + TTLFrequency int64 +} + +type Item struct { + ID string + Value string + Etag string + TTL *time.Time +} + +func NewRavenDB(logger logger.Logger) state.Store { + store := &RavenDB{ + features: []state.Feature{ + state.FeatureETag, + state.FeatureTransactional, + state.FeatureTTL, + }, + logger: logger, + } + store.BulkStore = state.NewDefaultBulkStore(store) + return store +} + +func (r *RavenDB) Init(ctx context.Context, metadata state.Metadata) (err error) { + r.metadata, err = getRavenDBMetaData(metadata) + if err != nil { + return err + } + // TODO: Operation timeout? + err = r.setRavenDBStore(ctx) + if err != nil { + return errors.New("error in creating Raven DB Store") + } + + r.initTTL() + r.setupDatabase() + + return nil +} + +// Features returns the features available in this state store. +func (r *RavenDB) Features() []state.Feature { + return r.features +} + +func (r *RavenDB) Delete(ctx context.Context, req *state.DeleteRequest) error { + session, err := r.documentStore.OpenSession("") + if err != nil { + return errors.New("error opening session while deleting") + } + defer session.Close() + + err = r.deleteInternal(ctx, req, session, false) + if err != nil { + return err + } + + err = session.SaveChanges() + if err != nil { + if _, ok := err.(*ravendb.ConcurrencyError); ok { + return state.NewETagError(state.ETagMismatch, err) + } + return errors.New("error saving changes") + } + + return nil +} + +func (r *RavenDB) Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) { + session, err := r.documentStore.OpenSession(r.metadata.DatabaseName) + if err != nil { + return &state.GetResponse{}, fmt.Errorf("opening session while storing data failed with error %s", err) + } + defer session.Close() + + var item *Item + err = session.Load(&item, req.Key) + if err != nil { + return &state.GetResponse{}, fmt.Errorf("error loading data %s", err) + } + if item == nil { + return &state.GetResponse{}, nil + } + ravenMeta, err := session.GetMetadataFor(item) + if err != nil { + return &state.GetResponse{}, fmt.Errorf("error getting metadata for %s", req.Key) + } + + meta := make(map[string]string) + ttl, okTTL := ravenMeta.Get(expires) + if okTTL { + meta = map[string]string{ + state.GetRespMetaKeyTTLExpireTime: ttl.(string), + } + } + + var etagResp string + eTag, okETag := ravenMeta.Get(changeVector) + if okETag { + etagResp = eTag.(string) + } else { + etagResp = "" + } + + resp := &state.GetResponse{ + Data: []byte(item.Value), + ETag: &etagResp, + Metadata: meta, + } + return resp, nil +} + +func (r *RavenDB) Set(ctx context.Context, req *state.SetRequest) error { + session, err := r.documentStore.OpenSession(r.metadata.DatabaseName) + if err != nil { + return fmt.Errorf("opening session while storing data failed with error %s", err) + } + defer session.Close() + err = r.setInternal(ctx, req, session) + if err != nil { + return fmt.Errorf("error processing item %s", err) + } + + err = session.SaveChanges() + if err != nil { + if _, ok := err.(*ravendb.ConcurrencyError); ok { + return state.NewETagError(state.ETagMismatch, err) + } + return fmt.Errorf("error saving changes %s", err) + } + return nil +} + +func (r *RavenDB) Ping(ctx context.Context) error { + session, err := r.documentStore.OpenSession("") + if err != nil { + return fmt.Errorf("pinging database failed with error %s", err) + } + defer session.Close() + + return nil +} + +func (r *RavenDB) Multi(ctx context.Context, request *state.TransactionalStateRequest) error { + session, err := r.documentStore.OpenSession(r.metadata.DatabaseName) + if err != nil { + return fmt.Errorf("opening session while sending transaction failed with error %s", err) + } + defer session.Close() + for _, o := range request.Operations { + switch req := o.(type) { + case state.SetRequest: + err = r.setInternal(ctx, &req, session) + case state.DeleteRequest: + err = r.deleteInternal(ctx, &req, session, true) + } + + if err != nil { + return fmt.Errorf("error parsing requests: %w", err) + } + } + + err = session.SaveChanges() + if err != nil { + if _, ok := err.(*ravendb.ConcurrencyError); ok { + return state.NewETagError(state.ETagMismatch, err) + } + return fmt.Errorf("error during transaction, aborting the transaction: %w", err) + } + + return nil +} + +func (r *RavenDB) BulkGet(ctx context.Context, req []state.GetRequest, _ state.BulkGetOpts) ([]state.BulkGetResponse, error) { + // If nothing is being requested, short-circuit + if len(req) == 0 { + return nil, nil + } + keys := make([]string, len(req)) + for i, r := range req { + keys[i] = r.Key + } + session, err := r.documentStore.OpenSession(r.metadata.DatabaseName) + if err != nil { + return []state.BulkGetResponse{}, fmt.Errorf("opening session while getting bulk data failed with error %s", err) + } + defer session.Close() + + items := make(map[string]*Item, len(keys)) + err = session.LoadMulti(items, keys) + if err != nil { + return []state.BulkGetResponse{}, fmt.Errorf("failed bulk get with error: %s", err) + } + + resp := make([]state.BulkGetResponse, 0, len(items)) + + for ID, current := range items { + if current == nil { + convert := state.BulkGetResponse{ + Key: ID, + Data: nil, + ETag: nil, + Metadata: make(map[string]string), + } + resp = append(resp, convert) + } else { + meta := make(map[string]string) + ravenMeta, err := session.GetMetadataFor(current) + etagResp := "" + if err == nil { + if eTag, okETag := ravenMeta.Get(changeVector); okETag { + etagResp = eTag.(string) + } + if ttl, okTTL := ravenMeta.Get(expires); okTTL { + meta[state.GetRespMetaKeyTTLExpireTime] = ttl.(string) + } + } + + convert := state.BulkGetResponse{ + Key: current.ID, + Data: []byte(current.Value), + ETag: &etagResp, + Metadata: meta, + } + resp = append(resp, convert) + } + } + + return resp, nil +} + +func (r *RavenDB) marshalToString(v interface{}) (string, error) { + if buf, ok := v.([]byte); ok { + return string(buf), nil + } + + return jsoniterator.ConfigFastest.MarshalToString(v) +} + +func (r *RavenDB) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + metadataStruct := RavenDBMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) + return +} + +func (r *RavenDB) setInternal(ctx context.Context, req *state.SetRequest, session *ravendb.DocumentSession) error { + data, err := r.marshalToString(req.Value) + if err != nil { + return fmt.Errorf("ravendb error: failed to marshal value for key %s: %w", req.Key, err) + } + + item := &Item{ + ID: req.Key, + Value: data, + } + + if req.Options.Concurrency == state.FirstWrite { + // First write wins, we send empty change vector to check if exists + + // current version of the go ravendb SDK doesn't let us check concurrency violation on items that are not in the database. + // we need to try to load, and do regullar save if item is not in DB (real first save) + // if we have item in DB we can try to override it with concurency check + var newItem *Item + err = session.Load(&newItem, req.Key) + if newItem == nil { + err = session.Store(item) + } else { + var eTag string + if req.HasETag() { + eTag = *req.ETag + } else { + eTag = RandStringRunes(5) + } + + if newItem.Value == item.Value { + return fmt.Errorf("error storing data: %s", err) + } + newItem.Value = item.Value + err = session.StoreWithChangeVectorAndID(newItem, eTag, req.Key) + } + if err != nil { + return fmt.Errorf("error storing data: %s", err) + } + } else { + // Last write wins + if req.HasETag() { + eTag := *req.ETag + err = session.StoreWithChangeVectorAndID(item, eTag, req.Key) + if err != nil { + return state.NewETagError(state.ETagMismatch, err) + } + } else { + err = session.Store(item) + } + + if err != nil { + return fmt.Errorf("error storing data: %s", err) + } + } + + reqTTL, err := stateutils.ParseTTL(req.Metadata) + if err != nil { + return fmt.Errorf("failed to parse TTL: %w", err) + } + + if reqTTL != nil { + metaData, err := session.Advanced().GetMetadataFor(item) + if err != nil { + return errors.New("failed to get metadata for item") + } + expiry := time.Now().Add(time.Second * time.Duration(*reqTTL)).UTC() + iso8601String := expiry.Format("2006-01-02T15:04:05.9999999Z07:00") + metaData.Put(expires, iso8601String) + } + return nil +} + +func (r *RavenDB) deleteInternal(ctx context.Context, req *state.DeleteRequest, session *ravendb.DocumentSession, fromTransaction bool) error { + var err error + if fromTransaction { + var itemToDelete *Item + err = session.Load(&itemToDelete, req.Key) + if err == nil && itemToDelete != nil { + err = session.Delete(itemToDelete) + } + } else { + if req.HasETag() { + err = session.DeleteByID(req.Key, *req.ETag) + } else { + // TODO: Fix after update to ravendb sdk + err = session.DeleteByID(req.Key, "") + } + } + + if err != nil { + return err + } + + return nil +} + +func (r *RavenDB) setRavenDBStore(ctx context.Context) error { + serverNodes := []string{r.metadata.ServerURL} + store := ravendb.NewDocumentStore(serverNodes, r.metadata.DatabaseName) + if strings.HasPrefix(r.metadata.ServerURL, httpsPrefix) { + cer, err := tls.LoadX509KeyPair(r.metadata.CertPath, r.metadata.KeyPath) + if err != nil { + return err + } + store.Certificate = &cer + x509cert, err := x509.ParseCertificate(cer.Certificate[0]) + if err != nil { + return err + } + store.TrustStore = x509cert + if store.TrustStore == nil { + panic("nil trust store") + } + } + + if err := store.Initialize(); err != nil { + return err + } + r.documentStore = store + + return nil +} + +func (r *RavenDB) Close() error { + if r.documentStore == nil { + return nil + } + + r.documentStore.Close() + return nil +} + +func (r *RavenDB) initTTL() { + configurationExpiration := ravendb.ExpirationConfiguration{ + Disabled: !r.metadata.EnableTTL, + DeleteFrequencyInSec: &r.metadata.TTLFrequency, + } + operation, err := ravendb.NewConfigureExpirationOperationWithConfiguration(&configurationExpiration) + if err != nil { + return + } + err = r.documentStore.Maintenance().Send(operation) + if err != nil { + r.logger.Debug(err) + } +} + +func (r *RavenDB) setupDatabase() { + operation := ravendb.NewGetDatabaseRecordOperation(r.metadata.DatabaseName) + err := r.documentStore.Maintenance().Server().Send(operation) + if err == nil { + if operation.Command != nil && operation.Command.RavenCommandBase.StatusCode == http.StatusNotFound { + databaseRecord := ravendb.DatabaseRecord{ + DatabaseName: r.metadata.DatabaseName, + Disabled: false, + } + createOp := ravendb.NewCreateDatabaseOperation(&databaseRecord, 1) + err = r.documentStore.Maintenance().Server().Send(createOp) + if err != nil { + return + } + } + } +} + +func getRavenDBMetaData(meta state.Metadata) (RavenDBMetadata, error) { + m := RavenDBMetadata{ + DatabaseName: defaultDatabaseName, + EnableTTL: defaultEnableTTL, + TTLFrequency: defaultTTLFrequency, + } + + err := kitmd.DecodeMetadata(meta.Properties, &m) + if err != nil { + return m, err + } + + if m.ServerURL == "" { + return m, errors.New("server url is required") + } + + if strings.HasPrefix(m.ServerURL, httpsPrefix) { + if m.CertPath == "" || m.KeyPath == "" { + return m, errors.New("certificate and key are required for secure connection") + } + } + + return m, nil +} + +func RandStringRunes(n int) string { + // Create a byte slice to hold the random bytes + bytes := make([]byte, n) + + // Fill the byte slice with random bytes + _, err := rand.Read(bytes) + if err != nil { + return "" + } + + // Encode the random bytes to a base64 string + // This will make it printable/usable as a string + return base64.URLEncoding.EncodeToString(bytes)[:n] +} diff --git a/state/ravendb/ravendb_test.go b/state/ravendb/ravendb_test.go new file mode 100644 index 0000000000..d097865b19 --- /dev/null +++ b/state/ravendb/ravendb_test.go @@ -0,0 +1,129 @@ +package ravendb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" +) + +func TestGetRavenDBMetadata(t *testing.T) { + t.Run("With default database name", func(t *testing.T) { + properties := map[string]string{ + serverURL: "127.0.0.1", + } + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + metadata, err := getRavenDBMetaData(m) + require.NoError(t, err) + assert.Equal(t, properties[serverURL], metadata.ServerURL) + assert.Equal(t, defaultDatabaseName, metadata.DatabaseName) + assert.Equal(t, defaultEnableTTL, metadata.EnableTTL) + assert.Equal(t, defaultTTLFrequency, metadata.TTLFrequency) + }) + + t.Run("With custom database name", func(t *testing.T) { + properties := map[string]string{ + serverURL: "127.0.0.1", + databaseName: "TestDB", + } + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + metadata, err := getRavenDBMetaData(m) + require.NoError(t, err) + assert.Equal(t, properties[serverURL], metadata.ServerURL) + assert.Equal(t, properties[databaseName], metadata.DatabaseName) + assert.Equal(t, defaultEnableTTL, metadata.EnableTTL) + assert.Equal(t, defaultTTLFrequency, metadata.TTLFrequency) + }) + + t.Run("With custom enable ttl values", func(t *testing.T) { + properties := map[string]string{ + serverURL: "127.0.0.1", + databaseName: "TestDB", + enableTTL: "false", + ttlFrequency: "15", + } + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + metadata, err := getRavenDBMetaData(m) + require.NoError(t, err) + assert.Equal(t, properties[serverURL], metadata.ServerURL) + assert.Equal(t, properties[databaseName], metadata.DatabaseName) + assert.False(t, metadata.EnableTTL) + assert.Equal(t, int64(15), metadata.TTLFrequency) + }) + + t.Run("with https without cert and key", func(t *testing.T) { + properties := map[string]string{ + serverURL: "https://test.live.ravendb.com", + databaseName: "TestDB", + } + + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + _, err := getRavenDBMetaData(m) + require.Errorf(t, err, "certificate and key are required for secure connection") + }) + + t.Run("with https without key", func(t *testing.T) { + properties := map[string]string{ + serverURL: "https://test.live.ravendb.com", + databaseName: "TestDB", + certPath: "/path/to/cert", + } + + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + _, err := getRavenDBMetaData(m) + require.Errorf(t, err, "certificate and key are required for secure connection") + }) + + t.Run("with https without cert", func(t *testing.T) { + properties := map[string]string{ + serverURL: "https://test.live.ravendb.com", + databaseName: "TestDB", + keyPath: "/path/to/key", + } + + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + _, err := getRavenDBMetaData(m) + require.Errorf(t, err, "certificate and key are required for secure connection") + }) + + t.Run("with https", func(t *testing.T) { + properties := map[string]string{ + serverURL: "https://test.live.ravendb.com", + databaseName: "TestDB", + certPath: "/path/to/cert", + keyPath: "/path/to/key", + } + + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + metadata, err := getRavenDBMetaData(m) + require.NoError(t, err) + assert.Equal(t, properties[serverURL], metadata.ServerURL) + assert.Equal(t, properties[databaseName], metadata.DatabaseName) + assert.Equal(t, properties[certPath], metadata.CertPath) + assert.Equal(t, properties[keyPath], metadata.KeyPath) + }) +} diff --git a/state/redis/metadata.yaml b/state/redis/metadata.yaml index cab536678f..5dc0fd3b34 100644 --- a/state/redis/metadata.yaml +++ b/state/redis/metadata.yaml @@ -63,9 +63,23 @@ authenticationProfiles: metadata: - name: redisHost required: true - description: Connection-string for the redis host + description: | + Connection-string for the redis host. If "redisType" is "cluster" it + can be multiple hosts separated by commas or just a single host. + The port can be included in the host string (e.g. "host:6379") or + provided separately via "redisPort". example: "redis-master.default.svc.cluster.local:6379" type: string + - name: redisPort + required: false + description: | + The Redis port. Optional: if "redisHost" already contains a port, this + field must either match or be omitted. When "redisHost" does not include + a port and this field is not set, the default Redis port 6379 is used. + In cluster mode, this port is applied to every host in the + comma-separated list. + example: "6379" + type: string - name: enableTLS required: false description: If the Redis instance supports TLS with public certificates, can be configured to be enabled or disabled. Defaults to false. diff --git a/state/redis/redis.go b/state/redis/redis.go index 79249a81ea..3a1ca2799f 100644 --- a/state/redis/redis.go +++ b/state/redis/redis.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "reflect" + "sort" "strconv" "strings" "sync/atomic" @@ -161,9 +162,9 @@ func (r *StateStore) Init(ctx context.Context, metadata state.Metadata) error { // Features returns the features available in this state store. func (r *StateStore) Features() []state.Feature { if r.clientHasJSON { - return []state.Feature{state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, state.FeatureQueryAPI} + return []state.Feature{state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, state.FeatureQueryAPI, state.FeatureKeysLike} } else { - return []state.Feature{state.FeatureETag, state.FeatureTransactional, state.FeatureTTL} + return []state.Feature{state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, state.FeatureKeysLike} } } @@ -222,6 +223,9 @@ func (r *StateStore) Delete(ctx context.Context, req *state.DeleteRequest) error func (r *StateStore) directGet(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) { res, err := r.client.DoRead(ctx, "GET", req.Key) if err != nil { + if r.client.IsNilValueError(err) { + return &state.GetResponse{}, nil + } return nil, err } @@ -239,6 +243,9 @@ func (r *StateStore) directGet(ctx context.Context, req *state.GetRequest) (*sta func (r *StateStore) getDefault(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) { res, err := r.client.DoRead(ctx, "HGETALL", req.Key) // Prefer values with ETags if err != nil { + if r.client.IsNilValueError(err) { + return &state.GetResponse{}, nil + } return r.directGet(ctx, req) // Falls back to original get for backward compats. } if res == nil { @@ -272,6 +279,9 @@ func (r *StateStore) getDefault(ctx context.Context, req *state.GetRequest) (*st func (r *StateStore) getJSON(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) { res, err := r.client.DoRead(ctx, "JSON.GET", req.Key) if err != nil { + if r.client.IsNilValueError(err) { + return &state.GetResponse{}, nil + } return nil, err } @@ -465,19 +475,43 @@ func (r *StateStore) registerSchemas(ctx context.Context) error { return nil } -func (r *StateStore) getKeyVersion(vals []interface{}) (data string, version *string, err error) { - seenData := false - seenVersion := false - for i := 0; i < len(vals); i += 2 { - field, _ := strconv.Unquote(fmt.Sprintf("%q", vals[i])) - switch field { - case "data": - data, _ = strconv.Unquote(fmt.Sprintf("%q", vals[i+1])) - seenData = true - case "version": - versionVal, _ := strconv.Unquote(fmt.Sprintf("%q", vals[i+1])) - version = ptr.Of(versionVal) - seenVersion = true +func (r *StateStore) getKeyVersion(vals []any) (data string, version *string, err error) { + var seenData, seenVersion bool + + // step by 2: key, value. we only expect string or byte slice + for i := 0; i+1 < len(vals); i += 2 { + switch key := vals[i].(type) { + case string: + switch key { + case "data": + if s, ok := toString(vals[i+1]); ok { + data = s + seenData = true + } + case "version": + if s, ok := toString(vals[i+1]); ok { + version = &s + seenVersion = true + } + } + case []byte: + switch string(key) { + case "data": + if s, ok := toString(vals[i+1]); ok { + data = s + seenData = true + } + case "version": + if s, ok := toString(vals[i+1]); ok { + version = &s + seenVersion = true + } + } + } + + // Early exit once both values have been found + if seenData && seenVersion { + break } } if !seenData || !seenVersion { @@ -487,6 +521,17 @@ func (r *StateStore) getKeyVersion(vals []interface{}) (data string, version *st return data, version, nil } +func toString(v any) (string, bool) { + switch x := v.(type) { + case string: + return x, true + case []byte: + return string(x), true // some allocation here unless we go to unsafe: return unsafe.String(unsafe.SliceData(x), len(x)), true + default: + return "", false + } +} + func (r *StateStore) parseETag(req *state.SetRequest) (int, error) { if req.Options.Concurrency == state.LastWrite || req.ETag == nil || *req.ETag == "" { return 0, nil @@ -552,3 +597,141 @@ func (r *StateStore) GetComponentMetadata() (metadataInfo daprmetadata.MetadataM daprmetadata.GetMetadataInfoFromStructType(reflect.TypeOf(settingsStruct), &metadataInfo, daprmetadata.StateStoreType) return } + +func (r *StateStore) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + glob, err := likeToRedisGlob(req.Pattern) + if err != nil { + return nil, fmt.Errorf("invalid pattern: %w", err) + } + + // --- 1) Gather ALL matching keys (finish the SCAN) --- + cursor := "0" + keys := make([]string, 0, 256) + + for { + res, err := r.client.DoRead(ctx, "SCAN", cursor, "MATCH", glob, "COUNT", 1000) + if err != nil { + return nil, fmt.Errorf("redis SCAN failed: %w", err) + } + if res == nil { + break + } + + arr, ok := res.([]any) + if !ok || len(arr) != 2 { + return nil, errors.New("unexpected SCAN response") + } + + // next cursor + if s, ok := toString(arr[0]); ok { + cursor = s + } else { + return nil, errors.New("unexpected SCAN cursor type") + } + + // keys + switch ks := arr[1].(type) { + case []any: + for _, v := range ks { + if s, ok := toString(v); ok { + keys = append(keys, s) + } + } + case []string: + keys = append(keys, ks...) + default: + if s, ok := toString(arr[1]); ok && s != "" { + keys = append(keys, s) + } + } + + if cursor == "0" { + break + } + } + + // --- 2) Stable deterministic order --- + sort.Strings(keys) + + // --- 3) Offset-based pagination --- + var pageSize uint32 + if req.PageSize != nil && *req.PageSize > 0 { + pageSize = *req.PageSize + } + + start := 0 + if req.ContinuationToken != nil && *req.ContinuationToken != "" { + if off, err := strconv.Atoi(*req.ContinuationToken); err == nil && off >= 0 { + start = off + } + } + + if start > len(keys) { + start = len(keys) + } + end := len(keys) + if pageSize > 0 { + //nolint:gosec + if rem := len(keys) - start; rem > 0 && uint32(rem) > pageSize { + //nolint:gosec + end = start + int(pageSize) + } + } + + page := keys[start:end] + + var cont *string + if end < len(keys) { + next := strconv.Itoa(end) + cont = &next + } + + return &state.KeysLikeResponse{ + Keys: page, + ContinuationToken: cont, + }, nil +} + +func likeToRedisGlob(pat string) (string, error) { + var b strings.Builder + b.Grow(len(pat)) + + escaped := false + for _, r := range pat { + if escaped { + switch r { + case '%', '_', '\\', '*', '?', '[', ']': + b.WriteByte('\\') + b.WriteRune(r) + default: + if r == '*' || r == '?' || r == '[' { + b.WriteByte('\\') + } + b.WriteRune(r) + } + escaped = false + continue + } + switch r { + case '\\': + escaped = true + case '%': + b.WriteByte('*') + case '_': + b.WriteByte('?') + case '*', '?', '[': + b.WriteByte('\\') + b.WriteRune(r) + default: + b.WriteRune(r) + } + } + if escaped { + b.WriteString(`\\`) + } + return b.String(), nil +} diff --git a/state/redis/redis_test.go b/state/redis/redis_test.go index 088af2e2a3..6708a78835 100644 --- a/state/redis/redis_test.go +++ b/state/redis/redis_test.go @@ -494,6 +494,24 @@ func TestTransactionalDeleteNoEtag(t *testing.T) { assert.Empty(t, vals) } +// TestGetMissingKeyReturnsEmptyNotError ensures that when a key does not exist +// we don't propagate 'redis: nil' as an error to users +func TestGetMissingKeyReturnsEmptyNotError(t *testing.T) { + s, c := setupMiniredis() + defer s.Close() + + ss := &StateStore{ + client: c, + json: jsoniter.ConfigFastest, + logger: logger.NewLogger("test"), + } + + resp, err := ss.Get(t.Context(), &state.GetRequest{Key: "nonexistent-key"}) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Empty(t, resp.Data, "missing key should return empty data") +} + func TestGetMetadata(t *testing.T) { s, c := setupMiniredis() defer s.Close() @@ -521,3 +539,49 @@ func setupMiniredis() (*miniredis.Miniredis, rediscomponent.RedisClient) { return s, rediscomponent.ClientFromV8Client(redis.NewClient(opts)) } + +func TestToString(t *testing.T) { + // happy paths + if s, ok := toString("abc"); assert.True(t, ok) { + assert.Equal(t, "abc", s) + } + if s, ok := toString([]byte("def")); assert.True(t, ok) { + assert.Equal(t, "def", s) + } + // unsupported + _, ok := toString(123) + assert.False(t, ok) +} + +func BenchmarkGetKeyVersion(b *testing.B) { + /* + On a Mac M1 Pro: + BenchmarkGetKeyVersion-10 13651144 83.84 ns/op 64 B/op 6 allocs/op + + // old getkeyversion method + BenchmarkGetKeyVersionOld-10 1631097 729.1 ns/op 96 B/op 10 allocs/op + + // ~8x speed - ~1/2 allocations + + // unsafe comparison + BenchmarkGetKeyVersion-10 28636363 41.53 ns/op 32 B/op 2 allocs/op + */ + store := newStateStore(logger.NewLogger("bench")) + input1 := []any{[]byte("data"), []byte("payload"), []byte("version"), []byte("42")} + input2 := []any{[]byte("data"), []byte("payload2"), []byte("version"), []byte("43")} + b.ReportAllocs() + for range b.N { + if _, _, err := store.getKeyVersion(input1); err != nil { + b.Fatal(err) + } + if _, _, err := store.getKeyVersion(input2); err != nil { + b.Fatal(err) + } + } +} + +func Test_KeyList(t *testing.T) { + s := NewRedisStateStore(logger.NewLogger("test")) + _, ok := s.(state.KeysLiker) + require.True(t, ok) +} diff --git a/state/requests.go b/state/requests.go index af2c566a6f..5c2416e98a 100644 --- a/state/requests.go +++ b/state/requests.go @@ -161,3 +161,16 @@ type QueryRequest struct { Query query.Query `json:"query"` Metadata map[string]string `json:"metadata,omitempty"` } + +type KeysLikeRequest struct { + // Pattern is the SQL LIKE pattern to match keys against. + Pattern string `json:"pattern"` + + // ContinuationToken is an optional parameter to indicate the key from which + // to start the search. + ContinuationToken *string `json:"startKey,omitempty"` + + // PageSize is an optional parameter to indicate the maximum number of keys + // to return. + PageSize *uint32 `json:"pageSize,omitempty"` +} diff --git a/state/responses.go b/state/responses.go index 2cb7564564..0c240efcf7 100644 --- a/state/responses.go +++ b/state/responses.go @@ -57,3 +57,13 @@ type QueryItem struct { type DeleteWithPrefixResponse struct { Count int64 `json:"count"` // count of items removed } + +// KeysLikeResponse is the response object for getting keys like a pattern. +type KeysLikeResponse struct { + Keys []string `json:"keys"` + + // ContinuationToken is an optional token which can be used to continue the + // search of keys. Usually only present if a `PageSize` was set on the + // request. + ContinuationToken *string +} diff --git a/state/rethinkdb/metadata.yaml b/state/rethinkdb/metadata.yaml index e12181cf5b..d1a9ed7dcd 100644 --- a/state/rethinkdb/metadata.yaml +++ b/state/rethinkdb/metadata.yaml @@ -45,8 +45,8 @@ authenticationProfiles: type: bool required: false description: Whether to enable TLS encryption. - example: false - default: false + example: "false" + default: "false" - name: clientCert type: string required: true @@ -69,8 +69,8 @@ metadata: type: bool required: false description: Whether to archive changes to a separate table. - example: false - default: false + example: "false" + default: "false" - name: timeout type: string required: false @@ -80,13 +80,13 @@ metadata: type: bool required: false description: Whether to use json.Number instead of float64. - example: false - default: false + example: "false" + default: "false" - name: numRetries type: number required: false description: Number of times to retry queries on connection errors. - example: 3 + example: "3" - name: hostDecayDuration type: string required: false @@ -97,8 +97,8 @@ metadata: type: bool required: false description: Whether to enable opentracing for queries. - example: false - default: false + example: "false" + default: "false" - name: writeTimeout type: string required: false @@ -113,7 +113,7 @@ metadata: type: number required: false description: Handshake version for RethinkDB." - example: 1 + example: "1" - name: keepAlivePeriod type: string required: false @@ -123,7 +123,7 @@ metadata: type: number required: false description: Maximum number of idle connections." - example: 5 + example: "5" - name: authKey type: string required: false @@ -134,17 +134,17 @@ metadata: type: number required: false description: Initial connection pool capacity." - example: 5 + example: "5" - name: maxOpen type: number required: false description: Maximum number of open connections." - example: 10 + example: "10" - name: discoverHosts type: bool required: false description: Whether to discover hosts." - example: false + example: "false" - name: nodeRefreshInterval type: string required: false diff --git a/state/sqlite/metadata.yaml b/state/sqlite/metadata.yaml index 8c1e6aead0..d78440f3c2 100644 --- a/state/sqlite/metadata.yaml +++ b/state/sqlite/metadata.yaml @@ -33,8 +33,8 @@ metadata: type: bool required: false description: Disable WAL journaling. Should not use WAL if database is stored on a network filesystem. - example: false - default: false + example: "false" + default: "false" - name: tableName type: string required: false diff --git a/state/sqlite/sqlite.go b/state/sqlite/sqlite.go index 573654b382..9da3735f65 100644 --- a/state/sqlite/sqlite.go +++ b/state/sqlite/sqlite.go @@ -49,6 +49,7 @@ func newSQLiteStateStore(logger logger.Logger, dba DBAccess) *SQLiteStore { state.FeatureETag, state.FeatureTransactional, state.FeatureTTL, + state.FeatureKeysLike, }, dbaccess: dba, } @@ -84,6 +85,10 @@ func (s *SQLiteStore) Get(ctx context.Context, req *state.GetRequest) (*state.Ge return s.dbaccess.Get(ctx, req) } +func (s *SQLiteStore) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + return s.dbaccess.KeysLike(ctx, req) +} + // BulkGet performs a bulks get operations. // Options are ignored because this component requests all values in a single query. func (s *SQLiteStore) BulkGet(ctx context.Context, req []state.GetRequest, _ state.BulkGetOpts) ([]state.BulkGetResponse, error) { diff --git a/state/sqlite/sqlite_dbaccess.go b/state/sqlite/sqlite_dbaccess.go index 9f58d192f0..8e866a31f5 100644 --- a/state/sqlite/sqlite_dbaccess.go +++ b/state/sqlite/sqlite_dbaccess.go @@ -35,6 +35,7 @@ import ( "github.com/dapr/components-contrib/state" stateutils "github.com/dapr/components-contrib/state/utils" "github.com/dapr/kit/logger" + "github.com/dapr/kit/ptr" ) // DBAccess is a private interface which enables unit testing of SQLite. @@ -46,6 +47,7 @@ type DBAccess interface { Delete(ctx context.Context, req *state.DeleteRequest) error BulkGet(ctx context.Context, req []state.GetRequest) ([]state.BulkGetResponse, error) ExecuteMulti(ctx context.Context, reqs []state.TransactionalStateOperation) error + KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) Close() error } @@ -522,3 +524,92 @@ func (a *sqliteDBAccess) GetConnection() *sql.DB { func (a *sqliteDBAccess) GetCleanupInterval() time.Duration { return a.metadata.CleanupInterval } + +func (a *sqliteDBAccess) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + where := []string{ + `key LIKE ? ESCAPE '\'`, + `(expiration_time IS NULL OR expiration_time > CURRENT_TIMESTAMP)`, + } + args := []any{req.Pattern} + + if req.ContinuationToken != nil { + where = append(where, `rowid > ?`) + args = append(args, *req.ContinuationToken) + } + + orderClause := ` ORDER BY rowid ASC` + + limitClause := `` + var fetchLimit uint32 + if req.PageSize != nil { + fetchLimit = *req.PageSize + limitClause = ` LIMIT ?` + args = append(args, fetchLimit) + } + + //nolint:gosec + query := fmt.Sprintf(` + SELECT key, rowid + FROM %s + WHERE %s%s%s`, + a.metadata.TableName, + strings.Join(where, " AND "), + orderClause, + limitClause, + ) + + rows, err := a.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + type rec struct { + key string + rowID int64 + } + + var recs []rec + if req.PageSize != nil { + recs = make([]rec, 0, min(*req.PageSize, 1024)) + } else { + recs = make([]rec, 0, 256) + } + + for rows.Next() { + var k string + var rid int64 + if err := rows.Scan(&k, &rid); err != nil { + return nil, err + } + recs = append(recs, rec{key: k, rowID: rid}) + } + if err := rows.Err(); err != nil { + return nil, err + } + + resp := &state.KeysLikeResponse{ + Keys: make([]string, 0, len(recs)), + } + + if req.PageSize != nil { + switch { + case uint32(len(recs)) == *req.PageSize: //nolint:gosec + next := recs[*req.PageSize-1] + resp.ContinuationToken = ptr.Of(strconv.FormatInt(next.rowID, 10)) + recs = recs[:*req.PageSize] + case uint32(len(recs)) > *req.PageSize: //nolint:gosec + return nil, fmt.Errorf("received %d records when a LIMIT of %d was given", len(recs), *req.PageSize) + } + } + + for _, r := range recs { + resp.Keys = append(resp.Keys, r.key) + } + + return resp, nil +} diff --git a/state/sqlite/sqlite_test.go b/state/sqlite/sqlite_test.go index dd5c70604c..00921ba1a9 100644 --- a/state/sqlite/sqlite_test.go +++ b/state/sqlite/sqlite_test.go @@ -284,6 +284,14 @@ func TestValidSetRequest(t *testing.T) { require.NoError(t, err) } +func Test_KeysLike(t *testing.T) { + t.Parallel() + + ods := createSqlite(t) + + var _ state.KeysLiker = state.KeysLiker(ods) +} + func TestValidMultiDeleteRequest(t *testing.T) { t.Parallel() @@ -346,6 +354,10 @@ func (m *fakeDBaccess) ExecuteMulti(ctx context.Context, reqs []state.Transactio return nil } +func (m *fakeDBaccess) KeysLike(ctx context.Context, req *state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + return nil, nil +} + func (m *fakeDBaccess) Close() error { return nil } diff --git a/state/sqlserver/sqlserver.go b/state/sqlserver/sqlserver.go index 35e2f7ed20..5f82fbf60f 100644 --- a/state/sqlserver/sqlserver.go +++ b/state/sqlserver/sqlserver.go @@ -388,3 +388,77 @@ func (s *SQLServer) CleanupExpired() error { } return nil } + +func (s *SQLServer) KeysLike(ctx context.Context, req state.KeysLikeRequest) (*state.KeysLikeResponse, error) { + if len(req.Pattern) == 0 { + return nil, state.ErrKeysLikeEmptyPattern + } + + table := fmt.Sprintf(`[%s].[%s]`, s.metadata.SchemaName, s.metadata.TableName) + + baseWhere := `WHERE [Key] LIKE @pat ESCAPE '\' AND ([ExpireDate] IS NULL OR [ExpireDate] > GETDATE())` + + args := []any{ + sql.Named("pat", req.Pattern), + } + + seekClause := `` + if req.ContinuationToken != nil && *req.ContinuationToken != "" { + seekClause = ` AND [Key] > @token` + args = append(args, sql.Named("token", *req.ContinuationToken)) + } + + orderBy := ` ORDER BY [Key] ASC` + + var pageSize uint32 + var query string + if req.PageSize != nil && *req.PageSize > 0 { + pageSize = *req.PageSize + take := int64(pageSize + 1) + + query = fmt.Sprintf(` +SELECT TOP (@take) [Key] +FROM %s +%s%s%s`, table, baseWhere, seekClause, orderBy) + + args = append(args, sql.Named("take", take)) + } else { + // No paging: return all keys (be careful on huge tables) + query = fmt.Sprintf(` +SELECT [Key] +FROM %s +%s%s%s`, table, baseWhere, seekClause, orderBy) + } + + rows, err := s.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + keys := make([]string, 0, 256) + for rows.Next() { + var k string + if err := rows.Scan(&k); err != nil { + return nil, err + } + keys = append(keys, k) + } + if err := rows.Err(); err != nil { + return nil, err + } + + resp := &state.KeysLikeResponse{ + Keys: make([]string, 0, len(keys)), + } + + //nolint:gosec + if pageSize > 0 && uint32(len(keys)) > pageSize { + next := keys[pageSize] + resp.ContinuationToken = &next + keys = keys[:pageSize] + } + + resp.Keys = append(resp.Keys, keys...) + return resp, nil +} diff --git a/state/sqlserver/v2/metadata.go b/state/sqlserver/v2/metadata.go new file mode 100644 index 0000000000..eeb2fdda4c --- /dev/null +++ b/state/sqlserver/v2/metadata.go @@ -0,0 +1,210 @@ +/* +Copyright 2023 The Dapr Authors +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 sqlserver + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + sqlserverAuth "github.com/dapr/components-contrib/common/authentication/sqlserver" + "github.com/dapr/kit/metadata" + "github.com/dapr/kit/ptr" +) + +const ( + keyColumnName = "Key" + rowVersionColumnName = "RowVersion" + + defaultKeyLength = 200 + defaultTable = "state" + defaultMetaTable = "dapr_metadata" + defaultCleanupInterval = time.Hour +) + +type sqlServerMetadata struct { + sqlserverAuth.SQLServerAuthMetadata `mapstructure:",squash"` + + TableName string + MetadataTableName string + KeyType string + KeyLength int + IndexedProperties string + CleanupInterval *time.Duration `mapstructure:"cleanupInterval" mapstructurealiases:"cleanupIntervalInSeconds"` + + // Internal properties + keyTypeParsed KeyType + keyLengthParsed int + indexedPropertiesParsed []IndexedProperty +} + +func newMetadata() sqlServerMetadata { + return sqlServerMetadata{ + TableName: defaultTable, + KeyLength: defaultKeyLength, + MetadataTableName: defaultMetaTable, + CleanupInterval: ptr.Of(defaultCleanupInterval), + } +} + +func (m *sqlServerMetadata) Parse(meta map[string]string) error { + // Reset first + m.SQLServerAuthMetadata.Reset() + + // Decode the metadata + err := metadata.DecodeMetadata(meta, &m) + if err != nil { + return err + } + + // Validate and parse the auth metadata + err = m.SQLServerAuthMetadata.Validate(meta) + if err != nil { + return err + } + + // Validate and sanitize more values + if !sqlserverAuth.IsValidSQLName(m.TableName) { + return errors.New("invalid table name, accepted characters are (A-Z, a-z, 0-9, _)") + } + if !sqlserverAuth.IsValidSQLName(m.MetadataTableName) { + return errors.New("invalid metadata table name, accepted characters are (A-Z, a-z, 0-9, _)") + } + + err = m.setKeyType() + if err != nil { + return err + } + err = m.setIndexedProperties() + if err != nil { + return err + } + + // Cleanup interval + if m.CleanupInterval != nil { + // Non-positive value from meta means disable auto cleanup. + if *m.CleanupInterval <= 0 { + val, _ := metadata.GetMetadataProperty(meta, "cleanupInterval", "cleanupIntervalInSeconds") + if val == "" { + // Unfortunately the mapstructure decoder decodes an empty string to 0, a missing key would be nil however + m.CleanupInterval = ptr.Of(defaultCleanupInterval) + } else { + m.CleanupInterval = nil + } + } + } + + return nil +} + +// Validates and returns the key type. +func (m *sqlServerMetadata) setKeyType() error { + if m.KeyType != "" { + kt, err := KeyTypeFromString(m.KeyType) + if err != nil { + return err + } + + m.keyTypeParsed = kt + } else { + m.keyTypeParsed = StringKeyType + } + + if m.keyTypeParsed != StringKeyType { + return nil + } + + if m.KeyLength <= 0 { + return fmt.Errorf("invalid key length value of %d", m.KeyLength) + } else { + m.keyLengthParsed = m.KeyLength + } + + return nil +} + +// Sets the validated index properties. +func (m *sqlServerMetadata) setIndexedProperties() error { + if m.IndexedProperties == "" { + return nil + } + + var indexedProperties []IndexedProperty + err := json.Unmarshal([]byte(m.IndexedProperties), &indexedProperties) + if err != nil { + return err + } + + err = m.validateIndexedProperties(indexedProperties) + if err != nil { + return err + } + + m.indexedPropertiesParsed = indexedProperties + + return nil +} + +// Validates that all the mandator index properties are supplied and that the +// values are valid. +func (m *sqlServerMetadata) validateIndexedProperties(indexedProperties []IndexedProperty) error { + for _, p := range indexedProperties { + if p.ColumnName == "" { + return errors.New("indexed property column cannot be empty") + } + + if p.Property == "" { + return errors.New("indexed property name cannot be empty") + } + + if p.Type == "" { + return errors.New("indexed property type cannot be empty") + } + + if !sqlserverAuth.IsValidSQLName(p.ColumnName) { + return errors.New("invalid indexed property column name, accepted characters are (A-Z, a-z, 0-9, _)") + } + + if !isValidIndexedPropertyName(p.Property) { + return errors.New("invalid indexed property name, accepted characters are (A-Z, a-z, 0-9, _, ., [, ])") + } + + if !isValidIndexedPropertyType(p.Type) { + return errors.New("invalid indexed property type, accepted characters are (A-Z, a-z, 0-9, _, (, ))") + } + } + + return nil +} + +func isValidIndexedPropertyName(s string) bool { + for _, c := range s { + if !(sqlserverAuth.IsLetterOrNumber(c) || (c == '_') || (c == '.') || (c == '[') || (c == ']')) { + return false + } + } + + return true +} + +func isValidIndexedPropertyType(s string) bool { + for _, c := range s { + if !(sqlserverAuth.IsLetterOrNumber(c) || (c == '(') || (c == ')')) { + return false + } + } + + return true +} diff --git a/state/sqlserver/v2/metadata.yaml b/state/sqlserver/v2/metadata.yaml new file mode 100644 index 0000000000..e0a79e03a1 --- /dev/null +++ b/state/sqlserver/v2/metadata.yaml @@ -0,0 +1,105 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: "v1" +type: "state" +name: "sqlserver" +version: "v2" +status: "stable" +title: "SQL Server" +description: "Microsoft SQL Server and Azure SQL" +urls: + - title: "Reference" + url: "https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-sqlserver/" +capabilities: + # If actorStateStore is present, the metadata key actorStateStore can be used + - "actorStateStore" + - "crud" + - "transactional" + - "etag" + - "ttl" +authenticationProfiles: + - title: "Connection string" + description: | + Authenticates using a connection string + metadata: + - name: connectionString + required: true + sensitive: true + description: | + The connection string used to connect. + If the connection string contains the database, it must already exist. Otherwise, if the database is omitted, a default database named "Dapr" is created. + example: | + "Server=myServerName\myInstanceName;Database=myDataBase;User Id=myUsername;Password=myPassword;" +builtinAuthenticationProfiles: + - name: "azuread" + metadata: + - name: useAzureAD + required: true + type: bool + description: | + Must be set to `true` to enable the component to retrieve access tokens from Azure AD. + This authentication method only works with Azure SQL databases. + example: "true" + - name: connectionString + required: true + sensitive: true + description: | + The connection string or URL of the Azure SQL database, without credentials. + If the connection string contains the database, it must already exist. Otherwise, if the database is omitted, a default database named "Dapr" is created. + example: | + "sqlserver://myServerName.database.windows.net:1433?database=myDataBase" +metadata: + - name: tableName + description: | + The name of the table to use. Alpha-numeric with underscores. + example: | + "table_name" + default: | + "state" + - name: metadataTableName + description: | + Name of the table Dapr uses to store metadata properties. + example: | + "dapr_metadata" + default: | + "dapr_metadata" + - name: schemaName + description: | + The schema to use. + example: | + "dapr" + default: | + "dbo" + - name: keyType + description: | + The type of key used + allowedValues: + - "string" + - "uuid" + - "integer" + default: | + "string" + example: | + "string" + - name: keyLength + type: number + description: | + The max length of key. Ignored if "keyType" is not `string`. + example: | + 200 + default: | + 200 + - name: indexedProperties + description: | + List of indexed properties, as a string containing a JSON document. + This will apply only to String data + example: | + '[{"column": "transactionid", "property": "id", "type": "int"}, {"column": "customerid", "property": "customer", "type": "nvarchar(100)"}]' + - name: cleanupInterval + type: number + description: | + Interval, in seconds, to clean up rows with an expired TTL. Default: 3600 (i.e. 1 hour). + Setting this to values <=0 disables the periodic cleanup. + default: | + "3600" + example: | + "1800", "-1" diff --git a/state/sqlserver/v2/migration.go b/state/sqlserver/v2/migration.go new file mode 100644 index 0000000000..0bc1925f4c --- /dev/null +++ b/state/sqlserver/v2/migration.go @@ -0,0 +1,351 @@ +/* +Copyright 2021 The Dapr Authors +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 sqlserver + +import ( + "context" + "database/sql" + "fmt" +) + +type migrator interface { + executeMigrations(context.Context) (migrationResult, error) +} + +type migration struct { + metadata *sqlServerMetadata +} + +type migrationResult struct { + itemRefTableTypeName string + upsertProcName string + upsertProcFullName string + pkColumnType string + getCommand string + deleteWithETagCommand string + deleteWithoutETagCommand string +} + +func newMigration(metadata *sqlServerMetadata) migrator { + return &migration{ + metadata: metadata, + } +} + +func (m *migration) newMigrationResult() migrationResult { + r := migrationResult{ + itemRefTableTypeName: fmt.Sprintf("[%s].%s_Table", m.metadata.SchemaName, m.metadata.TableName), + upsertProcName: "sp_Upsert_v5_" + m.metadata.TableName, + getCommand: fmt.Sprintf("SELECT [Data], [BinaryData], [isBinary], [RowVersion], [ExpireDate] FROM [%s].[%s] WHERE [Key] = @Key AND ([ExpireDate] IS NULL OR [ExpireDate] > GETDATE())", m.metadata.SchemaName, m.metadata.TableName), + deleteWithETagCommand: fmt.Sprintf(`DELETE [%s].[%s] WHERE [Key]=@Key AND [RowVersion]=@RowVersion`, m.metadata.SchemaName, m.metadata.TableName), + deleteWithoutETagCommand: fmt.Sprintf(`DELETE [%s].[%s] WHERE [Key]=@Key`, m.metadata.SchemaName, m.metadata.TableName), + } + + r.upsertProcFullName = fmt.Sprintf("[%s].%s", m.metadata.SchemaName, r.upsertProcName) + + //nolint:exhaustive + switch m.metadata.keyTypeParsed { + case StringKeyType: + r.pkColumnType = fmt.Sprintf("NVARCHAR(%d)", m.metadata.keyLengthParsed) + + case UUIDKeyType: + r.pkColumnType = "uniqueidentifier" + + case IntegerKeyType: + r.pkColumnType = "int" + } + + return r +} + +/* #nosec. */ +func (m *migration) executeMigrations(ctx context.Context) (migrationResult, error) { + r := m.newMigrationResult() + + conn, hasDatabase, err := m.metadata.GetConnector(false) + if err != nil { + return r, err + } + db := sql.OpenDB(conn) + + // If the user provides a database in the connection string do not attempt + // to create the database. This work as the component did before adding the + // support to create the db. + if hasDatabase { + // Schedule close of connection + defer db.Close() + } else { + err = m.ensureDatabaseExists(ctx, db) + if err != nil { + return r, fmt.Errorf("failed to create database: %w", err) + } + + // Close the existing connection + db.Close() + + // Re connect with a database-specific connection + conn, _, err = m.metadata.GetConnector(true) + if err != nil { + return r, err + } + db = sql.OpenDB(conn) + + // Schedule close of new connection + defer db.Close() + } + + err = m.ensureSchemaExists(ctx, db) + if err != nil { + return r, fmt.Errorf("failed to create db schema: %w", err) + } + + err = m.ensureTableExists(ctx, db, r) + if err != nil { + return r, fmt.Errorf("failed to create db table: %w", err) + } + + err = m.ensureStoredProcedureExists(ctx, db, r) + if err != nil { + return r, fmt.Errorf("failed to create stored procedures: %w", err) + } + + for _, ix := range m.metadata.indexedPropertiesParsed { + err = m.ensureIndexedPropertyExists(ctx, db, ix) + if err != nil { + return r, err + } + } + + return r, nil +} + +func runCommand(ctx context.Context, db *sql.DB, tsql string) error { + if _, err := db.ExecContext(ctx, tsql); err != nil { + return err + } + + return nil +} + +/* #nosec. */ +func (m *migration) ensureIndexedPropertyExists(ctx context.Context, db *sql.DB, ix IndexedProperty) error { + indexName := "IX_" + ix.ColumnName + + tsql := fmt.Sprintf(` + IF (NOT EXISTS(SELECT object_id + FROM sys.indexes + WHERE object_id = OBJECT_ID('[%s].%s') + AND name='%s')) + CREATE INDEX %s ON [%s].[%s]([%s])`, + m.metadata.SchemaName, + m.metadata.TableName, + indexName, + indexName, + m.metadata.SchemaName, + m.metadata.TableName, + ix.ColumnName) + + return runCommand(ctx, db, tsql) +} + +/* #nosec. */ +func (m *migration) ensureDatabaseExists(ctx context.Context, db *sql.DB) error { + tsql := fmt.Sprintf(` +IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = N'%s') + CREATE DATABASE [%s]`, + m.metadata.DatabaseName, m.metadata.DatabaseName) + + return runCommand(ctx, db, tsql) +} + +/* #nosec. */ +func (m *migration) ensureSchemaExists(ctx context.Context, db *sql.DB) error { + tsql := fmt.Sprintf(` + IF NOT EXISTS(SELECT * FROM sys.schemas WHERE name = N'%s') + EXEC('CREATE SCHEMA [%s]')`, + m.metadata.SchemaName, m.metadata.SchemaName) + + return runCommand(ctx, db, tsql) +} + +/* #nosec. */ +func (m *migration) ensureTableExists(ctx context.Context, db *sql.DB, r migrationResult) error { + tsql := fmt.Sprintf(` + IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s') + CREATE TABLE [%s].[%s] ( + [Key] %s CONSTRAINT PK_%s PRIMARY KEY, + [Data] NVARCHAR(MAX) NULL, + [BinaryData] VARBINARY(MAX) NULL, + [isBinary] BIT NOT NULL DEFAULT(0), + [InsertDate] DateTime2 NOT NULL DEFAULT(GETDATE()), + [UpdateDate] DateTime2 NULL, + [ExpireDate] DateTime2 NULL,`, + m.metadata.SchemaName, m.metadata.TableName, m.metadata.SchemaName, m.metadata.TableName, r.pkColumnType, m.metadata.TableName) + + for _, prop := range m.metadata.indexedPropertiesParsed { + if prop.Type != "" { + tsql += fmt.Sprintf("\n [%s] AS CONVERT(%s, JSON_VALUE(Data, '$.%s')) PERSISTED,", prop.ColumnName, prop.Type, prop.Property) + } else { + tsql += fmt.Sprintf("\n [%s] AS JSON_VALUE(Data, '$.%s') PERSISTED,", prop.ColumnName, prop.Property) + } + } + + tsql += ` + [RowVersion] ROWVERSION NOT NULL) + ` + + if err := runCommand(ctx, db, tsql); err != nil { + return err + } + + // Create metadata Table + tsql = fmt.Sprintf(` + IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '%[1]s' AND TABLE_NAME = '%[2]s') + CREATE TABLE [%[1]s].[%[2]s] ( + [Key] %[3]s CONSTRAINT PK_%[4]s PRIMARY KEY, + [Value] NVARCHAR(MAX) NOT NULL + )`, m.metadata.SchemaName, m.metadata.MetadataTableName, r.pkColumnType, m.metadata.MetadataTableName) + if err := runCommand(ctx, db, tsql); err != nil { + return err + } + + return nil +} + +/* #nosec. */ +func (m *migration) ensureTypeExists(ctx context.Context, db *sql.DB, mr migrationResult) error { + tsql := fmt.Sprintf(` + IF type_id('[%s].%s_Table') IS NULL + CREATE TYPE [%s].%s_Table AS TABLE + ( + [Key] %s NOT NULL, + [RowVersion] BINARY(8) + ) + `, m.metadata.SchemaName, m.metadata.TableName, m.metadata.SchemaName, m.metadata.TableName, mr.pkColumnType) + + return runCommand(ctx, db, tsql) +} + +func (m *migration) ensureStoredProcedureExists(ctx context.Context, db *sql.DB, mr migrationResult) error { + err := m.ensureTypeExists(ctx, db, mr) + if err != nil { + return err + } + + err = m.ensureUpsertStoredProcedureExists(ctx, db, mr) + if err != nil { + return err + } + + return nil +} + +/* #nosec. */ +func (m *migration) createStoredProcedureIfNotExists(ctx context.Context, db *sql.DB, name string, escapedDefinition string) error { + tsql := fmt.Sprintf(` + IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[%s].[%s]') AND type in (N'P', N'PC')) + BEGIN + execute ('%s') + END`, + m.metadata.SchemaName, + name, + escapedDefinition) + + return runCommand(ctx, db, tsql) +} + +/* #nosec. */ +//nolint:dupword +func (m *migration) ensureUpsertStoredProcedureExists(ctx context.Context, db *sql.DB, mr migrationResult) error { + tsql := fmt.Sprintf(` + CREATE PROCEDURE %[1]s ( + @Key %[2]s, + @Data NVARCHAR(MAX), + @BinaryData VARBINARY(MAX), + @isBinary BIT, + @TTL INT, + @RowVersion BINARY(8), + @FirstWrite BIT + ) AS + BEGIN + IF (@FirstWrite=1) + BEGIN + IF (@RowVersion IS NOT NULL) + BEGIN + BEGIN TRANSACTION; + IF NOT EXISTS (SELECT * FROM [%[3]s] WHERE [Key]=@Key AND RowVersion = @RowVersion) + BEGIN + THROW 2601, ''FIRST-WRITE: COMPETING RECORD ALREADY WRITTEN.'', 1 + END + BEGIN + UPDATE [%[3]s] + SET [Data]=@Data, [isBinary]=@isBinary, [BinaryData]=@BinaryData, UpdateDate=GETDATE(), ExpireDate=CASE WHEN @TTL IS NULL THEN NULL ELSE DATEADD(SECOND, @TTL, GETDATE()) END + WHERE [Key]=@Key AND RowVersion = @RowVersion + END + COMMIT; + END + ELSE + BEGIN + BEGIN TRANSACTION; + IF EXISTS (SELECT * FROM [%[3]s] WHERE [Key]=@Key) + BEGIN + THROW 2601, ''FIRST-WRITE: COMPETING RECORD ALREADY WRITTEN.'', 1 + END + BEGIN + BEGIN TRY + INSERT INTO [%[3]s] ([Key], [Data], [isBinary], [BinaryData], ExpireDate) VALUES (@Key, @Data, @isBinary, @BinaryData, CASE WHEN @TTL IS NULL THEN NULL ELSE DATEADD(SECOND, @TTL, GETDATE()) END) + END TRY + + BEGIN CATCH + IF ERROR_NUMBER() IN (2601, 2627) + UPDATE [%[3]s] + SET [Data]=@Data, [isBinary]=@isBinary, [BinaryData]=@BinaryData, UpdateDate=GETDATE(), ExpireDate=CASE WHEN @TTL IS NULL THEN NULL ELSE DATEADD(SECOND, @TTL, GETDATE()) END + WHERE [Key]=@Key AND RowVersion = ISNULL(@RowVersion, RowVersion) + END CATCH + END + COMMIT; + END + END + ELSE + BEGIN + IF (@RowVersion IS NOT NULL) + BEGIN + UPDATE [%[3]s] + SET [Data]=@Data, [isBinary]=@isBinary, [BinaryData]=@BinaryData, UpdateDate=GETDATE(), ExpireDate=CASE WHEN @TTL IS NULL THEN NULL ELSE DATEADD(SECOND, @TTL, GETDATE()) END + WHERE [Key]=@Key AND RowVersion = @RowVersion + RETURN + END + ELSE + BEGIN + BEGIN TRY + INSERT INTO [%[3]s] ([Key], [Data], [isBinary], [BinaryData], ExpireDate) VALUES (@Key, @Data, @isBinary, @BinaryData, CASE WHEN @TTL IS NULL THEN NULL ELSE DATEADD(SECOND, @TTL, GETDATE()) END) + END TRY + + BEGIN CATCH + IF ERROR_NUMBER() IN (2601, 2627) + UPDATE [%[3]s] + SET [Data]=@Data, [isBinary]=@isBinary, [BinaryData]=@BinaryData, UpdateDate=GETDATE(), ExpireDate=CASE WHEN @TTL IS NULL THEN NULL ELSE DATEADD(SECOND, @TTL, GETDATE()) END + WHERE [Key]=@Key AND RowVersion = ISNULL(@RowVersion, RowVersion) + END CATCH + END + END + END + `, + mr.upsertProcFullName, + mr.pkColumnType, + m.metadata.TableName, + ) + + return m.createStoredProcedureIfNotExists(ctx, db, mr.upsertProcName, tsql) +} diff --git a/state/sqlserver/v2/sqlserver.go b/state/sqlserver/v2/sqlserver.go new file mode 100644 index 0000000000..5736ac68df --- /dev/null +++ b/state/sqlserver/v2/sqlserver.go @@ -0,0 +1,420 @@ +/* +Copyright 2023 The Dapr Authors +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 sqlserver + +import ( + "context" + "database/sql" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "reflect" + "time" + + commonsql "github.com/dapr/components-contrib/common/component/sql" + sqltransactions "github.com/dapr/components-contrib/common/component/sql/transactions" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" + "github.com/dapr/components-contrib/state/utils" + "github.com/dapr/kit/logger" + "github.com/dapr/kit/ptr" +) + +// KeyType defines type of the table identifier. +type KeyType string + +// KeyTypeFromString tries to create a KeyType from a string value. +func KeyTypeFromString(k string) (KeyType, error) { + switch k { + case string(StringKeyType): + return StringKeyType, nil + case string(UUIDKeyType): + return UUIDKeyType, nil + case string(IntegerKeyType): + return IntegerKeyType, nil + } + + return InvalidKeyType, errors.New("invalid key type") +} + +const ( + // StringKeyType defines a key of type string. + StringKeyType KeyType = "string" + + // UUIDKeyType defines a key of type UUID/GUID. + UUIDKeyType KeyType = "uuid" + + // IntegerKeyType defines a key of type integer. + IntegerKeyType KeyType = "integer" + + // InvalidKeyType defines an invalid key type. + InvalidKeyType KeyType = "invalid" +) + +// New creates a new instance of a SQL Server transaction store. +func New(logger logger.Logger) state.Store { + s := &SQLServer{ + features: []state.Feature{ + state.FeatureETag, + state.FeatureTransactional, + state.FeatureTTL, + }, + logger: logger, + migratorFactory: newMigration, + } + s.BulkStore = state.NewDefaultBulkStore(s) + return s +} + +// IndexedProperty defines a indexed property. +type IndexedProperty struct { + ColumnName string `json:"column"` + Property string `json:"property"` + Type string `json:"type"` +} + +// SQLServer defines a MS SQL Server based state store. +type SQLServer struct { + state.BulkStore + + metadata sqlServerMetadata + + migratorFactory func(*sqlServerMetadata) migrator + + itemRefTableTypeName string + upsertCommand string + getCommand string + deleteWithETagCommand string + deleteWithoutETagCommand string + + features []state.Feature + logger logger.Logger + db *sql.DB + gc commonsql.GarbageCollector +} + +// Init initializes the SQL server state store. +func (s *SQLServer) Init(ctx context.Context, metadata state.Metadata) error { + s.metadata = newMetadata() + metadata.Base.GetProperty() + err := s.metadata.Parse(metadata.Properties) + if err != nil { + return err + } + + migration := s.migratorFactory(&s.metadata) + mr, err := migration.executeMigrations(ctx) + if err != nil { + return err + } + + s.itemRefTableTypeName = mr.itemRefTableTypeName + s.upsertCommand = mr.upsertProcFullName + s.getCommand = mr.getCommand + s.deleteWithETagCommand = mr.deleteWithETagCommand + s.deleteWithoutETagCommand = mr.deleteWithoutETagCommand + + conn, _, err := s.metadata.GetConnector(true) + if err != nil { + return err + } + s.db = sql.OpenDB(conn) + + if s.metadata.CleanupInterval != nil { + err = s.startGC() + if err != nil { + return err + } + } + + return nil +} + +func (s *SQLServer) startGC() error { + gc, err := commonsql.ScheduleGarbageCollector(commonsql.GCOptions{ + Logger: s.logger, + UpdateLastCleanupQuery: func(arg any) (string, any) { + return fmt.Sprintf(`BEGIN TRANSACTION; +BEGIN TRY +INSERT INTO [%[1]s].[%[2]s] ([Key], [Value]) VALUES ('last-cleanup', CONVERT(nvarchar(MAX), GETDATE(), 21)); +END TRY +BEGIN CATCH +UPDATE [%[1]s].[%[2]s] SET [Value] = CONVERT(nvarchar(MAX), GETDATE(), 21) WHERE [Key] = 'last-cleanup' AND Datediff_big(MS, [Value], GETUTCDATE()) > @Interval +END CATCH +COMMIT TRANSACTION;`, s.metadata.SchemaName, s.metadata.MetadataTableName), sql.Named("Interval", arg) + }, + DeleteExpiredValuesQuery: fmt.Sprintf( + `DELETE FROM [%s].[%s] WHERE [ExpireDate] IS NOT NULL AND [ExpireDate] < GETDATE()`, + s.metadata.SchemaName, s.metadata.TableName, + ), + CleanupInterval: *s.metadata.CleanupInterval, + DB: commonsql.AdaptDatabaseSQLConn(s.db), + }) + if err != nil { + return err + } + s.gc = gc + + return nil +} + +// Features returns the features available in this state store. +func (s *SQLServer) Features() []state.Feature { + return s.features +} + +// Multi performs batched updates on a SQL Server store. +func (s *SQLServer) Multi(ctx context.Context, request *state.TransactionalStateRequest) error { + if request == nil { + return nil + } + + // If there's only 1 operation, skip starting a transaction + switch len(request.Operations) { + case 0: + return nil + case 1: + return s.execMultiOperation(ctx, request.Operations[0], s.db) + default: + _, err := sqltransactions.ExecuteInTransaction(ctx, s.logger, s.db, func(ctx context.Context, tx *sql.Tx) (r struct{}, err error) { + for _, op := range request.Operations { + err = s.execMultiOperation(ctx, op, tx) + if err != nil { + return r, err + } + } + return r, nil + }) + return err + } +} + +func (s *SQLServer) execMultiOperation(ctx context.Context, op state.TransactionalStateOperation, db dbExecutor) error { + switch req := op.(type) { + case state.SetRequest: + return s.executeSet(ctx, db, &req) + case state.DeleteRequest: + return s.executeDelete(ctx, db, &req) + default: + return fmt.Errorf("unsupported operation: %s", op.Operation()) + } +} + +// Delete removes an entity from the store. +func (s *SQLServer) Delete(ctx context.Context, req *state.DeleteRequest) error { + return s.executeDelete(ctx, s.db, req) +} + +func (s *SQLServer) executeDelete(ctx context.Context, db dbExecutor, req *state.DeleteRequest) error { + var err error + var res sql.Result + if req.HasETag() { + var b []byte + b, err = hex.DecodeString(*req.ETag) + if err != nil { + return state.NewETagError(state.ETagInvalid, err) + } + + res, err = db.ExecContext(ctx, s.deleteWithETagCommand, sql.Named(keyColumnName, req.Key), sql.Named(rowVersionColumnName, b)) + } else { + res, err = db.ExecContext(ctx, s.deleteWithoutETagCommand, sql.Named(keyColumnName, req.Key)) + } + + // err represents errors thrown by the stored procedure or the database itself + if err != nil { + return err + } + + // if the row with matching key (and ETag if specified) is not found, then the stored procedure returns 0 rows affected + rows, err := res.RowsAffected() + if err != nil { + return err + } + + // When an ETAG is specified, a row must have been deleted or else we return an ETag mismatch error + if rows != 1 && req.ETag != nil && *req.ETag != "" { + return state.NewETagError(state.ETagMismatch, nil) + } + + // successful deletion, or noop if no ETAG specified + return nil +} + +// Get returns an entity from store. +func (s *SQLServer) Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) { + rows, err := s.db.QueryContext(ctx, s.getCommand, sql.Named(keyColumnName, req.Key)) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + defer rows.Close() + + if !rows.Next() { + return &state.GetResponse{}, nil + } + + var ( + data sql.NullString + binaryData []byte + isBinary bool + rowVersion []byte + expireDate sql.NullTime + ) + err = rows.Scan(&data, &binaryData, &isBinary, &rowVersion, &expireDate) + if err != nil { + return nil, err + } + + etag := hex.EncodeToString(rowVersion) + + var metadata map[string]string + if expireDate.Valid { + metadata = map[string]string{ + state.GetRespMetaKeyTTLExpireTime: expireDate.Time.UTC().Format(time.RFC3339), + } + } + + var bytes []byte + if isBinary { + bytes = binaryData + } else { + if !data.Valid { + return nil, errors.New("unexpected error: no item was found") + } + bytes = []byte(data.String) + } + + return &state.GetResponse{ + Data: bytes, + ETag: ptr.Of(etag), + Metadata: metadata, + }, nil +} + +// Set adds/updates an entity on store. +func (s *SQLServer) Set(ctx context.Context, req *state.SetRequest) error { + return s.executeSet(ctx, s.db, req) +} + +// dbExecutor implements a common functionality implemented by db or tx. +type dbExecutor interface { + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) +} + +func (s *SQLServer) executeSet(ctx context.Context, db dbExecutor, req *state.SetRequest) error { + bytes, isBinary := req.Value.([]byte) + namedData := sql.Named("Data", nil) + namedBinaryData := sql.Named("BinaryData", nil) + if !isBinary { + bt, err := json.Marshal(req.Value) + if err != nil { + return err + } + namedData = sql.Named("Data", string(bt)) + } else { + namedBinaryData = sql.Named("BinaryData", bytes) + } + + etag := sql.Named(rowVersionColumnName, nil) + if req.HasETag() { + b, err := hex.DecodeString(*req.ETag) + if err != nil { + return state.NewETagError(state.ETagInvalid, err) + } + etag = sql.Named(rowVersionColumnName, b) + } + + ttl, ttlerr := utils.ParseTTL(req.Metadata) + if ttlerr != nil { + return fmt.Errorf("error parsing TTL: %w", ttlerr) + } + + var res sql.Result + var err error + if req.Options.Concurrency == state.FirstWrite { + res, err = db.ExecContext(ctx, s.upsertCommand, + sql.Named(keyColumnName, req.Key), + namedData, + etag, + namedBinaryData, + sql.Named("isBinary", isBinary), + sql.Named("FirstWrite", 1), + sql.Named("TTL", ttl)) + } else { + res, err = db.ExecContext(ctx, s.upsertCommand, + sql.Named(keyColumnName, req.Key), + namedData, + etag, + namedBinaryData, + sql.Named("isBinary", isBinary), + sql.Named("FirstWrite", 0), + sql.Named("TTL", ttl)) + } + + if err != nil { + return err + } + + rows, err := res.RowsAffected() + if err != nil { + return err + } + + if rows != 1 { + if req.HasETag() { + return state.NewETagError(state.ETagMismatch, err) + } + return errors.New("no item was updated") + } + + return nil +} + +func (s *SQLServer) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + settingsStruct := sqlServerMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(settingsStruct), &metadataInfo, metadata.StateStoreType) + return +} + +// Close implements io.Closer. +func (s *SQLServer) Close() error { + if s.db != nil { + s.db.Close() + s.db = nil + } + + if s.gc != nil { + return s.gc.Close() + } + + return nil +} + +// GetCleanupInterval returns the cleanupInterval property. +// This is primarily used for tests. +func (s *SQLServer) GetCleanupInterval() *time.Duration { + return s.metadata.CleanupInterval +} + +func (s *SQLServer) CleanupExpired() error { + if s.gc != nil { + return s.gc.CleanupExpired() + } + return nil +} diff --git a/state/sqlserver/v2/sqlserver_integration_test.go b/state/sqlserver/v2/sqlserver_integration_test.go new file mode 100644 index 0000000000..230d1a5bed --- /dev/null +++ b/state/sqlserver/v2/sqlserver_integration_test.go @@ -0,0 +1,661 @@ +/* +Copyright 2021 The Dapr Authors +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 sqlserver + +import ( + "crypto/rand" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math" + "os" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + "github.com/dapr/components-contrib/common/proto/state/sqlserver" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" + "github.com/dapr/kit/logger" +) + +const ( + // connectionStringEnvKey defines the key containing the integration test connection string + // To use docker, server=localhost;user id=sa;password=Pass@Word1;port=1433; + // To use Azure SQL, server=.database.windows.net;User id=;port=1433;password=;database=dapr_test;. + connectionStringEnvKey = "DAPR_TEST_SQL_CONNSTRING" + usersTableName = "Users" + beverageTea = "tea" + invalidEtag = "FFFFFFFFFFFFFFFF" +) + +type user struct { + ID string + Name string + FavoriteBeverage string +} + +type userWithPets struct { + user + PetsCount int +} + +type userWithEtag struct { + user + etag string +} + +func TestIntegrationCases(t *testing.T) { + connectionString := os.Getenv(connectionStringEnvKey) + if connectionString == "" { + t.Skipf(`SQLServer state integration tests skipped. To enable this test, define the connection string using environment variable '%[1]s' (example 'export %[1]s="server=localhost;user id=sa;password=Pass@Word1;port=1433;")'`, connectionStringEnvKey) + } + + t.Run("Single operations", testSingleOperations) + t.Run("Set New Record With Invalid Etag Should Fail", testSetNewRecordWithInvalidEtagShouldFail) + t.Run("Indexed Properties", testIndexedProperties) + t.Run("Multi operations", testMultiOperations) + t.Run("Insert and Update Set Record Dates", testInsertAndUpdateSetRecordDates) + t.Run("Multiple initializations", testMultipleInitializations) + t.Run("Should preserve byte data when not base64 encoded", testNonBase64ByteData) + + // Run concurrent set tests 10 times + const executions = 10 + for i := range executions { + t.Run(fmt.Sprintf("Concurrent sets, try #%d", i+1), testConcurrentSets) + } +} + +func getUniqueDBSchema(t *testing.T) string { + b := make([]byte, 4) + _, err := io.ReadFull(rand.Reader, b) + require.NoError(t, err) + return "v" + hex.EncodeToString(b) +} + +func createMetadata(schema string, kt KeyType, indexedProperties string) state.Metadata { + metadata := state.Metadata{Base: metadata.Base{ + Properties: map[string]string{ + "connectionString": os.Getenv(connectionStringEnvKey), + "schema": schema, + "tableName": usersTableName, + "keyType": string(kt), + "databaseName": "dapr_test", + }, + }} + + if indexedProperties != "" { + metadata.Properties["indexedProperties"] = indexedProperties + } + + return metadata +} + +// Ensure the database is running +// For docker, use: docker run --name sqlserver -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@Word1" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04. +// For azure-sql-edge use: +// docker volume create sqlvolume +// docker run --name sqlserver -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Pass@Word1" -e "MSSQL_PID=Developer" -e "MSSQL_AGENT_ENABLED=TRUE" -e "MSSQL_COLLATION=SQL_Latin1_General_CP1_CI_AS" -e "MSSQL_LCID=1033" -p 1433:1433 -v sqlvolume:/var/opt/mssql -d mcr.microsoft.com/azure-sql-edge:latest +func getTestStore(t *testing.T, indexedProperties string) *SQLServer { + return getTestStoreWithKeyType(t, StringKeyType, indexedProperties) +} + +func getTestStoreWithKeyType(t *testing.T, kt KeyType, indexedProperties string) *SQLServer { + schema := getUniqueDBSchema(t) + metadata := createMetadata(schema, kt, indexedProperties) + store := &SQLServer{ + logger: logger.NewLogger("test"), + migratorFactory: newMigration, + } + store.BulkStore = state.NewDefaultBulkStore(store) + err := store.Init(t.Context(), metadata) + require.NoError(t, err) + + return store +} + +func assertUserExists(t *testing.T, store *SQLServer, key string) (user, string) { + getRes, err := store.Get(t.Context(), &state.GetRequest{Key: key}) + require.NoError(t, err) + assert.NotNil(t, getRes) + assert.NotNil(t, getRes.Data, "No data was returned") + require.NotNil(t, getRes.ETag) + + var loaded user + err = json.Unmarshal(getRes.Data, &loaded) + require.NoError(t, err) + + return loaded, *getRes.ETag +} + +func assertLoadedUserIsEqual(t *testing.T, store *SQLServer, key string, expected user) (user, string) { + loaded, etag := assertUserExists(t, store, key) + assert.Equal(t, expected.ID, loaded.ID) + assert.Equal(t, expected.Name, loaded.Name) + assert.Equal(t, expected.FavoriteBeverage, loaded.FavoriteBeverage) + + return loaded, etag +} + +func assertUserDoesNotExist(t *testing.T, store *SQLServer, key string) { + _, err := store.Get(t.Context(), &state.GetRequest{Key: key}) + require.NoError(t, err) +} + +func assertDBQuery(t *testing.T, store *SQLServer, query string, assertReader func(t *testing.T, rows *sql.Rows)) { + rows, err := store.db.Query(query) + require.NoError(t, err) + require.NoError(t, rows.Err()) + + defer rows.Close() + assertReader(t, rows) +} + +/* #nosec. */ +func assertUserCountIsEqualTo(t *testing.T, store *SQLServer, expected int) { + tsql := fmt.Sprintf("SELECT count(*) FROM [%s].[%s]", store.metadata.SchemaName, store.metadata.TableName) + assertDBQuery(t, store, tsql, func(t *testing.T, rows *sql.Rows) { + assert.True(t, rows.Next()) + var actual int + err := rows.Scan(&actual) + require.NoError(t, err) + assert.Equal(t, expected, actual) + }) +} + +type userKeyGenerator interface { + NextKey() string +} + +type numbericKeyGenerator struct { + seed int32 +} + +func (n *numbericKeyGenerator) NextKey() string { + val := atomic.AddInt32(&n.seed, 1) + + return strconv.Itoa(int(val)) +} + +type uuidKeyGenerator struct{} + +func (n uuidKeyGenerator) NextKey() string { + return uuid.New().String() +} + +func testSingleOperations(t *testing.T) { + invEtag := invalidEtag + + tests := []struct { + name string + kt KeyType + keyGen userKeyGenerator + }{ + {"Single operation string key type", StringKeyType, &numbericKeyGenerator{}}, + {"Single operation integer key type", IntegerKeyType, &numbericKeyGenerator{}}, + {"Single operation uuid key type", UUIDKeyType, &uuidKeyGenerator{}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store := getTestStoreWithKeyType(t, test.kt, "") + + john := user{test.keyGen.NextKey(), "John", "Coffee"} + + // Get fails as the item does not exist + assertUserDoesNotExist(t, store, john.ID) + + // Save and read + err := store.Set(t.Context(), &state.SetRequest{Key: john.ID, Value: john}) + require.NoError(t, err) + johnV1, etagFromInsert := assertLoadedUserIsEqual(t, store, john.ID, john) + + // Update with ETAG + waterJohn := johnV1 + waterJohn.FavoriteBeverage = "Water" + err = store.Set(t.Context(), &state.SetRequest{Key: waterJohn.ID, Value: waterJohn, ETag: &etagFromInsert}) + require.NoError(t, err) + + // Get updated + johnV2, _ := assertLoadedUserIsEqual(t, store, waterJohn.ID, waterJohn) + + // Update without ETAG + noEtagJohn := johnV2 + noEtagJohn.FavoriteBeverage = "No Etag John" + err = store.Set(t.Context(), &state.SetRequest{Key: noEtagJohn.ID, Value: noEtagJohn}) + require.NoError(t, err) + + // 7. Get updated + johnV3, _ := assertLoadedUserIsEqual(t, store, noEtagJohn.ID, noEtagJohn) + + // 8. Update with invalid ETAG should fail + failedJohn := johnV3 + failedJohn.FavoriteBeverage = "Will not work" + err = store.Set(t.Context(), &state.SetRequest{Key: failedJohn.ID, Value: failedJohn, ETag: &etagFromInsert}) + require.Error(t, err) + _, etag := assertLoadedUserIsEqual(t, store, johnV3.ID, johnV3) + + // 9. Delete with invalid ETAG should fail + err = store.Delete(t.Context(), &state.DeleteRequest{Key: johnV3.ID, ETag: &invEtag}) + require.Error(t, err) + assertLoadedUserIsEqual(t, store, johnV3.ID, johnV3) + + // 10. Delete with valid ETAG + err = store.Delete(t.Context(), &state.DeleteRequest{Key: johnV2.ID, ETag: &etag}) + require.NoError(t, err) + + assertUserDoesNotExist(t, store, johnV2.ID) + }) + } +} + +func testSetNewRecordWithInvalidEtagShouldFail(t *testing.T) { + store := getTestStore(t, "") + + u := user{uuid.New().String(), "John", "Coffee"} + + invEtag := invalidEtag + err := store.Set(t.Context(), &state.SetRequest{Key: u.ID, Value: u, ETag: &invEtag}) + require.Error(t, err) +} + +/* #nosec. */ +func testIndexedProperties(t *testing.T) { + store := getTestStore(t, `[{ "column":"FavoriteBeverage", "property":"FavoriteBeverage", "type":"nvarchar(100)"}, { "column":"PetsCount", "property":"PetsCount", "type": "INTEGER"}]`) + + err := store.BulkSet(t.Context(), []state.SetRequest{ + {Key: "1", Value: userWithPets{user{"1", "John", "Coffee"}, 3}}, + {Key: "2", Value: userWithPets{user{"2", "Laura", "Water"}, 1}}, + {Key: "3", Value: userWithPets{user{"3", "Carl", "Beer"}, 0}}, + {Key: "4", Value: userWithPets{user{"4", "Maria", "Wine"}, 100}}, + }, state.BulkStoreOpts{}) + + require.NoError(t, err) + + // Check the database for computed columns + assertDBQuery(t, store, fmt.Sprintf("SELECT count(*) from [%s].[%s] WHERE PetsCount < 3", store.metadata.SchemaName, usersTableName), func(t *testing.T, rows *sql.Rows) { + assert.True(t, rows.Next()) + + var c int + rows.Scan(&c) + assert.Equal(t, 2, c) + }) + + // Ensure we can get by beverage + assertDBQuery(t, store, fmt.Sprintf("SELECT count(*) from [%s].[%s] WHERE FavoriteBeverage = '%s'", store.metadata.SchemaName, usersTableName, "Coffee"), func(t *testing.T, rows *sql.Rows) { + assert.True(t, rows.Next()) + + var c int + rows.Scan(&c) + assert.Equal(t, 1, c) + }) +} + +func testMultiOperations(t *testing.T) { + tests := []struct { + name string + kt KeyType + keyGen userKeyGenerator + }{ + {"Multi operations string key type", StringKeyType, &numbericKeyGenerator{}}, + {"Multi operations integer key type", IntegerKeyType, &numbericKeyGenerator{}}, + {"Multi operations uuid key type", UUIDKeyType, &uuidKeyGenerator{}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store := getTestStoreWithKeyType(t, test.kt, `[{ "column":"FavoriteBeverage", "property":"FavoriteBeverage", "type":"nvarchar(100)"}]`) + + keyGen := test.keyGen + + initialUsers := []user{ + {keyGen.NextKey(), "John", "Coffee"}, + {keyGen.NextKey(), "Laura", "Water"}, + {keyGen.NextKey(), "Carl", "Beer"}, + {keyGen.NextKey(), "Maria", "Wine"}, + {keyGen.NextKey(), "Mark", "Juice"}, + {keyGen.NextKey(), "Sara", "Soda"}, + {keyGen.NextKey(), "Tony", "Milk"}, + {keyGen.NextKey(), "Hugo", "Juice"}, + } + + // 1. add bulk users + bulkSet := make([]state.SetRequest, len(initialUsers)) + for i, u := range initialUsers { + bulkSet[i] = state.SetRequest{Key: u.ID, Value: u} + } + + err := store.BulkSet(t.Context(), bulkSet, state.BulkStoreOpts{}) + require.NoError(t, err) + assertUserCountIsEqualTo(t, store, len(initialUsers)) + + // Ensure initial users are correctly stored + loadedUsers := make([]userWithEtag, len(initialUsers)) + for i, u := range initialUsers { + loaded, etag := assertLoadedUserIsEqual(t, store, u.ID, u) + loadedWithEtag := userWithEtag{loaded, etag} + loadedUsers[i] = loadedWithEtag + } + totalUsers := len(loadedUsers) + + userIndex := 0 + t.Run("Update and delete without etag should work", func(t *testing.T) { + toDelete := loadedUsers[userIndex].user + original := loadedUsers[userIndex+1] + modified := original.user + modified.FavoriteBeverage = beverageTea + + localErr := store.Multi(t.Context(), &state.TransactionalStateRequest{ + Operations: []state.TransactionalStateOperation{ + state.DeleteRequest{Key: toDelete.ID}, + state.SetRequest{Key: modified.ID, Value: modified}, + }, + }) + require.NoError(t, localErr) + assertLoadedUserIsEqual(t, store, modified.ID, modified) + assertUserDoesNotExist(t, store, toDelete.ID) + + totalUsers-- + assertUserCountIsEqualTo(t, store, totalUsers) + + userIndex += 2 + }) + + t.Run("Update, delete and insert should work", func(t *testing.T) { + toDelete := loadedUsers[userIndex] + toModify := loadedUsers[userIndex+1] + toInsert := user{keyGen.NextKey(), "Susan", "Soda"} + modified := toModify.user + modified.FavoriteBeverage = beverageTea + + err = store.Multi(t.Context(), &state.TransactionalStateRequest{ + Operations: []state.TransactionalStateOperation{ + state.DeleteRequest{Key: toDelete.ID, ETag: &toDelete.etag}, + state.SetRequest{Key: modified.ID, Value: modified, ETag: &toModify.etag}, + state.SetRequest{Key: toInsert.ID, Value: toInsert}, + }, + }) + require.NoError(t, err) + assertLoadedUserIsEqual(t, store, modified.ID, modified) + assertLoadedUserIsEqual(t, store, toInsert.ID, toInsert) + assertUserDoesNotExist(t, store, toDelete.ID) + + // we added 1 and deleted 1, so totalUsers should have no change + assertUserCountIsEqualTo(t, store, totalUsers) + + userIndex += 2 + }) + + t.Run("Update and delete with etag should work", func(t *testing.T) { + toDelete := loadedUsers[userIndex] + toModify := loadedUsers[userIndex+1] + modified := toModify.user + modified.FavoriteBeverage = beverageTea + + err = store.Multi(t.Context(), &state.TransactionalStateRequest{ + Operations: []state.TransactionalStateOperation{ + state.DeleteRequest{Key: toDelete.ID, ETag: &toDelete.etag}, + state.SetRequest{Key: modified.ID, Value: modified, ETag: &toModify.etag}, + }, + }) + require.NoError(t, err) + assertLoadedUserIsEqual(t, store, modified.ID, modified) + assertUserDoesNotExist(t, store, toDelete.ID) + + totalUsers-- + assertUserCountIsEqualTo(t, store, totalUsers) + + userIndex += 2 + }) + + t.Run("Delete fails, should abort insert", func(t *testing.T) { + toDelete := loadedUsers[userIndex] + toInsert := user{keyGen.NextKey(), "Wont-be-inserted", "Beer"} + + invEtag := invalidEtag + err = store.Multi(t.Context(), &state.TransactionalStateRequest{ + Operations: []state.TransactionalStateOperation{ + state.DeleteRequest{Key: toDelete.ID, ETag: &invEtag}, + state.SetRequest{Key: toInsert.ID, Value: toInsert}, + }, + }) + + require.Error(t, err) + assertUserDoesNotExist(t, store, toInsert.ID) + assertLoadedUserIsEqual(t, store, toDelete.ID, toDelete.user) + + assertUserCountIsEqualTo(t, store, totalUsers) + }) + + t.Run("Delete fails, should abort update", func(t *testing.T) { + toDelete := loadedUsers[userIndex] + toModify := loadedUsers[userIndex+1] + modified := toModify.user + modified.FavoriteBeverage = beverageTea + + invEtag := invalidEtag + err = store.Multi(t.Context(), &state.TransactionalStateRequest{ + Operations: []state.TransactionalStateOperation{ + state.DeleteRequest{Key: toDelete.ID, ETag: &invEtag}, + state.SetRequest{Key: modified.ID, Value: modified}, + }, + }) + require.Error(t, err) + assertLoadedUserIsEqual(t, store, toDelete.ID, toDelete.user) + assertLoadedUserIsEqual(t, store, toModify.ID, toModify.user) + + assertUserCountIsEqualTo(t, store, totalUsers) + }) + + t.Run("Update fails, should abort delete", func(t *testing.T) { + toDelete := loadedUsers[userIndex] + toModify := loadedUsers[userIndex+1] + modified := toModify.user + modified.FavoriteBeverage = beverageTea + + invEtag := invalidEtag + err = store.Multi(t.Context(), &state.TransactionalStateRequest{ + Operations: []state.TransactionalStateOperation{ + state.DeleteRequest{Key: toDelete.ID}, + state.SetRequest{Key: modified.ID, Value: modified, ETag: &invEtag}, + }, + }) + + require.Error(t, err) + assertLoadedUserIsEqual(t, store, toDelete.ID, toDelete.user) + assertLoadedUserIsEqual(t, store, toModify.ID, toModify.user) + + assertUserCountIsEqualTo(t, store, totalUsers) + }) + }) + } +} + +/* #nosec. */ +func testInsertAndUpdateSetRecordDates(t *testing.T) { + const maxDiffInMs = float64(500) + store := getTestStore(t, "") + + u := user{"1", "John", "Coffee"} + err := store.Set(t.Context(), &state.SetRequest{Key: u.ID, Value: u}) + require.NoError(t, err) + + var originalInsertTime time.Time + getUserTsql := fmt.Sprintf("SELECT [InsertDate], [UpdateDate] from [%s].[%s] WHERE [Key]='%s'", store.metadata.SchemaName, store.metadata.TableName, u.ID) + assertDBQuery(t, store, getUserTsql, func(t *testing.T, rows *sql.Rows) { + assert.True(t, rows.Next()) + + var insertDate, updateDate sql.NullTime + localErr := rows.Scan(&insertDate, &updateDate) + require.NoError(t, localErr) + + assert.True(t, insertDate.Valid) + insertDiff := float64(time.Now().UTC().Sub(insertDate.Time).Milliseconds()) + assert.LessOrEqual(t, math.Abs(insertDiff), maxDiffInMs) + assert.False(t, updateDate.Valid) + + originalInsertTime = insertDate.Time + }) + + modified := u + modified.FavoriteBeverage = beverageTea + err = store.Set(t.Context(), &state.SetRequest{Key: modified.ID, Value: modified}) + require.NoError(t, err) + assertDBQuery(t, store, getUserTsql, func(t *testing.T, rows *sql.Rows) { + assert.True(t, rows.Next()) + + var insertDate, updateDate sql.NullTime + err := rows.Scan(&insertDate, &updateDate) + require.NoError(t, err) + + assert.True(t, insertDate.Valid) + assert.Equal(t, originalInsertTime, insertDate.Time) + + assert.True(t, updateDate.Valid) + updateDiff := float64(time.Now().UTC().Sub(updateDate.Time).Milliseconds()) + assert.LessOrEqual(t, math.Abs(updateDiff), maxDiffInMs) + }) +} + +func testConcurrentSets(t *testing.T) { + const parallelism = 10 + + store := getTestStore(t, "") + + u := user{"1", "John", "Coffee"} + err := store.Set(t.Context(), &state.SetRequest{Key: u.ID, Value: u}) + require.NoError(t, err) + + _, etag := assertLoadedUserIsEqual(t, store, u.ID, u) + + var wc sync.WaitGroup + start := make(chan bool, parallelism) + totalErrors := int32(0) + totalSucceeds := int32(0) + for range parallelism { + wc.Add(1) + go func(id, etag string, start <-chan bool, wc *sync.WaitGroup, store *SQLServer) { + <-start + + defer wc.Done() + + modified := user{"1", "John", beverageTea} + err := store.Set(t.Context(), &state.SetRequest{Key: id, Value: modified, ETag: &etag}) + if err != nil { + atomic.AddInt32(&totalErrors, 1) + } else { + atomic.AddInt32(&totalSucceeds, 1) + } + }(u.ID, etag, start, &wc, store) + } + + close(start) + wc.Wait() + + assert.Equal(t, int32(parallelism-1), totalErrors) + assert.Equal(t, int32(1), totalSucceeds) +} + +func testMultipleInitializations(t *testing.T) { + tests := []struct { + name string + kt KeyType + indexedProperties string + }{ + {"No indexed properties", StringKeyType, ""}, + {"With indexed properties", StringKeyType, `[{ "column":"FavoriteBeverage", "property":"FavoriteBeverage", "type":"nvarchar(100)"}, { "column":"PetsCount", "property":"PetsCount", "type": "INTEGER"}]`}, + {"No indexed properties uuid key type", UUIDKeyType, ""}, + {"No indexed properties integer key type", IntegerKeyType, ""}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store := getTestStoreWithKeyType(t, test.kt, test.indexedProperties) + + store2 := &SQLServer{ + logger: logger.NewLogger("test"), + migratorFactory: newMigration, + } + store2.BulkStore = state.NewDefaultBulkStore(store2) + err := store2.Init(t.Context(), createMetadata(store.metadata.SchemaName, test.kt, test.indexedProperties)) + require.NoError(t, err) + }) + } +} + +func testNonBase64ByteData(t *testing.T) { + t.Run("Set And Get Proto", func(t *testing.T) { + store := getTestStore(t, "") + request := &sqlserver.TestEvent{ + EventId: -1, + } + requestBytes, err := proto.Marshal(request) + require.NoError(t, err) + require.NoError(t, store.Set(t.Context(), &state.SetRequest{Key: "1", Value: requestBytes})) + resp, err := store.Get(t.Context(), &state.GetRequest{Key: "1"}) + require.NoError(t, err) + + response := &sqlserver.TestEvent{} + err = proto.Unmarshal(resp.Data, response) + require.NoError(t, err) + + assert.EqualValues(t, request.GetEventId(), response.GetEventId()) + }) + + t.Run("Set And Get Json", func(t *testing.T) { + store := getTestStore(t, "") + request := &sqlserver.TestEvent{ + EventId: -1, + } + requestBytes, err := json.Marshal(request) + require.NoError(t, err) + require.NoError(t, store.Set(t.Context(), &state.SetRequest{Key: "1", Value: requestBytes})) + resp, err := store.Get(t.Context(), &state.GetRequest{Key: "1"}) + require.NoError(t, err) + + response := &sqlserver.TestEvent{} + err = json.Unmarshal(resp.Data, response) + require.NoError(t, err) + + assert.EqualValues(t, request.GetEventId(), response.GetEventId()) + }) + + t.Run("Set And Get Indexed Json", func(t *testing.T) { + store := getTestStore(t, `[{"column": "eventid", "property": "EventId", "type": "int"}]`) + request := &sqlserver.TestEvent{ + EventId: -1, + } + requestBytes, err := json.Marshal(request) + require.NoError(t, err) + require.NoError(t, store.Set(t.Context(), &state.SetRequest{Key: "1", Value: requestBytes})) + resp, err := store.Get(t.Context(), &state.GetRequest{Key: "1"}) + require.NoError(t, err) + + response := &sqlserver.TestEvent{} + err = json.Unmarshal(resp.Data, response) + require.NoError(t, err) + + assert.EqualValues(t, request.GetEventId(), response.GetEventId()) + }) +} diff --git a/state/sqlserver/v2/sqlserver_test.go b/state/sqlserver/v2/sqlserver_test.go new file mode 100644 index 0000000000..4f9c5117a5 --- /dev/null +++ b/state/sqlserver/v2/sqlserver_test.go @@ -0,0 +1,556 @@ +/* +Copyright 2021 The Dapr Authors +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 sqlserver + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/common/authentication/sqlserver" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" + "github.com/dapr/kit/logger" +) + +const ( + sampleConnectionString = "server=localhost;user id=sa;password=Pass@Word1;port=1433;database=sample;" + sampleUserTableName = "Users" + defaultDatabase = "dapr" + defaultSchema = "dbo" +) + +type mockMigrator struct{} + +func (m *mockMigrator) executeMigrations(context.Context) (migrationResult, error) { + r := migrationResult{} + + return r, nil +} + +type mockFailingMigrator struct{} + +func (m *mockFailingMigrator) executeMigrations(context.Context) (migrationResult, error) { + r := migrationResult{} + + return r, errors.New("migration failed") +} + +func TestValidConfiguration(t *testing.T) { + tests := map[string]struct { + props map[string]string + expected SQLServer + }{ + "No schema": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: sampleUserTableName, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "Custom schema": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "schema": "mytest"}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: "mytest", + }, + TableName: sampleUserTableName, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "String key type": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "keyType": "string"}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: sampleUserTableName, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "Unique identifier key type": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "keyType": "uuid"}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: sampleUserTableName, + keyTypeParsed: UUIDKeyType, + keyLengthParsed: 0, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "Integer identifier key type": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "keyType": "integer"}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: sampleUserTableName, + keyTypeParsed: IntegerKeyType, + keyLengthParsed: 0, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "Custom key length": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "keyLength": "100"}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: sampleUserTableName, + keyTypeParsed: StringKeyType, + keyLengthParsed: 100, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "Single indexed property": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "indexedProperties": `[{"column": "Age","property":"age", "type":"int"}]`}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: sampleUserTableName, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + indexedPropertiesParsed: []IndexedProperty{ + {ColumnName: "Age", Property: "age", Type: "int"}, + }, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "Multiple indexed properties": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "indexedProperties": `[{"column": "Age","property":"age", "type":"int"}, {"column": "Name","property":"name", "type":"nvarchar(100)"}]`}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: sampleUserTableName, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + indexedPropertiesParsed: []IndexedProperty{ + {ColumnName: "Age", Property: "age", Type: "int"}, + {ColumnName: "Name", Property: "name", Type: "nvarchar(100)"}, + }, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "Custom database": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "databaseName": "dapr_test_table"}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: "dapr_test_table", + SchemaName: defaultSchema, + }, + TableName: sampleUserTableName, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "No table": { + props: map[string]string{"connectionString": sampleConnectionString}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: defaultTable, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + MetadataTableName: defaultMetaTable, + }, + }, + }, + "Custom meta table": { + props: map[string]string{"connectionString": sampleConnectionString, "metadataTableName": "dapr_test_meta_table"}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: defaultTable, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + MetadataTableName: "dapr_test_meta_table", + }, + }, + }, + "Actor state store true": { + props: map[string]string{"connectionString": sampleConnectionString, "metadataTableName": "dapr_test_meta_table", "actorStateStore": "true"}, + expected: SQLServer{ + metadata: sqlServerMetadata{ + SQLServerAuthMetadata: sqlserver.SQLServerAuthMetadata{ + ConnectionString: sampleConnectionString, + DatabaseName: defaultDatabase, + SchemaName: defaultSchema, + }, + TableName: defaultTable, + keyTypeParsed: StringKeyType, + keyLengthParsed: defaultKeyLength, + MetadataTableName: "dapr_test_meta_table", + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + sqlStore := &SQLServer{ + logger: logger.NewLogger("test"), + migratorFactory: func(*sqlServerMetadata) migrator { + return &mockMigrator{} + }, + } + + metadata := state.Metadata{ + Base: metadata.Base{Properties: tt.props}, + } + + err := sqlStore.Init(t.Context(), metadata) + require.NoError(t, err) + assert.Equal(t, tt.expected.metadata.ConnectionString, sqlStore.metadata.ConnectionString) + assert.Equal(t, tt.expected.metadata.TableName, sqlStore.metadata.TableName) + assert.Equal(t, tt.expected.metadata.SchemaName, sqlStore.metadata.SchemaName) + assert.Equal(t, tt.expected.metadata.keyTypeParsed, sqlStore.metadata.keyTypeParsed) + assert.Equal(t, tt.expected.metadata.keyLengthParsed, sqlStore.metadata.keyLengthParsed) + assert.Equal(t, tt.expected.metadata.DatabaseName, sqlStore.metadata.DatabaseName) + assert.Equal(t, tt.expected.metadata.MetadataTableName, sqlStore.metadata.MetadataTableName) + + assert.Equal(t, len(tt.expected.metadata.indexedPropertiesParsed), len(sqlStore.metadata.indexedPropertiesParsed)) + if len(tt.expected.metadata.indexedPropertiesParsed) > 0 && len(tt.expected.metadata.indexedPropertiesParsed) == len(sqlStore.metadata.indexedPropertiesParsed) { + for i, e := range tt.expected.metadata.indexedPropertiesParsed { + assert.Equal(t, e.ColumnName, sqlStore.metadata.indexedPropertiesParsed[i].ColumnName) + assert.Equal(t, e.Property, sqlStore.metadata.indexedPropertiesParsed[i].Property) + assert.Equal(t, e.Type, sqlStore.metadata.indexedPropertiesParsed[i].Type) + } + } + }) + } +} + +func TestInvalidConfiguration(t *testing.T) { + tests := map[string]struct { + props map[string]string + expectedErr string + }{ + "Empty": { + props: map[string]string{}, + expectedErr: "missing connection string", + }, + "Empty connection string": { + props: map[string]string{"connectionString": ""}, + expectedErr: "missing connection string", + }, + "Negative maxKeyLength value": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "keyLength": "-1"}, + expectedErr: "invalid key length value of -1", + }, + "Indexes properties are not valid json": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": "no_json"}, + expectedErr: "invalid character", + }, + "Invalid table name with ;": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test;"}, + expectedErr: "invalid table name", + }, + "Invalid table name with space": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test GO DROP DATABASE dapr_test"}, + expectedErr: "invalid table name", + }, + "Invalid metadata table name with ;": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "metadataTableName": "test;"}, + expectedErr: "invalid metadata table name", + }, + "Invalid metadata table name with space": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "metadataTableName": "test GO DROP DATABASE dapr_test"}, + expectedErr: "invalid metadata table name", + }, + "Invalid schema name with ;": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "schema": "test;"}, + expectedErr: "invalid schema name", + }, + "Invalid schema name with space": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "schema": "test GO DROP DATABASE dapr_test"}, + expectedErr: "invalid schema name", + }, + "Invalid index property column name with ;": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"test;", "property": "age", "type": "INT"}]`}, + expectedErr: "invalid indexed property column name", + }, + "Invalid index property column name with space": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"test GO DROP DATABASE dapr_test", "property": "age", "type": "INT"}]`}, + expectedErr: "invalid indexed property column name", + }, + "Invalid index property name with ;": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"age", "property": "test;", "type": "INT"}]`}, + expectedErr: "invalid indexed property name", + }, + "Invalid index property name with space": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"age", "property": "test GO DROP DATABASE dapr_test", "type": "INT"}]`}, + expectedErr: "invalid indexed property name", + }, + "Invalid index property type with ;": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"age", "property": "age", "type": "INT;"}]`}, + expectedErr: "invalid indexed property type", + }, + "Invalid index property type with space": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"age", "property": "age", "type": "INT GO DROP DATABASE dapr_test"}]`}, + expectedErr: "invalid indexed property type", + }, + "Index property column cannot be empty": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"", "property": "age", "type": "INT"}]`}, + expectedErr: "indexed property column cannot be empty", + }, + "Invalid property name cannot be empty": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"age", "property": "", "type": "INT"}]`}, + expectedErr: "indexed property name cannot be empty", + }, + "Invalid property type cannot be empty": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "indexedProperties": `[{"column":"age", "property": "age", "type": ""}]`}, + expectedErr: "indexed property type cannot be empty", + }, + "Invalid database name with ;": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "databaseName": "test;"}, + expectedErr: "invalid database name", + }, + "Invalid database name with space": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "databaseName": "test GO DROP DATABASE dapr_test"}, + expectedErr: "invalid database name", + }, + "Invalid key type invalid": { + props: map[string]string{"connectionString": sampleConnectionString, "tableName": "test", "keyType": "invalid"}, + expectedErr: "invalid key type", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + sqlStore := &SQLServer{ + logger: logger.NewLogger("test"), + } + + metadata := state.Metadata{ + Base: metadata.Base{Properties: tt.props}, + } + + err := sqlStore.Init(t.Context(), metadata) + require.Error(t, err) + + if tt.expectedErr != "" { + require.ErrorContains(t, err, tt.expectedErr) + } + }) + } +} + +func TestCleanupInterval(t *testing.T) { + t.Run("cleanupInterval not set", func(t *testing.T) { + properties := map[string]string{ + "url": "test", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + assert.Equal(t, "test", md.ConnectionString) + require.NotNil(t, md.CleanupInterval) + assert.Equal(t, defaultCleanupInterval, *md.CleanupInterval) + }) + + t.Run("cleanupInterval as Go duration", func(t *testing.T) { + properties := map[string]string{ + "connectionString": "test", + "cleanupInterval": "1m", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + assert.Equal(t, "test", md.ConnectionString) + require.NotNil(t, md.CleanupInterval) + assert.Equal(t, time.Minute, *md.CleanupInterval) + }) + + t.Run("cleanupInterval as seconds", func(t *testing.T) { + properties := map[string]string{ + "connectionString": "test", + "cleanupInterval": "10", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + assert.Equal(t, "test", md.ConnectionString) + require.NotNil(t, md.CleanupInterval) + assert.Equal(t, 10*time.Second, *md.CleanupInterval) + }) + + t.Run("cleanupIntervalInSeconds as Go duration", func(t *testing.T) { + properties := map[string]string{ + "connectionString": "test", + "cleanupIntervalInSeconds": "1m", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + require.NotNil(t, md.CleanupInterval) + assert.Equal(t, time.Minute, *md.CleanupInterval) + }) + + t.Run("cleanupIntervalInSeconds as seconds", func(t *testing.T) { + properties := map[string]string{ + "connectionString": "test", + "cleanupIntervalInSeconds": "10", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + require.NotNil(t, md.CleanupInterval) + assert.Equal(t, 10*time.Second, *md.CleanupInterval) + }) + + t.Run("cleanupInterval as 0", func(t *testing.T) { + properties := map[string]string{ + "connectionString": "test", + "cleanupInterval": "0", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + require.Nil(t, md.CleanupInterval) + }) + + t.Run("cleanupIntervallInSeconds as 0", func(t *testing.T) { + properties := map[string]string{ + "connectionString": "test", + "cleanupIntervalInSeconds": "0", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + require.Nil(t, md.CleanupInterval) + }) + + t.Run("cleanupInterval negative", func(t *testing.T) { + properties := map[string]string{ + "connectionString": "test", + "cleanupInterval": "-1", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + require.Nil(t, md.CleanupInterval) + }) + + t.Run("cleanupIntervallInSeconds negative", func(t *testing.T) { + properties := map[string]string{ + "connectionString": "test", + "cleanupIntervalInSeconds": "-1", + } + + md := newMetadata() + err := md.Parse(properties) + require.NoError(t, err) + require.Nil(t, md.CleanupInterval) + }) +} + +// Test that if the migration fails the error is reported. +func TestExecuteMigrationFails(t *testing.T) { + sqlStore := &SQLServer{ + logger: logger.NewLogger("test"), + migratorFactory: func(*sqlServerMetadata) migrator { + return &mockFailingMigrator{} + }, + } + + metadata := state.Metadata{ + Base: metadata.Base{Properties: map[string]string{"connectionString": sampleConnectionString, "tableName": sampleUserTableName, "databaseName": "dapr_test_table"}}, + } + + err := sqlStore.Init(t.Context(), metadata) + require.Error(t, err) +} + +func TestSupportedFeatures(t *testing.T) { + sqlStore := &SQLServer{ + features: []state.Feature{state.FeatureETag, state.FeatureTransactional}, + logger: logger.NewLogger("test"), + } + + actual := sqlStore.Features() + assert.NotNil(t, actual) + assert.Equal(t, state.FeatureETag, actual[0]) + assert.Equal(t, state.FeatureTransactional, actual[1]) +} diff --git a/state/store.go b/state/store.go index 1e79b6a620..093ac6f23e 100644 --- a/state/store.go +++ b/state/store.go @@ -71,3 +71,9 @@ func Ping(ctx context.Context, store Store) error { type DeleteWithPrefix interface { DeleteWithPrefix(ctx context.Context, req DeleteWithPrefixRequest) (DeleteWithPrefixResponse, error) } + +// KeysLiker is an optional interface to list state keys with an +// optional SQL style wildcard pattern. +type KeysLiker interface { + KeysLike(ctx context.Context, req *KeysLikeRequest) (*KeysLikeResponse, error) +} diff --git a/state/zookeeper/metadata.yaml b/state/zookeeper/metadata.yaml index d9b381da86..80deb68671 100644 --- a/state/zookeeper/metadata.yaml +++ b/state/zookeeper/metadata.yaml @@ -20,17 +20,17 @@ metadata: description: Session timeout in seconds. example: "10s" - name: maxBufferSize - type: integer + type: number required: false description: The maximum buffer size in bytes. - example: 1048576 - default: 1048576 # 1MB + example: "1048576" + default: "1048576" # 1MB - name: maxConnBufferSize - type: integer + type: number required: false description: The maximum connection buffer size in bytes. - example: 1048576 - default: 1048576 # 1MB + example: "1048576" + default: "1048576" # 1MB - name: keyPrefixPath type: string required: false diff --git a/tests/certification/bindings/aws/s3/s3_test.go b/tests/certification/bindings/aws/s3/s3_test.go index c815901f8c..e4507f522f 100644 --- a/tests/certification/bindings/aws/s3/s3_test.go +++ b/tests/certification/bindings/aws/s3/s3_test.go @@ -37,7 +37,7 @@ import ( "github.com/dapr/components-contrib/tests/certification/flow" "github.com/dapr/components-contrib/tests/certification/flow/sidecar" - "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3" ) const ( diff --git a/tests/certification/bindings/kafka/components/consumer1/kafka.yaml b/tests/certification/bindings/kafka/components/consumer1/kafka.yaml index 5b8ab3c347..f01fb71c85 100644 --- a/tests/certification/bindings/kafka/components/consumer1/kafka.yaml +++ b/tests/certification/bindings/kafka/components/consumer1/kafka.yaml @@ -24,3 +24,5 @@ spec: value: 50ms - name: backOffDuration value: 50ms + - name: clientConnectionTopicMetadataRefreshInterval + value: 15s diff --git a/tests/certification/bindings/kafka/components/consumer2/kafka.yaml b/tests/certification/bindings/kafka/components/consumer2/kafka.yaml index ae472a2f31..3b979559f9 100644 --- a/tests/certification/bindings/kafka/components/consumer2/kafka.yaml +++ b/tests/certification/bindings/kafka/components/consumer2/kafka.yaml @@ -24,3 +24,5 @@ spec: value: 50ms - name: backOffDuration value: 50ms + - name: clientConnectionTopicMetadataRefreshInterval + value: 15s diff --git a/tests/certification/bindings/kafka/components/sasl-password/kafka-binding.yaml b/tests/certification/bindings/kafka/components/sasl-password/kafka-binding.yaml index 7eb27f0483..6b623b73af 100644 --- a/tests/certification/bindings/kafka/components/sasl-password/kafka-binding.yaml +++ b/tests/certification/bindings/kafka/components/sasl-password/kafka-binding.yaml @@ -24,3 +24,5 @@ spec: value: admin-secret - name: disableTls value: "true" + - name: clientConnectionTopicMetadataRefreshInterval + value: 15s diff --git a/tests/certification/bindings/mysql/README.md b/tests/certification/bindings/mysql/README.md new file mode 100644 index 0000000000..7a31cad86a --- /dev/null +++ b/tests/certification/bindings/mysql/README.md @@ -0,0 +1,25 @@ +# MySQL Output Binding Certification + +The purpose of this module is to provide tests that certify the MySQL Output Binding as a stable component. + +## Test plan + +* Verify the mysql is created/present + * Create component spec + * Run dapr application with component + * Ensure the mysql is present +* Verify the connection is established to mysql. + * Create component spec. + * Run dapr application with component. + * Ensure that you have access to mysql and connection to mysql DB is established. +* Verify data is getting stored in mysql DB. + * Create component spec with the data to be stored. + * Run dapr application with component to store data in mysql as output binding. + * Read stored data from mysql. + * Ensure that read data is same as the data that was stored. + * Verify the ability to use named parameters in queries. +* Verify reconnection to mysql for output binding. + * Simulate a network error before sending any messages. + * Run dapr application with the component. + * After the reconnection, send messages to mysql. + * Ensure that the messages sent after the reconnection are stored in mysql. diff --git a/tests/certification/bindings/mysql/components/standard/mysql.yaml b/tests/certification/bindings/mysql/components/standard/mysql.yaml new file mode 100644 index 0000000000..d1e7601e2d --- /dev/null +++ b/tests/certification/bindings/mysql/components/standard/mysql.yaml @@ -0,0 +1,10 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: standard-binding +spec: + type: bindings.mysql + version: v1 + metadata: + - name: url + value: "mysql:example@tcp(localhost:3306)/dapr_test?allowNativePasswords=true" diff --git a/tests/certification/bindings/mysql/config.yaml b/tests/certification/bindings/mysql/config.yaml new file mode 100644 index 0000000000..6c95e632ff --- /dev/null +++ b/tests/certification/bindings/mysql/config.yaml @@ -0,0 +1,6 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: keyvaultconfig +spec: + features: diff --git a/tests/certification/bindings/mysql/docker-compose.yml b/tests/certification/bindings/mysql/docker-compose.yml new file mode 100644 index 0000000000..4d45978d4c --- /dev/null +++ b/tests/certification/bindings/mysql/docker-compose.yml @@ -0,0 +1,13 @@ +version: '2' +services: + db: + image: mysql:8 + command: --mysql_native_password=ON + restart: always + ports: + - "3306:3306" + environment: + MYSQL_USER: mysql + MYSQL_PASSWORD: example + MYSQL_DATABASE: dapr_test + MYSQL_ROOT_PASSWORD: root diff --git a/tests/certification/bindings/mysql/mysql_test.go b/tests/certification/bindings/mysql/mysql_test.go new file mode 100644 index 0000000000..c306a381ff --- /dev/null +++ b/tests/certification/bindings/mysql/mysql_test.go @@ -0,0 +1,412 @@ +/* +Copyright 2022 The Dapr Authors +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 mysql_test + +import ( + "database/sql" + "fmt" + "strconv" + "testing" + "time" + + // MySQL driver for database/sql + _ "github.com/go-sql-driver/mysql" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/bindings" + binding_mysql "github.com/dapr/components-contrib/bindings/mysql" + bindings_loader "github.com/dapr/dapr/pkg/components/bindings" + dapr_testing "github.com/dapr/dapr/pkg/testing" + daprClient "github.com/dapr/go-sdk/client" + "github.com/dapr/kit/logger" + + "github.com/dapr/components-contrib/tests/certification/embedded" + "github.com/dapr/components-contrib/tests/certification/flow" + "github.com/dapr/components-contrib/tests/certification/flow/dockercompose" + "github.com/dapr/components-contrib/tests/certification/flow/network" + "github.com/dapr/components-contrib/tests/certification/flow/sidecar" +) + +const ( + dockerComposeYAML = "docker-compose.yml" + numOfMessages = 10 + dockerConnectionString = "mysql:example@tcp(localhost:3306)/dapr_test?allowNativePasswords=true" +) + +// MySQL doesn't accept RFC3339 formatted time, rejects trailing 'Z' for UTC indicator. +const mySQLDateTimeFormat = "2006-01-02 15:04:05" + +func TestMysql(t *testing.T) { + const tableName = "dapr_test_table" + + ports, _ := dapr_testing.GetFreePorts(3) + grpcPort := ports[0] + httpPort := ports[1] + + testExec := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) + require.NoError(t, err, "Could not initialize dapr client") + + ctx.Log("Invoking output binding for exec operation") + err = client.InvokeOutputBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": fmt.Sprintf("INSERT INTO %s (id, c1, ts) VALUES (1, 'demo', '%s');", tableName, time.Now().Format(mySQLDateTimeFormat)), + }, + }) + require.NoError(ctx, err, "error in output binding - exec") + + ctx.Log("Invoking output binding for exec operation with parameters") + err = client.InvokeOutputBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": fmt.Sprintf("INSERT INTO %s (id, c1, ts) VALUES (?, ?, ?);", tableName), + "params": fmt.Sprintf(`[2, "demo2", "%s"]`, time.Now().Add(time.Hour).Format(mySQLDateTimeFormat)), + }, + }) + require.NoError(ctx, err, "error in output binding - exec") + + return nil + } + + testQuery := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) + require.NoError(t, err, "Could not initialize dapr client") + + ctx.Log("Invoking output binding for query operation") + resp, err := client.InvokeBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "query", + Metadata: map[string]string{ + "sql": "SELECT * FROM " + tableName + " WHERE id = 1;", + }, + }) + require.NoError(ctx, err, "error in output binding - query") + assert.Contains(t, string(resp.Data), `"id":1`) + assert.Contains(t, string(resp.Data), `"c1":"demo"`) + + ctx.Log("Invoking output binding for query operation with parameters") + resp, err = client.InvokeBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "query", + Metadata: map[string]string{ + "sql": "SELECT * FROM " + tableName + " WHERE id IN (?, ?);", + "params": `[1, 2]`, + }, + }) + require.NoError(ctx, err, "error in output binding - query") + assert.Contains(t, string(resp.Data), `"id":1`) + assert.Contains(t, string(resp.Data), `"id":2`) + + return nil + } + + testClose := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) + require.NoError(ctx, err, "Could not initialize dapr client.") + + metadata := make(map[string]string) + + ctx.Log("Invoking output binding for close operation!") + req := &daprClient.InvokeBindingRequest{Name: "standard-binding", Operation: "close", Metadata: metadata} + errBinding := client.InvokeOutputBinding(ctx, req) + require.NoError(ctx, errBinding, "error in output binding - close") + + ctx.Log("Invoking output binding for query operation!") + req = &daprClient.InvokeBindingRequest{Name: "standard-binding", Operation: "query", Metadata: metadata} + req.Metadata["sql"] = "SELECT * FROM " + tableName + " WHERE id = 1;" + errBinding = client.InvokeOutputBinding(ctx, req) + require.Error(ctx, errBinding, "error in output binding - query") + + return nil + } + + createTable := func(ctx flow.Context) error { + db, err := sql.Open("mysql", dockerConnectionString) + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE " + tableName + " (id INT, c1 TEXT, ts TIMESTAMP);") + require.NoError(t, err) + db.Close() + return nil + } + + flow.New(t, "Run tests"). + Step(dockercompose.Run("db", dockerComposeYAML)). + Step("wait for component to start", flow.Sleep(10*time.Second)). + Step("Creating table", createTable). + Step(sidecar.Run("standardSidecar", + append(componentRuntimeOptions(), + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(grpcPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(httpPort)), + embedded.WithComponentsPath("./components/standard"), + )..., + )). + Step("Run exec test", testExec). + Step("Run query test", testQuery). + Step("Run close test", testClose). + Step("stop mysql", dockercompose.Stop("db", dockerComposeYAML, "db")). + Run() +} + +func TestMysqlNetworkError(t *testing.T) { + const tableName = "dapr_test_table_network" + + ports, _ := dapr_testing.GetFreePorts(3) + grpcPort := ports[0] + httpPort := ports[1] + + testExec := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) + require.NoError(t, err, "Could not initialize dapr client") + + ctx.Log("Invoking output binding for exec operation!") + errBinding := client.InvokeOutputBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": fmt.Sprintf("INSERT INTO %s (id, c1, ts) VALUES (1, 'demo', '%s');", tableName, time.Now().Format(mySQLDateTimeFormat)), + }, + }) + require.NoError(ctx, errBinding, "error in output binding - exec") + + return nil + } + + testQuery := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) + require.NoError(t, err, "Could not initialize dapr client") + + ctx.Log("Invoking output binding for query operation!") + _, errBinding := client.InvokeBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "query", + Metadata: map[string]string{ + "sql": "SELECT * FROM " + tableName + " WHERE id = 1;", + }, + }) + require.NoError(ctx, errBinding, "error in output binding - query") + + return nil + } + + createTable := func(ctx flow.Context) error { + db, err := sql.Open("mysql", dockerConnectionString) + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE " + tableName + " (id INT, c1 TEXT, ts TIMESTAMP);") + require.NoError(t, err) + db.Close() + return nil + } + + flow.New(t, "Run tests"). + Step(dockercompose.Run("db", dockerComposeYAML)). + Step("wait for component to start", flow.Sleep(10*time.Second)). + Step("Creating table", createTable). + Step(sidecar.Run("standardSidecar", + append(componentRuntimeOptions(), + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(grpcPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(httpPort)), + embedded.WithComponentsPath("./components/standard"), + )..., + )). + Step("Run exec test", testExec). + Step("Run query test", testQuery). + Step("wait for DB operations to complete", flow.Sleep(5*time.Second)). + Step("interrupt network", network.InterruptNetwork(20*time.Second, nil, nil, "3306")). + Step("wait for component to recover", flow.Sleep(10*time.Second)). + Step("Run query test", testQuery). + Run() +} + +func TestMysqlExecEncoding(t *testing.T) { + const tableName = "dapr_test_table_encoding" + + ports, _ := dapr_testing.GetFreePorts(3) + grpcPort := ports[0] + httpPort := ports[1] + + testExecEncoding := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) + require.NoError(t, err, "Could not initialize dapr client") + + ctx.Log("Testing exec binding response encoding for INSERT operation") + resp, err := client.InvokeBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": fmt.Sprintf("INSERT INTO %s (id, c1, ts) VALUES (1, 'test1', '%s');", tableName, time.Now().Format(mySQLDateTimeFormat)), + }, + }) + require.NoError(ctx, err, "error in output binding - exec") + require.NotNil(ctx, resp, "response should not be nil") + require.NotNil(ctx, resp.Metadata, "response metadata should not be nil") + + // Verify rows-affected metadata exists and is a string + rowsAffected, exists := resp.Metadata["rows-affected"] + require.True(ctx, exists, "rows-affected metadata should exist") + assert.Equal(t, "1", rowsAffected, "rows-affected should be '1' for single INSERT") + + // Verify the encoding is correct (string, not number) + // Parse to verify it's a valid integer string + rowsCount, err := strconv.ParseInt(rowsAffected, 10, 64) + require.NoError(ctx, err, "rows-affected should be parseable as int64") + assert.Equal(t, int64(1), rowsCount, "rows-affected should be 1") + + ctx.Log("Testing exec binding response encoding for UPDATE operation") + resp, err = client.InvokeBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": fmt.Sprintf("UPDATE %s SET c1 = 'updated' WHERE id = 1;", tableName), + }, + }) + require.NoError(ctx, err, "error in output binding - exec UPDATE") + require.NotNil(ctx, resp, "response should not be nil") + + rowsAffected, exists = resp.Metadata["rows-affected"] + require.True(ctx, exists, "rows-affected metadata should exist for UPDATE") + assert.Equal(t, "1", rowsAffected, "rows-affected should be '1' for single UPDATE") + + // Verify encoding again + rowsCount, err = strconv.ParseInt(rowsAffected, 10, 64) + require.NoError(ctx, err, "rows-affected should be parseable as int64") + assert.Equal(t, int64(1), rowsCount, "rows-affected should be 1") + + ctx.Log("Testing exec binding response encoding for INSERT with multiple rows") + resp, err = client.InvokeBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": fmt.Sprintf("INSERT INTO %s (id, c1, ts) VALUES (2, 'test2', '%s'), (3, 'test3', '%s');", + tableName, time.Now().Format(mySQLDateTimeFormat), time.Now().Format(mySQLDateTimeFormat)), + }, + }) + require.NoError(ctx, err, "error in output binding - exec multi-row INSERT") + require.NotNil(ctx, resp, "response should not be nil") + + rowsAffected, exists = resp.Metadata["rows-affected"] + require.True(ctx, exists, "rows-affected metadata should exist for multi-row INSERT") + assert.Equal(t, "2", rowsAffected, "rows-affected should be '2' for two-row INSERT") + + // Verify encoding for multiple rows + rowsCount, err = strconv.ParseInt(rowsAffected, 10, 64) + require.NoError(ctx, err, "rows-affected should be parseable as int64") + assert.Equal(t, int64(2), rowsCount, "rows-affected should be 2") + + ctx.Log("Testing exec binding response encoding for DELETE operation") + resp, err = client.InvokeBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": fmt.Sprintf("DELETE FROM %s WHERE id IN (2, 3);", tableName), + }, + }) + require.NoError(ctx, err, "error in output binding - exec DELETE") + require.NotNil(ctx, resp, "response should not be nil") + + rowsAffected, exists = resp.Metadata["rows-affected"] + require.True(ctx, exists, "rows-affected metadata should exist for DELETE") + assert.Equal(t, "2", rowsAffected, "rows-affected should be '2' for two-row DELETE") + + // Verify encoding for DELETE + rowsCount, err = strconv.ParseInt(rowsAffected, 10, 64) + require.NoError(ctx, err, "rows-affected should be parseable as int64") + assert.Equal(t, int64(2), rowsCount, "rows-affected should be 2") + + ctx.Log("Testing exec binding response encoding for UPDATE with no matching rows") + resp, err = client.InvokeBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": fmt.Sprintf("UPDATE %s SET c1 = 'no-match' WHERE id = 999;", tableName), + }, + }) + require.NoError(ctx, err, "error in output binding - exec UPDATE with no match") + require.NotNil(ctx, resp, "response should not be nil") + + rowsAffected, exists = resp.Metadata["rows-affected"] + require.True(ctx, exists, "rows-affected metadata should exist even for zero rows") + assert.Equal(t, "0", rowsAffected, "rows-affected should be '0' when no rows match") + + // Verify encoding for zero rows + rowsCount, err = strconv.ParseInt(rowsAffected, 10, 64) + require.NoError(ctx, err, "rows-affected should be parseable as int64") + assert.Equal(t, int64(0), rowsCount, "rows-affected should be 0") + + // Verify other metadata fields are present and correctly encoded + operation, exists := resp.Metadata["operation"] + require.True(ctx, exists, "operation metadata should exist") + assert.Equal(t, "exec", operation, "operation should be 'exec'") + + sql, exists := resp.Metadata["sql"] + require.True(ctx, exists, "sql metadata should exist") + assert.Contains(t, sql, "UPDATE", "sql metadata should contain the SQL statement") + + _, exists = resp.Metadata["start-time"] + require.True(ctx, exists, "start-time metadata should exist") + + _, exists = resp.Metadata["end-time"] + require.True(ctx, exists, "end-time metadata should exist") + + _, exists = resp.Metadata["duration"] + require.True(ctx, exists, "duration metadata should exist") + + return nil + } + + createTable := func(ctx flow.Context) error { + db, err := sql.Open("mysql", dockerConnectionString) + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE " + tableName + " (id INT, c1 TEXT, ts TIMESTAMP);") + require.NoError(t, err) + db.Close() + return nil + } + + flow.New(t, "Run exec encoding tests"). + Step(dockercompose.Run("db", dockerComposeYAML)). + Step("wait for component to start", flow.Sleep(10*time.Second)). + Step("Creating table", createTable). + Step(sidecar.Run("standardSidecar", + append(componentRuntimeOptions(), + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(grpcPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(httpPort)), + embedded.WithComponentsPath("./components/standard"), + )..., + )). + Step("Run exec encoding test", testExecEncoding). + Step("stop mysql", dockercompose.Stop("db", dockerComposeYAML, "db")). + Run() +} + +func componentRuntimeOptions() []embedded.Option { + log := logger.NewLogger("dapr.components") + + bindingsRegistry := bindings_loader.NewRegistry() + bindingsRegistry.Logger = log + bindingsRegistry.RegisterOutputBinding(func(l logger.Logger) bindings.OutputBinding { + return binding_mysql.NewMysql(l) + }, "mysql") + + return []embedded.Option{ + embedded.WithBindings(bindingsRegistry), + } +} diff --git a/tests/certification/bindings/zeebe/command/create_instance_test.go b/tests/certification/bindings/zeebe/command/create_instance_test.go index 1dada16fae..4fa96a01c1 100644 --- a/tests/certification/bindings/zeebe/command/create_instance_test.go +++ b/tests/certification/bindings/zeebe/command/create_instance_test.go @@ -290,6 +290,7 @@ func TestCreateInstanceOperation(t *testing.T) { flow.New(t, "Test create instance operation (async)"). Step(dockercompose.Run("zeebe", zeebe_test.DockerComposeYaml)). Step("Waiting for Zeebe Readiness...", retry.Do(time.Second*3, 10, zeebe_test.CheckZeebeConnection)). + Step(app.Run("workerApp", fmt.Sprintf(":%d", appPort), workers(0))). Step(sidecar.Run(zeebe_test.SidecarName, append(componentRuntimeOptions(), embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), @@ -298,7 +299,6 @@ func TestCreateInstanceOperation(t *testing.T) { embedded.WithResourcesPath("components/standard"), )..., )). - Step(app.Run("workerApp", fmt.Sprintf(":%d", appPort), workers(0))). Step("Waiting for the component to start", flow.Sleep(10*time.Second)). Step("Deploy process in version 1", deployVersion1). Step("Deploy process in version 2", deployVersion2). @@ -314,6 +314,7 @@ func TestCreateInstanceOperation(t *testing.T) { flow.New(t, "Test create instance operation (sync)"). Step(dockercompose.Run("zeebe", zeebe_test.DockerComposeYaml)). Step("Waiting for Zeebe Readiness...", retry.Do(time.Second*3, 10, zeebe_test.CheckZeebeConnection)). + Step(app.Run("workerApp", fmt.Sprintf(":%d", appPort), workers(20*time.Second))). Step(sidecar.Run(zeebe_test.SidecarName, append(componentRuntimeOptions(), embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), @@ -322,7 +323,6 @@ func TestCreateInstanceOperation(t *testing.T) { embedded.WithResourcesPath("components/syncProcessCreation"), )..., )). - Step(app.Run("workerApp", fmt.Sprintf(":%d", appPort), workers(20*time.Second))). Step("Waiting for the component to start", flow.Sleep(10*time.Second)). Step("Deploy process in version 1", deployVersion1). Step("Deploy process in version 2", deployVersion2). diff --git a/tests/certification/embedded/components.go b/tests/certification/embedded/components.go index 9a165099ab..4f2c7f3adc 100644 --- a/tests/certification/embedded/components.go +++ b/tests/certification/embedded/components.go @@ -18,7 +18,7 @@ import ( "github.com/dapr/kit/logger" // Name resolutions. - nrConsul "github.com/dapr/components-contrib/nameresolution/consul" + nrConsul "github.com/dapr/components-contrib/nameresolution/hashicorp/consul" nrKubernetes "github.com/dapr/components-contrib/nameresolution/kubernetes" nrMdns "github.com/dapr/components-contrib/nameresolution/mdns" diff --git a/tests/certification/embedded/embedded.go b/tests/certification/embedded/embedded.go index 8b94659fa0..4642795182 100644 --- a/tests/certification/embedded/embedded.go +++ b/tests/certification/embedded/embedded.go @@ -185,6 +185,7 @@ func NewRuntime(ctx context.Context, appID string, opts ...Option) (*runtime.Dap DaprAPIGRPCPort: strconv.Itoa(daprAPIGRPCPort), ApplicationPort: strconv.Itoa(appPort), ProfilePort: strconv.Itoa(profilePort), + SchedulerStreams: 1, // Bypass scheduler stream requirement for cert tests DaprAPIListenAddresses: "127.0.0.1", AppProtocol: string(protocol.HTTPProtocol), Mode: string(mode), diff --git a/tests/certification/flow/dockercompose/dockercompose.go b/tests/certification/flow/dockercompose/dockercompose.go index 83bb243b4e..3069ca11a3 100644 --- a/tests/certification/flow/dockercompose/dockercompose.go +++ b/tests/certification/flow/dockercompose/dockercompose.go @@ -54,6 +54,7 @@ func (c Compose) Up(ctx flow.Context) error { "-p", c.project, "-f", c.filename, "up", "-d", + "--wait", "--remove-orphans").CombinedOutput() ctx.Log(string(out)) diff --git a/tests/certification/flow/sidecar/sidecar.go b/tests/certification/flow/sidecar/sidecar.go index 05c903942a..062703d090 100644 --- a/tests/certification/flow/sidecar/sidecar.go +++ b/tests/certification/flow/sidecar/sidecar.go @@ -18,6 +18,7 @@ import ( "sync/atomic" "time" + "github.com/cenkalti/backoff" "github.com/dapr/dapr/pkg/runtime" "github.com/dapr/dapr/pkg/runtime/registry" "github.com/dapr/kit/logger" @@ -133,6 +134,21 @@ func (s Sidecar) Start(ctx flow.Context) error { options.clientCallback(&client) } + // Wait for the sidecar to be healthy + var bo backoff.BackOff = backoff.NewConstantBackOff(100 * time.Millisecond) + bo = backoff.WithMaxRetries(bo, 200) // 20 seconds + bo = backoff.WithContext(bo, ctx) + retryErr := backoff.Retry(func() error { + if !rtConf.Healthz.IsReady() { + return fmt.Errorf("sidecar is not ready") + } + return nil + }, bo) + + if retryErr != nil { + return retryErr + } + return nil } diff --git a/tests/certification/go.mod b/tests/certification/go.mod index 756f93f666..293e60c2b8 100644 --- a/tests/certification/go.mod +++ b/tests/certification/go.mod @@ -1,6 +1,6 @@ module github.com/dapr/components-contrib/tests/certification -go 1.24.4 +go 1.24.13 require ( cloud.google.com/go/pubsub v1.49.0 @@ -9,30 +9,36 @@ require ( github.com/IBM/sarama v1.45.2 github.com/a8m/documentdb v1.3.0 github.com/apache/dubbo-go-hessian2 v1.11.5 - github.com/apache/pulsar-client-go v0.14.0 + github.com/apache/pulsar-client-go v0.18.0 github.com/apache/thrift v0.13.0 - github.com/aws/aws-sdk-go v1.55.6 - github.com/camunda/zeebe/clients/go/v8 v8.2.12 + github.com/aws/aws-sdk-go-v2 v1.41.1 + github.com/aws/aws-sdk-go-v2/config v1.32.9 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 + github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 + github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 + github.com/camunda/zeebe/clients/go/v8 v8.5.25 github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.3.0 - github.com/cloudwego/kitex v0.5.0 + github.com/cloudwego/kitex v0.15.4 github.com/cloudwego/kitex-examples v0.1.1 - github.com/dapr/components-contrib v1.15.2 - github.com/dapr/dapr v1.16.0-rc.1.0.20250723233324-b5569ff8862e - github.com/dapr/go-sdk v1.10.0-rc-1.0.20240507160435-33180dd89a46 - github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 - github.com/eclipse/paho.mqtt.golang v1.4.3 - github.com/go-chi/chi/v5 v5.0.12 + github.com/dapr/components-contrib v1.16.8 + github.com/dapr/dapr v1.17.0-rc.5 + github.com/dapr/go-sdk v1.13.0 + github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d + github.com/eclipse/paho.mqtt.golang v1.5.1 + github.com/go-chi/chi/v5 v5.2.2 github.com/go-redis/redis/v8 v8.11.5 + github.com/go-sql-driver/mysql v1.8.1 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 github.com/lestrrat-go/jwx/v2 v2.0.21 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 - github.com/rabbitmq/amqp091-go v1.9.0 - github.com/riferrei/srclient v0.6.0 - github.com/stretchr/testify v1.10.0 + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/riferrei/srclient v0.7.3 + github.com/stretchr/testify v1.11.1 github.com/tylertreat/comcast v1.0.1 go.mongodb.org/mongo-driver v1.14.0 go.uber.org/multierr v1.11.0 @@ -45,13 +51,12 @@ require ( cloud.google.com/go v0.120.0 // indirect cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/datastore v1.20.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect - github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect - github.com/99designs/keyring v1.2.1 // indirect - github.com/AthenZ/athenz v1.10.39 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/AthenZ/athenz v1.12.13 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect @@ -71,6 +76,7 @@ require ( github.com/DataDog/zstd v1.5.2 // indirect github.com/PuerkitoBio/purell v1.2.1 // indirect github.com/RoaringBitmap/roaring v1.1.0 // indirect + github.com/RoaringBitmap/roaring/v2 v2.8.0 // indirect github.com/Workiva/go-datastructures v1.0.53 // indirect github.com/aavaz-ai/pii-scrubber v0.0.0-20220812094047-3fa450ab6973 // indirect github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect @@ -84,93 +90,102 @@ require ( github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1-0.20241125194140-078c08b8574a // indirect - github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect - github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.3.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect - github.com/aws/rolesanywhere-credential-helper v1.0.4 // indirect - github.com/aws/smithy-go v1.22.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect + github.com/aws/aws-sdk-go-v2/service/kinesis v1.42.10 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.60.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect + github.com/aws/smithy-go v1.24.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.4.0 // indirect + github.com/bits-and-blooms/bitset v1.12.0 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bufbuild/protocompile v0.6.0 // indirect - github.com/bytedance/gopkg v0.0.0-20240711085056-a03554c296f8 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chebyrash/promise v0.0.0-20230709133807-42ec49ba1459 // indirect - github.com/choleraehyq/pid v0.0.20 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2 // indirect github.com/cloudevents/sdk-go/v2 v2.15.2 // indirect - github.com/cloudwego/fastpb v0.0.4-0.20230131074846-6fc453d58b96 // indirect - github.com/cloudwego/frugal v0.2.0 // indirect - github.com/cloudwego/gopkg v0.1.0 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/cloudwego/netpoll v0.3.2 // indirect - github.com/cloudwego/thriftgo v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cloudwego/configmanager v0.2.3 // indirect + github.com/cloudwego/dynamicgo v0.7.1 // indirect + github.com/cloudwego/fastpb v0.0.5 // indirect + github.com/cloudwego/frugal v0.3.0 // indirect + github.com/cloudwego/gopkg v0.1.8 // indirect + github.com/cloudwego/kitex/pkg/protocol/bthrift v0.0.0-20260112072316-5cf426cf9e1b // indirect + github.com/cloudwego/localsession v0.2.1 // indirect + github.com/cloudwego/netpoll v0.7.2 // indirect + github.com/cloudwego/runtimex v0.1.1 // indirect + github.com/cloudwego/thriftgo v0.4.3 // indirect github.com/creasty/defaults v1.5.2 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect - github.com/dapr/durabletask-go v0.7.3-0.20250711135247-7a35af6fe0e5 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/dapr/durabletask-go v0.11.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/didip/tollbooth/v7 v7.0.1 // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dubbogo/gost v1.13.1 // indirect github.com/dubbogo/triple v1.1.8 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/eapache/go-resiliency v1.7.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect github.com/eapache/queue v1.1.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fatih/color v1.17.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gage-technologies/mistral-go v1.1.0 // indirect github.com/go-chi/cors v1.2.1 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/go-pkgz/expirable-cache v0.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.0 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gocql/gocql v1.5.2 // indirect - github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect @@ -178,6 +193,7 @@ require ( github.com/google/gnostic-models v0.6.9 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/s2a-go v0.1.9 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect @@ -185,10 +201,9 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/grandcat/zeroconf v1.0.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect - github.com/hamba/avro/v2 v2.28.0 // indirect + github.com/hamba/avro/v2 v2.29.0 // indirect github.com/hashicorp/consul/api v1.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -201,7 +216,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/serf v0.10.1 // indirect - github.com/imdario/mergo v0.3.16 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -213,11 +228,11 @@ require ( github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jhump/protoreflect v1.15.3 // indirect github.com/jinzhu/copier v0.3.5 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/k0kubun/pp v3.0.1+incompatible // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/knadh/koanf v1.4.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect @@ -226,9 +241,9 @@ require ( github.com/lestrrat-go/httprc v1.0.5 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/linkedin/goavro/v2 v2.12.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/linkedin/goavro/v2 v2.14.1 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matoous/go-nanoid/v2 v2.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -244,7 +259,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.0 // indirect github.com/mschoch/smat v0.2.0 // indirect - github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/ncruces/go-strftime v0.1.9 // indirect @@ -252,47 +266,48 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/oracle/coherence-go-client/v2 v2.2.0 // indirect - github.com/panjf2000/ants/v2 v2.8.1 // indirect + github.com/panjf2000/ants/v2 v2.11.3 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect - github.com/puzpuzpuz/xsync/v3 v3.0.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/redis/go-redis/v9 v9.6.3 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/segmentio/asm v1.2.0 // indirect - github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/smartystreets/assertions v1.1.0 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/cast v1.8.0 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tchap/go-patricia/v2 v2.3.2 // indirect - github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/gjson v1.17.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/tmc/langchaingo v0.1.13 // indirect - github.com/vmware/vmware-go-kcl v1.5.1 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/tmc/langchaingo v0.1.15-0.20251029190607-e35755df7084 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/vmware/vmware-go-kcl-v2 v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect @@ -301,50 +316,50 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect - github.com/zeebo/errs v1.4.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/zipkin v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.opentelemetry.io/proto/otlp v1.6.0 // indirect - go.uber.org/atomic v1.10.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/arch v0.10.0 // indirect - golang.org/x/crypto v0.39.0 // indirect + golang.org/x/arch v0.14.0 // indirect + golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.30.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 + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/tools v0.40.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/api v0.231.0 // indirect google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // 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 + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.31.0 // indirect + k8s.io/api v0.32.3 // indirect k8s.io/apiextensions-apiserver v0.31.0 // indirect k8s.io/apimachinery v0.33.0 // indirect - k8s.io/client-go v0.31.0 // indirect + k8s.io/client-go v0.32.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect modernc.org/libc v1.61.9 // indirect diff --git a/tests/certification/go.sum b/tests/certification/go.sum index 1c384a2b81..fcfeae1f54 100644 --- a/tests/certification/go.sum +++ b/tests/certification/go.sum @@ -25,8 +25,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= @@ -52,19 +52,15 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 contrib.go.opencensus.io/exporter/prometheus v0.4.1/go.mod h1:t9wvfitlUjGXG2IXAZsuFq26mDGid/JwCEXp+gTG/9U= contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/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= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dubbo.apache.org/dubbo-go/v3 v3.0.3-0.20230118042253-4f159a2b38f3 h1:j08GKvXilDMHuVuGy+X0CMTL+Wxrte5a4XrWGDypZf0= dubbo.apache.org/dubbo-go/v3 v3.0.3-0.20230118042253-4f159a2b38f3/go.mod h1:bxe6StRQ4PVbZa+B5nsREuez4agzmWiELS9NhEoDscI= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= -github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= -github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= -github.com/AthenZ/athenz v1.10.39 h1:mtwHTF/v62ewY2Z5KWhuZgVXftBej1/Tn80zx4DcawY= -github.com/AthenZ/athenz v1.10.39/go.mod h1:3Tg8HLsiQZp81BJY58JBeU2BR6B/H4/0MQGfCwhHNEA= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AthenZ/athenz v1.12.13 h1:OhZNqZsoBXNrKBJobeUUEirPDnwt0HRo4kQMIO1UwwQ= +github.com/AthenZ/athenz v1.12.13/go.mod h1:XXDXXgaQzXaBXnJX6x/bH4yF6eon2lkyzQZ0z/dxprE= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= @@ -101,8 +97,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.0 h1:lJwNFV+xYjHREUTH github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.0/go.mod h1:GfT0aGew8Qj5yiQVqOO5v7N8fanbJGyUoHqXg56qcVY= github.com/Azure/go-amqp v1.0.5 h1:po5+ljlcNSU8xtapHTe8gIc8yHxCzC03E8afH2g1ftU= github.com/Azure/go-amqp v1.0.5/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -122,13 +118,13 @@ github.com/IBM/sarama v1.45.2/go.mod h1:ppaoTcVdGv186/z6MEKsMm70A5fwJfRTpstI37kV github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= -github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= github.com/RoaringBitmap/roaring v1.1.0 h1:b10lZrZXaY6Q6EKIRrmOF519FIyQQ5anPgGr3niw2yY= github.com/RoaringBitmap/roaring v1.1.0/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= +github.com/RoaringBitmap/roaring/v2 v2.8.0 h1:y1rdtixfXvaITKzkfiKvScI0hlBJHe9sfzJp8cgeM7w= +github.com/RoaringBitmap/roaring/v2 v2.8.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= @@ -145,10 +141,7 @@ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -157,10 +150,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alibaba/sentinel-golang v1.0.4 h1:i0wtMvNVdy7vM4DdzYrlC4r/Mpk1OKUUBurKKkWhEo8= github.com/alibaba/sentinel-golang v1.0.4/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.30.5 h1:3r6kTHdKnuP4fkS8k2IrvSfxpxUTcW1SOL0wN7b7Dt0= -github.com/alicebob/miniredis/v2 v2.30.5/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg= +github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI= +github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= github.com/alphadose/haxmap v1.4.0 h1:1yn+oGzy2THJj1DMuJBzRanE3sMnDAjJVbU0L31Jp3w= @@ -177,8 +168,8 @@ github.com/apache/dubbo-go-hessian2 v1.9.3/go.mod h1:xQUjE7F8PX49nm80kChFvepA/Av github.com/apache/dubbo-go-hessian2 v1.11.0/go.mod h1:7rEw9guWABQa6Aqb8HeZcsYPHsOS7XT1qtJvkmI6c5w= github.com/apache/dubbo-go-hessian2 v1.11.5 h1:rcK22+yMw2Hejm6GRG7WrdZ0DinW2QMZc01c7YVZjcQ= github.com/apache/dubbo-go-hessian2 v1.11.5/go.mod h1:QP9Tc0w/B/mDopjusebo/c7GgEfl6Lz8jeuFg8JA6yw= -github.com/apache/pulsar-client-go v0.14.0 h1:P7yfAQhQ52OCAu8yVmtdbNQ81vV8bF54S2MLmCPJC9w= -github.com/apache/pulsar-client-go v0.14.0/go.mod h1:PNUE29x9G1EHMvm41Bs2vcqwgv7N8AEjeej+nEVYbX8= +github.com/apache/pulsar-client-go v0.18.0 h1:YsySoOds7WCXkRcOKHb85gk/v1Jndp+2oCkkRQEowUA= +github.com/apache/pulsar-client-go v0.18.0/go.mod h1:GKmTD1u5YLuhUnoVTNGdhdGNAYhoglWNWgwLJZTljAw= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -186,7 +177,6 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/ardielle/ardielle-go v1.5.2 h1:TilHTpHIQJ27R1Tl/iITBzMwiUGSlVfiVhwDNGM3Zj4= github.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI= -github.com/ardielle/ardielle-tools v1.5.4/go.mod h1:oZN+JRMnqGiIhrzkRN9l26Cej9dEx4jeNG6A+AdkShk= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= @@ -202,62 +192,77 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1-0.20241125194140-078c08b8574a h1:QFemvMGPnajaeRBkFc1HoEA7qzVjUv+rkYb1/ps1/UE= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1-0.20241125194140-078c08b8574a/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.32.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= -github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= -github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= -github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= +github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A= +github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI= github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= -github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3 h1:xQYRnbQ+ypDMCLiFlLw5cF7Xd6K+oaL7jco2zwIMqTs= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3/go.mod h1:X7RC8FFkx0bjNJRBddd3xdoDaDmNLSxICFdIdJ7asqw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.3.10 h1:z6fAXB4HSuYjrE/P8RU3NdCaN+EPaeq/+80aisCjuF8= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.3.10/go.mod h1:PoPjOi7j+/DtKIGC58HRfcdWKBPYYXwdKnRG+po+hzo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.4 h1:X2X1hn9CQk9G8Nis/xBs3YWJaNJCpQYpxcGWpl5Kgg4= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.4/go.mod h1:Vg7AqclrUJtnnahELZ8ZFWMDHoUHvEwArxrE7rpri58= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4 h1:Rv6o9v2AfdEIKoAa7pQpJ5ch9ji2HevFUvGY6ufawlI= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 h1:QHaS/SHXfyNycuu4GiWb+AfW5T3bput6X5E3Ai/Q31M= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.42.10 h1:9jBVTw8qxfekGSNtiFreb1e5m2vCz89XcC5C4pmDN9Y= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.42.10/go.mod h1:Fpex7CunMujL2O9qaKTDYG0xnl1ZP3pBZ68XyQCmhtA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8 h1:HD6R8K10gPbN9CNqRDOs42QombXlYeLOr4KkIxe2lQs= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8/go.mod h1:x66GdH8qjYTr6Kb4ik38Ewl6moLsg8igbceNsmxVxeA= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE= github.com/aws/aws-sdk-go-v2/service/sns v1.34.7/go.mod h1:4WYoZAhHt+dWYpoOQUgkUKfuQbE6Gg/hW4oXE0pKS9U= github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ= github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI= +github.com/aws/aws-sdk-go-v2/service/ssm v1.60.2 h1:ZvLR/SUQGk8sR+bHl8vXT00zgJ+U1fHDzrlokzz9DDo= +github.com/aws/aws-sdk-go-v2/service/ssm v1.60.2/go.mod h1:H5QEq6SthlWMh8PXfSupp6uTg7iaJ3J36Cf15CPG5zE= github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= -github.com/aws/rolesanywhere-credential-helper v1.0.4 h1:kHIVVdyQQiFZoKBP+zywBdFilGCS8It+UvW5LolKbW8= -github.com/aws/rolesanywhere-credential-helper v1.0.4/go.mod h1:QVGNxlDlYhjR0/ZUee7uGl0hNChWidNpe2+GD87Buqk= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= -github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= @@ -270,13 +275,11 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bits-and-blooms/bitset v1.4.0 h1:+YZ8ePm+He2pU3dZlIZiOeAKfrBkXi1lSrXJ/Xzgbu8= -github.com/bits-and-blooms/bitset v1.4.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/brianvoe/gofakeit/v6 v6.16.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= @@ -297,20 +300,24 @@ github.com/bytedance/gopkg v0.0.0-20210910103821-e4efae9c17c3/go.mod h1:birsdqRC github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= github.com/bytedance/gopkg v0.0.0-20220509134931-d1878f638986/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= github.com/bytedance/gopkg v0.0.0-20220531084716-665b4f21126f/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= -github.com/bytedance/gopkg v0.0.0-20220817015305-b879a72dc90f/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= -github.com/bytedance/gopkg v0.0.0-20240711085056-a03554c296f8 h1:rDwLxYTMoKHaw4cS0bQhaTZnkXp5e6ediCggGcRD/CA= -github.com/bytedance/gopkg v0.0.0-20240711085056-a03554c296f8/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ= +github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/mockey v1.0.0-rc.0/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= -github.com/bytedance/mockey v1.2.0 h1:847+X2fBSM4s/AIN4loO5d16PCgEj53j7Q8YVB+8P6c= -github.com/bytedance/mockey v1.2.0/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= -github.com/camunda/zeebe/clients/go/v8 v8.2.12 h1:VWkbyhcZFXLqrLXLjICFrKk7Y4Hia9ldKIFoO5V5M8o= -github.com/camunda/zeebe/clients/go/v8 v8.2.12/go.mod h1:4mpOks0uLXPbOCW82g/H9ZHDfdr90ikvFBWGwDV+fG8= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/camunda/zeebe/clients/go/v8 v8.5.25 h1:CwQWKBSR4PvOzcV0FmkSaNAayazjqtgJ+yjVQ2NFY1s= +github.com/camunda/zeebe/clients/go/v8 v8.5.25/go.mod h1:oLhBlv65aO2sV5FJ/dfEREyeqbrPHbJdv0lr5wJujJ8= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -321,13 +328,9 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chebyrash/promise v0.0.0-20230709133807-42ec49ba1459 h1:s7UrE2T8jRoriLIddT8fW5+Wf2sXcOgfteXUKD74SaU= github.com/chebyrash/promise v0.0.0-20230709133807-42ec49ba1459/go.mod h1:CQthfPdCoGmlBJAG/sP9Km5nfK1/jGpDf1RiG/LUxXw= github.com/chenzhuoyu/iasm v0.0.0-20220818063314-28c361dae733/go.mod h1:wOQ0nsbeOLa2awv8bUYFW/EHXbjQMlZ10fAlXDB2sz8= -github.com/chenzhuoyu/iasm v0.0.0-20230222070914-0b1b64b0e762/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/choleraehyq/pid v0.0.12/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= github.com/choleraehyq/pid v0.0.13/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= github.com/choleraehyq/pid v0.0.15/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= -github.com/choleraehyq/pid v0.0.16/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= -github.com/choleraehyq/pid v0.0.20 h1:FSOci0vLLkM/38cDpokosFPcYLpoSxjeTzYiipiu7is= -github.com/choleraehyq/pid v0.0.20/go.mod h1:uhzeFgxJZWQsZulelVQZwdASxQ9TIPZYL4TPkQMtL/U= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -342,27 +345,35 @@ github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2 h1:FIvfKlS2mcuP github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2/go.mod h1:POsdVp/08Mki0WD9QvvgRRpg9CQ6zhjfRrBoEY8JFS8= github.com/cloudevents/sdk-go/v2 v2.15.2 h1:54+I5xQEnI73RBhWHxbI1XJcqOFOVJN85vb41+8mHUc= github.com/cloudevents/sdk-go/v2 v2.15.2/go.mod h1:lL7kSWAE/V8VI4Wh0jbL2v/jvqsm6tjmaQBSvxcv4uE= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudwego/configmanager v0.2.3 h1:P0YTBgqDBnKeI/VARvut/Dc9Rfxt9Bw1Nv7sk0Ru4u8= +github.com/cloudwego/configmanager v0.2.3/go.mod h1:4GeSKjH6JLvKx4/Hrbh5dse8fDqj1n/Up8HfU4wHJ+w= +github.com/cloudwego/dynamicgo v0.7.1 h1:ITStSu+SaqXd+oFjg+OA920VTOd9GpYTFaUg9upHBKk= +github.com/cloudwego/dynamicgo v0.7.1/go.mod h1:f9le2ULWbFFkQ8WoP+7pGl1zEI2xRLZhaaif6ROLwDw= github.com/cloudwego/fastpb v0.0.2/go.mod h1:/V13XFTq2TUkxj2qWReV8MwfPC4NnPcy6FsrojnsSG0= -github.com/cloudwego/fastpb v0.0.4-0.20230131074846-6fc453d58b96 h1:61PQT0CXNUuQDiDKv/QQ+pFi9uthExZLQz8b5WfS7Qw= -github.com/cloudwego/fastpb v0.0.4-0.20230131074846-6fc453d58b96/go.mod h1:/V13XFTq2TUkxj2qWReV8MwfPC4NnPcy6FsrojnsSG0= +github.com/cloudwego/fastpb v0.0.5 h1:vYnBPsfbAtU5TVz5+f9UTlmSCixG9F9vRwaqE0mZPZU= +github.com/cloudwego/fastpb v0.0.5/go.mod h1:Bho7aAKBUtT9RPD2cNVkTdx4yQumfSv3If7wYnm1izk= github.com/cloudwego/frugal v0.1.3/go.mod h1:b981ViPYdhI56aFYsoMjl9kv6yeqYSO+iEz2jrhkCgI= -github.com/cloudwego/frugal v0.1.6/go.mod h1:9ElktKsh5qd2zDBQ5ENhPSQV7F2dZ/mXlr1eaZGDBFs= -github.com/cloudwego/frugal v0.2.0 h1:0ETSzQYoYqVvdl7EKjqJ9aJnDoG6TzvNKV3PMQiQTS8= -github.com/cloudwego/frugal v0.2.0/go.mod h1:cpnV6kdRMjN3ylxRo63RNbZ9rBK6oxs70Zk6QZ4Enj4= -github.com/cloudwego/gopkg v0.1.0 h1:N7CE4FS5crkZg3w7shw3UR3TG4+uofXXabGuBNmSrlE= -github.com/cloudwego/gopkg v0.1.0/go.mod h1:32yKw2zkpTMtuX6amJR0EMK79f0vGPr67UcArCOlZLU= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/frugal v0.3.0 h1:tgAP0nytiJuyoIM3V3TDOGzjrSNRAIlNG1HHOAzZ3Cs= +github.com/cloudwego/frugal v0.3.0/go.mod h1:pMk46fFyAwUbW7q7lfdK7c6HsD6bWtu6/3Vhz63CgsY= +github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= +github.com/cloudwego/gopkg v0.1.8 h1:ma9oACsY3v6xJwQ8NUc/h19GLV2ZCIjx0P6hqaSIlt4= +github.com/cloudwego/gopkg v0.1.8/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= github.com/cloudwego/kitex v0.0.1/go.mod h1:NTTu8szFfMKY9pxa7JmI/4FZpD15p5YUHLTYMqsXj9o= github.com/cloudwego/kitex v0.0.4/go.mod h1:EIjPJ4Dom2ornk7xDCdKpUpOnf4Tulevimh4Tn05OGc= github.com/cloudwego/kitex v0.2.0/go.mod h1:1p4rtGIIiFZMOePYbSPgLkIhdhdfhEtVOJSti/k9vK4= github.com/cloudwego/kitex v0.3.1/go.mod h1:VZ+G2ILJC98uErzapZd+680LgU5B/hYBFp9pVwnkNuE= github.com/cloudwego/kitex v0.3.2/go.mod h1:/XD07VpUD9VQWmmoepASgZ6iw//vgWikVA9MpzLC5i0= github.com/cloudwego/kitex v0.4.3/go.mod h1:7CV4Cs5oi7dWqI3fFoDTLJHyt/18z7nlHIIHsQQNnbE= -github.com/cloudwego/kitex v0.5.0 h1:f/rip2gp8mdeTpi0WQFv7BdDdkdZn/Q0KvBCm9Mi+7c= -github.com/cloudwego/kitex v0.5.0/go.mod h1:yhw7XikNVG4RstmlQAidBuxMlZYpIiCLsDU8eHPGEMo= +github.com/cloudwego/kitex v0.15.4 h1:mFg3Vg21aEsoQRNc1FK/hOEtEm47EZEJGyorlnsP9nQ= +github.com/cloudwego/kitex v0.15.4/go.mod h1:Zsr4TATU+M3/t+R7CuK7FKQzT2jIhc8ytzEDqjTZmwk= github.com/cloudwego/kitex-examples v0.1.1 h1:5uGqbGEobl8pKSVKwaWgltuf/JAa8Fg2MioX4WmlCXw= github.com/cloudwego/kitex-examples v0.1.1/go.mod h1:5V7LsSJtY18KnceJdvpxYswOfgV3kXE0BGm5mRYyuAg= +github.com/cloudwego/kitex/pkg/protocol/bthrift v0.0.0-20260112072316-5cf426cf9e1b h1:NYQss6yhM1D54n/zlkIC52bNtPsux7x/GepCoOSJt84= +github.com/cloudwego/kitex/pkg/protocol/bthrift v0.0.0-20260112072316-5cf426cf9e1b/go.mod h1:OP63V8YwwSlPVFqHZblV3mJXLPIjcIdwkT6ZYjEggcI= +github.com/cloudwego/localsession v0.2.1 h1:obiuwSP2MQX+fFot3HjOQjvR5o7FlSc8Z4e5EM+NqRY= +github.com/cloudwego/localsession v0.2.1/go.mod h1:J4uams2YT/2d4t7OI6A7NF7EcG8OlHJsOX2LdPbqoyc= github.com/cloudwego/netpoll v0.0.2/go.mod h1:rZOiNI0FYjuvNybXKKhAPUja03loJi/cdv2F55AE6E8= github.com/cloudwego/netpoll v0.0.3/go.mod h1:rZOiNI0FYjuvNybXKKhAPUja03loJi/cdv2F55AE6E8= github.com/cloudwego/netpoll v0.1.0/go.mod h1:rZOiNI0FYjuvNybXKKhAPUja03loJi/cdv2F55AE6E8= @@ -370,16 +381,17 @@ github.com/cloudwego/netpoll v0.2.0/go.mod h1:rZOiNI0FYjuvNybXKKhAPUja03loJi/cdv github.com/cloudwego/netpoll v0.2.2/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= github.com/cloudwego/netpoll v0.2.4/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= github.com/cloudwego/netpoll v0.2.6/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= -github.com/cloudwego/netpoll v0.3.2 h1:/998ICrNMVBo4mlul4j7qcIeY7QnEfuCCPPwck9S3X4= -github.com/cloudwego/netpoll v0.3.2/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +github.com/cloudwego/netpoll v0.7.2 h1:4qDBGQ6CG2SvEXhZSDxMdtqt/NLDxjAVk0PC/biKiJo= +github.com/cloudwego/netpoll v0.7.2/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= github.com/cloudwego/netpoll-http2 v0.0.4/go.mod h1:iFr5SzJCXIYgBg0ubL0fZiCQ6W36s9p0KjXpV04lmoY= github.com/cloudwego/netpoll-http2 v0.0.6/go.mod h1:+bjPyu2Cd4GDzKa0IegPgp1hjMjpZ6/kXTsSjIsmUk8= +github.com/cloudwego/runtimex v0.1.1 h1:lheZjFOyKpsq8TsGGfmX9/4O7F0TKpWmB8on83k7GE8= +github.com/cloudwego/runtimex v0.1.1/go.mod h1:23vL/HGV0W8nSCHbe084AgEBdDV4rvXenEUMnUNvUd8= github.com/cloudwego/thriftgo v0.0.1/go.mod h1:LzeafuLSiHA9JTiWC8TIMIq64iadeObgRUhmVG1OC/w= github.com/cloudwego/thriftgo v0.1.2/go.mod h1:LzeafuLSiHA9JTiWC8TIMIq64iadeObgRUhmVG1OC/w= github.com/cloudwego/thriftgo v0.2.1/go.mod h1:8i9AF5uDdWHGqzUhXDlubCjx4MEfKvWXGQlMWyR0tM4= -github.com/cloudwego/thriftgo v0.2.8/go.mod h1:dAyXHEmKXo0LfMCrblVEY3mUZsdeuA5+i0vF5f09j7E= -github.com/cloudwego/thriftgo v0.3.0 h1:BBb9hVcqmu9p4iKUP/PSIaDB21Vfutgd7k2zgK37Q9Q= -github.com/cloudwego/thriftgo v0.3.0/go.mod h1:AvH0iEjvKHu3cdxG7JvhSAaffkS4h2f4/ZxpJbm48W4= +github.com/cloudwego/thriftgo v0.4.3 h1:Ig80u/nQdOiB4K36BG4oqud2f8LMykZkbnk4R4QywiM= +github.com/cloudwego/thriftgo v0.4.3/go.mod h1:/D4zRAEj1t3/Tq1bVGDMnRt3wxpHfalXfZWvq/n4YmY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= @@ -388,13 +400,17 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= -github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= @@ -409,8 +425,8 @@ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= @@ -418,18 +434,16 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creasty/defaults v1.5.2 h1:/VfB6uxpyp6h0fr7SPp7n8WJBoV8jfxQXPCnkVSjyls= github.com/creasty/defaults v1.5.2/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= -github.com/dapr/dapr v1.16.0-rc.1.0.20250723233324-b5569ff8862e h1:PQjnQkSuU7OtiFmmqIRUxvpb14A9va4ZbGh7oXgStbY= -github.com/dapr/dapr v1.16.0-rc.1.0.20250723233324-b5569ff8862e/go.mod h1:3R5TgtcMOyNx2/kzam2YwgbYpFAj+uPGufRYCTczGnk= -github.com/dapr/durabletask-go v0.7.3-0.20250711135247-7a35af6fe0e5 h1:l8oBGwcfCwqvSYDZwla0A2fhENmXFc1Wk4lR0VEq+is= -github.com/dapr/durabletask-go v0.7.3-0.20250711135247-7a35af6fe0e5/go.mod h1:0Ts4rXp74JyG19gDWPcwNo5V6NBZzhARzHF5XynmA7Q= -github.com/dapr/go-sdk v1.10.0-rc-1.0.20240507160435-33180dd89a46 h1:0/WKEqAfTGnFAiFrqMpIEBMkCHaAqt5H9efU0hyKiG4= -github.com/dapr/go-sdk v1.10.0-rc-1.0.20240507160435-33180dd89a46/go.mod h1:b86mngq2m71QqKtL48whlXIPWHgoXIH0Z3BrHwE6P9U= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 h1:Q26gmPxs6WnnBYoudOlznPHsmrbTawcYEpHg4VoB7v8= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/dapr/dapr v1.17.0-rc.5 h1:j/tOovOA1Bdk2hPjfGV66T/7ABIguVIu3dtMp5gdTgs= +github.com/dapr/dapr v1.17.0-rc.5/go.mod h1:M2I7byLvdH+Jvl6BUKxOEB1JxnJJQF7TT2ghxN419qA= +github.com/dapr/durabletask-go v0.11.0 h1:e9Ns/3a2b6JDKGuvksvx6gCHn7rd+nwZZyAXbg5Ley4= +github.com/dapr/durabletask-go v0.11.0/go.mod h1:0Ts4rXp74JyG19gDWPcwNo5V6NBZzhARzHF5XynmA7Q= +github.com/dapr/go-sdk v1.13.0 h1:Qw2BmUonClQ9yK/rrEEaFL1PyDgq616RrvYj0CT67Lk= +github.com/dapr/go-sdk v1.13.0/go.mod h1:RsffVNZitDApmQqoS68tNKGMXDZUjTviAbKZupJSzts= +github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d h1:csljij9d1IO6u9nqbg+TuSRmTZ+OXT8G49yh6zie1yI= +github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -452,12 +466,12 @@ github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4w github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= -github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dubbogo/go-zookeeper v1.0.3/go.mod h1:fn6n2CAEer3novYgk9ULLwAjuV8/g4DdC2ENwRb6E+c= @@ -478,8 +492,6 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= -github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= @@ -488,12 +500,17 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4A github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= -github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= +github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -504,7 +521,12 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.0/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= @@ -518,10 +540,11 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= @@ -544,24 +567,19 @@ github.com/gage-technologies/mistral-go v1.1.0/go.mod h1:tF++Xt7U975GcLlzhrjSQb8 github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.9.0/go.mod h1:DbJm9kdgr1sEvWpHCA7dFFs/PGHPMil9/97EXCRPr4k= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -569,8 +587,6 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -580,27 +596,24 @@ github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNV github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= -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/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pkgz/expirable-cache v0.1.0 h1:3bw0m8vlTK8qlwz5KXuygNBTkiKRTPrAGXU0Ej2AC1g= github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= @@ -615,25 +628,22 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gocql/gocql v1.5.2 h1:WnKf8xRQImcT/KLaEWG2pjEeryDB7K0qQN9mPs1C58Q= github.com/gocql/gocql v1.5.2/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= @@ -644,7 +654,6 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= @@ -659,8 +668,9 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -726,7 +736,6 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -747,6 +756,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAx github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -767,12 +778,12 @@ github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1: github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -789,15 +800,13 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hamba/avro/v2 v2.28.0 h1:E8J5D27biyAulWKNiEBhV85QPc9xRMCUCGJewS0KYCE= -github.com/hamba/avro/v2 v2.28.0/go.mod h1:9TVrlt1cG1kkTUtm9u2eO5Qb7rZXlYzoKqPt8TSH+TA= +github.com/hamba/avro/v2 v2.29.0 h1:fkqoWEPxfygZxrkktgSHEpd0j/P7RKTBTDbcEeMdVEY= +github.com/hamba/avro/v2 v2.29.0/go.mod h1:Pk3T+x74uJoJOFmHrdJ8PRdgSEL/kEKteJ31NytCKxI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE= @@ -885,10 +894,10 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= @@ -901,8 +910,6 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 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/jawher/mow.cli v1.0.4/go.mod h1:5hQj2V8g+qYmLUVWqu4Wuja1pI57M83EChYLVZ0sMKk= -github.com/jawher/mow.cli v1.2.0/go.mod h1:y+pcA3jBAdo/GIZx/0rFjw/K2bVEODP9rfZOfaiq8Ko= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -924,10 +931,7 @@ github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4E github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -952,7 +956,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= @@ -966,11 +969,13 @@ github.com/kitex-contrib/monitor-prometheus v0.0.0-20210817080809-024dd7bd51e1/g github.com/kitex-contrib/obs-opentelemetry v0.0.0-20220601144657-c60210e3c928/go.mod h1:VvMzPMfgL7iUG92eVZGuRybGVMKzuSrsfMvHHpL7/Ac= github.com/kitex-contrib/obs-opentelemetry/logging/logrus v0.0.0-20220601144657-c60210e3c928/go.mod h1:Eml/0Z+CqgGIPf9JXzLGu+N9NJoy2x5pqypN+hmKArE= github.com/kitex-contrib/tracer-opentracing v0.0.2/go.mod h1:mprt5pxqywFQxlHb7ugfiMdKbABTLI9YrBYs9WmlK5Q= -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/kjk/httplogproxy v0.0.0-20190214011443-6743ea9a2d3d/go.mod h1:kkVhzcC9maw+0jdT2UfGGikRmobjydsBiD6ElexuTLk= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knadh/koanf v1.4.1 h1:Z0VGW/uo8NJmjd+L1Dc3S5frq6c62w5xQ9Yf4Mg3wFQ= github.com/knadh/koanf v1.4.1/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= @@ -988,6 +993,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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 v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 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/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= @@ -1009,16 +1015,17 @@ github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f/go.mod github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042/go.mod h1:TPpsiPUEh0zFL1Snz4crhMlBe60PYxRHr5oFF3rRYg0= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/linkedin/goavro/v2 v2.11.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= -github.com/linkedin/goavro/v2 v2.12.0 h1:rIQQSj8jdAUlKQh6DttK8wCRv4t4QO09g1C4aBWXslg= -github.com/linkedin/goavro/v2 v2.12.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/linkedin/goavro/v2 v2.13.1/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= +github.com/linkedin/goavro/v2 v2.14.1 h1:/8VjDpd38PRsy02JS0jflAu7JZPfJcGTwqWgMkFS2iI= +github.com/linkedin/goavro/v2 v2.14.1/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= @@ -1080,16 +1087,18 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= -github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1100,12 +1109,10 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= -github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -1165,8 +1172,8 @@ github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LD github.com/oracle/coherence-go-client/v2 v2.2.0 h1:ZO8tsN8Z4JTFGnoERez+rYYlQAyH7g1MwSY5THG7c+o= github.com/oracle/coherence-go-client/v2 v2.2.0/go.mod h1:IUOIVsyaeccST2AZa/F3/PpY8uukF5Sy3Ko79pqleO0= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/panjf2000/ants/v2 v2.8.1 h1:C+n/f++aiW8kHCExKlpX6X+okmxKXP7DWLutxuAPuwQ= -github.com/panjf2000/ants/v2 v2.8.1/go.mod h1:KIBmYG9QQX5U2qzFP/yQJaq/nSb6rahS9iEHkrCMgM8= +github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -1183,14 +1190,9 @@ github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -1203,14 +1205,17 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polarismesh/polaris-go v1.1.0/go.mod h1:tquawfjEKp1W3ffNJQSzhfditjjoZ7tvhOCElN7Efzs= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -1266,10 +1271,12 @@ github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3M github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/puzpuzpuz/xsync/v3 v3.0.0 h1:QwUcmah+dZZxy6va/QSU26M6O6Q422afP9jO8JlnRSA= -github.com/puzpuzpuz/xsync/v3 v3.0.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= -github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427 h1:hOnThDlsq0e4M7Sl3A3MnMlazYJsNuuDDqywa5mI7wQ= +github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427/go.mod h1:Zhu1DOotWGZcjom6CZH+8mJ2AD3fOx0QjVIrbpMxN04= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -1278,19 +1285,17 @@ github.com/redis/go-redis/v9 v9.6.3/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= -github.com/riferrei/srclient v0.6.0 h1:60LWpQW66AAL5TtWuMPZEplwgWLUdCK3OBUbag/JWFg= -github.com/riferrei/srclient v0.6.0/go.mod h1:e3nZcDdaOSsaYqiO18INPBK4qnJTjEEyL2rlJcsTtrA= +github.com/riferrei/srclient v0.7.3 h1:JRR6jgfINWUcYZhBRHEg/NAFv7giVmjkoouRbWbakgw= +github.com/riferrei/srclient v0.7.3/go.mod h1:byIzLF4UNZzclmzQXXr++Oe1GEH/hNFahUOSTXc7uSc= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= @@ -1304,11 +1309,14 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shirou/gopsutil v3.20.11+incompatible h1:LJr4ZQK4mPpIV5gOa4jCOKOGb4ty4DZO54I4FGqIpto= github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= github.com/shirou/gopsutil/v3 v3.22.2/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY= -github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= -github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY= +github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -1319,8 +1327,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= @@ -1356,8 +1364,8 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -1365,7 +1373,6 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -1382,8 +1389,10 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO 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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -1391,12 +1400,12 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ= -github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME= -github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E= +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/tevid/gohamcrest v1.1.1/go.mod h1:3UvtWlqm8j5JbwYZh80D/PVBt0mJ1eJiYgZMibh0H/k= github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -1408,22 +1417,24 @@ github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk1 github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= -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/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= -github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= +github.com/tmc/langchaingo v0.1.15-0.20251029190607-e35755df7084 h1:e7m315AqnlqGh/c7Dc1+pn8rFNONmXToKgaUrXdj2hM= +github.com/tmc/langchaingo v0.1.15-0.20251029190607-e35755df7084/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3/go.mod h1:QDlpd3qS71vYtakd2hmdpqhJ9nwv6mD6A30bQ1BPBFE= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/tylertreat/comcast v1.0.1 h1:+B8add2s9PrhX4lx5gGqOKUTebGD7lzdfwKZHYoF98Y= github.com/tylertreat/comcast v1.0.1/go.mod h1:8mA9mMCnmAGjTnrWNKQ7PXsBy6FfguO+U9pSxifaka8= github.com/uber/jaeger-client-go v2.29.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= @@ -1432,8 +1443,8 @@ github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vmware/vmware-go-kcl v1.5.1 h1:1rJLfAX4sDnCyatNoD/WJzVafkwST6u/cgY/Uf2VgHk= -github.com/vmware/vmware-go-kcl v1.5.1/go.mod h1:kXJmQ6h0dRMRrp1uWU9XbIXvwelDpTxSPquvQUBdpbo= +github.com/vmware/vmware-go-kcl-v2 v1.0.0 h1:HPT5vu+khRmGspBSc/+AilEWbRGoTZhjlYqdrBbRMZs= +github.com/vmware/vmware-go-kcl-v2 v1.0.0/go.mod h1:GBDu+P4Neo0vwZAk0ZUCEC8GYsUOWvi3XhFwAZR3SjA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -1456,15 +1467,12 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= -github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zouyx/agollo/v3 v3.4.5 h1:7YCxzY9ZYaH9TuVUBvmI6Tk0mwMggikah+cfbYogcHQ= github.com/zouyx/agollo/v3 v3.4.5/go.mod h1:LJr3kDmm23QSW+F1Ol4TMHDa7HvJvscMdVxJ2IpUTVc= go.einride.tech/aip v0.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ= @@ -1495,12 +1503,12 @@ go.opencensus.io v0.22.6-0.20201102222123-380f4078db9f/go.mod h1:5pWMHQbX5EPX2/6 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -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/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/contrib/instrumentation/runtime v0.32.0/go.mod h1:qtaLlIO4HC4DfedkYTOrvS2u7nA3N/v8w9mehrBD4O8= go.opentelemetry.io/contrib/propagators/b3 v1.7.0/go.mod h1:gXx7AhL4xXCF42gpm9dQvdohoDa2qeyEx4eIIxqK+h4= go.opentelemetry.io/contrib/propagators/jaeger v1.7.0/go.mod h1:kt2lNImfxV6dETRsDCENd6jU6G0mPRS+P0qlNuvtkTE= @@ -1508,41 +1516,41 @@ go.opentelemetry.io/contrib/propagators/opencensus v0.32.0/go.mod h1:rgmffkE6ivb go.opentelemetry.io/contrib/propagators/ot v1.7.0/go.mod h1:5qxBZR730yb71uXc3bazxt2Si8o8LQK3iJTnSLca/BU= go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdTiUde4= go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/bridge/opencensus v0.30.0/go.mod h1:jyERBSEU6EX7oR+LytaatX1UxNphEIRXj1q3n/6hIk0= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0/go.mod h1:M1hVZHNxcbkAlcvrOMlpQ4YOO3Awf+4N2dxkZL3xm04= go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.30.0/go.mod h1:8Lz1GGcrx1kPGE3zqDrK7ZcPzABEfIQqBjq7roQa5ZA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.30.0/go.mod h1:RejW0QAFotPIixlFZKZka4/70S5UaFOqDO9DYOgScIs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0/go.mod h1:ceUgdyfNv4h4gLxHR0WNfDiiVmZFodZhZSbOLhpxqXE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0/go.mod h1:E+/KKhwOSw8yoPxSSuUHG6vKppkvhN+S1Jc7Nib3k3o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.4.1/go.mod h1:BFiGsTMZdqtxufux8ANXuMeRz9dMPVFdJZadUWDFD7o= -go.opentelemetry.io/otel/exporters/zipkin v1.34.0 h1:GSjCkoYqsnvUMCjxF18j2tCWH8fhGZYjH3iYgechPTI= -go.opentelemetry.io/otel/exporters/zipkin v1.34.0/go.mod h1:h830hluwAqgSNnZbxL2rJhmAlE7/0SF9esoHVLU04Gc= +go.opentelemetry.io/otel/exporters/zipkin v1.40.0 h1:zu+I4j+FdO6xIxBVPeuncQVbjxUM4LiMgv6GwGe9REE= +go.opentelemetry.io/otel/exporters/zipkin v1.40.0/go.mod h1:zS6cC4nFBYXbu18e7aLfMzubBjOiN7ZcROu477qtMf8= go.opentelemetry.io/otel/metric v0.30.0/go.mod h1:/ShZ7+TS4dHzDFmfi1kSXMhMVubNoP0oIaBp70J6UXU= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.4.1/go.mod h1:NBwHDgDIBYjwK2WNu1OPgsIc2IJzmBXNnvIJxJc8BpE= go.opentelemetry.io/otel/sdk v1.7.0/go.mod h1:uTEOTwaqIVuTGiJN7ii13Ibp75wJmYUDe374q6cZwUU= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v0.30.0/go.mod h1:8AKFRi5HyvTR0RRty3paN1aMC9HMT+NzcEhw/BLkLX8= -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/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.4.1/go.mod h1:iYEVbroFCNut9QkwEczV9vMRPHNKSSwYZjulEtsmhFc= go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.16.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1550,13 +1558,12 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -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/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 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/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1581,9 +1588,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/arch v0.0.0-20220722155209-00200b7164a7/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= -golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1599,8 +1605,10 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +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-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1608,7 +1616,6 @@ golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= @@ -1621,16 +1628,6 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5N golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1652,10 +1649,10 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB 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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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= @@ -1703,20 +1700,20 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1725,8 +1722,8 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1742,8 +1739,9 @@ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/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.1.0/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.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1802,7 +1800,6 @@ golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1818,9 +1815,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1836,7 +1831,6 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1845,15 +1839,18 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1864,10 +1861,11 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/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.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1898,7 +1896,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1941,10 +1938,10 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -1953,12 +1950,10 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -2027,10 +2022,10 @@ google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 h1:0DnDgelxbooHLt0nyiPeCP0zrH/RL+UG558i1oNU1xE= google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:IuQRZAKkz+Mhos3ZZ0+hcGaTmLuuTuGw344uzwztGl8= -google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc= -google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= -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/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2063,10 +2058,10 @@ google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -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/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= -google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= 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= @@ -2083,15 +2078,14 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/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= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= @@ -2110,7 +2104,6 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= @@ -2126,6 +2119,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= @@ -2137,15 +2131,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= -k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= -k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/tests/certification/pubsub/aws/snssqs/snssqs_helper.go b/tests/certification/pubsub/aws/snssqs/snssqs_helper.go index a6ed483022..693c2c7c4d 100644 --- a/tests/certification/pubsub/aws/snssqs/snssqs_helper.go +++ b/tests/certification/pubsub/aws/snssqs/snssqs_helper.go @@ -14,16 +14,16 @@ limitations under the License. package snssqs_test import ( + "context" "encoding/json" "fmt" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sns" - "github.com/aws/aws-sdk-go/service/sns/snsiface" - "github.com/aws/aws-sdk-go/service/sqs" - "github.com/aws/aws-sdk-go/service/sts" - "github.com/aws/aws-sdk-go/service/sts/stsiface" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sns" + "github.com/aws/aws-sdk-go-v2/service/sqs" + sqsTypes "github.com/aws/aws-sdk-go-v2/service/sqs/types" + "github.com/aws/aws-sdk-go-v2/service/sts" ) var ( @@ -41,22 +41,22 @@ func deleteQueues(queues []string) error { return nil } -func deleteQueue(svc *sqs.SQS, queue string) error { +func deleteQueue(svc *sqs.Client, queue string) error { fmt.Printf("deleteQueue: %q\n", queue) queueUrl, err := getQueueURL(svc, queue) if err != nil { return fmt.Errorf("error getting the queue URL: %q err:%v", queue, err) } - _, err = svc.DeleteQueue(&sqs.DeleteQueueInput{ - QueueUrl: &queueUrl, + _, err = svc.DeleteQueue(context.Background(), &sqs.DeleteQueueInput{ + QueueUrl: aws.String(queueUrl), }) return err } -func getQueueURL(svc *sqs.SQS, queue string) (string, error) { - urlResult, err := svc.GetQueueUrl(&sqs.GetQueueUrlInput{ +func getQueueURL(svc *sqs.Client, queue string) (string, error) { + urlResult, err := svc.GetQueueUrl(context.Background(), &sqs.GetQueueUrlInput{ QueueName: aws.String(queue), }) @@ -67,19 +67,17 @@ func getQueueURL(svc *sqs.SQS, queue string) (string, error) { return *urlResult.QueueUrl, nil } -func getMessages(svc *sqs.SQS, queueURL string) (*sqs.ReceiveMessageOutput, error) { - input := sqs.ReceiveMessageInput{ +func getMessages(svc *sqs.Client, queueURL string) (*sqs.ReceiveMessageOutput, error) { + input := &sqs.ReceiveMessageInput{ // use this property to decide when a message should be discarded. - AttributeNames: []*string{ - aws.String(sqs.MessageSystemAttributeNameApproximateReceiveCount), - }, - MaxNumberOfMessages: aws.Int64(10), + AttributeNames: []sqsTypes.QueueAttributeName{"ApproximateReceiveCount"}, + MaxNumberOfMessages: 10, QueueUrl: aws.String(queueURL), - VisibilityTimeout: aws.Int64(5), - WaitTimeSeconds: aws.Int64(20), + VisibilityTimeout: 5, + WaitTimeSeconds: 20, } - msgResult, err := svc.ReceiveMessage(&input) + msgResult, err := svc.ReceiveMessage(context.Background(), input) if err != nil { return nil, err } @@ -87,8 +85,8 @@ func getMessages(svc *sqs.SQS, queueURL string) (*sqs.ReceiveMessageOutput, erro return msgResult, nil } -func deleteMessage(svc *sqs.SQS, queueURL, messageHandle string) error { - _, err := svc.DeleteMessage(&sqs.DeleteMessageInput{ +func deleteMessage(svc *sqs.Client, queueURL, messageHandle string) error { + _, err := svc.DeleteMessage(context.Background(), &sqs.DeleteMessageInput{ QueueUrl: aws.String(queueURL), ReceiptHandle: aws.String(messageHandle), }) @@ -100,14 +98,12 @@ func deleteMessage(svc *sqs.SQS, queueURL, messageHandle string) error { } func deleteTopics(topics []string, region string) error { - sess := session.Must( - session.NewSessionWithOptions( - session.Options{ - SharedConfigState: session.SharedConfigEnable, - }, - )) - svc := sns.New(sess) - id, err := getIdentity(sts.New(sess)) + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + return err + } + svc := sns.NewFromConfig(cfg) + id, err := getIdentity(sts.NewFromConfig(cfg)) if err != nil { return err } @@ -115,7 +111,7 @@ func deleteTopics(topics []string, region string) error { for _, topic := range topics { topicArn := buildARN(partition, serviceName, topic, region, id) fmt.Printf("Getting subscriptions for topicArn: %s\n", topicArn) - if subout, err := svc.ListSubscriptionsByTopic(&sns.ListSubscriptionsByTopicInput{ + if subout, err := svc.ListSubscriptionsByTopic(context.Background(), &sns.ListSubscriptionsByTopicInput{ TopicArn: aws.String(topicArn), }); err == nil { for _, sub := range subout.Subscriptions { @@ -134,37 +130,35 @@ func deleteTopics(topics []string, region string) error { return nil } -func deleteTopic(svc snsiface.SNSAPI, topic string) error { +func deleteTopic(svc *sns.Client, topic string) error { fmt.Printf("deleteTopic: %q\n", topic) - _, err := svc.DeleteTopic(&sns.DeleteTopicInput{ + _, err := svc.DeleteTopic(context.Background(), &sns.DeleteTopicInput{ TopicArn: aws.String(topic), }) return err } -func unsubscribeFromTopic(svc snsiface.SNSAPI, subscription string) error { - _, err := svc.Unsubscribe(&sns.UnsubscribeInput{ +func unsubscribeFromTopic(svc *sns.Client, subscription string) error { + _, err := svc.Unsubscribe(context.Background(), &sns.UnsubscribeInput{ SubscriptionArn: aws.String(subscription), }) return err } -func sqsService() *sqs.SQS { - sess := session.Must( - session.NewSessionWithOptions( - session.Options{ - SharedConfigState: session.SharedConfigEnable, - }, - )) - return sqs.New(sess) +func sqsService() *sqs.Client { + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + panic(err) + } + return sqs.NewFromConfig(cfg) } -func getIdentity(svc stsiface.STSAPI) (*sts.GetCallerIdentityOutput, error) { +func getIdentity(svc *sts.Client) (*sts.GetCallerIdentityOutput, error) { input := &sts.GetCallerIdentityInput{} - return svc.GetCallerIdentity(input) + return svc.GetCallerIdentity(context.Background(), input) } func buildARN(partition, serviceName, entityName, region string, id *sts.GetCallerIdentityOutput) string { @@ -172,7 +166,7 @@ func buildARN(partition, serviceName, entityName, region string, id *sts.GetCall } type QueueManager struct { - svc *sqs.SQS + svc *sqs.Client } type SNSMessagePayload struct { @@ -192,10 +186,11 @@ func NewQueueManager() *QueueManager { } func (qm *QueueManager) connect() error { - sess := session.Must(session.NewSessionWithOptions(session.Options{ - SharedConfigState: session.SharedConfigEnable, - })) - qm.svc = sqs.New(sess) + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + return err + } + qm.svc = sqs.NewFromConfig(cfg) return nil } @@ -212,7 +207,7 @@ func (qm *QueueManager) GetMessages(queue string, deleteMsg bool, mf MessageFunc numMgs := len(msgResult.Messages) for _, msg := range msgResult.Messages { - dm, err := extractDataMessage(msg) + dm, err := extractDataMessage(&msg) if err != nil { return -1, err } @@ -231,7 +226,7 @@ func (qm *QueueManager) GetMessages(queue string, deleteMsg bool, mf MessageFunc return numMgs, nil } -func extractDataMessage(msg *sqs.Message) (*DataMessage, error) { +func extractDataMessage(msg *sqsTypes.Message) (*DataMessage, error) { snsMP := SNSMessagePayload{} err := json.Unmarshal([]byte(*(msg.Body)), &snsMP) if err != nil { diff --git a/tests/certification/pubsub/azure/servicebus/queues/README.md b/tests/certification/pubsub/azure/servicebus/queues/README.md new file mode 100644 index 0000000000..6af0a608d9 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/README.md @@ -0,0 +1,206 @@ +# Azure Service Bus Queues Pubsub Certification + +The purpose of this module is to provide tests that certify the Azure Service Bus Queues Pubsub as a stable component. + +## Important: Queue vs Topic Semantics + +Unlike topics (publish-subscribe), queues use **competing consumer** semantics: +- Each message is delivered to **only ONE** consumer +- Multiple subscribers compete for messages (load balancing) +- Messages are NOT broadcast to all subscribers + +## Test Plan + +### Certification Tests + +1. **TestServicebusQueues** - Basic pub/sub functionality + - Run dapr application with 1 publisher and 2 subscribers + - Publisher publishes to 1 queue + - Both subscribers compete for messages (each message goes to only ONE subscriber) + - Verify that all expected messages were received (distributed among consumers) + +2. **TestServicebusQueuesMultipleSubsSameApp** - Multiple subscriptions in same app + - Run dapr application with 1 publisher and 1 subscriber app with multiple handlers + - Publisher publishes to 2 queues + - Verify messages are received on correct queues + +3. **TestServicebusQueuesNonexistingQueue** - Auto-creation of queues + - Run dapr application with 1 publisher and 1 subscriber + - Verify the creation of queue on first publish + - Send messages to the queue created + - Verify that subscriber received all the messages + +4. **TestServicebusQueuesNetworkInterruption** - Network resilience + - Run dapr application with 1 publisher and 1 subscriber + - Publisher publishes to 1 queue + - Simulate network interruptions using tc (traffic control) + - Verify that the component recovers and all messages are received + - **Note**: Requires root/sudo privileges + +5. **TestServicebusQueuesEntityManagement** - Disabled entity management + - Run dapr application with 1 publisher + - Publisher tries to publish to a queue that does not exist + - Verify that the queue does NOT get auto-created + - Verify that an error is returned + +6. **TestServicebusQueuesDefaultTtl** - Message TTL (Time-To-Live) + - Run dapr application with 1 publisher and 1 subscriber + - Publisher publishes to 1 queue, wait for TTL to expire + - Verify the message is deleted/expired before subscriber starts + +7. **TestServicebusQueuesAuthentication** - Azure AD authentication + - Run dapr application with 1 publisher and 1 subscriber + - Uses Service Principal authentication instead of connection string + - Publisher publishes to 1 queue + - Verify that all expected messages were received + +8. **TestServicebusQueuesMessageMetadata** - Message metadata handling + - Verify that custom metadata (partition key) is correctly passed + +9. **TestServicebusQueuesMultipleQueues** - Multiple queues + - Verify publishing to multiple queues simultaneously + +10. **TestServicebusQueuesLargeMessages** - Large message payloads + - Verify handling of larger message payloads (1KB+) + +11. **TestServicebusQueuesSequentialPublish** - Sequential batches + - Verify multiple sequential batch publishes + +12. **TestServicebusQueuesReconnection** - Sidecar restart recovery + - Run dapr application with 1 publisher and 1 subscriber + - Publish initial messages and verify receipt + - Stop and restart the sidecar + - Publish new messages after reconnection + - Verify component reconnects and new messages are received + - Uses unique queue per test run to avoid interference + +13. **TestServicebusQueuesEmptyMessages** - Minimal messages + - Verify handling of minimal/edge-case message payloads + - Uses unique queue per test run to avoid interference + +14. **TestServicebusQueuesConcurrentPublishers** - Multiple publishers + - Verify multiple sidecars publishing to the same queue + +## Prerequisites + +### Azure Resources +- Azure Service Bus namespace (Standard or Premium tier) +- Queues will be auto-created by tests (unless testing disabled entity management) + +### Required Queues +The following queues should exist or will be auto-created: +- `certification-pubsub-queue-active` +- `certification-pubsub-queue-passive` +- `certification-queue-per-test-run` + +**Note**: Some tests (Reconnection, EmptyMessages, EntityManagement) create unique queues with UUID suffixes to avoid interference between test runs. + +## Environment Variables + +### Required for Basic Tests (Connection String Authentication) + +```bash +# Azure Service Bus connection string (from Azure Portal > Service Bus > Shared access policies) +export AzureServiceBusConnectionString="Endpoint=sb://.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=" +``` + +### Required for Authentication Test (Service Principal / Azure AD) + +```bash +# Service Bus namespace (full FQDN) +export AzureServiceBusNamespace=".servicebus.windows.net" + +# Azure AD Tenant ID +export AzureCertificationTenantId="" + +# Service Principal Client ID (App ID) +export AzureCertificationServicePrincipalClientId="" + +# Service Principal Client Secret +export AzureCertificationServicePrincipalClientSecret="" +``` + +### Creating a Service Principal for Authentication Test + +```bash +# Create Service Principal with Contributor role on the Service Bus namespace +az ad sp create-for-rbac --name "dapr-cert-sp" \ + --role Contributor \ + --scopes /subscriptions//resourceGroups//providers/Microsoft.ServiceBus/namespaces/ \ + -o json + +# IMPORTANT: Also assign the Data Owner role for sending/receiving messages +az role assignment create \ + --assignee "" \ + --role "Azure Service Bus Data Owner" \ + --scope "/subscriptions//resourceGroups//providers/Microsoft.ServiceBus/namespaces/" +``` + +## Running Tests + +### Run All Tests +```bash +# Set environment variables first +export AzureServiceBusConnectionString="..." + +cd tests/certification/pubsub/azure/servicebus/queues +go test -v -timeout 30m +``` + +### Run Specific Test +```bash +go test -v -timeout 5m -run "TestServicebusQueues$" +go test -v -timeout 5m -run "TestServicebusQueuesMultipleSubsSameApp$" +go test -v -timeout 5m -run "TestServicebusQueuesAuthentication$" +``` + +### Run Network Interruption Test (requires sudo) +```bash +# Clean any residual tc rules first +sudo tc qdisc del dev eth0 root 2>/dev/null || true + +# Run the test with sudo +sudo -E go test -v -timeout 5m -run "TestServicebusQueuesNetworkInterruption$" +``` + +### Run All Tests with All Variables +```bash +export AzureServiceBusConnectionString="Endpoint=sb://.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=" +export AzureServiceBusNamespace=".servicebus.windows.net" +export AzureCertificationTenantId="" +export AzureCertificationServicePrincipalClientId="" +export AzureCertificationServicePrincipalClientSecret="" + +go test -v -timeout 30m +``` + +## Troubleshooting + +### "pubsub messagebus is not found" +- Check that environment variables are set correctly +- Verify the connection string format is correct + +### Network Interruption Test Fails with "packet rules already setup" +```bash +# Clean residual tc rules +sudo tc qdisc del dev eth0 root +``` + +### Authentication Test Times Out +- Ensure the Service Principal has the "Azure Service Bus Data Owner" role +- Wait a few minutes after role assignment for propagation +- The namespace name must be the full FQDN (e.g., `myns.servicebus.windows.net`) + +### Tests Hang or Timeout +- Check Azure Service Bus connectivity +- Verify the namespace is accessible from your network +- Check for any firewall rules blocking ports 5671/5672 (AMQP) + +### Tests Fail with "elements differ" or Unexpected Messages +- Some tests use shared queues that may have residual messages from previous runs +- Wait a few minutes for messages to expire or manually purge the queue +- Tests like Reconnection and EmptyMessages use unique queues to avoid this issue + +### Entity Management Test Fails +- Ensure the queue name used doesn't already exist in the namespace +- The test uses a unique UUID-based queue name to avoid conflicts diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/authentication/localsecrets.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/authentication/localsecrets.yaml new file mode 100644 index 0000000000..94bb7a2643 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/authentication/localsecrets.yaml @@ -0,0 +1,9 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store + namespace: default +spec: + type: secretstores.local.env + version: v1 + metadata: diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/authentication/service_bus.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/authentication/service_bus.yaml new file mode 100644 index 0000000000..f5451caf2d --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/authentication/service_bus.yaml @@ -0,0 +1,27 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus + namespace: default +spec: + type: pubsub.azure.servicebus.queues + version: v1 + metadata: + - name: namespaceName + secretKeyRef: + name: AzureServiceBusNamespace + key: AzureServiceBusNamespace + - name: azureTenantId + secretKeyRef: + name: AzureCertificationTenantId + key: AzureCertificationTenantId + - name: azureClientId + secretKeyRef: + name: AzureCertificationServicePrincipalClientId + key: AzureCertificationServicePrincipalClientId + - name: azureClientSecret + secretKeyRef: + name: AzureCertificationServicePrincipalClientSecret + key: AzureCertificationServicePrincipalClientSecret +auth: + secretstore: envvar-secret-store diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/consumer_one/localsecrets.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/consumer_one/localsecrets.yaml new file mode 100644 index 0000000000..94bb7a2643 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/consumer_one/localsecrets.yaml @@ -0,0 +1,9 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store + namespace: default +spec: + type: secretstores.local.env + version: v1 + metadata: diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/consumer_one/service_bus.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/consumer_one/service_bus.yaml new file mode 100644 index 0000000000..4f394ab382 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/consumer_one/service_bus.yaml @@ -0,0 +1,17 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus + namespace: default +spec: + type: pubsub.azure.servicebus.queues + version: v1 + metadata: + - name: connectionString + secretKeyRef: + name: AzureServiceBusConnectionString + key: AzureServiceBusConnectionString + - name: disableEntityManagement + value: "false" +auth: + secretstore: envvar-secret-store diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/consumer_two/localsecrets.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/consumer_two/localsecrets.yaml new file mode 100644 index 0000000000..94bb7a2643 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/consumer_two/localsecrets.yaml @@ -0,0 +1,9 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store + namespace: default +spec: + type: secretstores.local.env + version: v1 + metadata: diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/consumer_two/service_bus.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/consumer_two/service_bus.yaml new file mode 100644 index 0000000000..4f394ab382 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/consumer_two/service_bus.yaml @@ -0,0 +1,17 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus + namespace: default +spec: + type: pubsub.azure.servicebus.queues + version: v1 + metadata: + - name: connectionString + secretKeyRef: + name: AzureServiceBusConnectionString + key: AzureServiceBusConnectionString + - name: disableEntityManagement + value: "false" +auth: + secretstore: envvar-secret-store diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/default_ttl/localsecrets.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/default_ttl/localsecrets.yaml new file mode 100644 index 0000000000..94bb7a2643 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/default_ttl/localsecrets.yaml @@ -0,0 +1,9 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store + namespace: default +spec: + type: secretstores.local.env + version: v1 + metadata: diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/default_ttl/service_bus.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/default_ttl/service_bus.yaml new file mode 100644 index 0000000000..37cb17841f --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/default_ttl/service_bus.yaml @@ -0,0 +1,19 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus + namespace: default +spec: + type: pubsub.azure.servicebus.queues + version: v1 + metadata: + - name: connectionString + secretKeyRef: + name: AzureServiceBusConnectionString + key: AzureServiceBusConnectionString + - name: disableEntityManagement + value: "false" + - name: ttlInSeconds + value: "10" +auth: + secretstore: envvar-secret-store diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/entity_mgmt/localsecrets.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/entity_mgmt/localsecrets.yaml new file mode 100644 index 0000000000..94bb7a2643 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/entity_mgmt/localsecrets.yaml @@ -0,0 +1,9 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store + namespace: default +spec: + type: secretstores.local.env + version: v1 + metadata: diff --git a/tests/certification/pubsub/azure/servicebus/queues/components/entity_mgmt/service_bus.yaml b/tests/certification/pubsub/azure/servicebus/queues/components/entity_mgmt/service_bus.yaml new file mode 100644 index 0000000000..28e5ae73a4 --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/components/entity_mgmt/service_bus.yaml @@ -0,0 +1,17 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus + namespace: default +spec: + type: pubsub.azure.servicebus.queues + version: v1 + metadata: + - name: connectionString + secretKeyRef: + name: AzureServiceBusConnectionString + key: AzureServiceBusConnectionString + - name: disableEntityManagement + value: "true" +auth: + secretstore: envvar-secret-store diff --git a/tests/certification/pubsub/azure/servicebus/queues/config.yaml b/tests/certification/pubsub/azure/servicebus/queues/config.yaml new file mode 100644 index 0000000000..d43ae2bbbc --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/config.yaml @@ -0,0 +1,2 @@ +componentType: pubsub +componentName: azure.servicebus.queues diff --git a/tests/certification/pubsub/azure/servicebus/queues/servicebus_test.go b/tests/certification/pubsub/azure/servicebus/queues/servicebus_test.go new file mode 100644 index 0000000000..fd185ca05b --- /dev/null +++ b/tests/certification/pubsub/azure/servicebus/queues/servicebus_test.go @@ -0,0 +1,1369 @@ +/* +Copyright 2021 The Dapr Authors +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 servicebusqueues_test + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/multierr" + + // Pub-Sub. + pubsub_servicebus "github.com/dapr/components-contrib/pubsub/azure/servicebus/queues" + secretstore_env "github.com/dapr/components-contrib/secretstores/local/env" + pubsub_loader "github.com/dapr/dapr/pkg/components/pubsub" + secretstores_loader "github.com/dapr/dapr/pkg/components/secretstores" + "github.com/dapr/dapr/pkg/config/protocol" + "github.com/dapr/kit/logger" + + "github.com/dapr/dapr/pkg/runtime" + dapr "github.com/dapr/go-sdk/client" + "github.com/dapr/go-sdk/service/common" + + // Certification testing runnables + "github.com/dapr/components-contrib/tests/certification/embedded" + "github.com/dapr/components-contrib/tests/certification/flow" + "github.com/dapr/components-contrib/tests/certification/flow/app" + "github.com/dapr/components-contrib/tests/certification/flow/network" + "github.com/dapr/components-contrib/tests/certification/flow/sidecar" + "github.com/dapr/components-contrib/tests/certification/flow/simulate" + "github.com/dapr/components-contrib/tests/certification/flow/watcher" +) + +const ( + sidecarName1 = "dapr-1" + sidecarName2 = "dapr-2" + + appID1 = "app-1" + appID2 = "app-2" + + numMessages = 10 + appPort = 8000 + portOffset = 2 + messageKey = "partitionKey" + pubsubName = "messagebus" + queueActiveName = "certification-pubsub-queue-active" + queuePassiveName = "certification-pubsub-queue-passive" + queueToBeCreated = "certification-queue-per-test-run" + queueDefaultName = "certification-queue-default" + partition0 = "partition-0" + partition1 = "partition-1" +) + +func TestServicebusQueues(t *testing.T) { + // For queues, messages are competing between consumers - each message is received by only ONE consumer. + // Use a single shared watcher that both apps will feed into. + sharedWatcher := watcher.NewUnordered() + + // subscriber of the given queue + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + // Simulate periodic errors. + sim := simulate.PeriodicError(ctx, 100) + // Setup the /orders event handler. + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + if err := sim(); err != nil { + return true, err + } + + // Track/Observe the data of the event. + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received appID: %s, pubsub: %s, queue: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // prepare the messages + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + // add the messages as expectations to the watchers + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + // get the sidecar (dapr) client + client := sidecar.GetClient(ctx, sidecarName) + + // publish messages + ctx.Logf("Publishing messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + var publishOptions dapr.PublishEventOption + + if metadata != nil { + publishOptions = dapr.PublishEventWithMetadata(metadata) + } + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + var err error + + if publishOptions != nil { + err = client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + } else { + err = client.PublishEvent(ctx, pubsubName, queueName, message) + } + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // assert for messages + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + + return nil + } + } + + flow.New(t, "servicebus queues certification basic test"). + + // Run subscriberApplication app1 - both apps use the same sharedWatcher + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, queueActiveName, sharedWatcher))). + + // Run the Dapr sidecar with the servicebus queues component 1 + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + + // Run subscriberApplication app2 - both apps use the same sharedWatcher + Step(app.Run(appID2, fmt.Sprintf(":%d", appPort+portOffset), + subscriberApplication(appID2, queueActiveName, sharedWatcher))). + + // Run the Dapr sidecar with the component 2. + Step(sidecar.Run(sidecarName2, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_two"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort+portOffset)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort+portOffset)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort+portOffset)), + embedded.WithProfilePort(strconv.Itoa(runtime.DefaultProfilePort+portOffset)), + )..., + )). + // For queues, only register expectations once since messages compete between consumers + Step("publish messages to queue", publishMessages(nil, sidecarName1, queueActiveName, sharedWatcher)). + Step("publish messages to unused queue", publishMessages(nil, sidecarName1, queuePassiveName)). + Step("verify all messages received by competing consumers", assertMessages(10*time.Second, sharedWatcher)). + Step("reset", flow.Reset(sharedWatcher)). + Run() +} + +func TestServicebusQueuesMultipleSubsSameApp(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + // Set the partition key on all messages so they are written to the same partition. + metadata := map[string]string{ + messageKey: partition0, + } + + // subscriber of the given queue + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + // Simulate periodic errors. + sim := simulate.PeriodicError(ctx, 100) + // Setup the /orders event handler. + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + if err := sim(); err != nil { + return true, err + } + + // Track/Observe the data of the event. + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received appID: %s, pubsub: %s, queue: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // prepare the messages + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + // add the messages as expectations to the watchers + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + // get the sidecar (dapr) client + client := sidecar.GetClient(ctx, sidecarName) + + // publish messages + ctx.Logf("Publishing messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + var publishOptions dapr.PublishEventOption + + if metadata != nil { + publishOptions = dapr.PublishEventWithMetadata(metadata) + } + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + var err error + + if publishOptions != nil { + err = client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + } else { + err = client.PublishEvent(ctx, pubsubName, queueName, message) + } + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // assert for messages + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + + return nil + } + } + + flow.New(t, "servicebus queues certification - multiple subscribers same app"). + + // Run subscriberApplication app1 + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, queueActiveName, consumerGroup1))). + + // Run the Dapr sidecar with the servicebus queues component 1 + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish messages to queue", publishMessages(metadata, sidecarName1, queueActiveName, consumerGroup1)). + Step("verify if app1 has received messages published to queue", assertMessages(10*time.Second, consumerGroup1)). + Step("reset", flow.Reset(consumerGroup1)). + Run() +} + +func TestServicebusQueuesNonexistingQueue(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + // Set the partition key on all messages so they are written to the same partition. + metadata := map[string]string{ + messageKey: partition0, + } + + // subscriber of the given queue + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + // Simulate periodic errors. + sim := simulate.PeriodicError(ctx, 100) + // Setup the /orders event handler. + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + if err := sim(); err != nil { + return true, err + } + + // Track/Observe the data of the event. + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received appID: %s, pubsub: %s, queue: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // prepare the messages + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + // add the messages as expectations to the watchers + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + // get the sidecar (dapr) client + client := sidecar.GetClient(ctx, sidecarName) + + // publish messages + ctx.Logf("Publishing messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + var publishOptions dapr.PublishEventOption + + if metadata != nil { + publishOptions = dapr.PublishEventWithMetadata(metadata) + } + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + var err error + + if publishOptions != nil { + err = client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + } else { + err = client.PublishEvent(ctx, pubsubName, queueName, message) + } + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // assert for messages + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + + return nil + } + } + + flow.New(t, "servicebus queues certification - non-existing queue"). + + // Run subscriberApplication app1 + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort+portOffset*3), + subscriberApplication(appID1, queueToBeCreated, consumerGroup1))). + + // Run the Dapr sidecar with the component entitymanagement + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort+portOffset*3)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort+portOffset*3)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort+portOffset*3)), + embedded.WithProfilePort(strconv.Itoa(runtime.DefaultProfilePort+portOffset*3)), + )..., + )). + Step(fmt.Sprintf("publish messages to queueToBeCreated: %s", queueToBeCreated), publishMessages(metadata, sidecarName1, queueToBeCreated, consumerGroup1)). + Step("wait", flow.Sleep(30*time.Second)). + Step("verify if app1 has received messages published to newly created queue", assertMessages(10*time.Second, consumerGroup1)). + Run() +} + +func TestServicebusQueuesNetworkInterruption(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + // Set the partition key on all messages so they are written to the same partition. + metadata := map[string]string{ + messageKey: partition0, + } + + // subscriber of the given queue + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + // Simulate periodic errors. + sim := simulate.PeriodicError(ctx, 100) + // Setup the /orders event handler. + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + if err := sim(); err != nil { + return true, err + } + + // Track/Observe the data of the event. + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received appID: %s, pubsub: %s, queue: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // prepare the messages + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + // add the messages as expectations to the watchers + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + // get the sidecar (dapr) client + client := sidecar.GetClient(ctx, sidecarName) + + // publish messages + ctx.Logf("Publishing messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + var publishOptions dapr.PublishEventOption + + if metadata != nil { + publishOptions = dapr.PublishEventWithMetadata(metadata) + } + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + var err error + + if publishOptions != nil { + err = client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + } else { + err = client.PublishEvent(ctx, pubsubName, queueName, message) + } + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // assert for messages + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + + return nil + } + } + + flow.New(t, "servicebus queues certification - network interruption"). + + // Run subscriberApplication app1 + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort+portOffset), + subscriberApplication(appID1, queueActiveName, consumerGroup1))). + + // Run the Dapr sidecar with the component entitymanagement + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort+portOffset)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort+portOffset)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort+portOffset)), + embedded.WithProfilePort(strconv.Itoa(runtime.DefaultProfilePort+portOffset)), + )..., + )). + Step(fmt.Sprintf("publish messages to queue: %s", queueActiveName), publishMessages(metadata, sidecarName1, queueActiveName, consumerGroup1)). + Step("interrupt network", network.InterruptNetwork(time.Minute, []string{}, []string{}, "5671", "5672")). + Step("wait", flow.Sleep(30*time.Second)). + Step("verify if app1 has received messages published to queue", assertMessages(10*time.Second, consumerGroup1)). + Run() +} + +func TestServicebusQueuesEntityManagement(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + // Use a unique queue name that does NOT exist in Azure + // This test verifies that publishing fails when entity management is disabled + // and the queue doesn't exist + nonExistentQueue := fmt.Sprintf("non-existent-queue-%s", uuid.New().String()) + + // Set the partition key on all messages so they are written to the same partition. + metadata := map[string]string{ + messageKey: partition0, + } + + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + // Setup the /orders event handler. + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + // Track/Observe the data of the event. + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received appID: %s, pubsub: %s, queue: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // prepare the messages + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + // add the messages as expectations to the watchers + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + // get the sidecar (dapr) client + client := sidecar.GetClient(ctx, sidecarName) + + // publish messages + ctx.Logf("Publishing messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + var publishOptions dapr.PublishEventOption + + if metadata != nil { + publishOptions = dapr.PublishEventWithMetadata(metadata) + } + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + var err error + + if publishOptions != nil { + err = client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + } else { + err = client.PublishEvent(ctx, pubsubName, queueName, message) + } + // Error is expected as the queue does not exist + require.Error(ctx, err, "error publishing message") + } + return nil + } + } + + flow.New(t, "servicebus queues certification - entity management disabled"). + + // Run subscriberApplication app1 + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort+portOffset), + subscriberApplication(appID1, queueActiveName, consumerGroup1))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/entity_mgmt"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort+portOffset)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort+portOffset)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort+portOffset)), + embedded.WithProfilePort(strconv.Itoa(runtime.DefaultProfilePort+portOffset)), + )..., + )). + Step(fmt.Sprintf("publish messages to non-existent queue: %s", nonExistentQueue), publishMessages(metadata, sidecarName1, nonExistentQueue, consumerGroup1)). + Run() +} + +func TestServicebusQueuesDefaultTtl(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + // Set the partition key on all messages so they are written to the same partition. + metadata := map[string]string{ + messageKey: partition0, + } + + // subscriber of the given queue + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + // Setup the /orders event handler. + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + ctx.Logf("Got message: %s", e.Data) + messagesWatcher.FailIfNotExpected(t, e.Data) + return false, nil + }), + ) + } + } + + testTtlPublishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // prepare the messages + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + // get the sidecar (dapr) client + client := sidecar.GetClient(ctx, sidecarName) + + // publish messages + ctx.Logf("Publishing messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + var publishOptions dapr.PublishEventOption + + if metadata != nil { + publishOptions = dapr.PublishEventWithMetadata(metadata) + } + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + var err error + + if publishOptions != nil { + err = client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + } else { + err = client.PublishEvent(ctx, pubsubName, queueName, message) + } + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // assert for messages + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + + return nil + } + } + + flow.New(t, "servicebus queues certification - default ttl attribute"). + + // Run subscriberApplication app1 + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort+portOffset), + subscriberApplication(appID1, queueActiveName, consumerGroup1))). + + // Run the Dapr sidecar with the component default_ttl + Step(sidecar.Run("initialSidecar", + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/default_ttl"), + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort+portOffset)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort+portOffset)), + embedded.WithProfilePort(strconv.Itoa(runtime.DefaultProfilePort+portOffset)), + )..., + )). + Step(fmt.Sprintf("publish messages to queue: %s", queueActiveName), testTtlPublishMessages(metadata, "initialSidecar", queueActiveName, consumerGroup1)). + Step("stop initial sidecar", sidecar.Stop("initialSidecar")). + Step("wait", flow.Sleep(20*time.Second)). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/default_ttl"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort+portOffset)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort+portOffset*2)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort+portOffset*2)), + embedded.WithProfilePort(strconv.Itoa(runtime.DefaultProfilePort+portOffset*2)), + )..., + )). + Step("verify if app1 has received messages published to queue", assertMessages(10*time.Second, consumerGroup1)). + Run() +} + +func TestServicebusQueuesAuthentication(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + // Set the partition key on all messages so they are written to the same partition. + metadata := map[string]string{ + messageKey: partition0, + } + + // subscriber of the given queue + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + // Simulate periodic errors. + sim := simulate.PeriodicError(ctx, 100) + // Setup the /orders event handler. + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + if err := sim(); err != nil { + return true, err + } + + // Track/Observe the data of the event. + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received appID: %s, pubsub: %s, queue: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // prepare the messages + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + // add the messages as expectations to the watchers + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + // get the sidecar (dapr) client + client := sidecar.GetClient(ctx, sidecarName) + + // publish messages + ctx.Logf("Publishing messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + var publishOptions dapr.PublishEventOption + + if metadata != nil { + publishOptions = dapr.PublishEventWithMetadata(metadata) + } + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + var err error + + if publishOptions != nil { + err = client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + } else { + err = client.PublishEvent(ctx, pubsubName, queueName, message) + } + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // assert for messages + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + + return nil + } + } + + flow.New(t, "servicebus queues certification - authentication"). + + // Run subscriberApplication app1 + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort+portOffset), + subscriberApplication(appID1, queueActiveName, consumerGroup1))). + + // Run the Dapr sidecar with the authentication component + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/authentication"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort+portOffset)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort+portOffset)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort+portOffset)), + embedded.WithProfilePort(strconv.Itoa(runtime.DefaultProfilePort+portOffset)), + )..., + )). + Step("wait for sidecar to be ready", flow.Sleep(10*time.Second)). + Step(fmt.Sprintf("publish messages to queue: %s", queueActiveName), publishMessages(metadata, sidecarName1, queueActiveName, consumerGroup1)). + Step("wait", flow.Sleep(30*time.Second)). + Step("verify if app1 has received messages published to queue", assertMessages(10*time.Second, consumerGroup1)). + Run() +} + +// TestServicebusQueuesMessageMetadata tests that message metadata is correctly passed +func TestServicebusQueuesMessageMetadata(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + metadata := map[string]string{ + messageKey: partition0, + } + + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received appID: %s, pubsub: %s, queue: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + client := sidecar.GetClient(ctx, sidecarName) + ctx.Logf("Publishing messages with metadata. sidecarName: %s, queueName: %s", sidecarName, queueName) + + publishOptions := dapr.PublishEventWithMetadata(metadata) + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + err := client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + return nil + } + } + + flow.New(t, "servicebus queues certification - message metadata"). + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, queueActiveName, consumerGroup1))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish messages with metadata", publishMessages(metadata, sidecarName1, queueActiveName, consumerGroup1)). + Step("verify messages received", assertMessages(10*time.Second, consumerGroup1)). + Step("reset", flow.Reset(consumerGroup1)). + Run() +} + +// TestServicebusQueuesMultipleQueues tests publishing to multiple queues +func TestServicebusQueuesMultipleQueues(t *testing.T) { + activeQueueWatcher := watcher.NewUnordered() + passiveQueueWatcher := watcher.NewUnordered() + + metadata := map[string]string{ + messageKey: partition0, + } + + multiQueueSubscriber := func(appID string, activeWatcher, passiveWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueActiveName, + Route: "/active", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + activeWatcher.Observe(e.Data) + ctx.Logf("Active Queue - Message Received: %s", e.Data) + return false, nil + }), + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queuePassiveName, + Route: "/passive", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + passiveWatcher.Observe(e.Data) + ctx.Logf("Passive Queue - Message Received: %s", e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("partitionKey: %s, message for queue: %s, index: %03d, uniqueId: %s", metadata[messageKey], queueName, i, uuid.New().String()) + } + + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + client := sidecar.GetClient(ctx, sidecarName) + ctx.Logf("Publishing messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + publishOptions := dapr.PublishEventWithMetadata(metadata) + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + err := client.PublishEvent(ctx, pubsubName, queueName, message, publishOptions) + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + return nil + } + } + + flow.New(t, "servicebus queues certification - multiple queues"). + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + multiQueueSubscriber(appID1, activeQueueWatcher, passiveQueueWatcher))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish to active queue", publishMessages(metadata, sidecarName1, queueActiveName, activeQueueWatcher)). + Step("publish to passive queue", publishMessages(metadata, sidecarName1, queuePassiveName, passiveQueueWatcher)). + Step("verify active queue messages", assertMessages(10*time.Second, activeQueueWatcher)). + Step("verify passive queue messages", assertMessages(10*time.Second, passiveQueueWatcher)). + Step("reset", flow.Reset(activeQueueWatcher, passiveQueueWatcher)). + Run() +} + +// TestServicebusQueuesLargeMessages tests handling of larger message payloads +func TestServicebusQueuesLargeMessages(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + messagesWatcher.Observe(e.Data) + ctx.Logf("Large Message Received, length: %d", len(fmt.Sprintf("%v", e.Data))) + return false, nil + }), + ) + } + } + + publishLargeMessages := func(sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // Create larger messages (1KB each) + messages := make([]string, 5) + for i := range messages { + // Create a 1KB payload + payload := make([]byte, 1024) + for j := range payload { + payload[j] = byte('A' + (j % 26)) + } + messages[i] = fmt.Sprintf("large-message-%03d-%s-%s", i, uuid.New().String(), string(payload)) + } + + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + client := sidecar.GetClient(ctx, sidecarName) + ctx.Logf("Publishing large messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + for _, message := range messages { + ctx.Logf("Publishing large message of length: %d", len(message)) + err := client.PublishEvent(ctx, pubsubName, queueName, message) + require.NoError(ctx, err, "error publishing large message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + return nil + } + } + + flow.New(t, "servicebus queues certification - large messages"). + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, queueActiveName, consumerGroup1))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish large messages", publishLargeMessages(sidecarName1, queueActiveName, consumerGroup1)). + Step("verify large messages received", assertMessages(15*time.Second, consumerGroup1)). + Step("reset", flow.Reset(consumerGroup1)). + Run() +} + +// TestServicebusQueuesSequentialPublish tests sequential message publishing and ordering +func TestServicebusQueuesSequentialPublish(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + messagesWatcher.Observe(e.Data) + ctx.Logf("Sequential Message Received: %s", e.Data) + return false, nil + }), + ) + } + } + + publishSequentialMessages := func(sidecarName string, queueName string, batchNum int, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("batch-%d-message-%03d-%s", batchNum, i, uuid.New().String()) + } + + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + client := sidecar.GetClient(ctx, sidecarName) + ctx.Logf("Publishing batch %d messages. sidecarName: %s, queueName: %s", batchNum, sidecarName, queueName) + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + err := client.PublishEvent(ctx, pubsubName, queueName, message) + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + return nil + } + } + + flow.New(t, "servicebus queues certification - sequential publish"). + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, queueActiveName, consumerGroup1))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish batch 1", publishSequentialMessages(sidecarName1, queueActiveName, 1, consumerGroup1)). + Step("publish batch 2", publishSequentialMessages(sidecarName1, queueActiveName, 2, consumerGroup1)). + Step("verify all messages received", assertMessages(15*time.Second, consumerGroup1)). + Step("reset", flow.Reset(consumerGroup1)). + Run() +} + +// TestServicebusQueuesReconnection tests that the component reconnects after sidecar restart +func TestServicebusQueuesReconnection(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + // Use unique queue name to avoid interference from other tests + uniqueQueueName := fmt.Sprintf("certification-reconnection-%s", uuid.New().String()[:8]) + + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received after reconnection: %s", e.Data) + return false, nil + }), + ) + } + } + + publishMessages := func(sidecarName string, queueName string, prefix string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + messages := make([]string, numMessages) + for i := range messages { + messages[i] = fmt.Sprintf("%s-message-%03d-%s", prefix, i, uuid.New().String()) + } + + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + client := sidecar.GetClient(ctx, sidecarName) + ctx.Logf("Publishing %s messages. sidecarName: %s, queueName: %s", prefix, sidecarName, queueName) + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + err := client.PublishEvent(ctx, pubsubName, queueName, message) + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + return nil + } + } + + flow.New(t, "servicebus queues certification - reconnection"). + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, uniqueQueueName, consumerGroup1))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish initial messages", publishMessages(sidecarName1, uniqueQueueName, "initial", consumerGroup1)). + Step("verify initial messages", assertMessages(10*time.Second, consumerGroup1)). + Step("reset watcher", flow.Reset(consumerGroup1)). + Step("stop sidecar", sidecar.Stop(sidecarName1)). + Step("wait for shutdown", flow.Sleep(5*time.Second)). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish after reconnect", publishMessages(sidecarName1, uniqueQueueName, "reconnected", consumerGroup1)). + Step("verify reconnected messages", assertMessages(10*time.Second, consumerGroup1)). + Step("reset", flow.Reset(consumerGroup1)). + Run() +} + +// TestServicebusQueuesEmptyMessages tests handling of minimal message payloads +func TestServicebusQueuesEmptyMessages(t *testing.T) { + consumerGroup1 := watcher.NewUnordered() + // Use unique queue name to avoid interference from other tests + uniqueQueueName := fmt.Sprintf("certification-minimal-messages-%s", uuid.New().String()[:8]) + + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + messagesWatcher.Observe(e.Data) + ctx.Logf("Message Received: %v (length: %d)", e.Data, len(fmt.Sprintf("%v", e.Data))) + return false, nil + }), + ) + } + } + + publishMinimalMessages := func(sidecarName string, queueName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + // Test minimal valid messages with unique identifiers to avoid collisions + testID := uuid.New().String()[:8] + messages := []string{ + fmt.Sprintf("minimal-a-%s", testID), + fmt.Sprintf("minimal-ab-%s", testID), + fmt.Sprintf("minimal-test-%s", testID), + fmt.Sprintf("minimal-hello-%s", testID), + fmt.Sprintf("minimal-12345-%s", testID), + } + + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + client := sidecar.GetClient(ctx, sidecarName) + ctx.Logf("Publishing minimal messages. sidecarName: %s, queueName: %s", sidecarName, queueName) + + for _, message := range messages { + ctx.Logf("Publishing minimal message: %q", message) + err := client.PublishEvent(ctx, pubsubName, queueName, message) + require.NoError(ctx, err, "error publishing minimal message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + return nil + } + } + + flow.New(t, "servicebus queues certification - minimal messages"). + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, uniqueQueueName, consumerGroup1))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish minimal messages", publishMinimalMessages(sidecarName1, uniqueQueueName, consumerGroup1)). + Step("verify minimal messages received", assertMessages(10*time.Second, consumerGroup1)). + Step("reset", flow.Reset(consumerGroup1)). + Run() +} + +// TestServicebusQueuesConcurrentPublishers tests multiple publishers sending to the same queue +func TestServicebusQueuesConcurrentPublishers(t *testing.T) { + sharedWatcher := watcher.NewUnordered() + + subscriberApplication := func(appID string, queueName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: queueName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + messagesWatcher.Observe(e.Data) + ctx.Logf("Message from concurrent publisher: %s", e.Data) + return false, nil + }), + ) + } + } + + publishFromSidecar := func(sidecarName string, queueName string, publisherID string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + messages := make([]string, 5) // 5 messages per publisher + for i := range messages { + messages[i] = fmt.Sprintf("publisher-%s-message-%03d-%s", publisherID, i, uuid.New().String()) + } + + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + client := sidecar.GetClient(ctx, sidecarName) + ctx.Logf("Publisher %s sending messages via %s", publisherID, sidecarName) + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + err := client.PublishEvent(ctx, pubsubName, queueName, message) + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + for _, m := range messageWatchers { + m.Assert(ctx, 25*timeout) + } + return nil + } + } + + flow.New(t, "servicebus queues certification - concurrent publishers"). + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, queueActiveName, sharedWatcher))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step(sidecar.Run(sidecarName2, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_two"), + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort+portOffset)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort+portOffset)), + embedded.WithProfilePort(strconv.Itoa(runtime.DefaultProfilePort+portOffset)), + )..., + )). + Step("publisher 1 sends messages", publishFromSidecar(sidecarName1, queueActiveName, "1", sharedWatcher)). + Step("publisher 2 sends messages", publishFromSidecar(sidecarName2, queueActiveName, "2", sharedWatcher)). + Step("verify all messages from both publishers", assertMessages(15*time.Second, sharedWatcher)). + Step("reset", flow.Reset(sharedWatcher)). + Run() +} + +func componentRuntimeOptions() []embedded.Option { + log := logger.NewLogger("dapr.components") + log.SetOutputLevel(logger.DebugLevel) + + pubsubRegistry := pubsub_loader.NewRegistry() + pubsubRegistry.Logger = log + pubsubRegistry.RegisterComponent(pubsub_servicebus.NewAzureServiceBusQueues, "azure.servicebus.queues") + + secretstoreRegistry := secretstores_loader.NewRegistry() + secretstoreRegistry.Logger = log + secretstoreRegistry.RegisterComponent(secretstore_env.NewEnvSecretStore, "local.env") + + return []embedded.Option{ + embedded.WithPubSubs(pubsubRegistry), + embedded.WithSecretStores(secretstoreRegistry), + } +} diff --git a/tests/certification/pubsub/azure/servicebus/topics/servicebus_test.go b/tests/certification/pubsub/azure/servicebus/topics/servicebus_test.go index de47f202f1..2fbdca1a7b 100644 --- a/tests/certification/pubsub/azure/servicebus/topics/servicebus_test.go +++ b/tests/certification/pubsub/azure/servicebus/topics/servicebus_test.go @@ -18,6 +18,8 @@ import ( "fmt" "regexp" "strconv" + "sync" + "sync/atomic" "testing" "time" @@ -1084,10 +1086,16 @@ func TestServicebusWithSessionsFIFO(t *testing.T) { sessionWatcher := watcher.NewOrdered() + // Track active messages per session to ensure no parallel processing within a session + var ( + fifoMu sync.Mutex + fifoActivePerSess = make(map[string]int) + fifoParallelIssues atomic.Int32 + ) + // subscriber of the given topic subscriberApplicationWithSessions := func(appID string, topicName string, messagesWatcher *watcher.Watcher) app.SetupFn { return func(ctx flow.Context, s common.Service) error { - // Setup the /orders event handler. return multierr.Combine( s.AddTopicEventHandler(&common.Subscription{ PubsubName: pubsubName, @@ -1098,9 +1106,31 @@ func TestServicebusWithSessionsFIFO(t *testing.T) { "maxConcurrentSessions": "1", }, }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { - // Track/Observe the data of the event. + // Extract session ID (if present) to track concurrency + var sessionID string + if m := sessionIDRegex.FindStringSubmatch(fmt.Sprintf("%s", e.Data)); len(m) > 1 { + sessionID = m[1] + } + + fifoMu.Lock() + active := fifoActivePerSess[sessionID] + if active > 0 { + fifoParallelIssues.Add(1) + ctx.Logf("Session %s already has %d active messages", sessionID, active) + } + fifoActivePerSess[sessionID] = active + 1 + fifoMu.Unlock() + + // Simulate handler work to widen potential overlap window + time.Sleep(20 * time.Millisecond) + messagesWatcher.Observe(e.Data) ctx.Logf("Message Received appID: %s,pubsub: %s, topic: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + + fifoMu.Lock() + fifoActivePerSess[sessionID]-- + fifoMu.Unlock() + return false, nil }), ) @@ -1221,6 +1251,9 @@ func TestServicebusWithSessionsFIFO(t *testing.T) { if !assert.Equal(t, ordered, observed) { t.Errorf("expected: %v, observed: %v", ordered, observed) } + + // Assert no parallel violations within the single session + assert.Equal(t, int32(0), fifoParallelIssues.Load(), "no parallel processing within a session expected") } return nil @@ -1256,6 +1289,215 @@ func TestServicebusWithSessionsFIFO(t *testing.T) { Run() } +// TestServicebusWithConcurrentSessionsFIFO validates that multiple sessions can be +// processed concurrently while each session maintains strict FIFO ordering. +func TestServicebusWithConcurrentSessionsFIFO(t *testing.T) { + topic := "sessions-concurrent-fifo" + numSessions := 5 + + sessionWatcher := watcher.NewUnordered() + + var ( + mu sync.Mutex + globalOrder []string // tracks session IDs in the order messages were received + activePerSession = make(map[string]int) + parallelIssues atomic.Int32 + ) + + subscriberApplicationWithSessions := func(appID string, topicName string, messagesWatcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: topicName, + Route: "/orders", + Metadata: map[string]string{ + "requireSessions": "true", + "maxConcurrentSessions": "5", + }, + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + messagesWatcher.Observe(e.Data) + + // Track session ID and enforce single in-flight per session + match := sessionIDRegex.FindStringSubmatch(fmt.Sprintf("%s", e.Data)) + var sessionID string + if len(match) > 1 { + sessionID = match[1] + mu.Lock() + inflight := activePerSession[sessionID] + if inflight > 0 { + parallelIssues.Add(1) + ctx.Logf("Session %s already has %d active messages", sessionID, inflight) + } + activePerSession[sessionID] = inflight + 1 + globalOrder = append(globalOrder, sessionID) + mu.Unlock() + } + + // Simulate work + time.Sleep(30 * time.Millisecond) + + ctx.Logf("Message Received appID: %s, pubsub: %s, topic: %s, id: %s, data: %s", + appID, e.PubsubName, e.Topic, e.ID, e.Data) + + if sessionID != "" { + mu.Lock() + activePerSession[sessionID]-- + mu.Unlock() + } + + return false, nil + }), + ) + } + } + + publishMessages := func(metadata map[string]string, sidecarName string, topicName string, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + messages := make([]string, numMessages) + for i := range messages { + var msgSuffix string + if metadata["SessionId"] != "" { + msgSuffix = fmt.Sprintf(", sessionId: %s", metadata["SessionId"]) + } + messages[i] = fmt.Sprintf("partitionKey: %s, message for topic: %s, index: %03d, uniqueId: %s%s", + metadata[messageKey], topicName, i, uuid.New().String(), msgSuffix) + } + + for _, messageWatcher := range messageWatchers { + messageWatcher.ExpectStrings(messages...) + } + + client := sidecar.GetClient(ctx, sidecarName) + ctx.Logf("Publishing messages. sidecarName: %s, topicName: %s", sidecarName, topicName) + + var publishOptions dapr.PublishEventOption + if metadata != nil { + publishOptions = dapr.PublishEventWithMetadata(metadata) + } + + for _, message := range messages { + ctx.Logf("Publishing: %q", message) + var err error + if publishOptions != nil { + err = client.PublishEvent(ctx, pubsubName, topicName, message, publishOptions) + } else { + err = client.PublishEvent(ctx, pubsubName, topicName, message) + } + require.NoError(ctx, err, "error publishing message") + } + return nil + } + } + + assertMessages := func(timeout time.Duration, messageWatchers ...*watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + for _, m := range messageWatchers { + _, exp, obs := m.Partial(ctx, timeout) + + var observed []string + if obs != nil { + for _, v := range obs.([]interface{}) { + observed = append(observed, v.(string)) + } + } + var expected []string + if exp != nil { + for _, v := range exp.([]interface{}) { + expected = append(expected, v.(string)) + } + } + + // Group messages by session + sessionMessages := make(map[string][]string) + for _, msg := range observed { + match := sessionIDRegex.FindStringSubmatch(msg) + if len(match) > 0 { + sessionID := match[1] + sessionMessages[sessionID] = append(sessionMessages[sessionID], msg) + } else { + t.Error("session id not found in message") + } + } + + require.Greater(t, len(sessionMessages), 1, + "should receive messages from multiple sessions concurrently") + + // Verify FIFO ordering per session + for sessionID, msgs := range sessionMessages { + var expectedForSession []string + for _, msg := range expected { + match := sessionIDRegex.FindStringSubmatch(msg) + if len(match) > 0 && match[1] == sessionID { + expectedForSession = append(expectedForSession, msg) + } + } + + require.Equal(t, expectedForSession, msgs, + "session %s messages must be in FIFO order", sessionID) + } + + // Check global order to prove concurrent processing + // If processed sequentially, all messages from one session would come before the next + // If processed concurrently, session IDs will be interleaved + mu.Lock() + orderCopy := make([]string, len(globalOrder)) + copy(orderCopy, globalOrder) + mu.Unlock() + + if len(orderCopy) > 1 { + // Check if we have session interleaving + hasInterleaving := false + seenSessions := make(map[string]bool) + lastSession := "" + + for _, sessionID := range orderCopy { + if sessionID != lastSession && seenSessions[sessionID] { + // We've seen this session before but with a different session in between + hasInterleaving = true + break + } + seenSessions[sessionID] = true + lastSession = sessionID + } + + require.True(t, hasInterleaving, + "global order must show session interleaving, proving concurrent processing") + + ctx.Logf("Successfully processed %d sessions concurrently with FIFO ordering maintained", + len(sessionMessages)) + + // Assert no parallel violations within a single session + assert.Equal(t, int32(0), parallelIssues.Load(), "no parallel processing within a session expected") + } + } + return nil + } + } + + f := flow.New(t, "servicebus certification concurrent sessions FIFO test"). + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplicationWithSessions(appID1, topic, sessionWatcher))). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath("./components/consumer_one"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )) + + for i := 0; i < numSessions; i++ { + sessionID := fmt.Sprintf("session-%d", i) + f = f.Step(fmt.Sprintf("publish messages to %s", sessionID), + publishMessages(map[string]string{"SessionId": sessionID}, sidecarName1, topic, sessionWatcher)) + } + + f.Step("verify all sessions processed with FIFO ordering and concurrency", assertMessages(30*time.Second, sessionWatcher)). + Step("reset", flow.Reset(sessionWatcher)). + Run() +} + // TestServicebusWithSessionsRoundRobin tests that if we publish messages to the same // topic but with 2 different session ids (session1 and session2), then eventually // the receiver will receive messages from both the sessions. @@ -1266,10 +1508,16 @@ func TestServicebusWithSessionsRoundRobin(t *testing.T) { sessionWatcher := watcher.NewUnordered() + // Concurrency tracking for round-robin scenario + var ( + rrMu sync.Mutex + rrActivePerSess = make(map[string]int) + rrParallelIssues atomic.Int32 + ) + // subscriber of the given topic subscriberApplicationWithSessions := func(appID string, topicName string, messageWatcher *watcher.Watcher) app.SetupFn { return func(ctx flow.Context, s common.Service) error { - // Setup the /orders event handler. return multierr.Combine( s.AddTopicEventHandler(&common.Subscription{ PubsubName: pubsubName, @@ -1281,9 +1529,31 @@ func TestServicebusWithSessionsRoundRobin(t *testing.T) { "sessionIdleTimeoutInSec": "2", // timeout and try another session }, }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { - // Track/Observe the data of the event. + // Extract session ID + var sessionID string + if m := sessionIDRegex.FindStringSubmatch(fmt.Sprintf("%s", e.Data)); len(m) > 1 { + sessionID = m[1] + } + + rrMu.Lock() + active := rrActivePerSess[sessionID] + if active > 0 { + rrParallelIssues.Add(1) + ctx.Logf("Session %s already has %d active messages", sessionID, active) + } + rrActivePerSess[sessionID] = active + 1 + rrMu.Unlock() + + // Simulate work + time.Sleep(15 * time.Millisecond) + messageWatcher.Observe(e.Data) ctx.Logf("Message Received appID: %s,pubsub: %s, topic: %s, id: %s, data: %s", appID, e.PubsubName, e.Topic, e.ID, e.Data) + + rrMu.Lock() + rrActivePerSess[sessionID]-- + rrMu.Unlock() + return false, nil }), ) @@ -1341,6 +1611,9 @@ func TestServicebusWithSessionsRoundRobin(t *testing.T) { m.Assert(ctx, 25*timeout) } + // Assert no parallel violations + assert.Equal(t, int32(0), rrParallelIssues.Load(), "no parallel processing within a session expected") + return nil } } diff --git a/tests/certification/pubsub/kafka/components/auth_oidc_certs/kafka.yaml b/tests/certification/pubsub/kafka/components/auth_oidc_certs/kafka.yaml new file mode 100644 index 0000000000..37237e0999 --- /dev/null +++ b/tests/certification/pubsub/kafka/components/auth_oidc_certs/kafka.yaml @@ -0,0 +1,37 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus +spec: + type: pubsub.kafka + version: v1 + metadata: + - name: brokers + value: localhost:9092 + - name: authType + value: "oidc_private_key_jwt" + - name: oidcTokenEndpoint + value: "http://localhost:8080/realms/local/protocol/openid-connect/token" + - name: oidcClientID + value: "dapr-kafka-client-jwt" + - name: oidcClientAssertionCert + secretKeyRef: + name: OIDC_CLIENT_ASSERTION_CERT + key: OIDC_CLIENT_ASSERTION_CERT + - name: oidcClientAssertionKey + secretKeyRef: + name: OIDC_CLIENT_ASSERTION_KEY + key: OIDC_CLIENT_ASSERTION_KEY + - name: oidcAudience + value: "http://keycloak:8080/realms/local/protocol/openid-connect/token" + - name: oidcScopes + value: "openid" + - name: disableTls + value: "true" + - name: oidcKid + secretKeyRef: + name: OIDC_CLIENT_KID + key: OIDC_CLIENT_KID + +auth: + secretStore: envvar-secret-store diff --git a/tests/certification/pubsub/kafka/components/auth_oidc_certs/localsecrets.yaml b/tests/certification/pubsub/kafka/components/auth_oidc_certs/localsecrets.yaml new file mode 100644 index 0000000000..38936ccf08 --- /dev/null +++ b/tests/certification/pubsub/kafka/components/auth_oidc_certs/localsecrets.yaml @@ -0,0 +1,8 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store +spec: + type: secretstores.local.env + version: v1 + metadata: diff --git a/tests/certification/pubsub/kafka/components/auth_oidc_secret_key/kafka.yaml b/tests/certification/pubsub/kafka/components/auth_oidc_secret_key/kafka.yaml new file mode 100644 index 0000000000..b1c93d0bb6 --- /dev/null +++ b/tests/certification/pubsub/kafka/components/auth_oidc_secret_key/kafka.yaml @@ -0,0 +1,22 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus +spec: + type: pubsub.kafka + version: v1 + metadata: + - name: brokers + value: localhost:9092 + - name: authType + value: "oidc" + - name: oidcTokenEndpoint + value: "http://localhost:8080/realms/local/protocol/openid-connect/token" + - name: oidcClientID + value: "dapr-kafka-client-secret" + - name: oidcClientSecret + value: "dapr-kafka-secret" + - name: oidcScopes + value: "openid" + - name: disableTls + value: "true" diff --git a/tests/certification/pubsub/kafka/components/consumer1/kafka.yaml b/tests/certification/pubsub/kafka/components/consumer1/kafka.yaml index 10bc98386d..a53a4befca 100644 --- a/tests/certification/pubsub/kafka/components/consumer1/kafka.yaml +++ b/tests/certification/pubsub/kafka/components/consumer1/kafka.yaml @@ -16,3 +16,5 @@ spec: value: oldest - name: backOffDuration value: 50ms + - name: clientConnectionTopicMetadataRefreshInterval + value: 15s diff --git a/tests/certification/pubsub/kafka/components/consumer2/kafka.yaml b/tests/certification/pubsub/kafka/components/consumer2/kafka.yaml index ad2bc1789c..d4ed63678a 100644 --- a/tests/certification/pubsub/kafka/components/consumer2/kafka.yaml +++ b/tests/certification/pubsub/kafka/components/consumer2/kafka.yaml @@ -16,4 +16,6 @@ spec: - name: initialOffset value: oldest - name: backOffDuration - value: 50ms \ No newline at end of file + value: 50ms + - name: clientConnectionTopicMetadataRefreshInterval + value: 15s diff --git a/tests/certification/pubsub/kafka/components/consumerAvro/kafka.yaml b/tests/certification/pubsub/kafka/components/consumerAvro/kafka.yaml index e866f53ea9..7d3d86d6dd 100644 --- a/tests/certification/pubsub/kafka/components/consumerAvro/kafka.yaml +++ b/tests/certification/pubsub/kafka/components/consumerAvro/kafka.yaml @@ -17,5 +17,7 @@ spec: value: oldest - name: backOffDuration value: 50ms + - name: clientConnectionTopicMetadataRefreshInterval + value: 15s - name: schemaRegistryURL - value: http://localhost:8081 \ No newline at end of file + value: http://localhost:8081 diff --git a/tests/certification/pubsub/kafka/data/realm-export.json b/tests/certification/pubsub/kafka/data/realm-export.json new file mode 100644 index 0000000000..bf87a12009 --- /dev/null +++ b/tests/certification/pubsub/kafka/data/realm-export.json @@ -0,0 +1,43 @@ + +{ + "realm": "local", + "enabled": true, + "clients": [ + { + "clientId": "dapr-kafka-client-jwt", + "secret": "dapr-kafka-secret", + "clientAuthenticatorType": "client-jwt", + "attributes": { + "use.jwks.url": "true", + "jwks.url": "http://jwks:80/jwks.json" + }, + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "directAccessGrantsEnabled": false, + "standardFlowEnabled": false + }, + { + "clientId": "dapr-kafka-client-secret", + "secret": "dapr-kafka-secret", + "clientAuthenticatorType": "client-secret", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "directAccessGrantsEnabled": false, + "standardFlowEnabled": false + }, + { + "clientId": "kafka-broker", + "secret": "kafka-broker-secret", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "directAccessGrantsEnabled": false, + "standardFlowEnabled": false + } + ] +} diff --git a/tests/certification/pubsub/kafka/docker-compose.auth.yml b/tests/certification/pubsub/kafka/docker-compose.auth.yml new file mode 100644 index 0000000000..41bf1a36dc --- /dev/null +++ b/tests/certification/pubsub/kafka/docker-compose.auth.yml @@ -0,0 +1,73 @@ +version: "3.7" +services: + kafka_oidc_with_certificates: + image: confluentinc/cp-server:7.7.5 + hostname: kafka_oidc_with_certificates + container_name: kafka_oidc_with_certificates + depends_on: + keycloak: + condition: service_healthy + healthcheck: + test: kafka-topics --bootstrap-server kafka_oidc_with_certificates:29092 --list + start_period: 20s + interval: 2s + timeout: 2s + retries: 5 + ports: + - "9092:9092" + environment: + KAFKA_NODE_ID: 0 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_CONTROLLER_QUORUM_VOTERS: '0@kafka_oidc_with_certificates:29093' + CLUSTER_ID: 'JikQ_wHyRRSqpLUFRjMqwA' + KAFKA_MIN_INSYNC_REPLICAS: 1 + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAIN9092:SASL_PLAINTEXT + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:29093,PLAIN9092://0.0.0.0:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:29092,PLAIN9092://localhost:9092 + KAFKA_SASL_ENABLED_MECHANISMS: OAUTHBEARER + KAFKA_SASL_OAUTHBEARER_JWKS_ENDPOINT_URL: http://keycloak:8080/realms/local/protocol/openid-connect/certs + KAFKA_SASL_OAUTHBEARER_EXPECTED_ISSUER: http://keycloak:8080/realms/local + KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: HTTPS + + # Listener configuration: PLAIN9092 + KAFKA_LISTENER_NAME_PLAIN9092_SASL_ENABLED_MECHANISMS: OAUTHBEARER + KAFKA_LISTENER_NAME_PLAIN9092_OAUTHBEARER_SASL_SERVER_CALLBACK_HANDLER_CLASS: org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallbackHandler + KAFKA_LISTENER_NAME_PLAIN9092_OAUTHBEARER_SASL_JAAS_CONFIG: org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required unsecuredLoginStringClaim_sub="thePrincipalName" oauth.client.id="kafka-broker" oauth.valid.issuer.uri="http://keycloak:8080/realms/local" oauth.jwks.endpoint.uri="http://keycloak:8080/realms/local/protocol/openid-connect/certs" oauth.config.id="PLAIN9092" oauth.client.secret="kafka-broker-secret"; + + keycloak: + image: quay.io/keycloak/keycloak:23.0.5 + hostname: keycloak + container_name: keycloak + command: ["start-dev", "--http-enabled=true", "--health-enabled=true", "--import-realm"] + healthcheck: + test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080; echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n' >&3;cat <&3 | grep -q '\"status\": \"UP\"' && exit 0 || exit 1"] + start_period: 20s + interval: 2s + timeout: 2s + retries: 5 + environment: + - OIDC_CLIENT_ASSERTION_CERT_ONELINE + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + - KC_HOSTNAME_URL=http://keycloak:8080 + ports: + - "8080:8080" + volumes: + - ./data/realm-export.json:/opt/keycloak/data/import/local-realm.json + + jwks: + image: nginx:1.27-alpine + hostname: jwks + container_name: kafka_jwks + environment: + - OIDC_CLIENT_KID + - OIDC_CLIENT_JWK_N + - OIDC_CLIENT_JWK_E + command: > + /bin/sh -c 'printf "{\n \"keys\": [\n {\n \"kty\": \"RSA\",\n \"use\": \"sig\",\n \"alg\": \"RS256\",\n \"kid\": \"%s\",\n \"n\": \"%s\",\n \"e\": \"%s\"\n }\n ]\n}\n" "$$OIDC_CLIENT_KID" "$$OIDC_CLIENT_JWK_N" "$$OIDC_CLIENT_JWK_E" > /usr/share/nginx/html/jwks.json && exec nginx -g "daemon off;"' diff --git a/tests/certification/pubsub/kafka/kafka_test.go b/tests/certification/pubsub/kafka/kafka_test.go index da50d180a0..6344852cae 100644 --- a/tests/certification/pubsub/kafka/kafka_test.go +++ b/tests/certification/pubsub/kafka/kafka_test.go @@ -15,9 +15,18 @@ package kafka_test import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" "encoding/json" + "encoding/pem" "fmt" + "math/big" + "os" "strconv" + "strings" "testing" "time" @@ -31,7 +40,9 @@ import ( // Pub/Sub. pubsub_kafka "github.com/dapr/components-contrib/pubsub/kafka" + secretstore_env "github.com/dapr/components-contrib/secretstores/local/env" pubsub_loader "github.com/dapr/dapr/pkg/components/pubsub" + secretstores_loader "github.com/dapr/dapr/pkg/components/secretstores" "github.com/dapr/dapr/pkg/config/protocol" // Dapr runtime and Go-SDK @@ -54,20 +65,26 @@ import ( ) const ( - sidecarName1 = "dapr-1" - sidecarName2 = "dapr-2" - sidecarName3 = "dapr-3" - sidecarNameAvro = "dapr-avro" - appID1 = "app-1" - appID2 = "app-2" - appID3 = "app-3" - appIDAvro = "app-avro" - clusterName = "kafkacertification" - dockerComposeYAML = "docker-compose.yml" - numMessages = 1000 - appPort = 8000 - portOffset = 2 - messageKey = "partitionKey" + sidecarName1 = "dapr-1" + sidecarName2 = "dapr-2" + sidecarName3 = "dapr-3" + sidecarNameOIDCCerts = "dapr-oidc-certs" + sidecarNameOIDCSecretKey = "dapr-oidc-secret-key" + sidecarNameAvro = "dapr-avro" + appID1 = "app-1" + appID2 = "app-2" + appID3 = "app-3" + appIDOIDCCerts = "app-oidc-certs" + appIDOIDCSecretKey = "app-oidc-secret-key" + appIDAvro = "app-avro" + clusterName = "kafkacertification" + clusterNameAuth = "kafkacertification-auth" + dockerComposeYAML = "docker-compose.yml" + dockerComposeYAMLAuth = "docker-compose.auth.yml" + numMessages = 1000 + appPort = 8000 + portOffset = 2 + messageKey = "partitionKey" pubsubName = "messagebus" topicName = "neworder" @@ -454,6 +471,104 @@ func TestKafka(t *testing.T) { Run() } +func TestKafkaAuth(t *testing.T) { + consumerGroupOIDCCerts := watcher.NewOrdered() + consumerGroupOIDCSecretKey := watcher.NewOrdered() + + application := func(appName string, watcher *watcher.Watcher) app.SetupFn { + return func(ctx flow.Context, s common.Service) error { + return multierr.Combine( + s.AddTopicEventHandler(&common.Subscription{ + PubsubName: pubsubName, + Topic: topicName, + Route: "/orders", + }, func(_ context.Context, e *common.TopicEvent) (retry bool, err error) { + watcher.Observe(e.Data) + return false, nil + }), + ) + } + } + + sendTest := func(sidecarName string, watcher *watcher.Watcher) flow.Runnable { + return func(ctx flow.Context) error { + client := sidecar.GetClient(ctx, sidecarName) + + message := fmt.Sprintf("Hello! %s", uuid.New().String()) + + watcher.ExpectStrings(message) + + err := client.PublishEvent(ctx, pubsubName, topicName, message) + require.NoError(ctx, err, "error publishing message") + + watcher.Assert(ctx, time.Minute) + return nil + } + } + + // Generate a private key and certificate for the OIDC client + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Dapr"}, + }, + } + cert, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + os.Setenv("OIDC_CLIENT_ASSERTION_CERT", string(certPEM)) + os.Setenv("OIDC_CLIENT_ASSERTION_KEY", string(keyPEM)) + os.Setenv("OIDC_CLIENT_ASSERTION_CERT_ONELINE", strings.ReplaceAll(string(certPEM), "\n", "\\n")) + + modulus := key.PublicKey.N.Bytes() + os.Setenv("OIDC_CLIENT_JWK_N", base64.RawURLEncoding.EncodeToString(modulus)) + exponent := big.NewInt(int64(key.PublicKey.E)).Bytes() + os.Setenv("OIDC_CLIENT_JWK_E", base64.RawURLEncoding.EncodeToString(exponent)) + os.Setenv("OIDC_CLIENT_KID", uuid.New().String()) + + flow.New(t, "kafka authentication"). + Step(dockercompose.Run(clusterNameAuth, dockerComposeYAMLAuth)). + Step("wait for broker sockets", + network.WaitForAddresses(5*time.Minute, "localhost:9092")). + Step("wait", flow.Sleep(10*time.Second)). + + // OIDC with secret key + Step(app.Run(appIDOIDCSecretKey, fmt.Sprintf(":%d", appPort), + application(appID1, consumerGroupOIDCSecretKey))). + Step(sidecar.Run(sidecarNameOIDCSecretKey, + append(componentRuntimeOptions(), + embedded.WithResourcesPath("./components/auth_oidc_secret_key"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("wait", flow.Sleep(10*time.Second)). + Step("send and wait(in-order)", sendTest(sidecarNameOIDCSecretKey, consumerGroupOIDCSecretKey)). + Step("stop sidecar", sidecar.Stop(sidecarNameOIDCSecretKey)). + Step("stop app", app.Stop(appIDOIDCSecretKey)). + + // OIDC with certificates + Step(app.Run(appIDOIDCCerts, fmt.Sprintf(":%d", appPort), + application(appID1, consumerGroupOIDCCerts))). + Step(sidecar.Run(sidecarNameOIDCCerts, + append(componentRuntimeOptions(), + embedded.WithResourcesPath("./components/auth_oidc_certs"), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("wait", flow.Sleep(10*time.Second)). + Step("send and wait(in-order)", sendTest(sidecarNameOIDCCerts, consumerGroupOIDCCerts)). + Step("stop sidecar", sidecar.Stop(sidecarNameOIDCCerts)). + Step("stop app", app.Stop(appIDOIDCCerts)). + Run() +} + func componentRuntimeOptions() []embedded.Option { log := logger.NewLogger("dapr.components") @@ -461,7 +576,12 @@ func componentRuntimeOptions() []embedded.Option { pubsubRegistry.Logger = log pubsubRegistry.RegisterComponent(pubsub_kafka.NewKafka, "kafka") + secretstoreRegistry := secretstores_loader.NewRegistry() + secretstoreRegistry.Logger = log + secretstoreRegistry.RegisterComponent(secretstore_env.NewEnvSecretStore, "local.env") + return []embedded.Option{ embedded.WithPubSubs(pubsubRegistry), + embedded.WithSecretStores(secretstoreRegistry), } } diff --git a/tests/certification/pubsub/pulsar/components/auth-oauth2/consumer_eight/pulsar.yml.tmpl b/tests/certification/pubsub/pulsar/components/auth-oauth2/consumer_eight/pulsar.yml.tmpl new file mode 100644 index 0000000000..aacdcf3a29 --- /dev/null +++ b/tests/certification/pubsub/pulsar/components/auth-oauth2/consumer_eight/pulsar.yml.tmpl @@ -0,0 +1,24 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus +spec: + type: pubsub.pulsar + version: v1 + metadata: + - name: host + value: "localhost:6650" + - name: consumerID + value: certification8 + - name: redeliveryDelay + value: 200ms + - name: oauth2TokenURL + value: https://localhost:8085/issuer1/token + - name: oauth2CredentialsFile + value: "{{ .CredentialsJSONFilePath }}" + - name: oauth2Scopes + value: openid + - name: oauth2Audiences + value: pulsar + - name: oauth2TokenCAPEM + value: "{{ .OAuth2CAPEM }}" \ No newline at end of file diff --git a/tests/certification/pubsub/pulsar/components/auth-oauth2/consumer_seven/pulsar.yml.tmpl b/tests/certification/pubsub/pulsar/components/auth-oauth2/consumer_seven/pulsar.yml.tmpl new file mode 100644 index 0000000000..e83f18a99c --- /dev/null +++ b/tests/certification/pubsub/pulsar/components/auth-oauth2/consumer_seven/pulsar.yml.tmpl @@ -0,0 +1,26 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: messagebus +spec: + type: pubsub.pulsar + version: v1 + metadata: + - name: host + value: "localhost:6650" + - name: consumerID + value: certification7 + - name: redeliveryDelay + value: 200ms + - name: oauth2TokenURL + value: https://localhost:8085/issuer1/token + - name: oauth2ClientID + value: foo + - name: oauth2ClientSecretPath + value: "{{ .CredentialsFilePath }}" + - name: oauth2Scopes + value: openid + - name: oauth2Audiences + value: pulsar + - name: oauth2TokenCAPEM + value: "{{ .OAuth2CAPEM }}" \ No newline at end of file diff --git a/tests/certification/pubsub/pulsar/pulsar_test.go b/tests/certification/pubsub/pulsar/pulsar_test.go index c8578dbb53..bcd6ff28c8 100644 --- a/tests/certification/pubsub/pulsar/pulsar_test.go +++ b/tests/certification/pubsub/pulsar/pulsar_test.go @@ -156,12 +156,30 @@ func TestPulsar(t *testing.T) { outf.Close() inf.Close() + // Create credentials files for testing oauth2ClientSecretPath + plainTextCredsFile := filepath.Join(dir, "credentials-plain.txt") + require.NoError(t, os.WriteFile(plainTextCredsFile, []byte("bar"), 0o644)) + + jsonCredsFile := filepath.Join(dir, "credentials.json") + jsonCreds := map[string]string{ + "client_id": "foo", + "client_secret": "bar", + "issuer_url": "https://localhost:8085/issuer1/token", + } + jsonCredsBytes, err := json.Marshal(jsonCreds) + require.NoError(t, err) + require.NoError(t, os.WriteFile(jsonCredsFile, jsonCredsBytes, 0o644)) + td := struct { - TmpDir string - OAuth2CAPEM string + TmpDir string + OAuth2CAPEM string + CredentialsFilePath string + CredentialsJSONFilePath string }{ - TmpDir: dir, - OAuth2CAPEM: strings.ReplaceAll(string(oauth2CA), "\n", "\\n"), + TmpDir: dir, + OAuth2CAPEM: strings.ReplaceAll(string(oauth2CA), "\n", "\\n"), + CredentialsFilePath: plainTextCredsFile, + CredentialsJSONFilePath: jsonCredsFile, } tmpl, err := template.New("").ParseFiles(dockerComposeAuthOAuth2YAML) @@ -942,6 +960,108 @@ func (p *pulsarSuite) TestPulsarSchema() { Run() } +// TestOAuth2WithPlainTextCredentialsFile tests OAuth2 authentication using oauth2ClientSecretPath +// with a plain text credentials file (backward compatibility). +func (p *pulsarSuite) TestOAuth2WithPlainTextCredentialsFile() { + t := p.T() + consumerGroup1 := watcher.NewUnordered() + + if p.authType != "oauth2" { + t.Skip("Skipping OAuth2 credentials file test for non-OAuth2 auth type") + return + } + + flow.New(t, "pulsar certification oauth2 plain text credentials file test"). + + // Run subscriberApplication app1 + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, topicActiveName, consumerGroup1))). + Step(dockercompose.Run(clusterName, p.dockerComposeYAML)). + Step("wait", flow.Sleep(10*time.Second)). + Step("wait for pulsar readiness", retry.Do(10*time.Second, 30, func(ctx flow.Context) error { + client, err := p.client(t) + if err != nil { + return fmt.Errorf("could not create pulsar client: %v", err) + } + + defer client.Close() + + consumer, err := client.Subscribe(pulsar.ConsumerOptions{ + Topic: "topic-1", + SubscriptionName: "my-sub", + Type: pulsar.Shared, + }) + if err != nil { + return fmt.Errorf("could not create pulsar Topic: %v", err) + } + defer consumer.Close() + + return err + })). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath(filepath.Join(p.componentsPath, "consumer_seven")), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish messages to topic1", publishMessages(nil, sidecarName1, topicActiveName, consumerGroup1)). + Step("verify if app1 has received messages published to topic", assertMessages(10*time.Second, consumerGroup1)). + Run() +} + +// TestOAuth2WithJSONCredentialsFile tests OAuth2 authentication using oauth2CredentialsFile +// with a JSON credentials file containing both client_id and client_secret. +func (p *pulsarSuite) TestOAuth2WithJSONCredentialsFile() { + t := p.T() + consumerGroup1 := watcher.NewUnordered() + + if p.authType != "oauth2" { + t.Skip("Skipping OAuth2 credentials file test for non-OAuth2 auth type") + return + } + + flow.New(t, "pulsar certification oauth2 json credentials file test"). + + // Run subscriberApplication app1 + Step(app.Run(appID1, fmt.Sprintf(":%d", appPort), + subscriberApplication(appID1, topicActiveName, consumerGroup1))). + Step(dockercompose.Run(clusterName, p.dockerComposeYAML)). + Step("wait", flow.Sleep(10*time.Second)). + Step("wait for pulsar readiness", retry.Do(10*time.Second, 30, func(ctx flow.Context) error { + client, err := p.client(t) + if err != nil { + return fmt.Errorf("could not create pulsar client: %v", err) + } + + defer client.Close() + + consumer, err := client.Subscribe(pulsar.ConsumerOptions{ + Topic: "topic-1", + SubscriptionName: "my-sub", + Type: pulsar.Shared, + }) + if err != nil { + return fmt.Errorf("could not create pulsar Topic: %v", err) + } + defer consumer.Close() + + return err + })). + Step(sidecar.Run(sidecarName1, + append(componentRuntimeOptions(), + embedded.WithComponentsPath(filepath.Join(p.componentsPath, "consumer_eight")), + embedded.WithAppProtocol(protocol.HTTPProtocol, strconv.Itoa(appPort)), + embedded.WithDaprGRPCPort(strconv.Itoa(runtime.DefaultDaprAPIGRPCPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(runtime.DefaultDaprHTTPPort)), + )..., + )). + Step("publish messages to topic1", publishMessages(nil, sidecarName1, topicActiveName, consumerGroup1)). + Step("verify if app1 has received messages published to topic", assertMessages(10*time.Second, consumerGroup1)). + Run() +} + func componentRuntimeOptions() []embedded.Option { log := logger.NewLogger("dapr.components") diff --git a/tests/certification/state/aws/dynamodb/README.md b/tests/certification/state/aws/dynamodb/README.md index 984bea06a8..0c5830e2d5 100644 --- a/tests/certification/state/aws/dynamodb/README.md +++ b/tests/certification/state/aws/dynamodb/README.md @@ -11,3 +11,49 @@ This project aims to test the AWS DynamoDB State Store component under various c ### TTL Test using master key authentication 1. Able to create and test connection. 2. Able to do set TTL, fetch (expired and non-expired) data and delete. + +### Run tests locally + +1. Run `docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -sharedDb -inMemory` +2. Create the table with +``` +AWS_ACCESS_KEY_ID=fake AWS_SECRET_ACCESS_KEY=fake AWS_DEFAULT_REGION=us-east-1 aws dynamodb create-table \ + --table-name dapr-state-1 \ + --attribute-definitions AttributeName=key,AttributeType=S \ + --key-schema AttributeName=key,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ + --endpoint-url http://localhost:8000 \ + --region us-east-1 +``` + +and + +``` +AWS_ACCESS_KEY_ID=fake AWS_SECRET_ACCESS_KEY=fake AWS_DEFAULT_REGION=us-east-1 aws dynamodb update-time-to-live \ + --table-name dapr-state-1 \ + --time-to-live-specification "Enabled=true,AttributeName=ttlExpireTime" \ + --endpoint-url http://localhost:8000 +``` +3. Replace the state store with: +``` +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore-basic +spec: + type: state.aws.dynamodb + version: v1 + metadata: + - name: table + value: "dapr-state-1" + - name: region + value: "us-east-1" + - name: endpoint + value: "http://localhost:8000" + - name: accessKey + value: "fakeMyKeyId" + - name: secretKey + value: "fakeSecretAccessKey" + - name: partitionKey + value: "key" +``` diff --git a/tests/certification/state/aws/dynamodb/dynamodb_test.go b/tests/certification/state/aws/dynamodb/dynamodb_test.go index 49a6c2c38f..3490936e64 100644 --- a/tests/certification/state/aws/dynamodb/dynamodb_test.go +++ b/tests/certification/state/aws/dynamodb/dynamodb_test.go @@ -19,19 +19,20 @@ import ( "testing" "time" - dynamodb "github.com/dapr/components-contrib/state/aws/dynamodb" + "github.com/dapr/components-contrib/state/aws/dynamodb" "github.com/dapr/components-contrib/tests/certification/embedded" "github.com/dapr/components-contrib/tests/certification/flow" "github.com/dapr/go-sdk/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + secretstore_env "github.com/dapr/components-contrib/secretstores/local/env" "github.com/dapr/components-contrib/tests/certification/flow/sidecar" secretstores_loader "github.com/dapr/dapr/pkg/components/secretstores" state_loader "github.com/dapr/dapr/pkg/components/state" dapr_testing "github.com/dapr/dapr/pkg/testing" "github.com/dapr/kit/logger" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( @@ -143,11 +144,9 @@ func TestAWSDynamoDBStorage(t *testing.T) { { Type: client.StateOperationTypeUpsert, Item: &client.SetStateItem{ - Key: ktx1, - Value: []byte("reqValTx1"), - Etag: &client.ETag{ - Value: "test", - }, + Key: ktx1, + Value: []byte("reqValTx1"), + Etag: nil, Metadata: map[string]string{}, }, }, @@ -161,11 +160,9 @@ func TestAWSDynamoDBStorage(t *testing.T) { { Type: client.StateOperationTypeUpsert, Item: &client.SetStateItem{ - Key: ktx2, - Value: []byte("reqValTx2"), - Etag: &client.ETag{ - Value: "test", - }, + Key: ktx2, + Value: []byte("reqValTx2"), + Etag: nil, Metadata: map[string]string{}, }, }, diff --git a/tests/certification/state/cassandra/docker-compose-cluster.yml b/tests/certification/state/cassandra/docker-compose-cluster.yml index 61cb15c625..ba946cd883 100644 --- a/tests/certification/state/cassandra/docker-compose-cluster.yml +++ b/tests/certification/state/cassandra/docker-compose-cluster.yml @@ -1,7 +1,7 @@ version: '2' services: cassandra: - image: docker.io/bitnami/cassandra:4.0.1 + image: docker.io/bitnamilegacy/cassandra:4.0.1 ports: - '7000:7000' - '9042:9042' @@ -14,7 +14,7 @@ services: - CASSANDRA_PASSWORD_SEEDER=yes cassandra2: - image: docker.io/bitnami/cassandra:4.0.1 + image: docker.io/bitnamilegacy/cassandra:4.0.1 ports: - 7001:7000 - 9043:9042 diff --git a/tests/certification/state/cassandra/docker-compose-single.yml b/tests/certification/state/cassandra/docker-compose-single.yml index 8a01cce0ec..ef20c03a12 100644 --- a/tests/certification/state/cassandra/docker-compose-single.yml +++ b/tests/certification/state/cassandra/docker-compose-single.yml @@ -1,7 +1,7 @@ version: '2' services: cassandra: - image: docker.io/bitnami/cassandra:4.0.1 + image: docker.io/bitnamilegacy/cassandra:4.0.1 ports: - '7002:7000' - '9044:9042' diff --git a/tests/certification/state/mysql/mysql_test.go b/tests/certification/state/mysql/mysql_test.go index 9548a05d42..c21951adc1 100644 --- a/tests/certification/state/mysql/mysql_test.go +++ b/tests/certification/state/mysql/mysql_test.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "strconv" + "strings" "sync/atomic" "testing" "time" @@ -273,8 +274,7 @@ func TestMySQL(t *testing.T) { // Should fail err = component.Ping(t.Context()) require.Error(t, err) - assert.Equal(t, "driver: bad connection", err.Error()) - + assert.True(t, strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "bad connection")) return nil } } diff --git a/tests/certification/state/postgresql/v1/postgresql_test.go b/tests/certification/state/postgresql/v1/postgresql_test.go index 06baed839a..7d7f53e8d3 100644 --- a/tests/certification/state/postgresql/v1/postgresql_test.go +++ b/tests/certification/state/postgresql/v1/postgresql_test.go @@ -58,7 +58,7 @@ const ( keyMetadataTableName = "metadataTableName" // Update this constant if you add more migrations - migrationLevel = "2" + migrationLevel = "3" ) func TestPostgreSQL(t *testing.T) { diff --git a/tests/certification/state/postgresql/v2/postgresql_test.go b/tests/certification/state/postgresql/v2/postgresql_test.go index 595911c318..b780aadb11 100644 --- a/tests/certification/state/postgresql/v2/postgresql_test.go +++ b/tests/certification/state/postgresql/v2/postgresql_test.go @@ -57,7 +57,7 @@ const ( keyMetadataTableName = "metadataTableName" // Update this constant if you add more migrations - migrationLevel = "1" + migrationLevel = "2" ) func TestPostgreSQL(t *testing.T) { diff --git a/tests/certification/state/ravendb/README.md b/tests/certification/state/ravendb/README.md new file mode 100644 index 0000000000..972a41da7d --- /dev/null +++ b/tests/certification/state/ravendb/README.md @@ -0,0 +1,17 @@ +# RavenDB State Store certification testing + +This project aims to test the RavenDB State Store component under various conditions. + +## Test plan +Run: +go test -v -tags "unit certtests" -count=1 . + +## Basic Test for CRUD operations: +1. Able to create and test connection. +2. Able to do set, fetch, update and delete. +3. Negative test to fetch record with key, that is not present. + +## Component must reconnect when server or network errors are encountered + +## Infra test: +1. When RavenDB goes down and then comes back up - client is able to connect \ No newline at end of file diff --git a/tests/certification/state/ravendb/components/default/ravendb.yaml b/tests/certification/state/ravendb/components/default/ravendb.yaml new file mode 100644 index 0000000000..19cabb4e70 --- /dev/null +++ b/tests/certification/state/ravendb/components/default/ravendb.yaml @@ -0,0 +1,14 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.ravendb + version: v1 + metadata: + - name: DatabaseName + value: "testdapr" + - name: ServerURL + value: "http://127.0.0.1:8080" + - name: TTLFrequency + value: "1" diff --git a/tests/certification/state/ravendb/config.yaml b/tests/certification/state/ravendb/config.yaml new file mode 100644 index 0000000000..fdfcd802dc --- /dev/null +++ b/tests/certification/state/ravendb/config.yaml @@ -0,0 +1,6 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: keyvaultconfig +spec: + features: \ No newline at end of file diff --git a/tests/certification/state/ravendb/docker-compose.yml b/tests/certification/state/ravendb/docker-compose.yml new file mode 100644 index 0000000000..5797de6d39 --- /dev/null +++ b/tests/certification/state/ravendb/docker-compose.yml @@ -0,0 +1,14 @@ +services: + ravendb: + image: ravendb/ravendb + container_name: RavenDb + ports: + - "8080:8080" + environment: + - RAVEN_LICENSE={"Id":"b75b6995-5f1f-440f-bcaa-40f7388ecf90","Name":"Vega IT","Keys":["jwX1/epkfQHHLKF0UX8+DRCGP","zwoUaohApGfVHZ0rmZ4Cvaj4a","653+JiPtKVTANiZw6wxdD7XB6","3OfgLC8++bvE8T8JhuTD12PQt","nFzHL2zUPhlP5q+9mI8NmAzbu","tTXscNl13tFX1GLgcWkZYNN80","H7RrWQXwKc+HJ4q1d2catABYE","DNy4xBSYoSQMqKywtLi8wJzEy","MzQVFjc4OTo7PD0+nwIfIJ8CI","CCfAiEgnwIjIJ8CJCCfAiUgnw","ImIJ8CJyCfAiggnwIpIJ8CKiC","fAisgnwIsIJ8CLSCfAi4gnwIv","IJ8CMCCfAzZAAZ8CQiCfAkMgn","wJEIJ8CRSCfAkYgnwJHIJ8CSA","BDJEQJYgVdnwRBYAJd"]} + - RAVEN_DATABASE=testdapr + - RAVEN_Setup_Mode=None + - RAVEN_License_Eula_Accepted=true + - RAVEN_Security_UnsecuredAccessAllowed=PrivateNetwork + - RAVEN_License_ThrowOnInvalidOrMissingLicense=true + restart: unless-stopped \ No newline at end of file diff --git a/tests/certification/state/ravendb/ravendb_test.go b/tests/certification/state/ravendb/ravendb_test.go new file mode 100644 index 0000000000..e4ca48511c --- /dev/null +++ b/tests/certification/state/ravendb/ravendb_test.go @@ -0,0 +1,270 @@ +package ravendb_test + +import ( + "fmt" + "github.com/dapr/components-contrib/state" + stateRavenDB "github.com/dapr/components-contrib/state/ravendb" + "github.com/dapr/components-contrib/tests/certification/embedded" + "github.com/dapr/components-contrib/tests/certification/flow" + "github.com/dapr/components-contrib/tests/certification/flow/dockercompose" + "github.com/dapr/components-contrib/tests/certification/flow/network" + "github.com/dapr/components-contrib/tests/certification/flow/sidecar" + stateLoader "github.com/dapr/dapr/pkg/components/state" + daprTesting "github.com/dapr/dapr/pkg/testing" + daprClient "github.com/dapr/go-sdk/client" + "github.com/dapr/kit/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "strconv" + "testing" + "time" +) + +const ( + sidecarNamePrefix = "ravendb-sidecar-" + stateStoreName = "statestore" + certificationTestPrefix = "stable-certification-" + dockerComposeYAML = "docker-compose.yml" +) + +func TestRavenDB(t *testing.T) { + fmt.Printf("testing started:") + log := logger.NewLogger("dapr.components") + stateStore := stateRavenDB.NewRavenDB(log).(*stateRavenDB.RavenDB) + ports, err := daprTesting.GetFreePorts(2) + require.NoError(t, err) + + stateRegistry := stateLoader.NewRegistry() + stateRegistry.Logger = log + stateRegistry.RegisterComponent(func(l logger.Logger) state.Store { + return stateStore + }, "ravenDb") + + currentGrpcPort := ports[0] + currentHTTPPort := ports[1] + + basicTest := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key1", []byte("ravenCert1"), nil) + require.NoError(t, err) + + //this is set for the test after restart + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key2", []byte("ravenCert2"), nil) + require.NoError(t, err) + + //this is set for the test after restart + err = client.SaveState(ctx, stateStoreName, "deleteInTransaction", []byte("ravenCert3"), nil) + require.NoError(t, err) + + // get state + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + require.NoError(t, err) + assert.Equal(t, "ravenCert1", string(item.Value)) + + errUpdate := client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key1", []byte("ravenCertUpdate"), nil) + require.NoError(t, errUpdate) + item, errUpdatedGet := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + require.NoError(t, errUpdatedGet) + assert.Equal(t, "ravenCertUpdate", string(item.Value)) + + // delete state + err = client.DeleteState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + require.NoError(t, err) + + return nil + } + + eTagTest := func() func(ctx flow.Context) error { + return func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + err = client.SaveState(ctx, stateStoreName, "k", []byte("v1"), nil) + require.NoError(t, err) + + resp1, err := client.GetState(ctx, stateStoreName, "k", nil) + require.NoError(t, err) + + err = client.SaveStateWithETag(ctx, stateStoreName, "k", []byte("v2"), resp1.Etag, nil) + require.NoError(t, err) + + resp2, err := client.GetState(ctx, stateStoreName, "k", nil) + require.NoError(t, err) + + err = client.SaveStateWithETag(ctx, stateStoreName, "k", []byte("v3"), "900invalid", nil) + require.Error(t, err) + + resp3, err := client.GetState(ctx, stateStoreName, "k", nil) + require.NoError(t, err) + assert.Equal(t, resp2.Etag, resp3.Etag) + assert.Equal(t, "v2", string(resp3.Value)) + + return nil + } + } + + timeToLiveTest := func() func(ctx flow.Context) error { + return func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprint(currentGrpcPort)) + require.NoError(t, err) + defer client.Close() + + assert.Error(t, client.SaveState(ctx, stateStoreName, certificationTestPrefix+"ttl1", []byte("revendbCert"), map[string]string{ + "ttlInSeconds": "mock value", + })) + require.NoError(t, client.SaveState(ctx, stateStoreName, certificationTestPrefix+"ttl2", []byte("revendbCert2"), map[string]string{ + "ttlInSeconds": "-1", + })) + require.NoError(t, client.SaveState(ctx, stateStoreName, certificationTestPrefix+"ttl3", []byte("revendbCert3"), map[string]string{ + "ttlInSeconds": "3", + })) + + // get state + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"ttl3", nil) + require.NoError(t, err) + assert.Equal(t, "revendbCert3", string(item.Value)) + assert.Contains(t, item.Metadata, "ttlExpireTime") + expireTime, err := time.Parse(time.RFC3339, item.Metadata["ttlExpireTime"]) + require.NoError(t, err) + assert.InDelta(t, time.Now().Add(time.Second*3).Unix(), expireTime.Unix(), 2) + + assert.Eventually(t, func() bool { + item, err = client.GetState(ctx, stateStoreName, certificationTestPrefix+"ttl3", nil) + require.NoError(t, err) + return len(item.Value) == 0 + }, time.Second*10, time.Second*1) + + return nil + } + } + + transactionsTest := func() func(ctx flow.Context) error { + return func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + err = client.ExecuteStateTransaction(ctx, stateStoreName, nil, []*daprClient.StateOperation{ + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey1", + Value: []byte("reqVal1"), + Metadata: map[string]string{ + "ttlInSeconds": "-1", + }, + }, + }, + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey2", + Value: []byte("reqVal2"), + Metadata: map[string]string{ + "ttlInSeconds": "222", + }, + }, + }, + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey3", + Value: []byte("reqVal3"), + }, + }, + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey4", + Value: []byte("reqVal101"), + Metadata: map[string]string{ + "ttlInSeconds": "50", + }, + }, + }, + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey5", + Value: []byte("reqVal103"), + Metadata: map[string]string{ + "ttlInSeconds": "50", + }, + }, + }, + { + Type: daprClient.StateOperationTypeDelete, + Item: &daprClient.SetStateItem{ + Key: "deleteInTransaction", + }, + }, + }) + require.NoError(t, err) + + resp1, err := client.GetState(ctx, stateStoreName, "reqKey1", nil) + require.NoError(t, err) + assert.Equal(t, "reqVal1", string(resp1.Value)) + + resp3, err := client.GetState(ctx, stateStoreName, "reqKey3", nil) + require.NoError(t, err) + assert.Equal(t, "reqVal3", string(resp3.Value)) + + resp4, err := client.GetState(ctx, stateStoreName, "deleteInTransaction", nil) + require.NoError(t, err) + assert.Nil(t, resp4.Value) + + return nil + } + } + + testGetAfterRavenDBRestart := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprint(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + // get state + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key2", nil) + require.NoError(t, err) + assert.Equal(t, "ravenCert2", string(item.Value)) + + return nil + } + + flow.New(t, "Connecting RavenDB And Verifying majority of the tests."). + Step(dockercompose.Run("ravendb", dockerComposeYAML)). + Step("Waiting for component to start...", flow.Sleep(20*time.Second)). + Step(sidecar.Run(sidecarNamePrefix+"dockerClusterDefault", + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(currentGrpcPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(currentHTTPPort)), + embedded.WithResourcesPath("components/default"), + embedded.WithStates(stateRegistry))). + Step("Waiting for component to load...", flow.Sleep(10*time.Second)). + Step("Run basic test", basicTest). + Step("Run Etag test", eTagTest()). + Step("Run transaction test", transactionsTest()). + Step("Run time to live test", timeToLiveTest()). + Step("Interrupt network", + network.InterruptNetwork(5*time.Second, nil, nil, "27017:27017")). + // Component should recover at this point. + Step("Wait", flow.Sleep(10*time.Second)). + Step("Run basic test again to verify reconnection occurred", basicTest). + Step("Stop RavenDB server", dockercompose.Stop("ravendb", dockerComposeYAML)). + Step("Start RavenDB server", dockercompose.Start("ravendb", dockerComposeYAML)). + Step("Waiting for component to start...", flow.Sleep(10*time.Second)). + Step("Get Values Saved Earlier And Not Expired, after RavenDB restart", testGetAfterRavenDBRestart). + Step("Wait to check documents", flow.Sleep(10*time.Second)). + Run() +} diff --git a/tests/certification/state/redis/redis_test.go b/tests/certification/state/redis/redis_test.go index 7e65913da9..d07eeacab8 100644 --- a/tests/certification/state/redis/redis_test.go +++ b/tests/certification/state/redis/redis_test.go @@ -44,6 +44,7 @@ const ( stateStoreName = "statestore" certificationTestPrefix = "stable-certification-" stateStoreNoConfigError = "error saving state: rpc error: code = FailedPrecondition desc = state store statestore is not configured" + testNonexistentKey = "ThisKeyDoesNotExistInTheStateStore" ) func TestRedis(t *testing.T) { @@ -75,11 +76,21 @@ func TestRedis(t *testing.T) { } defer client.Close() + // HGETALL path + item, err := client.GetState(ctx, stateStoreName, testNonexistentKey, nil) + require.NoError(t, err) + assert.Nil(t, item.Value) + + // JSON.GET path + item, err = client.GetState(ctx, stateStoreName, testNonexistentKey, map[string]string{"contentType": "application/json"}) + require.NoError(t, err) + assert.Nil(t, item.Value) + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key1", []byte("redisCert"), nil) require.NoError(t, err) // get state - item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + item, err = client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) require.NoError(t, err) assert.Equal(t, "redisCert", string(item.Value)) diff --git a/tests/certification/state/sqlserver/v2/README.md b/tests/certification/state/sqlserver/v2/README.md new file mode 100644 index 0000000000..88aba143b8 --- /dev/null +++ b/tests/certification/state/sqlserver/v2/README.md @@ -0,0 +1,46 @@ +# SQL Server certification testing + +This project aims to test the SQL Server State Store component under various conditions. + +## Test plan + +### SQL Injection + +* Not prone to SQL injection on write +* Not prone to SQL injection on read +* Not prone to SQL injection on delete + +### Indexed Properties + +* Verifies Indices are created for each indexed property in component metadata +* Verifies JSON data properties are parsed and written to dedicated database columns + +### Custom Properties + +* Verifies the use of custom tablename (default is states) +* Verifies the use of a custom schema (default is dbo) + +### Connection to different SQL Server types + +* Verifies connection handling with Azure SQL Server +* Verifies connection handling with SQL Server in Docker to represent self hosted SQL Server options + +## TTLs and cleanups + +1. Correctly parse the `cleanupIntervalInSeconds` metadata property: + - No value uses the default value (3600 seconds) + - A positive value sets the interval to the given number of seconds + - A zero or negative value disables the cleanup +2. The cleanup method deletes expired records and updates the metadata table with the last time it ran +3. The cleanup method doesn't run if the last iteration was less than `cleanupIntervalInSeconds` or if another process is doing the cleanup + +### Other tests + +* Client reconnects (if applicable) upon network interruption + + +### Running the tests + +This must be run in the GitHub Actions Workflow configured for test infrastructure setup. + +If you have access to an Azure subscription you can run this locally on Mac or Linux after running `setup-azure-conf-test.sh` in `.github/infrastructure/conformance/azure` and then sourcing the generated bash rc file. diff --git a/tests/certification/state/sqlserver/v2/components/azure/localsecrets.yaml b/tests/certification/state/sqlserver/v2/components/azure/localsecrets.yaml new file mode 100644 index 0000000000..94bb7a2643 --- /dev/null +++ b/tests/certification/state/sqlserver/v2/components/azure/localsecrets.yaml @@ -0,0 +1,9 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store + namespace: default +spec: + type: secretstores.local.env + version: v1 + metadata: diff --git a/tests/certification/state/sqlserver/v2/components/azure/sqlserver.yaml b/tests/certification/state/sqlserver/v2/components/azure/sqlserver.yaml new file mode 100644 index 0000000000..10b38f35df --- /dev/null +++ b/tests/certification/state/sqlserver/v2/components/azure/sqlserver.yaml @@ -0,0 +1,24 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: dapr-state-store +spec: + type: state.sqlserver + metadata: + - name: connectionString + secretKeyRef: + name: AzureSqlServerConnectionString + value: AzureSqlServerConnectionString + - name: databaseName + value: stablecertification_v2 + - name: tableName + value: dapr_certification_test + - name: keyType + value: string + - name: keyLength + value: 120 + - name: schema + value: proto + +auth: + secretStore: envvar-secret-store diff --git a/tests/certification/state/sqlserver/v2/components/docker/customschemawithindex/sqlserver.yaml b/tests/certification/state/sqlserver/v2/components/docker/customschemawithindex/sqlserver.yaml new file mode 100644 index 0000000000..69d51f0cf3 --- /dev/null +++ b/tests/certification/state/sqlserver/v2/components/docker/customschemawithindex/sqlserver.yaml @@ -0,0 +1,18 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: dapr-state-store +spec: + type: state.sqlserver + metadata: + - name: url + value: "server=localhost;user id=sa;password=Pass@Word1;port=1433;Connection Timeout=30;" + - name: databaseName + value: certificationtest_v2 + - name: schemaName + value: customschema + - name: tableName + value: mystates + - name: indexedProperties + value: '[{"column": "transactionid", "property": "id", "type": "int"}, {"column": "customerid", "property": "customer", "type": "nvarchar(100)"}]' + diff --git a/tests/certification/state/sqlserver/v2/components/docker/default/sqlserver.yaml b/tests/certification/state/sqlserver/v2/components/docker/default/sqlserver.yaml new file mode 100644 index 0000000000..7f962a4667 --- /dev/null +++ b/tests/certification/state/sqlserver/v2/components/docker/default/sqlserver.yaml @@ -0,0 +1,11 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: dapr-state-store +spec: + type: state.sqlserver + metadata: + - name: connectionString + value: "server=localhost;user id=sa;password=Pass@Word1;port=1433;Connection Timeout=5;" + - name: databaseName + value: certificationtest_v2 diff --git a/tests/certification/state/sqlserver/v2/config.yaml b/tests/certification/state/sqlserver/v2/config.yaml new file mode 100644 index 0000000000..6c95e632ff --- /dev/null +++ b/tests/certification/state/sqlserver/v2/config.yaml @@ -0,0 +1,6 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: keyvaultconfig +spec: + features: diff --git a/tests/certification/state/sqlserver/v2/docker-compose.yml b/tests/certification/state/sqlserver/v2/docker-compose.yml new file mode 100644 index 0000000000..63510b7688 --- /dev/null +++ b/tests/certification/state/sqlserver/v2/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.7" +services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2019-latest + ports: + - "1433:1433" + environment: + ACCEPT_EULA: Y + SA_PASSWORD: "Pass@Word1" diff --git a/tests/certification/state/sqlserver/v2/sqlserver_test.go b/tests/certification/state/sqlserver/v2/sqlserver_test.go new file mode 100644 index 0000000000..2bfaa97701 --- /dev/null +++ b/tests/certification/state/sqlserver/v2/sqlserver_test.go @@ -0,0 +1,664 @@ +/* +Copyright 2021 The Dapr Authors +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 sqlserver_test + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "os" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/contenttype" + // State. + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" + state_sqlserver "github.com/dapr/components-contrib/state/sqlserver/v2" + state_loader "github.com/dapr/dapr/pkg/components/state" + "github.com/dapr/kit/logger" + + // Secret stores. + secretstore_env "github.com/dapr/components-contrib/secretstores/local/env" + secretstores_loader "github.com/dapr/dapr/pkg/components/secretstores" + + // Dapr runtime and Go-SDK + + dapr_testing "github.com/dapr/dapr/pkg/testing" + "github.com/dapr/go-sdk/client" + + // Certification testing runnables + "github.com/dapr/components-contrib/tests/certification/embedded" + "github.com/dapr/components-contrib/tests/certification/flow" + "github.com/dapr/components-contrib/tests/certification/flow/dockercompose" + "github.com/dapr/components-contrib/tests/certification/flow/network" + "github.com/dapr/components-contrib/tests/certification/flow/retry" + "github.com/dapr/components-contrib/tests/certification/flow/sidecar" +) + +const ( + sidecarNamePrefix = "sqlserver-sidecar-" + dockerComposeYAML = "docker-compose.yml" + stateStoreName = "dapr-state-store" + certificationTestPrefix = "stable-certification-" + dockerConnectionString = "server=localhost;user id=sa;password=Pass@Word1;port=1433;" +) + +func TestSqlServer(t *testing.T) { + // The default certificate created by the docker container sometimes contains a negative serial number. + // A TLS certificate with a negative serial number is invalid, although it was tolerated until 1.22 + // Since Go 1.23 the default behavior has changed and the certificate is rejected. + // This environment variable is used to revert to the old behavior. + // Ref: https://github.com/microsoft/mssql-docker/issues/895 + oldDebugValue := os.Getenv("GODEBUG") + t.Setenv("GODEBUG", "x509negativeserial=1") + + if os.Getenv("GODEBUG") != "x509negativeserial=1" { + t.Fatal("Failed to set GODEBUG environment variable, actual value: " + os.Getenv("GODEBUG")) + } + defer func() { + t.Setenv("GODEBUG", oldDebugValue) + }() + + ports, err := dapr_testing.GetFreePorts(2) + require.NoError(t, err) + + currentGrpcPort := ports[0] + currentHTTPPort := ports[1] + + basicTest := func(ctx flow.Context) error { + ctx.T.Run("basic test", func(t *testing.T) { + client, err := client.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + // save state, default options: strong, last-write + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key1", []byte("certificationdata"), nil) + require.NoError(t, err) + + // get state + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + require.NoError(t, err) + assert.Equal(t, "certificationdata", string(item.Value)) + + // delete state + err = client.DeleteState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + require.NoError(t, err) + }) + return nil + } + + basicTTLTest := func(ctx flow.Context) error { + ctx.T.Run("basic TTL test", func(t *testing.T) { + client, err := client.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key2", []byte("certificationdata"), map[string]string{ + "ttlInSeconds": "86400", + }) + require.NoError(t, err) + + // get state + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key2", nil) + require.NoError(t, err) + assert.Equal(t, "certificationdata", string(item.Value)) + assert.Contains(t, item.Metadata, "ttlExpireTime") + expireTime, err := time.Parse(time.RFC3339, item.Metadata["ttlExpireTime"]) + require.NoError(t, err) + assert.InDelta(t, time.Now().Add(24*time.Hour).Unix(), expireTime.Unix(), 10) + + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key2", []byte("certificationdata"), map[string]string{ + "ttlInSeconds": "1", + }) + require.NoError(t, err) + + time.Sleep(2 * time.Second) + + item, err = client.GetState(ctx, stateStoreName, certificationTestPrefix+"key2", nil) + require.NoError(t, err) + assert.Nil(t, item.Value) + assert.Nil(t, item.Metadata) + }) + + return nil + } + + // this test function heavily depends on the values defined in ./components/docker/customschemawithindex + verifyIndexedPopertiesTest := func(ctx flow.Context) error { + // verify indices were created by Dapr as specified in the component metadata + db, err := sql.Open("mssql", fmt.Sprintf("%sdatabase=certificationtest_v2;", dockerConnectionString)) + require.NoError(ctx.T, err) + defer db.Close() + + rows, err := db.Query("sp_helpindex '[customschema].[mystates]'") + require.NoError(ctx.T, err) + assert.NoError(ctx.T, rows.Err()) + defer rows.Close() + + indexFoundCount := 0 + for rows.Next() { + var indexedField, otherdata1, otherdata2 string + err = rows.Scan(&indexedField, &otherdata1, &otherdata2) + assert.NoError(ctx.T, err) + + expectedIndices := []string{"IX_customerid", "IX_transactionid", "PK_mystates"} + for _, item := range expectedIndices { + if item == indexedField { + indexFoundCount++ + break + } + } + } + assert.Equal(ctx.T, 3, indexFoundCount) + + // write JSON data to the state store (which will automatically be indexed in separate columns) + client, err := client.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + order := struct { + ID int `json:"id"` + Customer string `json:"customer"` + Description string `json:"description"` + }{123456, "John Doe", "something"} + + data, err := json.Marshal(order) + assert.NoError(ctx.T, err) + + // save state with the key certificationkey1, default options: strong, last-write + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key1", data, map[string]string{metadata.ContentType: contenttype.JSONContentType}) + require.NoError(ctx.T, err) + require.NoError(ctx.T, err) + + // get state for key certificationkey1 + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + assert.NoError(ctx.T, err) + assert.JSONEq(ctx.T, string(data), string(item.Value)) + + // check that Dapr wrote the indexed properties to separate columns + rows, err = db.Query("SELECT TOP 1 transactionid, customerid FROM [customschema].[mystates];") + assert.NoError(ctx.T, err) + assert.NoError(ctx.T, rows.Err()) + defer rows.Close() + if rows.Next() { + var transactionID int + var customerID string + err = rows.Scan(&transactionID, &customerID) + assert.NoError(ctx.T, err) + assert.Equal(ctx.T, transactionID, order.ID) + assert.Equal(ctx.T, customerID, order.Customer) + } else { + assert.Fail(ctx.T, "no rows returned") + } + + // delete state for key certificationkey1 + err = client.DeleteState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + assert.NoError(ctx.T, err) + + return nil + } + + // helper function for testing the use of an existing custom schema + createCustomSchema := func(ctx flow.Context) error { + db, err := sql.Open("mssql", dockerConnectionString) + assert.NoError(ctx.T, err) + _, err = db.Exec("CREATE SCHEMA customschema;") + assert.NoError(ctx.T, err) + db.Close() + return nil + } + + // helper function to insure the SQL Server Docker Container is truly ready + checkSQLServerAvailability := func(ctx flow.Context) error { + db, err := sql.Open("mssql", dockerConnectionString) + if err != nil { + return err + } + _, err = db.Exec("SELECT * FROM INFORMATION_SCHEMA.TABLES;") + if err != nil { + return err + } + return nil + } + + // checks the state store component is not vulnerable to SQL injection + verifySQLInjectionTest := func(ctx flow.Context) error { + client, err := client.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + // common SQL injection techniques for SQL Server + sqlInjectionAttempts := []string{ + "; DROP states--", + "dapr' OR '1'='1", + } + + for _, sqlInjectionAttempt := range sqlInjectionAttempts { + // save state with sqlInjectionAttempt's value as key, default options: strong, last-write + err = client.SaveState(ctx, stateStoreName, sqlInjectionAttempt, []byte(sqlInjectionAttempt), nil) + assert.NoError(ctx.T, err) + + // get state for key sqlInjectionAttempt's value + item, err := client.GetState(ctx, stateStoreName, sqlInjectionAttempt, nil) + assert.NoError(ctx.T, err) + assert.Equal(ctx.T, sqlInjectionAttempt, string(item.Value)) + + // delete state for key sqlInjectionAttempt's value + err = client.DeleteState(ctx, stateStoreName, sqlInjectionAttempt, nil) + assert.NoError(ctx.T, err) + } + + return nil + } + + // Validates TTLs and garbage collections + ttlTest := func(connString string) func(ctx flow.Context) error { + return func(ctx flow.Context) error { + log := logger.NewLogger("dapr.components") + + ctx.T.Run("parse cleanupIntervalInSeconds", func(t *testing.T) { + t.Run("default value", func(t *testing.T) { + // Default value is 1 hr + md := state.Metadata{ + Base: metadata.Base{ + Name: "ttltest", + Properties: map[string]string{ + "connectionString": connString, + "databaseName": "certificationtest", + "tableName": "ttltest", + "metadataTableName": "ttltest_metadata", + "schema": "ttlschema", + }, + }, + } + storeObj := state_sqlserver.New(log).(*state_sqlserver.SQLServer) + + err := storeObj.Init(t.Context(), md) + require.NoError(t, err, "failed to init") + defer storeObj.Close() + + cleanupInterval := storeObj.GetCleanupInterval() + require.NotNil(t, cleanupInterval) + assert.Equal(t, time.Duration(1*time.Hour), *cleanupInterval) + }) + + t.Run("positive value", func(t *testing.T) { + // A positive value is interpreted in seconds + md := state.Metadata{ + Base: metadata.Base{ + Name: "ttltest", + Properties: map[string]string{ + "connectionString": connString, + "databaseName": "certificationtest", + "tableName": "ttltest", + "metadataTableName": "ttltest_metadata", + "schema": "ttlschema", + "cleanupInterval": "10", + }, + }, + } + storeObj := state_sqlserver.New(log).(*state_sqlserver.SQLServer) + + err := storeObj.Init(t.Context(), md) + require.NoError(t, err, "failed to init") + defer storeObj.Close() + + cleanupInterval := storeObj.GetCleanupInterval() + require.NotNil(t, cleanupInterval) + assert.Equal(t, time.Duration(10*time.Second), *cleanupInterval) + }) + + t.Run("disabled", func(t *testing.T) { + // A value of <=0 means that the cleanup is disabled + md := state.Metadata{ + Base: metadata.Base{ + Name: "ttltest", + Properties: map[string]string{ + "connectionString": connString, + "databaseName": "certificationtest", + "tableName": "ttltest", + "metadataTableName": "ttltest_metadata", + "schema": "ttlschema", + "cleanupIntervalInSeconds": "0", + }, + }, + } + storeObj := state_sqlserver.New(log).(*state_sqlserver.SQLServer) + + err := storeObj.Init(t.Context(), md) + require.NoError(t, err, "failed to init") + defer storeObj.Close() + + cleanupInterval := storeObj.GetCleanupInterval() + assert.Nil(t, cleanupInterval) + }) + }) + + ctx.T.Run("cleanup", func(t *testing.T) { + dbClient, err := sql.Open("mssql", connString) + require.NoError(t, err) + + t.Run("automatically delete expiredate records", func(t *testing.T) { + // Run every second + md := state.Metadata{ + Base: metadata.Base{ + Name: "ttltest", + Properties: map[string]string{ + "connectionString": connString, + "databaseName": "certificationtest", + "tableName": "ttltest", + "metadataTableName": "ttltest_metadata", + "schema": "ttlschema", + "cleanupInterval": "1", + }, + }, + } + + storeObj := state_sqlserver.New(log).(*state_sqlserver.SQLServer) + err := storeObj.Init(t.Context(), md) + require.NoError(t, err, "failed to init") + defer storeObj.Close() + + // Seed the database with some records + err = clearTable(ctx, dbClient, "ttlschema", "ttltest") + require.NoError(t, err, "failed to clear table") + err = populateTTLRecords(ctx, dbClient, "ttlschema", "ttltest") + require.NoError(t, err, "failed to seed records") + + cleanupInterval := storeObj.GetCleanupInterval() + require.NotNil(t, cleanupInterval) + assert.Equal(t, time.Duration(time.Second), *cleanupInterval) + + // Wait up to 3 seconds then verify we have only 10 rows left + var count int + assert.Eventually(t, func() bool { + count, err = countRowsInTable(ctx, dbClient, "ttlschema", "ttltest") + require.NoError(t, err, "failed to run query to count rows") + return count == 10 + }, 3*time.Second, 10*time.Millisecond, "expected 10 rows, got %d", count) + + // The "last-cleanup" value should be <= 1 second (+ a bit of buffer) + lastCleanup, err := loadLastCleanupInterval(ctx, dbClient, "ttlschema", "ttltest_metadata") + require.NoError(t, err, "failed to load value for 'last-cleanup'") + assert.LessOrEqual(t, lastCleanup, int64(1200)) + + // Wait 6 more seconds and verify there are no more rows left + assert.Eventually(t, func() bool { + count, err = countRowsInTable(ctx, dbClient, "ttlschema", "ttltest") + require.NoError(t, err, "failed to run query to count rows") + return count == 0 + }, 6*time.Second, 10*time.Millisecond, "expected 0 rows, got %d", count) + + // The "last-cleanup" value should be <= 1 second (+ a bit of buffer) + lastCleanup, err = loadLastCleanupInterval(ctx, dbClient, "ttlschema", "ttltest_metadata") + require.NoError(t, err, "failed to load value for 'last-cleanup'") + assert.LessOrEqual(t, lastCleanup, int64(1200)) + }) + + t.Run("cleanup concurrency", func(t *testing.T) { + // Set to run every hour + // (we'll manually trigger more frequent iterations) + md := state.Metadata{ + Base: metadata.Base{ + Name: "ttltest", + Properties: map[string]string{ + "connectionString": connString, + "databaseName": "certificationtest", + "tableName": "ttltest", + "metadataTableName": "ttltest_metadata", + "schema": "ttlschema", + "cleanupInterval": "1h", + }, + }, + } + + storeObj := state_sqlserver.New(log).(*state_sqlserver.SQLServer) + err := storeObj.Init(t.Context(), md) + require.NoError(t, err, "failed to init") + defer storeObj.Close() + + cleanupInterval := storeObj.GetCleanupInterval() + assert.NotNil(t, cleanupInterval) + assert.Equal(t, time.Hour, *cleanupInterval) + + // Seed the database with some records + err = clearTable(ctx, dbClient, "ttlschema", "ttltest") + require.NoError(t, err, "failed to clear table") + err = populateTTLRecords(ctx, dbClient, "ttlschema", "ttltest") + require.NoError(t, err, "failed to seed records") + + // Validate that 20 records are present + count, err := countRowsInTable(ctx, dbClient, "ttlschema", "ttltest") + require.NoError(t, err, "failed to run query to count rows") + assert.Equal(t, 20, count) + + // Set last-cleanup to 1s ago + _, err = dbClient.ExecContext(ctx, `UPDATE [ttlschema].[ttltest_metadata] SET [Value] = CONVERT(nvarchar(MAX), DATEADD(second, -1, GETDATE()), 21) WHERE [Key] = 'last-cleanup'`) + require.NoError(t, err, "failed to set last-cleanup") + + // The "last-cleanup" value + lastCleanup, err := loadLastCleanupInterval(ctx, dbClient, "ttlschema", "ttltest_metadata") + require.NoError(t, err, "failed to load value for 'last-cleanup'") + assert.LessOrEqual(t, lastCleanup, int64(1200)) + lastCleanupValueOrig, err := getValueFromMetadataTable(ctx, dbClient, "ttlschema", "ttltest_metadata", "'last-cleanup'") + require.NoError(t, err, "failed to load absolute value for 'last-cleanup'") + require.NotEmpty(t, lastCleanupValueOrig) + + // Trigger the background cleanup, which should do nothing because the last cleanup was < 3600s + require.NoError(t, storeObj.CleanupExpired(), "CleanupExpired returned an error") + + // Validate that 20 records are still present + count, err = countRowsInTable(ctx, dbClient, "ttlschema", "ttltest") + require.NoError(t, err, "failed to run query to count rows") + assert.Equal(t, 20, count) + + // The "last-cleanup" value should not have been changed + lastCleanupValue, err := getValueFromMetadataTable(ctx, dbClient, "ttlschema", "ttltest_metadata", "'last-cleanup'") + require.NoError(t, err, "failed to load absolute value for 'last-cleanup'") + assert.Equal(t, lastCleanupValueOrig, lastCleanupValue) + }) + }) + + return nil + } + } + + t.Run("SQLServer certification using SQL Server Docker", func(t *testing.T) { + flow.New(t, "SQLServer certification using SQL Server Docker"). + // Run SQL Server using Docker Compose. + Step(dockercompose.Run("sqlserver", dockerComposeYAML)). + Step("wait for SQL Server readiness", retry.Do(time.Second*3, 10, checkSQLServerAvailability)). + + // Run the Dapr sidecar with the SQL Server component. + Step(sidecar.Run(sidecarNamePrefix+"dockerDefault", + append(componentRuntimeOptions(), + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(currentGrpcPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(currentHTTPPort)), + embedded.WithResourcesPath("components/docker/default"), + embedded.WithProfilingEnabled(false), + )..., + )). + Step("Run basic test", basicTest). + Step("Run basic TTL test", basicTTLTest). + // Introduce network interruption of 10 seconds + // Note: the connection timeout is set to 5 seconds via the component metadata connection string. + Step("interrupt network", + network.InterruptNetwork(10*time.Second, nil, nil, "1433", "1434")). + + // Component should recover at this point. + Step("wait", flow.Sleep(5*time.Second)). + Step("Run basic test again to verify reconnection occurred", basicTest). + Step("Run SQL injection test", verifySQLInjectionTest, sidecar.Stop(sidecarNamePrefix+"dockerDefault")). + Step("run TTL test", ttlTest(dockerConnectionString+"database=certificationtest_v2;")). + Step("Stopping SQL Server Docker container", dockercompose.Stop("sqlserver", dockerComposeYAML)). + Run() + }) + + ports, err = dapr_testing.GetFreePorts(2) + require.NoError(t, err) + + currentGrpcPort = ports[0] + currentHTTPPort = ports[1] + + t.Run("Using existing custom schema with indexed data", func(t *testing.T) { + flow.New(t, "Using existing custom schema with indexed data"). + // Run SQL Server using Docker Compose. + Step(dockercompose.Run("sqlserver", dockerComposeYAML)). + Step("wait for SQL Server readiness", retry.Do(time.Second*3, 10, checkSQLServerAvailability)). + Step("Creating schema", createCustomSchema). + + // Run the Dapr sidecar with the SQL Server component. + Step(sidecar.Run(sidecarNamePrefix+"dockerCustomSchema", + append(componentRuntimeOptions(), + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(currentGrpcPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(currentHTTPPort)), + embedded.WithResourcesPath("components/docker/customschemawithindex"), + embedded.WithProfilingEnabled(false), + )..., + )). + Step("Run indexed properties verification test", verifyIndexedPopertiesTest, sidecar.Stop(sidecarNamePrefix+"dockerCustomSchema")). + Step("Stopping SQL Server Docker container", dockercompose.Stop("sqlserver", dockerComposeYAML)). + Run() + }) + + ports, err = dapr_testing.GetFreePorts(2) + require.NoError(t, err) + + currentGrpcPort = ports[0] + currentHTTPPort = ports[1] + + t.Run("SQL Server certification using Azure SQL", func(t *testing.T) { + flow.New(t, "SQL Server certification using Azure SQL"). + // Run the Dapr sidecar with the SQL Server component. + Step(sidecar.Run(sidecarNamePrefix+"azure", + append(componentRuntimeOptions(), + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(currentGrpcPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(currentHTTPPort)), + embedded.WithResourcesPath("components/azure"), + embedded.WithProfilingEnabled(false), + )..., + )). + Step("Run basic test", basicTest). + Step("Run basic TTL test", basicTTLTest). + Step("interrupt network", + network.InterruptNetwork(15*time.Second, nil, nil, "1433", "1434")). + + // Component should recover at this point. + Step("wait", flow.Sleep(10*time.Second)). + Step("Run basic test again to verify reconnection occurred", basicTest). + Step("Run SQL injection test", verifySQLInjectionTest, sidecar.Stop(sidecarNamePrefix+"azure")). + Step("run TTL test", ttlTest(os.Getenv("AzureSqlServerConnectionString")+"database=stablecertification_v2;")). + Run() + }) +} + +func componentRuntimeOptions() []embedded.Option { + log := logger.NewLogger("dapr.components") + + stateRegistry := state_loader.NewRegistry() + stateRegistry.Logger = log + stateRegistry.RegisterComponent(state_sqlserver.New, "sqlserver") + + secretstoreRegistry := secretstores_loader.NewRegistry() + secretstoreRegistry.Logger = log + secretstoreRegistry.RegisterComponent(secretstore_env.NewEnvSecretStore, "local.env") + + return []embedded.Option{ + embedded.WithStates(stateRegistry), + embedded.WithSecretStores(secretstoreRegistry), + } +} + +func countRowsInTable(ctx context.Context, dbClient *sql.DB, schema, table string) (count int, err error) { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + err = dbClient.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM [%s].[%s]", schema, table)).Scan(&count) + return +} + +func clearTable(ctx context.Context, dbClient *sql.DB, schema, table string) error { + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + _, err := dbClient.ExecContext(ctx, fmt.Sprintf("DELETE FROM [%s].[%s]", schema, table)) + return err +} + +func loadLastCleanupInterval(ctx context.Context, dbClient *sql.DB, schema, table string) (lastCleanup int64, err error) { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + err = dbClient. + QueryRowContext(ctx, + fmt.Sprintf("SELECT DATEDIFF(MILLISECOND, CAST([Value] AS DATETIME2), GETDATE()) FROM [%s].[%s] WHERE [Key] = 'last-cleanup'", schema, table), + ). + Scan(&lastCleanup) + return +} + +func populateTTLRecords(ctx context.Context, dbClient *sql.DB, schema, table string) error { + // Insert 10 records that have expired, and 10 that will expire in 4 + // seconds. + rows := make([][]any, 20) + for i := 0; i < 10; i++ { + rows[i] = []any{ + fmt.Sprintf("'expired_%d'", i), + json.RawMessage(fmt.Sprintf("'value_%d'", i)), + "DATEADD(MINUTE, -1, GETDATE())", + } + } + for i := 0; i < 10; i++ { + rows[i+10] = []any{ + fmt.Sprintf("'notexpired_%d'", i), + json.RawMessage(fmt.Sprintf(`'value_%d'`, i)), + "DATEADD(SECOND, 4, GETDATE())", + } + } + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + for _, row := range rows { + _, err := dbClient.ExecContext(ctx, fmt.Sprintf( + "INSERT INTO [%[1]s].[%[2]s] ([Key], [Data], [ExpireDate]) VALUES (%[3]s, %[4]s, %[5]s)", + schema, table, row[0], row[1], row[2]), + ) + if err != nil { + return err + } + } + return nil +} + +func getValueFromMetadataTable(ctx context.Context, dbClient *sql.DB, schema, table, key string) (value string, err error) { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + err = dbClient. + QueryRowContext(ctx, fmt.Sprintf("SELECT [Value] FROM [%[1]s].[%[2]s] WHERE [Key] = %[3]s", schema, table, key)). + Scan(&value) + return +} diff --git a/tests/config/conversation/README.md b/tests/config/conversation/README.md index c8f097a7a0..a1991f23e0 100644 --- a/tests/config/conversation/README.md +++ b/tests/config/conversation/README.md @@ -35,6 +35,7 @@ The tests will automatically skip components for which the required environment ### Using a .env file (Recommended) 1. Copy the template file: + ```bash cp env.template .env ``` @@ -46,56 +47,84 @@ cp env.template .env Alternatively, you can set the following environment variables to run the respective tests: -### OpenAI +#### Model Configuration (Optional) + +You can override the default models used by each component by setting these environment variables: + +```bash +export OPENAI_MODEL="gpt-5-nano" # Default: gpt-5-nano +export AZURE_OPENAI_MODEL="gpt-4.1-nano" # Default: gpt-4.1-nano +export ANTHROPIC_MODEL="claude-3-5-sonnet-20240620" # Default: claude-3-5-sonnet-20240620 +export GOOGLEAI_MODEL="gemini-1.5-flash" # Default: gemini-1.5-flash +export MISTRAL_MODEL="open-mistral-7b" # Default: open-mistral-7b +export HUGGINGFACE_MODEL="deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" # Default: deepseek-ai/DeepSeek-R1-Distill-Qwen-32B +export OLLAMA_MODEL="llama3.2:latest" # Default: llama3.2:latest +``` + +#### OpenAI + ```bash export OPENAI_API_KEY="your_openai_api_key" ``` + Get your API key from: https://platform.openai.com/api-keys ### Azure OpenAI + ```bash export AZURE_OPENAI_API_KEY="your_openai_api_key" export AZURE_OPENAI_ENDPOINT="your_azureopenai_endpoint_here" export AZURE_OPENAI_API_VERSION="your_azreopenai_api_version_here" ``` + Get your configuration values from: https://ai.azure.com/ -### Anthropic ```bash export ANTHROPIC_API_KEY="your_anthropic_api_key" ``` -Get your API key from: https://console.anthropic.com/ -### Google AI +Get your API key from: + +#### Google AI + ```bash export GOOGLE_AI_API_KEY="your_google_ai_api_key" ``` -Get your API key from: https://aistudio.google.com/app/apikey -### Mistral +Get your API key from: + +#### Mistral + ```bash export MISTRAL_API_KEY="your_mistral_api_key" ``` -Get your API key from: https://console.mistral.ai/ -### HuggingFace +Get your API key from: + +#### HuggingFace + ```bash export HUGGINGFACE_API_KEY="your_huggingface_api_key" ``` -Get your API key from: https://huggingface.co/settings/tokens -### AWS Bedrock +Get your API key from: + +#### AWS Bedrock + ```bash export AWS_ACCESS_KEY_ID="your_aws_access_key" export AWS_SECRET_ACCESS_KEY="your_aws_secret_key" export AWS_REGION="us-east-1" # Optional, defaults to us-east-1 ``` + Get your credentials from AWS Console -### Ollama +#### Ollama + ```bash export OLLAMA_ENABLED="1" ``` + Requires a local Ollama server running with the `llama3.2:latest` model available. ## Test Configuration @@ -103,7 +132,7 @@ Requires a local Ollama server running with the `llama3.2:latest` model availabl Each component has its own configuration file in this directory: - `echo/echo.yml` - Echo component configuration -- `openai/openai.yml` - OpenAI configuration with gpt-4o-mini model +- `openai/openai.yml` - OpenAI configuration with gpt-5-nano model - `anthropic/anthropic.yml` - Anthropic configuration with Claude 3 Haiku - `googleai/googleai.yml` - Google AI configuration with Gemini 1.5 Flash - `mistral/mistral.yml` - Mistral configuration with open-mistral-7b @@ -117,13 +146,15 @@ The configurations use cost-effective models where possible to minimize testing The HuggingFace component uses a workaround due to issues with the native HuggingFace implementation in langchaingo. Instead of using the HuggingFace SDK directly, it uses the OpenAI SDK with HuggingFace's OpenAI-compatible API endpoints. -### How it works: +### How it works + - **Model Selection**: Any HuggingFace model can be used by specifying its full name (e.g., `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B`) - **Dynamic Endpoints**: The endpoint URL is automatically generated based on the model name using the template: `https://router.huggingface.co/hf-inference/models/{{model}}/v1` - **Custom Endpoints**: You can override the endpoint by specifying a custom `endpoint` parameter - **Authentication**: Uses the same HuggingFace API key authentication -### Example Configuration: +### Example Configuration + ```yaml apiVersion: dapr.io/v1alpha1 kind: Component diff --git a/tests/config/conversation/anthropic/anthropic.yml b/tests/config/conversation/anthropic/anthropic.yml index 22353e19cc..bff41c83a4 100644 --- a/tests/config/conversation/anthropic/anthropic.yml +++ b/tests/config/conversation/anthropic/anthropic.yml @@ -9,4 +9,4 @@ spec: - name: key value: "${{ANTHROPIC_API_KEY}}" - name: model - value: "claude-3-haiku-20240307" \ No newline at end of file + value: "" # use default for provider or customize via environment variable as defined in conversation/models.go \ No newline at end of file diff --git a/tests/config/conversation/googleai/googleai.yml b/tests/config/conversation/googleai/googleai.yml index d2ad6ee25f..ab9469b881 100644 --- a/tests/config/conversation/googleai/googleai.yml +++ b/tests/config/conversation/googleai/googleai.yml @@ -9,4 +9,4 @@ spec: - name: key value: "${{GOOGLE_AI_API_KEY}}" - name: model - value: "gemini-1.5-flash" \ No newline at end of file + value: "" # use default for provider or customize via environment variable as defined in conversation/models.go \ No newline at end of file diff --git a/tests/config/conversation/huggingface/huggingface.yml b/tests/config/conversation/huggingface/huggingface.yml index c4ca9f2fea..2a1eee1884 100644 --- a/tests/config/conversation/huggingface/huggingface.yml +++ b/tests/config/conversation/huggingface/huggingface.yml @@ -9,4 +9,4 @@ spec: - name: key value: "${{HUGGINGFACE_API_KEY}}" - name: model - value: "HuggingFaceTB/SmolLM3-3B" \ No newline at end of file + value: "" # use default for provider or customize via environment variable as defined in conversation/models.go \ No newline at end of file diff --git a/tests/config/conversation/mistral/mistral.yml b/tests/config/conversation/mistral/mistral.yml index 016a8b5317..e3a9a20f41 100644 --- a/tests/config/conversation/mistral/mistral.yml +++ b/tests/config/conversation/mistral/mistral.yml @@ -9,4 +9,5 @@ spec: - name: key value: "${{MISTRAL_API_KEY}}" - name: model - value: "open-mistral-7b" \ No newline at end of file + value: "mistral-large-latest" # use default for provider or customize via environment variable as defined in conversation/models.go + # NOTE: I had to up the model for the conformance test confirming that the json response is correct... \ No newline at end of file diff --git a/tests/config/conversation/ollama/ollama.yml b/tests/config/conversation/ollama/ollama.yml index c144669c53..6d4bfc7c41 100644 --- a/tests/config/conversation/ollama/ollama.yml +++ b/tests/config/conversation/ollama/ollama.yml @@ -7,4 +7,4 @@ spec: version: v1 metadata: - name: model - value: "llama3.2:latest" \ No newline at end of file + value: "" # use default for provider or customize via environment variable as defined in conversation/models.go \ No newline at end of file diff --git a/tests/config/conversation/openai/azure/openai.yml b/tests/config/conversation/openai/azure/openai.yml index 106c59dc57..9545d3f3a4 100644 --- a/tests/config/conversation/openai/azure/openai.yml +++ b/tests/config/conversation/openai/azure/openai.yml @@ -9,7 +9,7 @@ spec: - name: key value: "${{AZURE_OPENAI_API_KEY}}" - name: model - value: "gpt-4o-mini" + value: "" # use default for provider or customize via environment variable as defined in conversation/models.go - name: endpoint value: "${{AZURE_OPENAI_ENDPOINT}}" - name: apiType diff --git a/tests/config/conversation/openai/openai/openai.yml b/tests/config/conversation/openai/openai/openai.yml index 19eb55302f..f53f2395fc 100644 --- a/tests/config/conversation/openai/openai/openai.yml +++ b/tests/config/conversation/openai/openai/openai.yml @@ -9,4 +9,4 @@ spec: - name: key value: "${{OPENAI_API_KEY}}" - name: model - value: "gpt-4o-mini" \ No newline at end of file + value: "gpt-4o" # use default for provider or customize via environment variable as defined in conversation/models.go \ No newline at end of file diff --git a/tests/config/conversation/tests.yml b/tests/config/conversation/tests.yml index 0dbeba0c7a..4adb4a4c78 100644 --- a/tests/config/conversation/tests.yml +++ b/tests/config/conversation/tests.yml @@ -18,3 +18,4 @@ components: operations: [] - component: bedrock operations: [] +# TODO: We are missing tests for deepseek. \ No newline at end of file diff --git a/tests/config/state/ravendb/statestore.yaml b/tests/config/state/ravendb/statestore.yaml new file mode 100644 index 0000000000..2abd8333f3 --- /dev/null +++ b/tests/config/state/ravendb/statestore.yaml @@ -0,0 +1,14 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.ravendb + version: v1 + metadata: + - name: DatabaseName + value: "testdapr" + - name: ServerURL + value: "http://127.0.0.1:8080" + - name: TTLFrequency + value: "1" \ No newline at end of file diff --git a/tests/config/state/sqlserver/docker/statestore.yml b/tests/config/state/sqlserver/docker/statestore.yml new file mode 100644 index 0000000000..98d1403d33 --- /dev/null +++ b/tests/config/state/sqlserver/docker/statestore.yml @@ -0,0 +1,11 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.sqlserver + metadata: + - name: connectionString + value: "server=localhost;user id=sa;password=Pass@Word1;port=1433;" + - name: tableName + value: mytable diff --git a/tests/config/state/sqlserver/v2/docker/statestore.yml b/tests/config/state/sqlserver/v2/docker/statestore.yml new file mode 100644 index 0000000000..2dfa4b8451 --- /dev/null +++ b/tests/config/state/sqlserver/v2/docker/statestore.yml @@ -0,0 +1,11 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.sqlserver + metadata: + - name: connectionString + value: "server=localhost;user id=sa;password=Pass@Word1;port=1433;" + - name: tableName + value: mytable_v2 diff --git a/tests/config/state/sqlserver/v2/statestore.yml b/tests/config/state/sqlserver/v2/statestore.yml new file mode 100644 index 0000000000..2dfa4b8451 --- /dev/null +++ b/tests/config/state/sqlserver/v2/statestore.yml @@ -0,0 +1,11 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.sqlserver + metadata: + - name: connectionString + value: "server=localhost;user id=sa;password=Pass@Word1;port=1433;" + - name: tableName + value: mytable_v2 diff --git a/tests/config/state/tests.yml b/tests/config/state/tests.yml index ed0f60de09..f8c7bbfb71 100644 --- a/tests/config/state/tests.yml +++ b/tests/config/state/tests.yml @@ -4,18 +4,18 @@ componentType: state components: - component: redis.v6 - operations: [ "transaction", "etag", "first-write", "query", "ttl" ] + operations: [ "transaction", "etag", "first-write", "query", "ttl", "actorStateStore", "keyslike" ] config: # This component requires etags to be numeric badEtag: "9999999" - component: redis.v7 # "query" is not included because redisjson hasn't been updated to Redis v7 yet - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore", "keyslike" ] config: # This component requires etags to be numeric badEtag: "9999999" - component: mongodb - operations: [ "transaction", "etag", "first-write", "query", "ttl" ] + operations: [ "transaction", "etag", "first-write", "query", "ttl", "actorStateStore" ] - component: memcached operations: [ "ttl" ] - component: azure.cosmosdb @@ -36,32 +36,47 @@ components: config: # This component requires etags to be hex-encoded numbers badEtag: "FFFF" + - component: sqlserver.v2 + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore" ] + config: + # This component requires etags to be hex-encoded numbers + badEtag: "FFFF" + - component: sqlserver.docker + operations: [ "transaction", "etag", "first-write", "ttl" ] + config: + # This component requires etags to be hex-encoded numbers + badEtag: "FFFF" + - component: sqlserver.v2.docker + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore" ] + config: + # This component requires etags to be hex-encoded numbers + badEtag: "FFFF" - component: postgresql.v1.docker - operations: [ "transaction", "etag", "first-write", "query", "ttl" ] + operations: [ "transaction", "etag", "first-write", "query", "ttl", "actorStateStore", "keyslike" ] config: # This component requires etags to be numeric badEtag: "1" - component: postgresql.v1.azure - operations: [ "transaction", "etag", "first-write", "query", "ttl" ] + operations: [ "transaction", "etag", "first-write", "query", "ttl", "actorStateStore", "keyslike" ] config: # This component requires etags to be numeric badEtag: "1" - component: postgresql.v2.docker - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore", "keyslike" ] config: # This component requires etags to be UUIDs badEtag: "e9b9e142-74b1-4a2e-8e90-3f4ffeea2e70" - component: postgresql.v2.azure - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore", "keyslike" ] config: # This component requires etags to be UUIDs badEtag: "e9b9e142-74b1-4a2e-8e90-3f4ffeea2e70" - component: sqlite - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore", "keyslike" ] - component: mysql.mysql - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore", "keyslike" ] - component: mysql.mariadb - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore", "keyslike" ] - component: azure.tablestorage.storage operations: [ "etag", "first-write"] config: @@ -73,36 +88,38 @@ components: # This component requires etags to be in this format badEtag: "W/\"datetime'2023-05-09T12%3A28%3A54.1442151Z'\"" - component: oracledatabase - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore" ] - component: cassandra operations: [ "ttl" ] - component: cloudflare.workerskv # Although this component supports TTLs, the minimum TTL is 60s, which makes it not suitable for our conformance tests operations: [] - component: cockroachdb.v1 - operations: [ "transaction", "etag", "first-write", "query", "ttl" ] + operations: [ "transaction", "etag", "first-write", "query", "ttl", "keyslike" ] config: # This component requires etags to be numeric badEtag: "9999999" - component: cockroachdb.v2 - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "keyslike" ] config: # This component requires etags to be UUIDs badEtag: "7b104dbd-1ae2-4772-bfa0-e29c7b89bc9b" - component: rethinkdb operations: [] - component: in-memory - operations: [ "transaction", "etag", "first-write", "ttl", "delete-with-prefix" ] + operations: [ "transaction", "etag", "first-write", "ttl", "delete-with-prefix", "actorStateStore", "keyslike" ] - component: aws.dynamodb.docker # In the Docker variant, we do not set ttlAttributeName in the metadata, so TTLs are not enabled operations: [ "transaction", "etag", "first-write" ] - component: aws.dynamodb.terraform operations: [ "transaction", "etag", "first-write", "ttl" ] - component: etcd.v1 - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore", "keyslike" ] - component: etcd.v2 - operations: [ "transaction", "etag", "first-write", "ttl" ] + operations: [ "transaction", "etag", "first-write", "ttl", "actorStateStore", "keyslike" ] - component: gcp.firestore.docker operations: [] - component: gcp.firestore.cloud operations: [] + - component: ravendb + operations: [ "first-write", "etag", "ttl", "transaction" ] diff --git a/tests/conformance/README.md b/tests/conformance/README.md index 1efb00257b..d845044cba 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -112,6 +112,11 @@ Noting, the pattern to run other specific conformance tests ```bash go test -v -tags=conftests -count=1 ./tests/conformance -run="TestConformance/" ``` + +For conversation components you must run the Init() with the subtest, so use a command such as: +```bash +go test -v -tags=conftests -count=1 ./tests/conformance -run="TestConversationConformance/.*(init|)" +``` If you want to combine VS Code & dlv for debugging so you can set breakpoints in the IDE, create a debug launch configuration as follows: diff --git a/tests/conformance/conversation/conversation.go b/tests/conformance/conversation/conversation.go index 4b9cfda368..12cec68288 100644 --- a/tests/conformance/conversation/conversation.go +++ b/tests/conformance/conversation/conversation.go @@ -15,14 +15,16 @@ package conversation import ( "context" + "encoding/json" "slices" + "strings" "testing" "time" "github.com/tmc/langchaingo/llms" "github.com/dapr/components-contrib/conversation" - "github.com/dapr/components-contrib/conversation/mistral" + "github.com/dapr/components-contrib/conversation/langchaingokit" "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/tests/conformance/utils" @@ -46,7 +48,10 @@ func NewTestConfig(componentName string) TestConfig { } func ConformanceTests(t *testing.T, props map[string]string, conv conversation.Conversation, component string) { - providerStopReasons := []string{"stop", "end_turn", "FinishReasonStop", "tool_calls"} + // load, unload, and length are stop reasons for ollama + // FINISH, stop_sequence, max_tokens are stop reasons for bedrock + // unknown we custom add as langchaingo does not provide stop reasons for ollama, so we put unknown for this value + providerStopReasons := []string{"stop", "end_turn", "FinishReasonStop", "tool_calls", "load", "unload", "length", "FINISH", "stop_sequence", "max_tokens", "unknown"} t.Run("init", func(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) @@ -69,6 +74,8 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C ctx, cancel := context.WithTimeout(t.Context(), 25*time.Second) defer cancel() + // Note: Temperature is set to 1 for OpenAI models to avoid issues with GPT-5 which does not support temperature=0. + // This can be removed once langchaingo is updated to handle this automatically (tmc/langchaingo#1374). req := &conversation.Request{ Message: &[]llms.MessageContent{ { @@ -79,9 +86,12 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C }, }, } + if component == "openai" { + req.Temperature = 1 + } resp, err := conv.Converse(ctx, req) - require.NoError(t, err) + require.NotNil(t, resp) assert.Len(t, resp.Outputs, 1) assert.NotEmpty(t, resp.Outputs[0].Choices[0].Message.Content) }) @@ -100,12 +110,16 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C req := &conversation.Request{ Message: &userMsgs, } + if component == "openai" { + req.Temperature = 1 + } resp, err := conv.Converse(ctx, req) require.NoError(t, err) assert.Len(t, resp.Outputs, 1) assert.NotEmpty(t, resp.Outputs[0].Choices[0].Message.Content) - // anthropic responds with end_turn but other llm providers return with stop + // anthropic responds with end_turn but other llm providers return with + // also, ollama due to limitations in langchaingo does not provide a stop reason, so we in the backend (contrib normalizeFinishReason() provide one) assert.True(t, slices.Contains(providerStopReasons, resp.Outputs[0].StopReason)) assert.Empty(t, resp.Outputs[0].Choices[0].Message.ToolCallRequest) }) @@ -132,6 +146,9 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C req := &conversation.Request{ Message: &systemMsgs, } + if component == "openai" { + req.Temperature = 1 + } resp, err := conv.Converse(ctx, req) require.NoError(t, err) @@ -139,6 +156,7 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C assert.NotEmpty(t, resp.Outputs[0].Choices[0].Message.Content) // anthropic responds with end_turn but other llm providers return with stop assert.True(t, slices.Contains(providerStopReasons, resp.Outputs[0].StopReason), resp.Outputs[0].StopReason) + t.Logf("Stop reason for %s: '%s'", component, resp.Outputs[0].StopReason) assert.Empty(t, resp.Outputs[0].Choices[0].Message.ToolCallRequest) }) t.Run("test assistant message type", func(t *testing.T) { @@ -162,8 +180,8 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C var assistantMsgs []llms.MessageContent - // mistral must have tool info wrapped as text - if component != "mistral" { + // mistral and ollama must have tool info wrapped as text + if component == "mistral" || component == "ollama" { assistantMsgs = []llms.MessageContent{ { Role: llms.ChatMessageTypeAI, @@ -174,18 +192,13 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C { Role: llms.ChatMessageTypeAI, Parts: []llms.ContentPart{ - toolCall, + langchaingokit.CreateToolCallPart(&toolCall), }, }, // anthropic expects a conversation to end with a user message to generate a response, // therefore, in testing an assistant msg we must also include a human msg for it to generate an output for us; // otherwise it assumes the conversation is over. - { - Role: llms.ChatMessageTypeTool, - Parts: []llms.ContentPart{ - toolResponse, - }, - }, + langchaingokit.CreateToolResponseMessage(toolResponse), { Role: llms.ChatMessageTypeHuman, Parts: []llms.ContentPart{ @@ -204,13 +217,18 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C { Role: llms.ChatMessageTypeAI, Parts: []llms.ContentPart{ - mistral.CreateToolCallPart(&toolCall), + toolCall, }, }, // anthropic expects a conversation to end with a user message to generate a response, // therefore, in testing an assistant msg we must also include a human msg for it to generate an output for us; // otherwise it assumes the conversation is over. - mistral.CreateToolResponseMessage(toolResponse), + { + Role: llms.ChatMessageTypeTool, + Parts: []llms.ContentPart{ + toolResponse, + }, + }, { Role: llms.ChatMessageTypeHuman, Parts: []llms.ContentPart{ @@ -223,8 +241,10 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C req := &conversation.Request{ Message: &assistantMsgs, } + if component == "openai" { + req.Temperature = 1 + } resp, err := conv.Converse(ctx, req) - require.NoError(t, err) // We expect a single output. In the future, depending on request (so probably a different test), @@ -233,6 +253,7 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C assert.NotEmpty(t, resp.Outputs[0].Choices[0].Message.Content) // anthropic responds with end_turn but other llm providers return with stop assert.True(t, slices.Contains(providerStopReasons, resp.Outputs[0].StopReason)) + t.Logf("Stop reason for %s: '%s'", component, resp.Outputs[0].StopReason) if resp.Outputs[0].Choices[0].Message.ToolCallRequest != nil && len(*resp.Outputs[0].Choices[0].Message.ToolCallRequest) > 0 { assert.NotEmpty(t, resp.Outputs[0].Choices[0].Message.ToolCallRequest) require.JSONEq(t, `{"test": "value"}`, (*resp.Outputs[0].Choices[0].Message.ToolCallRequest)[0].FunctionCall.Arguments) @@ -254,6 +275,9 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C req := &conversation.Request{ Message: &developerMsgs, } + if component == "openai" { + req.Temperature = 1 + } resp, err := conv.Converse(ctx, req) require.NoError(t, err) @@ -261,6 +285,7 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C assert.NotEmpty(t, resp.Outputs[0].Choices[0].Message.Content) // anthropic responds with end_turn but other llm providers return with stop assert.True(t, slices.Contains(providerStopReasons, resp.Outputs[0].StopReason)) + t.Logf("Stop reason for %s: '%s'", component, resp.Outputs[0].StopReason) if resp.Outputs[0].Choices[0].Message.ToolCallRequest != nil { assert.Empty(t, *resp.Outputs[0].Choices[0].Message.ToolCallRequest) } @@ -303,6 +328,9 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C Message: &messages, Tools: &tools, } + if component == "openai" { + req.Temperature = 1 + } resp, err := conv.Converse(ctx, req) require.NoError(t, err) @@ -338,38 +366,43 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C } // mistral must have tool info wrapped as text - if component != "mistral" { + if component == "mistral" || component == "ollama" { responseMessages = append(responseMessages, llms.MessageContent{ Role: llms.ChatMessageTypeAI, - Parts: []llms.ContentPart{toolCall}, - }, - llms.MessageContent{ - Role: llms.ChatMessageTypeTool, - Parts: []llms.ContentPart{toolResponse}, + Parts: []llms.ContentPart{langchaingokit.CreateToolCallPart(&toolCall)}, }, + langchaingokit.CreateToolResponseMessage(toolResponse), ) } else { responseMessages = append(responseMessages, llms.MessageContent{ Role: llms.ChatMessageTypeAI, - Parts: []llms.ContentPart{mistral.CreateToolCallPart(&toolCall)}, + Parts: []llms.ContentPart{toolCall}, + }, + llms.MessageContent{ + Role: llms.ChatMessageTypeTool, + Parts: []llms.ContentPart{toolResponse}, }, - mistral.CreateToolResponseMessage(toolResponse), ) } req2 := &conversation.Request{ Message: &responseMessages, } + if component == "openai" { + req2.Temperature = 1 + } resp2, err2 := conv.Converse(ctx, req2) require.NoError(t, err2) assert.Len(t, resp2.Outputs, 1) assert.NotEmpty(t, resp2.Outputs[0].Choices[0].Message.Content) + t.Logf("Stop reason for %s (tool resp2): '%s'", component, resp2.Outputs[0].StopReason) assert.True(t, slices.Contains(providerStopReasons, resp2.Outputs[0].StopReason)) } else { assert.NotEmpty(t, resp.Outputs[0].Choices[0].Message.Content) + t.Logf("Stop reason for %s (tool resp): '%s'", component, resp.Outputs[0].StopReason) assert.True(t, slices.Contains(providerStopReasons, resp.Outputs[0].StopReason)) } }) @@ -402,8 +435,15 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C "type": "string", "description": "The transaction id.", }, + "items": map[string]any{ + "type": "array", + "description": "List of items in the transaction", + "items": map[string]any{ + "type": "string", + }, + }, }, - "required": []string{"transaction_id"}, + "required": []string{"transaction_id", "items"}, }, }, }, @@ -413,6 +453,9 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C Message: &messages, Tools: &tools, } + if component == "openai" { + req1.Temperature = 1 + } resp1, err := conv.Converse(ctx, req1) require.NoError(t, err) @@ -448,7 +491,7 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C } var toolResponseMessages []llms.MessageContent - if component != "mistral" { + if component == "mistral" || component == "ollama" { toolResponseMessages = []llms.MessageContent{ { Role: llms.ChatMessageTypeHuman, @@ -458,14 +501,9 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C }, { Role: llms.ChatMessageTypeAI, - Parts: []llms.ContentPart{toolCall}, - }, - { - Role: llms.ChatMessageTypeTool, - Parts: []llms.ContentPart{ - toolResponse, - }, + Parts: []llms.ContentPart{langchaingokit.CreateToolCallPart(&toolCall)}, }, + langchaingokit.CreateToolResponseMessage(toolResponse), } } else { toolResponseMessages = []llms.MessageContent{ @@ -477,26 +515,227 @@ func ConformanceTests(t *testing.T, props map[string]string, conv conversation.C }, { Role: llms.ChatMessageTypeAI, - Parts: []llms.ContentPart{mistral.CreateToolCallPart(&toolCall)}, + Parts: []llms.ContentPart{toolCall}, + }, + { + Role: llms.ChatMessageTypeTool, + Parts: []llms.ContentPart{ + toolResponse, + }, }, - mistral.CreateToolResponseMessage(toolResponse), } } req2 := &conversation.Request{ Message: &toolResponseMessages, } + if component == "openai" { + req2.Temperature = 1 + } resp2, err := conv.Converse(ctx, req2) require.NoError(t, err) assert.Len(t, resp2.Outputs, 1) assert.NotEmpty(t, resp2.Outputs[0].Choices[0].Message.Content) + t.Logf("Stop reason for %s (history resp2): '%s'", component, resp2.Outputs[0].StopReason) assert.True(t, slices.Contains(providerStopReasons, resp2.Outputs[0].StopReason)) } else { // it is valid too if no tool call was generated assert.NotEmpty(t, resp1.Outputs[0].Choices[0].Message.Content) + t.Logf("Stop reason for %s (history resp1): '%s'", component, resp1.Outputs[0].StopReason) assert.True(t, slices.Contains(providerStopReasons, resp1.Outputs[0].StopReason)) } }) + + t.Run("test response format returned", func(t *testing.T) { + if component == "echo" || component == "ollama" || component == "bedrock" { + t.Skipf("component %s doesn't support structured output", component) + } + ctx, cancel := context.WithTimeout(t.Context(), 25*time.Second) + defer cancel() + + responseFormat := map[string]any{ + "type": "object", + "strict": true, + "properties": map[string]any{ + "calculation": map[string]any{ + "type": "string", + "description": "The mathematical expression being calculated", + }, + "result": map[string]any{ + "type": "number", + "description": "The numerical result of the calculation", + }, + "explanation": map[string]any{ + "type": "string", + "description": "A brief explanation of how you got the result", + }, + "confidence": map[string]any{ + "type": "string", + "enum": []any{"low", "medium", "high"}, + "description": "Your confidence level in this answer", + }, + }, + "required": []any{"calculation", "result", "explanation", "confidence"}, + "additionalProperties": false, + } + + req := &conversation.Request{ + Message: &[]llms.MessageContent{ + { + Role: llms.ChatMessageTypeHuman, + Parts: []llms.ContentPart{ + // NOTE: this is the part that guarantees the field addition works. + llms.TextContent{Text: "What is 2+2? Provide your answer in the structured format that I provided."}, + }, + }, + }, + ResponseFormatAsJSONSchema: responseFormat, + } + if component == "openai" { + req.Temperature = 1 + } + + resp, err := conv.Converse(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Len(t, resp.Outputs, 1) + content := resp.Outputs[0].Choices[0].Message.Content + assert.NotEmpty(t, content) + t.Logf("Structured output response content: %s", content) + + var result struct { + Calculation string `json:"calculation"` + Result float64 `json:"result"` + Explanation string `json:"explanation"` + Confidence string `json:"confidence"` + } + + err = json.Unmarshal([]byte(content), &result) + require.NoError(t, err, "Response should be valid JSON matching the provided schema") + assert.NotEmpty(t, result.Calculation, "Response should contain the 'calculation' field") + assert.Equal(t, float64(4), result.Result, "Result should be 4") + assert.NotEmpty(t, result.Explanation, "Response should contain the 'explanation' field") + assert.Contains(t, []string{"low", "medium", "high"}, result.Confidence, "Confidence should be one of the enum values") + t.Run("test prompt cache retention", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 25*time.Second) + defer cancel() + + // create a long prompt to ensure it's cacheable (OpenAI requires >1024 tokens) + longPrompt := "You are a helpful assistant. " + strings.Repeat("This is important context information that should be cached. ", 100) + "The end of the prompt." + + promptCacheRetention := 24 * time.Hour + + // first request - no cached tokens + req1 := &conversation.Request{ + Message: &[]llms.MessageContent{ + { + Role: llms.ChatMessageTypeSystem, + Parts: []llms.ContentPart{ + llms.TextContent{Text: longPrompt}, + }, + }, + { + Role: llms.ChatMessageTypeHuman, + Parts: []llms.ContentPart{ + llms.TextContent{Text: "what is 2+2?"}, + }, + }, + }, + PromptCacheRetention: &promptCacheRetention, + Metadata: map[string]string{ + "prompt_cache_retention": "24h", + }, + } + if component == "openai" { + req1.Temperature = 1 + } + + resp1, err := conv.Converse(ctx, req1) + require.NoError(t, err) + require.NotNil(t, resp1) + assert.Len(t, resp1.Outputs, 1) + assert.NotEmpty(t, resp1.Outputs[0].Choices[0].Message.Content) + t.Logf("Request 1 Response Content: %q", resp1.Outputs[0].Choices[0].Message.Content) + t.Logf("Request 1 Response Length: %d characters", len(resp1.Outputs[0].Choices[0].Message.Content)) + + // verify usage data is returned as this lets us check how many cached tokens there are. + // we need usage metrics to verify this prompt cache feature works. + if resp1.Usage != nil { + t.Logf("Request 1 Usage - Total: %d, Prompt: %d, Completion: %d", + resp1.Usage.TotalTokens, resp1.Usage.PromptTokens, resp1.Usage.CompletionTokens) + if resp1.Usage.PromptTokensDetails != nil { + t.Logf("Request 1 Prompt Details - Cached: %d, Audio: %d", + resp1.Usage.PromptTokensDetails.CachedTokens, resp1.Usage.PromptTokensDetails.AudioTokens) + } + if resp1.Usage.CompletionTokensDetails != nil { + t.Logf("Request 1 Completion Details - Reasoning: %d, Audio: %d, AcceptedPrediction: %d, RejectedPrediction: %d", + resp1.Usage.CompletionTokensDetails.ReasoningTokens, + resp1.Usage.CompletionTokensDetails.AudioTokens, + resp1.Usage.CompletionTokensDetails.AcceptedPredictionTokens, + resp1.Usage.CompletionTokensDetails.RejectedPredictionTokens) + } + assert.Greater(t, resp1.Usage.TotalTokens, uint64(0), "Total tokens should be greater than 0") + } else { + t.Logf("Request 1: No usage data returned") + } + + // second request with same prompt - should have cached tokens + // NOTE: Sometimes it actually takes a few requests for this to show up pending model/provider... + req2 := &conversation.Request{ + Message: &[]llms.MessageContent{ + { + Role: llms.ChatMessageTypeSystem, + Parts: []llms.ContentPart{ + llms.TextContent{Text: longPrompt}, + }, + }, + { + Role: llms.ChatMessageTypeHuman, + Parts: []llms.ContentPart{ + llms.TextContent{Text: "what is 3+3?"}, + }, + }, + }, + PromptCacheRetention: &promptCacheRetention, + Metadata: map[string]string{ + "prompt_cache_retention": "24h", + }, + } + if component == "openai" { + req2.Temperature = 1 + } + + resp2, err := conv.Converse(ctx, req2) + require.NoError(t, err) + require.NotNil(t, resp2) + assert.Len(t, resp2.Outputs, 1) + assert.NotEmpty(t, resp2.Outputs[0].Choices[0].Message.Content) + + // Verify cached tokens are used in the second request + if resp2.Usage != nil { + t.Logf("Request 2 Usage - Total: %d, Prompt: %d, Completion: %d", + resp2.Usage.TotalTokens, resp2.Usage.PromptTokens, resp2.Usage.CompletionTokens) + if resp2.Usage.PromptTokensDetails != nil { + t.Logf("Request 2 Prompt Details - Cached: %d, Audio: %d", + resp2.Usage.PromptTokensDetails.CachedTokens, resp2.Usage.PromptTokensDetails.AudioTokens) + t.Logf("Cached tokens on second request: %d", resp2.Usage.PromptTokensDetails.CachedTokens) + // For OpenAI with proper caching, we'd expect cached tokens > 0 + // but this depends on timing and cache state, so we don't assert + } else { + t.Logf("Request 2: No prompt token details returned") + } + if resp2.Usage.CompletionTokensDetails != nil { + t.Logf("Request 2 Completion Details - Reasoning: %d, Audio: %d, AcceptedPrediction: %d, RejectedPrediction: %d", + resp2.Usage.CompletionTokensDetails.ReasoningTokens, + resp2.Usage.CompletionTokensDetails.AudioTokens, + resp2.Usage.CompletionTokensDetails.AcceptedPredictionTokens, + resp2.Usage.CompletionTokensDetails.RejectedPredictionTokens) + } + } else { + t.Logf("Request 2: No usage data returned for second request to verify that prompt caching is working") + } + }) + }) }) } diff --git a/tests/conformance/conversation_test.go b/tests/conformance/conversation_test.go index 5ed08c3460..08668aed68 100644 --- a/tests/conformance/conversation_test.go +++ b/tests/conformance/conversation_test.go @@ -136,6 +136,9 @@ func loadConversationComponent(name string) conversation.Conversation { return ollama.NewOllama(testLogger) case "bedrock": return bedrock.NewAWSBedrock(testLogger) + case "deepseek": + testLogger.Infof("TODO add deepseek conformance tests") + return nil default: return nil } diff --git a/tests/conformance/pubsub/pubsub.go b/tests/conformance/pubsub/pubsub.go index ec6dcb59bc..28024426df 100644 --- a/tests/conformance/pubsub/pubsub.go +++ b/tests/conformance/pubsub/pubsub.go @@ -197,6 +197,9 @@ func ConformanceTests(t *testing.T, props map[string]string, ps pubsub.PubSub, c // Sleep to allow messages to pile up and be delivered as a batch. time.Sleep(1 * time.Second) t.Logf("Simulating subscriber error") + mu.Lock() + delete(awaitingMessages, dataString) // drop the message to avoid infinite re-delivery, dapr resiliency policy will apply if defined + mu.Unlock() return errors.New("conf test simulated error") } @@ -285,6 +288,10 @@ func ConformanceTests(t *testing.T, props map[string]string, ps pubsub.PubSub, c time.Sleep(1 * time.Second) t.Logf("Simulating subscriber error") + muBulk.Lock() + delete(awaitingMessagesBulk, dataString) // drop the message to avoid infinite re-delivery, dapr resiliency policy will apply if defined + muBulk.Unlock() + bulkResponses[i].EntryId = msg.EntryId bulkResponses[i].Error = errors.New("conf test simulated error") hasAnyError = true @@ -530,15 +537,17 @@ func ConformanceTests(t *testing.T, props map[string]string, ps pubsub.PubSub, c data := []byte(fmt.Sprintf("%s%d", dataPrefix, k)) var topic string if k%2 == 0 { - topic = config.TestMultiTopic1Name - if sent1Ch != nil { - sent1Ch <- string(data) + if sent1Ch == nil { + continue // subscriber 1 is closed, do not publish to this topic } + topic = config.TestMultiTopic1Name + sent1Ch <- string(data) } else { - topic = config.TestMultiTopic2Name - if sent2Ch != nil { - sent2Ch <- string(data) + if sent2Ch == nil { + continue // subscriber 2 is closed, do not publish to this topic } + topic = config.TestMultiTopic2Name + sent2Ch <- string(data) } err := ps.Publish(ctx, &pubsub.PublishRequest{ Data: data, diff --git a/tests/conformance/state/state.go b/tests/conformance/state/state.go index 00b6074928..2f6018a411 100644 --- a/tests/conformance/state/state.go +++ b/tests/conformance/state/state.go @@ -27,7 +27,9 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "github.com/dapr/components-contrib/common/proto/state/sqlserver" "github.com/dapr/components-contrib/contenttype" "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/state" @@ -452,6 +454,36 @@ func ConformanceTests(t *testing.T, props map[string]string, statestore state.St } }) + t.Run("set and get proto", func(t *testing.T) { + if !config.HasOperation("actorStateStore") { + t.Skipf("skipping test for %s", config.ComponentName) + } + + protoBytes, err := proto.Marshal(&sqlserver.TestEvent{ + EventId: -1, + }) + require.NoError(t, err) + + err = statestore.Set(t.Context(), &state.SetRequest{ + Key: key + "-proto", + Value: protoBytes, + }) + require.NoError(t, err) + + // Request immediately + res, err := statestore.Get(t.Context(), &state.GetRequest{ + Key: key + "-proto", + }) + require.NoError(t, err) + assertEquals(t, protoBytes, res) + + response := &sqlserver.TestEvent{} + err = proto.Unmarshal(res.Data, response) + require.NoError(t, err) + + assert.EqualValues(t, -1, response.GetEventId()) + }) + if config.HasOperation("query") { t.Run("query", func(t *testing.T) { // Check if query feature is listed @@ -1340,6 +1372,11 @@ func ConformanceTests(t *testing.T, props map[string]string, statestore state.St } t.Run("set and get expire time", func(t *testing.T) { + if config.ComponentName == "sqlserver" || + config.ComponentName == "sqlserver.docker" { + t.Skip() + } + now := time.Now() err := statestore.Set(t.Context(), &state.SetRequest{ Key: key + "-ttl-expire-time", @@ -1585,6 +1622,390 @@ func ConformanceTests(t *testing.T, props map[string]string, statestore state.St require.False(t, state.FeatureDeleteWithPrefix.IsPresent(features)) }) } + + if config.HasOperation("keyslike") { + keys := []string{ + "prefix||key1", + "prefix||key2", + "prefix||prefix2||key3", + "other-prefix||key1", + "no-prefix", + "abc1", + "abc2", + "abc3", + "abc33", + "xyz1", + "xyz2", + "xyz3", + } + + var store state.KeysLiker + t.Run("component implements KeysLiker interface", func(t *testing.T) { + var ok bool + store, ok = statestore.(state.KeysLiker) + require.True(t, ok) + }) + + t.Run("KeysLike feature present", func(t *testing.T) { + features := statestore.Features() + require.True(t, state.FeatureKeysLike.IsPresent(features)) + }) + + t.Run("empty", func(t *testing.T) { + got, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "", + }) + require.ErrorIs(t, err, state.ErrKeysLikeEmptyPattern) + assert.Nil(t, got) + }) + + t.Run("check simple keys", func(t *testing.T) { + got, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + + for _, key := range got.Keys { + require.NoError(t, statestore.Delete(t.Context(), &state.DeleteRequest{ + Key: key, + })) + } + + for _, key := range keys { + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: key, + Value: []byte("value for " + key), + })) + } + + got, err = store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + assert.ElementsMatch(t, keys, got.Keys) + assert.Nil(t, got.ContinuationToken) + }) + + t.Run("matching", func(t *testing.T) { + for pattern, exp := range map[string][]string{ + "%": keys, + "prefix||%": { + "prefix||key1", + "prefix||key2", + "prefix||prefix2||key3", + }, + "%key1": { + "prefix||key1", + "other-prefix||key1", + }, + "%||%": { + "prefix||key1", + "prefix||key2", + "prefix||prefix2||key3", + "other-prefix||key1", + }, + "%||%||%": { + "prefix||prefix2||key3", + }, + "abc_": { + "abc1", + "abc2", + "abc3", + }, + } { + t.Run(pattern, func(t *testing.T) { + got, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: pattern, + }) + require.NoError(t, err) + assert.ElementsMatchf(t, exp, got.Keys, "pattern: %s", pattern) + }) + } + }) + + t.Run("page size", func(t *testing.T) { + got, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + require.Len(t, got.Keys, 12) + assert.ElementsMatch(t, keys, got.Keys) + assert.Nil(t, got.ContinuationToken) + + got, err = store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + PageSize: ptr.Of(uint32(6)), + }) + require.NoError(t, err) + require.Len(t, got.Keys, 6) + + gotKeys := got.Keys + require.NotNil(t, got.ContinuationToken) + + got, err = store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + PageSize: ptr.Of(uint32(5)), + ContinuationToken: got.ContinuationToken, + }) + require.NoError(t, err) + require.Len(t, got.Keys, 5) + gotKeys = append(gotKeys, got.Keys...) + require.NotNil(t, got.ContinuationToken) + + got, err = store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + PageSize: ptr.Of(uint32(100)), + ContinuationToken: got.ContinuationToken, + }) + require.NoError(t, err) + require.Len(t, got.Keys, 1) + gotKeys = append(gotKeys, got.Keys...) + require.Nil(t, got.ContinuationToken) + + assert.ElementsMatch(t, keys, gotKeys) + }) + + t.Run("no page size limit", func(t *testing.T) { + got, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + + for _, key := range got.Keys { + require.NoError(t, statestore.Delete(t.Context(), &state.DeleteRequest{ + Key: key, + })) + } + + for i := range 1025 { + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: strconv.Itoa(i), + Value: nil, + })) + } + + got, err = store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + assert.Len(t, got.Keys, 1025) + assert.Nil(t, got.ContinuationToken) + + got, err = store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + PageSize: ptr.Of(uint32(100000)), + }) + require.NoError(t, err) + assert.Len(t, got.Keys, 1025) + assert.Nil(t, got.ContinuationToken) + }) + + t.Run("escaping", func(t *testing.T) { + got, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + for _, key := range got.Keys { + require.NoError(t, statestore.Delete(t.Context(), &state.DeleteRequest{ + Key: key, + })) + } + + keys := []string{ + "%", + "hello%%wor.kflow", + "%%wor.kflow", + "hello%%", + "_", + "hello_workflow", + "_workflow", + "hello_", + "%hello_workflow%_yoyo", + } + for _, key := range keys { + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: key, + })) + } + + for pattern, exp := range map[string][]string{ + "%": keys, + "hello%": { + "hello%%wor.kflow", + "hello%%", + "hello_workflow", + "hello_", + }, + "hello_": { + "hello_", + }, + "hello_workflo_": { + "hello_workflow", + }, + `hello\_workflow`: { + "hello_workflow", + }, + `hello\_`: { + "hello_", + }, + `hello%%`: { + "hello%%wor.kflow", + "hello%%", + "hello_workflow", + "hello_", + }, + `hello\%\%`: { + "hello%%", + }, + `hello%\%`: { + "hello%%", + }, + `hello\%\%%wor.kflow`: { + "hello%%wor.kflow", + }, + `\%hello\_workflow\%\_yoyo`: { + "%hello_workflow%_yoyo", + }, + } { + t.Run(pattern, func(t *testing.T) { + got, err = store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: pattern, + }) + require.NoError(t, err) + assert.ElementsMatchf(t, exp, got.Keys, "pattern: %s", pattern) + }) + } + }) + + t.Run("pagination deleted", func(t *testing.T) { + got1, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + for _, key := range got1.Keys { + require.NoError(t, statestore.Delete(t.Context(), &state.DeleteRequest{ + Key: key, + })) + } + + keys1 := []string{ + "key1", + "key2", + "key3", + "key4", + } + + for _, key := range keys1 { + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: key, + })) + } + got2, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + PageSize: ptr.Of(uint32(3)), + }) + require.NoError(t, err) + assert.Len(t, got2.Keys, 3) + assert.NotNil(t, got2.ContinuationToken) + + require.NoError(t, statestore.Delete(t.Context(), &state.DeleteRequest{Key: "key1"})) + require.NoError(t, statestore.Delete(t.Context(), &state.DeleteRequest{Key: "key3"})) + + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: "key5", + })) + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: "key0", + })) + + got3, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + ContinuationToken: got2.ContinuationToken, + }) + require.NoError(t, err) + assert.GreaterOrEqual(t, len(got3.Keys), 1) + assert.Nil(t, got3.ContinuationToken) + + got4, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + ContinuationToken: got3.ContinuationToken, + PageSize: ptr.Of(uint32(3)), + }) + require.NoError(t, err) + assert.Len(t, got4.Keys, 3) + assert.NotNil(t, got4.ContinuationToken) + + gotX, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + ContinuationToken: got3.ContinuationToken, + PageSize: ptr.Of(uint32(2)), + }) + require.NoError(t, err) + assert.Len(t, gotX.Keys, 2) + assert.NotNil(t, gotX.ContinuationToken) + + got5, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + ContinuationToken: got3.ContinuationToken, + }) + require.NoError(t, err) + assert.Len(t, got5.Keys, 4) + assert.Nil(t, got5.ContinuationToken) + }) + + t.Run("expiration", func(t *testing.T) { + got, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + for _, key := range got.Keys { + require.NoError(t, statestore.Delete(t.Context(), &state.DeleteRequest{ + Key: key, + })) + } + + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: "1", + Metadata: map[string]string{"ttlInSeconds": "1"}, + })) + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: "2", + })) + require.NoError(t, statestore.Set(t.Context(), &state.SetRequest{ + Key: "3", + Metadata: map[string]string{"ttlInSeconds": "1"}, + })) + + time.Sleep(time.Second * 5) + + got, err = store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + assert.Equal(t, []string{"2"}, got.Keys) + assert.Nil(t, got.ContinuationToken) + }) + + got, err := store.KeysLike(t.Context(), &state.KeysLikeRequest{ + Pattern: "%", + }) + require.NoError(t, err) + for _, key := range got.Keys { + require.NoError(t, statestore.Delete(t.Context(), &state.DeleteRequest{ + Key: key, + })) + } + } else { + t.Run("component does not implement KeysLike interface", func(t *testing.T) { + _, ok := statestore.(state.KeysLiker) + require.False(t, ok) + }) + + t.Run("KeysLike feature not present", func(t *testing.T) { + features := statestore.Features() + require.False(t, state.FeatureKeysLike.IsPresent(features)) + }) + } } func assertEquals(t *testing.T, value any, res *state.GetResponse) { diff --git a/tests/conformance/state_test.go b/tests/conformance/state_test.go index e102a68b27..78da04f26c 100644 --- a/tests/conformance/state_test.go +++ b/tests/conformance/state_test.go @@ -41,10 +41,12 @@ import ( s_oracledatabase "github.com/dapr/components-contrib/state/oracledatabase" s_postgresql_v1 "github.com/dapr/components-contrib/state/postgresql/v1" s_postgresql_v2 "github.com/dapr/components-contrib/state/postgresql/v2" + s_ravendb "github.com/dapr/components-contrib/state/ravendb" s_redis "github.com/dapr/components-contrib/state/redis" s_rethinkdb "github.com/dapr/components-contrib/state/rethinkdb" s_sqlite "github.com/dapr/components-contrib/state/sqlite" s_sqlserver "github.com/dapr/components-contrib/state/sqlserver" + s_sqlserver_v2 "github.com/dapr/components-contrib/state/sqlserver/v2" conf_state "github.com/dapr/components-contrib/tests/conformance/state" ) @@ -93,6 +95,12 @@ func loadStateStore(name string) state.Store { return s_sqlserver.New(testLogger) case "sqlserver": return s_sqlserver.New(testLogger) + case "sqlserver.v2": + return s_sqlserver_v2.New(testLogger) + case "sqlserver.docker": + return s_sqlserver.New(testLogger) + case "sqlserver.v2.docker": + return s_sqlserver_v2.New(testLogger) case "postgresql.v1.docker": return s_postgresql_v1.NewPostgreSQLStateStore(testLogger) case "postgresql.v1.azure": @@ -141,6 +149,8 @@ func loadStateStore(name string) state.Store { return s_gcpfirestore.NewFirestoreStateStore(testLogger) case "gcp.firestore.cloud": return s_gcpfirestore.NewFirestoreStateStore(testLogger) + case "ravendb": + return s_ravendb.NewRavenDB(testLogger) case "coherence": return s_coherence.NewCoherenceStateStore(testLogger) default: diff --git a/tests/e2e/bindings/kitex/EchoKitexServer.go b/tests/e2e/bindings/kitex/EchoKitexServer.go index 8b0b1ea16d..91d2bfe3d5 100644 --- a/tests/e2e/bindings/kitex/EchoKitexServer.go +++ b/tests/e2e/bindings/kitex/EchoKitexServer.go @@ -17,19 +17,16 @@ import ( "context" "time" - "github.com/apache/thrift/lib/go/thrift" + "github.com/cloudwego/gopkg/protocol/thrift" "github.com/cloudwego/kitex-examples/kitex_gen/api" "github.com/cloudwego/kitex-examples/kitex_gen/api/echo" "github.com/cloudwego/kitex/pkg/klog" - "github.com/cloudwego/kitex/pkg/utils" ) func GenerateEchoReq(message string) (reqData []byte, err error) { - codec := utils.NewThriftMessageCodec() - req := &api.EchoEchoArgs{Req: &api.Request{Message: message}} - reqData, err = codec.Encode("echo", thrift.CALL, 0, req) + reqData, err = thrift.MarshalFastMsg("echo", thrift.CALL, 0, req) if err != nil { return nil, err } diff --git a/tests/e2e/pubsub/jetstream/go.mod b/tests/e2e/pubsub/jetstream/go.mod index f986bcdec7..cad21d1ff1 100644 --- a/tests/e2e/pubsub/jetstream/go.mod +++ b/tests/e2e/pubsub/jetstream/go.mod @@ -1,34 +1,33 @@ module github.com/dapr/components-contrib/tests/e2e/pubsub/jetstream -go 1.24.4 +go 1.24.13 require ( github.com/dapr/components-contrib v1.10.6-0.20230403162214-9ee9d56cb7ea - github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 + github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d ) require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0 // indirect + github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2 // indirect github.com/cloudevents/sdk-go/v2 v2.15.2 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.3 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nats-io/nats.go v1.31.0 // indirect - github.com/nats-io/nkeys v0.4.6 // indirect + github.com/nats-io/nats.go v1.39.1 // indirect + github.com/nats-io/nkeys v0.4.10 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cast v1.8.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/sys v0.33.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/sys v0.40.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/apimachinery v0.33.0 // indirect ) diff --git a/tests/e2e/pubsub/jetstream/go.sum b/tests/e2e/pubsub/jetstream/go.sum index 504230d61a..dd5fbf0e30 100644 --- a/tests/e2e/pubsub/jetstream/go.sum +++ b/tests/e2e/pubsub/jetstream/go.sum @@ -1,11 +1,11 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0 h1:dEopBSOSjB5fM9r76ufM44AVj9Dnz2IOM0Xs6FVxZRM= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0/go.mod h1:qDSbb0fgIfFNjZrNTPtS5MOMScAGyQtn1KlSvoOdqYw= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2 h1:FIvfKlS2mcuP0qYY6yzdIU9xdrRd/YMP0bNwFjXd0u8= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2/go.mod h1:POsdVp/08Mki0WD9QvvgRRpg9CQ6zhjfRrBoEY8JFS8= github.com/cloudevents/sdk-go/v2 v2.15.2 h1:54+I5xQEnI73RBhWHxbI1XJcqOFOVJN85vb41+8mHUc= github.com/cloudevents/sdk-go/v2 v2.15.2/go.mod h1:lL7kSWAE/V8VI4Wh0jbL2v/jvqsm6tjmaQBSvxcv4uE= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 h1:Q26gmPxs6WnnBYoudOlznPHsmrbTawcYEpHg4VoB7v8= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= +github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d h1:csljij9d1IO6u9nqbg+TuSRmTZ+OXT8G49yh6zie1yI= +github.com/dapr/kit v0.16.2-0.20251124175541-3ac186dff64d/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -16,8 +16,6 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -27,14 +25,14 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -42,14 +40,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak= -github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= -github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU= -github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0= -github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= -github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= -github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= -github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= +github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE= +github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4= +github.com/nats-io/nats-server/v2 v2.10.27 h1:A/i3JqtrP897UHc2/Jia/mqaXkqj9+HGdpz+R0mC+sM= +github.com/nats-io/nats-server/v2 v2.10.27/go.mod h1:SGzoWGU8wUVnMr/HJhEMv4R8U4f7hF4zDygmRxpNsvg= +github.com/nats-io/nats.go v1.39.1 h1:oTkfKBmz7W047vRxV762M67ZdXeOtUgvbBaNoQ+3PPk= +github.com/nats-io/nats.go v1.39.1/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM= +github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc= +github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -57,17 +55,16 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -79,8 +76,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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.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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -93,9 +90,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ 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= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= @@ -108,12 +104,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= diff --git a/tests/utils/sftpproxy/proxy.go b/tests/utils/sftpproxy/proxy.go new file mode 100644 index 0000000000..8be38d38d6 --- /dev/null +++ b/tests/utils/sftpproxy/proxy.go @@ -0,0 +1,121 @@ +/* +Copyright 2025 The Dapr Authors +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 sftpproxy + +import ( + "errors" + "io" + "log" + "net" + "sync/atomic" + "time" +) + +type Proxy struct { + ListenAddr string + UpstreamAddr string + Client net.Conn + Server net.Conn + ReconnectionCount atomic.Int32 + Listener net.Listener +} + +func (p *Proxy) ListenAndServe() error { + ln, err := net.Listen("tcp", p.ListenAddr) + if err != nil { + log.Fatalf("listen: %v", err) + } + log.Printf("Proxy listening on %s -> %s", p.ListenAddr, p.UpstreamAddr) + p.Listener = ln + + for { + client, err := ln.Accept() + if err != nil { + if errors.Is(err, net.ErrClosed) { + return nil + } + log.Printf("accept error: %v", err) + continue + } + go p.handle(client) + } +} + +func (p *Proxy) handle(client net.Conn) { + defer client.Close() + + // Connect to upstream SFTP server + var server net.Conn + var err error + for i := 0; i < 10 && server == nil; i++ { + server, err = net.Dial("tcp", p.UpstreamAddr) + if err != nil { + log.Printf("dial upstream: %v", err) + time.Sleep(200 * time.Millisecond) + } + } + + if server == nil { + log.Printf("failed to connect to upstream after 5 attempts") + return + } + defer server.Close() + + p.Client = client + p.Server = server + p.ReconnectionCount.Add(1) + errCh := make(chan error, 2) + + // client -> server + go func() { + _, cErr := io.Copy(server, client) + errCh <- cErr + }() + + // server -> client + go func() { + _, cErr := io.Copy(client, server) + errCh <- cErr + }() + + // When either direction ends, close both ends + if err := <-errCh; err != nil && !isUsefullyClosed(err) { + log.Printf("proxy stream ended with error: %v", err) + } +} + +func (p *Proxy) KillServerConn() error { + return p.Server.Close() +} + +func (p *Proxy) Close() { + if p.Client != nil { + _ = p.Client.Close() + } + + if p.Server != nil { + _ = p.Server.Close() + } + + if p.Listener != nil { + _ = p.Listener.Close() + } + + p.ReconnectionCount.Store(0) +} + +// isUsefullyClosed filters common close conditions from logging noise +func isUsefullyClosed(err error) bool { + return err == io.EOF || errors.Is(err, net.ErrClosed) +}