From a2bc8cf6e03698f7cfddd7e27fb2bfb85cbba057 Mon Sep 17 00:00:00 2001 From: Josh van Leeuwen Date: Tue, 27 Jun 2023 18:47:39 +0100 Subject: [PATCH 01/19] Cassandra: return ttlExpiryTime in GetResponse (#2889) Signed-off-by: joshvanl --- state/cassandra/cassandra.go | 19 +++++++++++++++++-- .../state/cassandra/cassandra_test.go | 17 +++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/state/cassandra/cassandra.go b/state/cassandra/cassandra.go index 0006897d48..51453e5e08 100644 --- a/state/cassandra/cassandra.go +++ b/state/cassandra/cassandra.go @@ -15,9 +15,11 @@ package cassandra import ( "context" + "errors" "fmt" "reflect" "strconv" + "time" "github.com/gocql/gocql" jsoniter "github.com/json-iterator/go" @@ -240,7 +242,8 @@ func (c *Cassandra) Get(ctx context.Context, req *state.GetRequest) (*state.GetR session = sess } - results, err := session.Query(fmt.Sprintf("SELECT value FROM %s WHERE key = ?", c.table), req.Key).WithContext(ctx).Iter().SliceMap() + const selectQuery = "SELECT value, TTL(value) AS ttl, toTimestamp(now()) AS now FROM %s WHERE key = ?" + results, err := session.Query(fmt.Sprintf(selectQuery, c.table), req.Key).WithContext(ctx).Iter().SliceMap() if err != nil { return nil, err } @@ -249,8 +252,20 @@ func (c *Cassandra) Get(ctx context.Context, req *state.GetRequest) (*state.GetR return &state.GetResponse{}, nil } + var metadata map[string]string + if ttl := results[0]["ttl"].(int); ttl > 0 { + now, ok := results[0]["now"].(time.Time) + if !ok { + return nil, errors.New("failed to parse cassandra timestamp") + } + metadata = map[string]string{ + state.GetRespMetaKeyTTLExpireTime: now.Add(time.Duration(ttl) * time.Second).UTC().Format(time.RFC3339), + } + } + return &state.GetResponse{ - Data: results[0]["value"].([]byte), + Data: results[0]["value"].([]byte), + Metadata: metadata, }, nil } diff --git a/tests/certification/state/cassandra/cassandra_test.go b/tests/certification/state/cassandra/cassandra_test.go index 0111c099bc..f77573e32f 100644 --- a/tests/certification/state/cassandra/cassandra_test.go +++ b/tests/certification/state/cassandra/cassandra_test.go @@ -15,6 +15,13 @@ package cassandra_test import ( "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/dapr/components-contrib/state" state_cassandra "github.com/dapr/components-contrib/state/cassandra" "github.com/dapr/components-contrib/tests/certification/embedded" @@ -27,10 +34,6 @@ import ( dapr_testing "github.com/dapr/dapr/pkg/testing" goclient "github.com/dapr/go-sdk/client" "github.com/dapr/kit/logger" - "github.com/stretchr/testify/assert" - "strconv" - "testing" - "time" ) const ( @@ -77,12 +80,14 @@ func TestCassandra(t *testing.T) { item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) assert.NoError(t, err) assert.Equal(t, "cassandraCert", string(item.Value)) + assert.NotContains(t, item.Metadata, "ttlExpireTime") errUpdate := client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key1", []byte("cassandraCertUpdate"), nil) assert.NoError(t, errUpdate) item, errUpdatedGet := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) assert.NoError(t, errUpdatedGet) assert.Equal(t, "cassandraCertUpdate", string(item.Value)) + assert.NotContains(t, item.Metadata, "ttlExpireTime") // delete state err = client.DeleteState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) @@ -128,6 +133,10 @@ func TestCassandra(t *testing.T) { item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"ttl3", nil) assert.NoError(t, err) assert.Equal(t, "cassandraCert3", string(item.Value)) + require.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*5).Unix(), expireTime.Unix(), 3) time.Sleep(5 * time.Second) //entry should be expired now itemAgain, errAgain := client.GetState(ctx, stateStoreName, certificationTestPrefix+"ttl3", nil) From d08852c1733840f89a90ecbd2456ee69f4bafb9a Mon Sep 17 00:00:00 2001 From: Roberto Rojas Date: Wed, 28 Jun 2023 19:20:08 -0400 Subject: [PATCH 02/19] [GCP SecretStores SecretManager] Adds Component Metadata Schema (#2937) Signed-off-by: Roberto J Rojas Co-authored-by: Bernd Verst --- secretstores/gcp/secretmanager/metadata.yaml | 94 ++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 secretstores/gcp/secretmanager/metadata.yaml diff --git a/secretstores/gcp/secretmanager/metadata.yaml b/secretstores/gcp/secretmanager/metadata.yaml new file mode 100644 index 0000000000..3877d1c27b --- /dev/null +++ b/secretstores/gcp/secretmanager/metadata.yaml @@ -0,0 +1,94 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: secretstores +name: gcp.secretmanager +version: v1 +status: alpha +title: "GCP Secret Manager" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-secret-stores/gcp-secret-manager/ +authenticationProfiles: + - title: "GCP API call Authentication" + description: | + Authenticate authenticates API calls with the given service account or refresh token JSON credentials. + metadata: + - name: private_key_id + required: true + sensitive: true + description: | + GCP private key id. + example: '"privateKeyId"' + - name: private_key + required: true + sensitive: true + description: | + GCP credentials private key. Replace with x509 cert. + example: '"12345-12345"' +metadata: + - name: type + type: string + required: true + description: | + The GCP credentials type. + example: '"service_account"' + - name: project_id + type: string + required: true + description: | + GCP project id. + example: '"projectId"' + - name: client_email + type: string + required: true + description: | + GCP client email. + example: '"client@email.com"' + - name: client_id + type: string + required: false + description: | + GCP client id. + example: '"11111111"' + - name: auth_uri + type: string + required: false + description: | + Google account OAuth endpoint. + example: '"https://accounts.google.com/o/oauth2/auth"' + - name: token_uri + type: string + required: false + description: | + Google account token uri. + example: '"https://oauth2.googleapis.com/token"' + - name: auth_provider_x509_cert_url + type: string + required: false + description: | + GCP credentials cert url. + example: '"https://www.googleapis.com/oauth2/v1/certs"' + - name: client_x509_cert_url + type: string + required: false + description: | + GCP credentials project x509 cert url. + example: '"https://www.googleapis.com/robot/v1/metadata/x509/.iam.gserviceaccount.com"' + - name: decodeBase64 + type: bool + required: false + default: 'false' + description: | + Configuration to decode base64 file content before saving to bucket storage. + (In case of saving a file with binary content). true is the only allowed positive value. + Other positive variations like "True", "1" are not acceptable. Defaults to false. + example: '"true, false"' + - name: encodeBase64 + type: bool + required: false + default: 'false' + description: | + Configuration to encode base64 file content before return the content. + (In case of opening a file with binary content). true is the only allowed positive value. + Other positive variations like "True", "1" are not acceptable. Defaults to false. + example: '"true, false"' \ No newline at end of file From b242ef9ec6268eb445755f8152fceaa5fecec81e Mon Sep 17 00:00:00 2001 From: Alexandre Date: Fri, 30 Jun 2023 14:34:34 +0000 Subject: [PATCH 03/19] feat: close mongo db (#2954) Fixes #2949 Signed-off-by: Alexandre Signed-off-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> --- state/mongodb/mongodb.go | 9 +++++++++ state/mongodb/mongodb_test.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/state/mongodb/mongodb.go b/state/mongodb/mongodb.go index 5c3fe7b78c..26a8847f6d 100644 --- a/state/mongodb/mongodb.go +++ b/state/mongodb/mongodb.go @@ -681,3 +681,12 @@ func (m *MongoDB) GetComponentMetadata() map[string]string { metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) return metadataInfo } + +// Close connection to the database. +func (m *MongoDB) Close(ctx context.Context) (err error) { + if m.client == nil { + return nil + } + + return m.client.Disconnect(ctx) +} diff --git a/state/mongodb/mongodb_test.go b/state/mongodb/mongodb_test.go index 1bc32c9375..dec15e52ff 100644 --- a/state/mongodb/mongodb_test.go +++ b/state/mongodb/mongodb_test.go @@ -191,7 +191,7 @@ func TestGetMongoDBMetadata(t *testing.T) { assert.Equal(t, expected, err.Error()) }) - t.Run("Connectionstring ignores all other connection details", func(t *testing.T) { + t.Run("Connection string ignores all other connection details", func(t *testing.T) { properties := map[string]string{ host: "localhost:27017", databaseName: "TestDB", From 5adc33d0793bf960cf193d298d85c0343f480440 Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Fri, 30 Jun 2023 12:32:41 -0700 Subject: [PATCH 04/19] Azure Blob Storage: gracefully handle setting "endpoint" to non-emulator (#2959) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- .../component/azure/blobstorage/client.go | 44 ++++++-- .../azure/blobstorage/client_test.go | 100 +++++++++++++++++- 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/internal/component/azure/blobstorage/client.go b/internal/component/azure/blobstorage/client.go index 154be61d26..858cd2b7ff 100644 --- a/internal/component/azure/blobstorage/client.go +++ b/internal/component/azure/blobstorage/client.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "net/url" + "strings" "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -49,8 +50,10 @@ func CreateContainerStorageClient(parentCtx context.Context, log logger.Logger, return nil, nil, err } - if val, _ := mdutils.GetMetadataProperty(meta, azauth.MetadataKeys["StorageEndpoint"]...); val != "" { - m.customEndpoint = val + // Check if using a custom endpoint + err = m.setCustomEndpoint(log, meta, azEnvSettings) + if err != nil { + return nil, nil, err } // Get the container client @@ -74,21 +77,50 @@ func CreateContainerStorageClient(parentCtx context.Context, log logger.Logger, return client, m, nil } +// Sets the customEndpoint property if needed +func (opts *ContainerClientOpts) setCustomEndpoint(log logger.Logger, meta map[string]string, azEnvSettings azauth.EnvironmentSettings) error { + val, _ := mdutils.GetMetadataProperty(meta, azauth.MetadataKeys["StorageEndpoint"]...) + if val == "" { + return nil + } + + endpointURL, err := url.Parse(val) + if err != nil { + return fmt.Errorf("failed to parse custom endpoint %q: %w", val, err) + } + + // Check if the custom endpoint is set to an Azure Blob Storage public endpoint + azbURL := opts.getAzureBlobStorageContainerURL(azEnvSettings) + if endpointURL.Hostname() == azbURL.Hostname() && azbURL.Path == endpointURL.Path { + log.Warn("Metadata property endpoint is set to an Azure Blob Storage endpoint and will be ignored") + } else { + log.Info("Using custom endpoint for Azure Blob Storage") + opts.customEndpoint = strings.TrimSuffix(endpointURL.String(), "/") + } + + return nil +} + // GetContainerURL returns the URL of the container, needed by some auth methods. -func (opts ContainerClientOpts) GetContainerURL(azEnvSettings azauth.EnvironmentSettings) (u *url.URL, err error) { +func (opts *ContainerClientOpts) GetContainerURL(azEnvSettings azauth.EnvironmentSettings) (u *url.URL, err error) { if opts.customEndpoint != "" { u, err = url.Parse(fmt.Sprintf("%s/%s/%s", opts.customEndpoint, opts.AccountName, opts.ContainerName)) if err != nil { return nil, fmt.Errorf("failed to get container's URL with custom endpoint") } } else { - u, _ = url.Parse(fmt.Sprintf("https://%s.blob.%s/%s", opts.AccountName, azEnvSettings.EndpointSuffix(azauth.ServiceAzureStorage), opts.ContainerName)) + u = opts.getAzureBlobStorageContainerURL(azEnvSettings) } return u, nil } +func (opts *ContainerClientOpts) getAzureBlobStorageContainerURL(azEnvSettings azauth.EnvironmentSettings) *url.URL { + u, _ := url.Parse(fmt.Sprintf("https://%s.blob.%s/%s", opts.AccountName, azEnvSettings.EndpointSuffix(azauth.ServiceAzureStorage), opts.ContainerName)) + return u +} + // InitContainerClient returns a new container.Client object from the given options. -func (opts ContainerClientOpts) InitContainerClient(azEnvSettings azauth.EnvironmentSettings) (client *container.Client, err error) { +func (opts *ContainerClientOpts) InitContainerClient(azEnvSettings azauth.EnvironmentSettings) (client *container.Client, err error) { clientOpts := &container.ClientOptions{ ClientOptions: azcore.ClientOptions{ Retry: policy.RetryOptions{ @@ -149,7 +181,7 @@ func (opts ContainerClientOpts) InitContainerClient(azEnvSettings azauth.Environ // EnsureContainer creates the container if it doesn't already exist. // Property "accessLevel" indicates the public access level; nil-value means the container is private -func (opts ContainerClientOpts) EnsureContainer(ctx context.Context, client *container.Client, accessLevel *azblob.PublicAccessType) error { +func (opts *ContainerClientOpts) EnsureContainer(ctx context.Context, client *container.Client, accessLevel *azblob.PublicAccessType) error { // Create the container // This will return an error if it already exists _, err := client.Create(ctx, &container.CreateOptions{ diff --git a/internal/component/azure/blobstorage/client_test.go b/internal/component/azure/blobstorage/client_test.go index 85835fc4d5..0b6dc3bf0d 100644 --- a/internal/component/azure/blobstorage/client_test.go +++ b/internal/component/azure/blobstorage/client_test.go @@ -14,23 +14,25 @@ limitations under the License. package blobstorage import ( + "bytes" "context" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" azauth "github.com/dapr/components-contrib/internal/authentication/azure" "github.com/dapr/kit/logger" ) -type scenario struct { - metadata map[string]string - expectedFailureSubString string -} - func TestClientInitFailures(t *testing.T) { log := logger.NewLogger("test") + type scenario struct { + metadata map[string]string + expectedFailureSubString string + } + scenarios := map[string]scenario{ "missing accountName": { metadata: createTestMetadata(false, true, true), @@ -50,6 +52,94 @@ func TestClientInitFailures(t *testing.T) { } } +func TestSetCustomEndpoint(t *testing.T) { + logDest := &bytes.Buffer{} + log := logger.NewLogger("test") + log.SetOutput(logDest) + + t.Run("no custom endpoint", func(t *testing.T) { + meta := createTestMetadata(true, true, true) + m, err := parseMetadata(meta) + require.NoError(t, err) + + azEnvSettings, err := azauth.NewEnvironmentSettings(meta) + require.NoError(t, err) + + err = m.setCustomEndpoint(log, meta, azEnvSettings) + require.NoError(t, err) + + assert.Equal(t, "", m.customEndpoint) + + u, err := m.GetContainerURL(azEnvSettings) + require.NoError(t, err) + assert.Equal(t, "https://account.blob.core.windows.net/test", u.String()) + }) + + t.Run("custom endpoint set", func(t *testing.T) { + meta := createTestMetadata(true, true, true) + meta[azauth.MetadataKeys["StorageEndpoint"][0]] = "https://localhost:8080" + + m, err := parseMetadata(meta) + require.NoError(t, err) + + azEnvSettings, err := azauth.NewEnvironmentSettings(meta) + require.NoError(t, err) + + err = m.setCustomEndpoint(log, meta, azEnvSettings) + require.NoError(t, err) + + assert.Equal(t, "https://localhost:8080", m.customEndpoint) + + u, err := m.GetContainerURL(azEnvSettings) + require.NoError(t, err) + assert.Equal(t, "https://localhost:8080/account/test", u.String()) + }) + + t.Run("custom endpoint set with trailing slash removed", func(t *testing.T) { + meta := createTestMetadata(true, true, true) + meta[azauth.MetadataKeys["StorageEndpoint"][0]] = "https://localhost:8080/" + + m, err := parseMetadata(meta) + require.NoError(t, err) + + azEnvSettings, err := azauth.NewEnvironmentSettings(meta) + require.NoError(t, err) + + err = m.setCustomEndpoint(log, meta, azEnvSettings) + require.NoError(t, err) + + assert.Equal(t, "https://localhost:8080", m.customEndpoint) + + u, err := m.GetContainerURL(azEnvSettings) + require.NoError(t, err) + assert.Equal(t, "https://localhost:8080/account/test", u.String()) + }) + + t.Run("custom endpoint set to Azure Blob Storage endpoint", func(t *testing.T) { + logDest.Reset() + + meta := createTestMetadata(true, true, true) + meta[azauth.MetadataKeys["StorageEndpoint"][0]] = "https://account.blob.core.windows.net/test" + + m, err := parseMetadata(meta) + require.NoError(t, err) + + azEnvSettings, err := azauth.NewEnvironmentSettings(meta) + require.NoError(t, err) + + err = m.setCustomEndpoint(log, meta, azEnvSettings) + require.NoError(t, err) + + assert.Equal(t, "", m.customEndpoint) + + u, err := m.GetContainerURL(azEnvSettings) + require.NoError(t, err) + assert.Equal(t, "https://account.blob.core.windows.net/test", u.String()) + + assert.Contains(t, logDest.String(), "Metadata property endpoint is set to an Azure Blob Storage endpoint and will be ignored") + }) +} + func createTestMetadata(accountName bool, accountKey bool, container bool) map[string]string { m := map[string]string{} if accountName { From c62e7c9ad79db775e9d01c0562944f80ff74f37a Mon Sep 17 00:00:00 2001 From: Alexandre Date: Sat, 1 Jul 2023 19:00:19 +0000 Subject: [PATCH 05/19] feat: add close method for Cassandra (#2961) Signed-off-by: Alexandre Liberato Signed-off-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> --- state/cassandra/cassandra.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/state/cassandra/cassandra.go b/state/cassandra/cassandra.go index 51453e5e08..9a74a962a0 100644 --- a/state/cassandra/cassandra.go +++ b/state/cassandra/cassandra.go @@ -326,3 +326,14 @@ func (c *Cassandra) GetComponentMetadata() map[string]string { metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) return metadataInfo } + +// Close the connection to Cassandra. +func (c *Cassandra) Close() error { + if c.session == nil { + return nil + } + + c.session.Close() + + return nil +} From 58228efd0104cc1e8c2a63b9457f0e1806b38bae Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Thu, 6 Jul 2023 23:38:11 -0700 Subject: [PATCH 06/19] Add Azure AD support to Postgres state store component (#2970) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- .../azure/conf-test-azure-postgres.bicep | 68 +++++++++++++++ .../azure/conf-test-azure-sqlserver.bicep | 1 + .../azure/conf-test-azure-storage.bicep | 34 ++++---- .../conformance/azure/conf-test-azure.bicep | 29 ++++++- .../azure/setup-azure-conf-test.sh | 28 +++++- .github/scripts/test-info.mjs | 18 +++- internal/authentication/azure/services.go | 11 +++ internal/component/postgresql/metadata.go | 86 +++++++++++++++++-- .../component/postgresql/postgresdbaccess.go | 11 ++- internal/component/postgresql/postgresql.go | 7 +- state/postgresql/metadata.yaml | 31 ++++++- state/postgresql/postgresql.go | 5 +- .../state/postgresql/azure/statestore.yml | 12 +++ .../postgresql/{ => docker}/statestore.yml | 0 tests/config/state/tests.yml | 7 +- tests/conformance/common.go | 9 +- 16 files changed, 310 insertions(+), 47 deletions(-) create mode 100644 .github/infrastructure/conformance/azure/conf-test-azure-postgres.bicep create mode 100644 tests/config/state/postgresql/azure/statestore.yml rename tests/config/state/postgresql/{ => docker}/statestore.yml (100%) diff --git a/.github/infrastructure/conformance/azure/conf-test-azure-postgres.bicep b/.github/infrastructure/conformance/azure/conf-test-azure-postgres.bicep new file mode 100644 index 0000000000..9d92fdd952 --- /dev/null +++ b/.github/infrastructure/conformance/azure/conf-test-azure-postgres.bicep @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +param postgresServerName string +param sdkAuthSpId string +param sdkAuthSpName string +param rgLocation string = resourceGroup().location +param confTestTags object = {} +param postgresqlVersion string = '14' +param tenantId string = subscription().tenantId + +resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = { + name: postgresServerName + location: rgLocation + tags: confTestTags + sku: { + name: 'Standard_B1ms' + tier: 'Burstable' + } + properties: { + storage: { + storageSizeGB: 32 + autoGrow: 'Disabled' + } + authConfig: { + activeDirectoryAuth: 'Enabled' + passwordAuth: 'Disabled' + tenantId: tenantId + } + network: {} + version: postgresqlVersion + } + + resource daprTestDB 'databases@2023-03-01-preview' = { + name: 'dapr_test' + properties: { + charset: 'UTF8' + collation: 'en_US.utf8' + } + } + + resource fwRules 'firewallRules@2023-03-01-preview' = { + name: 'allowall' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } + } + + resource azureAdAdmin 'administrators@2023-03-01-preview' = { + name: sdkAuthSpId + properties: { + principalType: 'ServicePrincipal' + principalName: sdkAuthSpName + tenantId: tenantId + } + } +} diff --git a/.github/infrastructure/conformance/azure/conf-test-azure-sqlserver.bicep b/.github/infrastructure/conformance/azure/conf-test-azure-sqlserver.bicep index 1433bba525..f00798aac6 100644 --- a/.github/infrastructure/conformance/azure/conf-test-azure-sqlserver.bicep +++ b/.github/infrastructure/conformance/azure/conf-test-azure-sqlserver.bicep @@ -14,6 +14,7 @@ param sqlServerName string param rgLocation string = resourceGroup().location param confTestTags object = {} +@secure() param sqlServerAdminPassword string var sqlServerAdminName = '${sqlServerName}-admin' diff --git a/.github/infrastructure/conformance/azure/conf-test-azure-storage.bicep b/.github/infrastructure/conformance/azure/conf-test-azure-storage.bicep index 92fd9d10b9..762c71cee3 100644 --- a/.github/infrastructure/conformance/azure/conf-test-azure-storage.bicep +++ b/.github/infrastructure/conformance/azure/conf-test-azure-storage.bicep @@ -15,7 +15,7 @@ param storageName string param rgLocation string = resourceGroup().location param confTestTags object = {} -resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { +resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { name: storageName sku: { name: 'Standard_RAGRS' @@ -23,27 +23,23 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { kind: 'StorageV2' location: rgLocation tags: confTestTags -} -resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2021-02-01' = { - parent: storageAccount - name: 'default' - properties: { - deleteRetentionPolicy: { - enabled: true - days: 1 + resource blobServices 'blobServices@2022-09-01' = { + name: 'default' + properties: { + deleteRetentionPolicy: { + enabled: true + days: 1 + } } } -} -resource tableServices 'Microsoft.Storage/storageAccounts/tableServices@2021-09-01' = { - parent: storageAccount - name: 'default' - properties: {} -} + resource tableServices 'tableServices@2022-09-01' = { + name: 'default' + properties: {} -resource certificationTable 'Microsoft.Storage/storageAccounts/tableServices/tables@2021-09-01' = { - name: 'certificationTable' - parent: tableServices - properties: {} + resource certificationTable 'tables@2022-09-01' = { + name: 'certificationTable' + } + } } diff --git a/.github/infrastructure/conformance/azure/conf-test-azure.bicep b/.github/infrastructure/conformance/azure/conf-test-azure.bicep index 7c2e9a16cd..5ffe55a280 100644 --- a/.github/infrastructure/conformance/azure/conf-test-azure.bicep +++ b/.github/infrastructure/conformance/azure/conf-test-azure.bicep @@ -33,9 +33,12 @@ param adminId string @minLength(36) @maxLength(36) -@description('Provide the objectId of the Service Principal using secret auth with get access to secrets in Azure Key Vault.') +@description('Provide the objectId of the Service Principal using secret auth with get access to secrets in Azure Key Vault and access Azure PostgreSQL') param sdkAuthSpId string +@description('Provide the name of the Service Principal using secret auth with get access to secrets in Azure Key Vault and access Azure PostgreSQL') +param sdkAuthSpName string + @minLength(36) @maxLength(36) @description('Provide the objectId of the Service Principal using cert auth with get and list access to all assets in Azure Key Vault.') @@ -43,6 +46,7 @@ param certAuthSpId string @minLength(16) @description('Provide the SQL server admin password of at least 16 characters.') +@secure() param sqlServerAdminPassword string var confTestRgName = '${toLower(namePrefix)}-conf-test-rg' @@ -54,6 +58,7 @@ var iotHubName = '${toLower(namePrefix)}-conf-test-iothub' var keyVaultName = '${toLower(namePrefix)}-conf-test-kv' var serviceBusName = '${toLower(namePrefix)}-conf-test-servicebus' var sqlServerName = '${toLower(namePrefix)}-conf-test-sql' +var postgresServerName = '${toLower(namePrefix)}-conf-test-pg' var storageName = '${toLower(namePrefix)}ctstorage' resource confTestRg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -72,6 +77,7 @@ module cosmosDb 'conf-test-azure-cosmosdb.bicep' = { params: { confTestTags: confTestTags cosmosDbName: cosmosDbName + rgLocation: rgLocation } } @@ -81,6 +87,7 @@ module cosmosDbTable 'conf-test-azure-cosmosdb-table.bicep' = { params: { confTestTags: confTestTags cosmosDbTableAPIName: cosmosDbTableAPIName + rgLocation: rgLocation } } @@ -90,6 +97,7 @@ module eventGridTopic 'conf-test-azure-eventgrid.bicep' = { params: { confTestTags: confTestTags eventGridTopicName: eventGridTopicName + rgLocation: rgLocation } } @@ -99,6 +107,7 @@ module eventHubsNamespace 'conf-test-azure-eventhubs.bicep' = { params: { confTestTags: confTestTags eventHubsNamespaceName: eventHubsNamespaceName + rgLocation: rgLocation } } @@ -108,6 +117,7 @@ module iotHub 'conf-test-azure-iothub.bicep' = { params: { confTestTags: confTestTags iotHubName: iotHubName + rgLocation: rgLocation } } @@ -120,6 +130,7 @@ module keyVault 'conf-test-azure-keyvault.bicep' = { certAuthSpId: certAuthSpId keyVaultName: keyVaultName sdkAuthSpId: sdkAuthSpId + rgLocation: rgLocation } } @@ -129,6 +140,7 @@ module serviceBus 'conf-test-azure-servicebus.bicep' = { params: { confTestTags: confTestTags serviceBusName: serviceBusName + rgLocation: rgLocation } } @@ -139,6 +151,7 @@ module sqlServer 'conf-test-azure-sqlserver.bicep' = { confTestTags: confTestTags sqlServerName: sqlServerName sqlServerAdminPassword: sqlServerAdminPassword + rgLocation: rgLocation } } @@ -148,6 +161,19 @@ module storage 'conf-test-azure-storage.bicep' = { params: { confTestTags: confTestTags storageName: storageName + rgLocation: rgLocation + } +} + +module postgres 'conf-test-azure-postgres.bicep' = { + name: postgresServerName + scope: resourceGroup(confTestRg.name) + params: { + confTestTags: confTestTags + postgresServerName: postgresServerName + sdkAuthSpId: sdkAuthSpId + sdkAuthSpName: sdkAuthSpName + rgLocation: rgLocation } } @@ -176,4 +202,5 @@ output keyVaultName string = keyVault.name output serviceBusName string = serviceBus.name output sqlServerName string = sqlServer.name output sqlServerAdminName string = sqlServer.outputs.sqlServerAdminName +output postgresServerName string = postgres.name output storageName string = storage.name diff --git a/.github/infrastructure/conformance/azure/setup-azure-conf-test.sh b/.github/infrastructure/conformance/azure/setup-azure-conf-test.sh index 88b4285832..8220f57fcd 100755 --- a/.github/infrastructure/conformance/azure/setup-azure-conf-test.sh +++ b/.github/infrastructure/conformance/azure/setup-azure-conf-test.sh @@ -229,6 +229,8 @@ SQL_SERVER_NAME_VAR_NAME="AzureSqlServerName" SQL_SERVER_DB_NAME_VAR_NAME="AzureSqlServerDbName" SQL_SERVER_CONNECTION_STRING_VAR_NAME="AzureSqlServerConnectionString" +AZURE_DB_POSTGRES_CONNSTRING_VAR_NAME="AzureDBPostgresConnectionString" + STORAGE_ACCESS_KEY_VAR_NAME="AzureBlobStorageAccessKey" STORAGE_ACCOUNT_VAR_NAME="AzureBlobStorageAccount" STORAGE_CONTAINER_VAR_NAME="AzureBlobStorageContainer" @@ -269,7 +271,7 @@ if [[ -n ${CREDENTIALS_PATH} ]]; then fi SDK_AUTH_SP_NAME="$(az ad sp show --id "${SDK_AUTH_SP_APPID}" --query "appDisplayName" --output tsv)" SDK_AUTH_SP_ID="$(az ad sp show --id "${SDK_AUTH_SP_APPID}" --query "id" --output tsv)" - echo "Using Service Principal from ${CREDENTIALS_PATH} for SDK Auth: ${SDK_AUTH_SP_NAME}" + echo "Using Service Principal from ${CREDENTIALS_PATH} for SDK Auth: ${SDK_AUTH_SP_NAME} (ID: ${SDK_AUTH_SP_ID})" else SDK_AUTH_SP_NAME="${PREFIX}-conf-test-runner-sp" SDK_AUTH_SP_INFO="$(az ad sp create-for-rbac --name "${SDK_AUTH_SP_NAME}" --sdk-auth --years 1)" @@ -277,7 +279,7 @@ else SDK_AUTH_SP_CLIENT_SECRET="$(echo "${SDK_AUTH_SP_INFO}" | jq -r '.clientSecret')" SDK_AUTH_SP_ID="$(az ad sp list --display-name "${SDK_AUTH_SP_NAME}" --query "[].id" --output tsv)" echo "${SDK_AUTH_SP_INFO}" - echo "Created Service Principal for SDK Auth: ${SDK_AUTH_SP_NAME}" + echo "Created Service Principal for SDK Auth: ${SDK_AUTH_SP_NAME} (ID: ${SDK_AUTH_SP_ID})" AZURE_CREDENTIALS_FILENAME="${OUTPUT_PATH}/AZURE_CREDENTIALS" echo "${SDK_AUTH_SP_INFO}" > "${AZURE_CREDENTIALS_FILENAME}" fi @@ -292,7 +294,17 @@ echo "Building conf-test-azure.bicep to ${ARM_TEMPLATE_FILE} ..." az bicep build --file conf-test-azure.bicep --outfile "${ARM_TEMPLATE_FILE}" echo "Creating azure deployment ${DEPLOY_NAME} in ${DEPLOY_LOCATION} and resource prefix ${PREFIX}-* ..." -az deployment sub create --name "${DEPLOY_NAME}" --location "${DEPLOY_LOCATION}" --template-file "${ARM_TEMPLATE_FILE}" -p namePrefix="${PREFIX}" -p adminId="${ADMIN_ID}" -p certAuthSpId="${CERT_AUTH_SP_ID}" -p sdkAuthSpId="${SDK_AUTH_SP_ID}" -p rgLocation="${DEPLOY_LOCATION}" -p sqlServerAdminPassword="${SQL_SERVER_ADMIN_PASSWORD}" +az deployment sub create \ + --name "${DEPLOY_NAME}" \ + --location "${DEPLOY_LOCATION}" \ + --template-file "${ARM_TEMPLATE_FILE}" \ + -p namePrefix="${PREFIX}" \ + -p adminId="${ADMIN_ID}" \ + -p certAuthSpId="${CERT_AUTH_SP_ID}" \ + -p sdkAuthSpId="${SDK_AUTH_SP_ID}" \ + -p sdkAuthSpName="${SDK_AUTH_SP_NAME}" \ + -p rgLocation="${DEPLOY_LOCATION}" \ + -p sqlServerAdminPassword="${SQL_SERVER_ADMIN_PASSWORD}" echo "Sleeping for 5s to allow created ARM deployment info to propagate to query endpoints ..." sleep 5 @@ -546,6 +558,7 @@ az keyvault secret set --name "${KEYVAULT_SERVICE_PRINCIPAL_CLIENT_ID_VAR_NAME}" KEYVAULT_SERVICE_PRINCIPAL_CLIENT_SECRET=${AKV_SPAUTH_SP_CLIENT_SECRET} echo export ${KEYVAULT_SERVICE_PRINCIPAL_CLIENT_SECRET_VAR_NAME}=\"${KEYVAULT_SERVICE_PRINCIPAL_CLIENT_SECRET}\" >> "${ENV_CONFIG_FILENAME}" az keyvault secret set --name "${KEYVAULT_SERVICE_PRINCIPAL_CLIENT_SECRET_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${KEYVAULT_SERVICE_PRINCIPAL_CLIENT_SECRET}" + # ------------------------------------ # Populate Blob Storage test settings # ------------------------------------ @@ -671,6 +684,15 @@ SQL_SERVER_CONNECTION_STRING="Server=${SQL_SERVER_NAME}.database.windows.net;por echo export ${SQL_SERVER_CONNECTION_STRING_VAR_NAME}=\"${SQL_SERVER_CONNECTION_STRING}\" >> "${ENV_CONFIG_FILENAME}" az keyvault secret set --name "${SQL_SERVER_CONNECTION_STRING_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${SQL_SERVER_CONNECTION_STRING}" +# ---------------------------------- +# Populate Azure Database for PostgreSQL test settings +# ---------------------------------- +echo "Configuring Azure Database for PostgreSQL test settings ..." + +AZURE_DB_POSTGRES_CONNSTRING="host=${PREFIX}-conf-test-pg.postgres.database.azure.com user=${SDK_AUTH_SP_NAME} port=5432 connect_timeout=30 database=dapr_test" +echo export ${AZURE_DB_POSTGRES_CONNSTRING_VAR_NAME}=\"${AZURE_DB_POSTGRES_CONNSTRING}\" >> "${ENV_CONFIG_FILENAME}" +az keyvault secret set --name "${AZURE_DB_POSTGRES_CONNSTRING_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${AZURE_DB_POSTGRES_CONNSTRING}" + # ---------------------------------- # Populate Event Hubs test settings # ---------------------------------- diff --git a/.github/scripts/test-info.mjs b/.github/scripts/test-info.mjs index 959f5d3543..8bfeb0187f 100644 --- a/.github/scripts/test-info.mjs +++ b/.github/scripts/test-info.mjs @@ -582,8 +582,15 @@ const components = { conformanceSetup: 'docker-compose.sh oracledatabase', }, 'state.postgresql': { - conformance: true, certification: true, + sourcePkg: [ + 'state/postgresql', + 'internal/component/postgresql', + 'internal/component/sql', + ], + }, + 'state.postgresql.docker': { + conformance: true, conformanceSetup: 'docker-compose.sh postgresql', sourcePkg: [ 'state/postgresql', @@ -591,6 +598,15 @@ const components = { 'internal/component/sql', ], }, + 'state.postgresql.azure': { + conformance: true, + requiredSecrets: ['AzureDBPostgresConnectionString'], + sourcePkg: [ + 'state/postgresql', + 'internal/component/postgresql', + 'internal/component/sql', + ], + }, 'state.redis': { certification: true, sourcePkg: ['state/redis', 'internal/component/redis'], diff --git a/internal/authentication/azure/services.go b/internal/authentication/azure/services.go index 1b19bf6ad4..62966f04bc 100644 --- a/internal/authentication/azure/services.go +++ b/internal/authentication/azure/services.go @@ -20,6 +20,8 @@ import ( const ( // Service configuration for Azure SQL. Namespaced with dapr.io ServiceAzureSQL cloud.ServiceName = "dapr.io/azuresql" + // Service configuration for OSS RDBMS (Azure Database for PostgreSQL and MySQL). Namespaced with dapr.io + ServiceOSSRDBMS cloud.ServiceName = "dapr.io/oss-rdbms" ) func init() { @@ -33,4 +35,13 @@ func init() { cloud.AzurePublic.Services[ServiceAzureSQL] = cloud.ServiceConfiguration{ Audience: "https://database.windows.net", } + cloud.AzureChina.Services[ServiceOSSRDBMS] = cloud.ServiceConfiguration{ + Audience: "https://ossrdbms-aad.database.chinacloudapi.cn", + } + cloud.AzureGovernment.Services[ServiceOSSRDBMS] = cloud.ServiceConfiguration{ + Audience: "https://ossrdbms-aad.database.usgovcloudapi.net", + } + cloud.AzurePublic.Services[ServiceOSSRDBMS] = cloud.ServiceConfiguration{ + Audience: "https://ossrdbms-aad.database.windows.net", + } } diff --git a/internal/component/postgresql/metadata.go b/internal/component/postgresql/metadata.go index 8c4eb110e4..fb10bed6a0 100644 --- a/internal/component/postgresql/metadata.go +++ b/internal/component/postgresql/metadata.go @@ -14,9 +14,15 @@ limitations under the License. package postgresql import ( + "context" "fmt" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/dapr/components-contrib/internal/authentication/azure" "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/state" "github.com/dapr/kit/ptr" @@ -33,13 +39,19 @@ const ( ) type postgresMetadataStruct struct { - ConnectionString string - ConnectionMaxIdleTime time.Duration - TableName string // Could be in the format "schema.table" or just "table" - MetadataTableName string // Could be in the format "schema.table" or just "table" + ConnectionString string `mapstructure:"connectionString"` + ConnectionMaxIdleTime time.Duration `mapstructure:"connectionMaxIdleTime"` + TableName string `mapstructure:"tableName"` // Could be in the format "schema.table" or just "table" + MetadataTableName string `mapstructure:"metadataTableName"` // Could be in the format "schema.table" or just "table" + Timeout time.Duration `mapstructure:"timeoutInSeconds"` + CleanupInterval *time.Duration `mapstructure:"cleanupIntervalInSeconds"` + MaxConns int `mapstructure:"maxConns"` + UseAzureAD bool `mapstructure:"useAzureAD"` - Timeout time.Duration `mapstructure:"timeoutInSeconds"` - CleanupInterval *time.Duration `mapstructure:"cleanupIntervalInSeconds"` + // Set to true if the component can support authentication with Azure AD. + // This is different from the "useAzureAD" property above, which is provided by the user and instructs the component to authenticate using Azure AD. + azureADEnabled bool + azureEnv azure.EnvironmentSettings } func (m *postgresMetadataStruct) InitWithMetadata(meta state.Metadata) error { @@ -79,5 +91,67 @@ func (m *postgresMetadataStruct) InitWithMetadata(meta state.Metadata) error { } } + // Populate the Azure environment if using Azure AD + if m.azureADEnabled && m.UseAzureAD { + m.azureEnv, err = azure.NewEnvironmentSettings(meta.Properties) + if err != nil { + return err + } + } + return nil } + +// GetPgxPoolConfig returns the pgxpool.Config object that contains the credentials for connecting to Postgres. +func (m *postgresMetadataStruct) GetPgxPoolConfig() (*pgxpool.Config, error) { + // Get the config from the connection string + config, err := pgxpool.ParseConfig(m.ConnectionString) + if err != nil { + return nil, fmt.Errorf("failed to parse connection string: %w", err) + } + if m.ConnectionMaxIdleTime > 0 { + config.MaxConnIdleTime = m.ConnectionMaxIdleTime + } + if m.MaxConns > 1 { + config.MaxConns = int32(m.MaxConns) + } + + // Check if we should use Azure AD + if m.azureADEnabled && m.UseAzureAD { + tokenCred, errToken := m.azureEnv.GetTokenCredential() + if errToken != nil { + return nil, errToken + } + + // Reset the password + config.ConnConfig.Password = "" + + /*// For Azure AD, using SSL is required + // If not already enabled, configure TLS without certificate validation + if config.ConnConfig.TLSConfig == nil { + config.ConnConfig.TLSConfig = &tls.Config{ + //nolint:gosec + InsecureSkipVerify: true, + } + }*/ + + // We need to retrieve the token every time we attempt a new connection + // This is because tokens expire, and connections can drop and need to be re-established at any time + // Fortunately, we can do this with the "BeforeConnect" hook + config.BeforeConnect = func(ctx context.Context, cc *pgx.ConnConfig) error { + at, err := tokenCred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{ + m.azureEnv.Cloud.Services[azure.ServiceOSSRDBMS].Audience + "/.default", + }, + }) + if err != nil { + return err + } + + cc.Password = at.Token + return nil + } + } + + return config, nil +} diff --git a/internal/component/postgresql/postgresdbaccess.go b/internal/component/postgresql/postgresdbaccess.go index edf4e9b48b..5a5ec16e1b 100644 --- a/internal/component/postgresql/postgresdbaccess.go +++ b/internal/component/postgresql/postgresdbaccess.go @@ -67,7 +67,10 @@ func newPostgresDBAccess(logger logger.Logger, opts Options) *PostgresDBAccess { logger.Debug("Instantiating new Postgres state store") return &PostgresDBAccess{ - logger: logger, + logger: logger, + metadata: postgresMetadataStruct{ + azureADEnabled: opts.EnableAzureAD, + }, migrateFn: opts.MigrateFn, setQueryFn: opts.SetQueryFn, etagColumn: opts.ETagColumn, @@ -84,15 +87,11 @@ func (p *PostgresDBAccess) Init(ctx context.Context, meta state.Metadata) error return err } - config, err := pgxpool.ParseConfig(p.metadata.ConnectionString) + config, err := p.metadata.GetPgxPoolConfig() if err != nil { - err = fmt.Errorf("failed to parse connection string: %w", err) p.logger.Error(err) return err } - if p.metadata.ConnectionMaxIdleTime > 0 { - config.MaxConnIdleTime = p.metadata.ConnectionMaxIdleTime - } connCtx, connCancel := context.WithTimeout(ctx, p.metadata.Timeout) p.db, err = pgxpool.NewWithConfig(connCtx, config) diff --git a/internal/component/postgresql/postgresql.go b/internal/component/postgresql/postgresql.go index d5fc0f401d..f44d8abdfa 100644 --- a/internal/component/postgresql/postgresql.go +++ b/internal/component/postgresql/postgresql.go @@ -31,9 +31,10 @@ type PostgreSQL struct { } type Options struct { - MigrateFn func(context.Context, PGXPoolConn, MigrateOptions) error - SetQueryFn func(*state.SetRequest, SetQueryOptions) string - ETagColumn string + MigrateFn func(context.Context, PGXPoolConn, MigrateOptions) error + SetQueryFn func(*state.SetRequest, SetQueryOptions) string + ETagColumn string + EnableAzureAD bool } type MigrateOptions struct { diff --git a/state/postgresql/metadata.yaml b/state/postgresql/metadata.yaml index 6169696cc9..2d88cf6b6b 100644 --- a/state/postgresql/metadata.yaml +++ b/state/postgresql/metadata.yaml @@ -16,14 +16,35 @@ capabilities: - etag - query - ttl +builtinAuthenticationProfiles: + - name: "azuread" + metadata: + - name: useAzureAD + required: true + type: bool + example: '"true"' + description: | + Must be set to `true` to enable the component to retrieve access tokens from Azure AD. + This authentication method only works with Azure Database for PostgreSQL databases. + - name: connectionString + required: true + sensitive: true + description: | + The connection string for the PostgreSQL database + This must contain the user, which corresponds to the name of the user created inside PostgreSQL that maps to the Azure AD identity; this is often the name of the corresponding principal (e.g. the name of the Azure AD application). This connection string should not contain any password. + example: | + "host=mydb.postgres.database.azure.com user=myapplication port=5432 database=dapr_test sslmode=require" + type: string authenticationProfiles: - title: "Connection string" description: "Authenticate using a Connection String." metadata: - name: connectionString required: true + sensitive: true description: The connection string for the PostgreSQL database - example: "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" + example: | + "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" type: string metadata: - name: timeoutInSeconds @@ -56,6 +77,14 @@ metadata: example: "1800" default: "3600" # 1h type: number + - name: maxConns + required: false + description: | + Maximum number of connections pooled by this component. + Set to 0 or lower to use the default value, which is the greater of 4 or the number of CPUs. + example: "4" + default: "0" + type: number - name: connectionMaxIdleTime required: false description: | diff --git a/state/postgresql/postgresql.go b/state/postgresql/postgresql.go index 638999c017..84563242ff 100644 --- a/state/postgresql/postgresql.go +++ b/state/postgresql/postgresql.go @@ -22,8 +22,9 @@ import ( // NewPostgreSQLStateStore creates a new instance of PostgreSQL state store. func NewPostgreSQLStateStore(logger logger.Logger) state.Store { return postgresql.NewPostgreSQLStateStore(logger, postgresql.Options{ - ETagColumn: "xmin", - MigrateFn: performMigration, + ETagColumn: "xmin", + EnableAzureAD: true, + MigrateFn: performMigration, SetQueryFn: func(req *state.SetRequest, opts postgresql.SetQueryOptions) string { // Sprintf is required for table name because the driver does not substitute parameters for table names. if !req.HasETag() { diff --git a/tests/config/state/postgresql/azure/statestore.yml b/tests/config/state/postgresql/azure/statestore.yml new file mode 100644 index 0000000000..1788537e8e --- /dev/null +++ b/tests/config/state/postgresql/azure/statestore.yml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.postgresql + version: v1 + metadata: + - name: connectionString + value: "${{AzureDBPostgresConnectionString}}" + - name: useAzureAD + value: "true" \ No newline at end of file diff --git a/tests/config/state/postgresql/statestore.yml b/tests/config/state/postgresql/docker/statestore.yml similarity index 100% rename from tests/config/state/postgresql/statestore.yml rename to tests/config/state/postgresql/docker/statestore.yml diff --git a/tests/config/state/tests.yml b/tests/config/state/tests.yml index 8c7c6bb615..199a540c88 100644 --- a/tests/config/state/tests.yml +++ b/tests/config/state/tests.yml @@ -32,7 +32,12 @@ components: config: # This component requires etags to be hex-encoded numbers badEtag: "FFFF" - - component: postgresql + - component: postgresql.docker + operations: [ "transaction", "etag", "first-write", "query", "ttl" ] + config: + # This component requires etags to be numeric + badEtag: "1" + - component: postgresql.azure operations: [ "transaction", "etag", "first-write", "query", "ttl" ] config: # This component requires etags to be numeric diff --git a/tests/conformance/common.go b/tests/conformance/common.go index 7c55ce600a..7ba46cd7e5 100644 --- a/tests/conformance/common.go +++ b/tests/conformance/common.go @@ -119,7 +119,6 @@ const ( eventhubs = "azure.eventhubs" redisv6 = "redis.v6" redisv7 = "redis.v7" - postgres = "postgres" kafka = "kafka" generateUUID = "$((uuid))" generateEd25519PrivateKey = "$((ed25519PrivateKey))" @@ -438,7 +437,7 @@ func loadConfigurationStore(tc TestComponent) (configuration.Store, configupdate case redisv7: store = c_redis.NewRedisConfigurationStore(testLogger) updater = cu_redis.NewRedisConfigUpdater(testLogger) - case postgres: + case "postgres": store = c_postgres.NewPostgresConfigurationStore(testLogger) updater = cu_postgres.NewPostgresConfigUpdater(testLogger) default: @@ -543,10 +542,12 @@ func loadStateStore(tc TestComponent) state.Store { case "mongodb": store = s_mongodb.NewMongoDB(testLogger) case "azure.sql": - fallthrough + store = s_sqlserver.New(testLogger) case "sqlserver": store = s_sqlserver.New(testLogger) - case "postgresql": + case "postgresql.docker": + store = s_postgresql.NewPostgreSQLStateStore(testLogger) + case "postgresql.azure": store = s_postgresql.NewPostgreSQLStateStore(testLogger) case "sqlite": store = s_sqlite.NewSQLiteStateStore(testLogger) From 3bfa20bda1977b120dc46282e6b8e7a332432a07 Mon Sep 17 00:00:00 2001 From: Jack Robertson Date: Fri, 7 Jul 2023 22:24:25 +0100 Subject: [PATCH 07/19] rabbitmq pub/sub: add queue name to subscription request metadata (#2962) Signed-off-by: Jack Robertson Signed-off-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> --- pubsub/rabbitmq/metadata.go | 1 + pubsub/rabbitmq/rabbitmq.go | 9 +- pubsub/rabbitmq/rabbitmq_test.go | 137 ++++++++++++++++++++----------- 3 files changed, 96 insertions(+), 51 deletions(-) diff --git a/pubsub/rabbitmq/metadata.go b/pubsub/rabbitmq/metadata.go index f743b64e30..0cbad4584a 100644 --- a/pubsub/rabbitmq/metadata.go +++ b/pubsub/rabbitmq/metadata.go @@ -77,6 +77,7 @@ const ( metadataPublisherConfirmKey = "publisherConfirm" metadataSaslExternal = "saslExternal" metadataMaxPriority = "maxPriority" + metadataQueueNameKey = "queueName" defaultReconnectWaitSeconds = 3 diff --git a/pubsub/rabbitmq/rabbitmq.go b/pubsub/rabbitmq/rabbitmq.go index 4f60f992a2..249d5d8b70 100644 --- a/pubsub/rabbitmq/rabbitmq.go +++ b/pubsub/rabbitmq/rabbitmq.go @@ -308,11 +308,14 @@ func (r *rabbitMQ) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, h return errors.New("component is closed") } - if r.metadata.ConsumerID == "" { - return errors.New("consumerID is required for subscriptions") + queueName := req.Metadata[metadataQueueNameKey] + if queueName == "" { + if r.metadata.ConsumerID == "" { + return errors.New("consumerID is required for subscriptions that don't specify a queue name") + } + queueName = fmt.Sprintf("%s-%s", r.metadata.ConsumerID, req.Topic) } - queueName := fmt.Sprintf("%s-%s", r.metadata.ConsumerID, req.Topic) r.logger.Infof("%s subscribe to topic/queue '%s/%s'", logMessagePrefix, req.Topic, queueName) // Do not set a timeout on the context, as we're just waiting for the first ack; we're using a semaphore instead diff --git a/pubsub/rabbitmq/rabbitmq_test.go b/pubsub/rabbitmq/rabbitmq_test.go index 864b8b5b37..f9248df87c 100644 --- a/pubsub/rabbitmq/rabbitmq_test.go +++ b/pubsub/rabbitmq/rabbitmq_test.go @@ -36,7 +36,7 @@ func newBroker() *rabbitMQInMemoryBroker { } } -func newRabbitMQTest(broker *rabbitMQInMemoryBroker) pubsub.PubSub { +func newRabbitMQTest(broker *rabbitMQInMemoryBroker) *rabbitMQ { return &rabbitMQ{ declaredExchanges: make(map[string]bool), logger: logger.NewLogger("test"), @@ -48,7 +48,7 @@ func newRabbitMQTest(broker *rabbitMQInMemoryBroker) pubsub.PubSub { } } -func TestNoConsumer(t *testing.T) { +func TestNoConsumerOrQueueName(t *testing.T) { broker := newBroker() pubsubRabbitMQ := newRabbitMQTest(broker) metadata := pubsub.Metadata{Base: mdata.Base{ @@ -59,7 +59,7 @@ func TestNoConsumer(t *testing.T) { err := pubsubRabbitMQ.Init(context.Background(), metadata) assert.NoError(t, err) err = pubsubRabbitMQ.Subscribe(context.Background(), pubsub.SubscribeRequest{}, nil) - assert.Contains(t, err.Error(), "consumerID is required for subscriptions") + assert.Contains(t, err.Error(), "consumerID is required for subscriptions that don't specify a queue name") } func TestPublishAndSubscribeWithPriorityQueue(t *testing.T) { @@ -118,7 +118,7 @@ func TestConcurrencyMode(t *testing.T) { }} err := pubsubRabbitMQ.Init(context.Background(), metadata) assert.Nil(t, err) - assert.Equal(t, pubsub.Parallel, pubsubRabbitMQ.(*rabbitMQ).metadata.Concurrency) + assert.Equal(t, pubsub.Parallel, pubsubRabbitMQ.metadata.Concurrency) }) t.Run("single", func(t *testing.T) { @@ -133,7 +133,7 @@ func TestConcurrencyMode(t *testing.T) { }} err := pubsubRabbitMQ.Init(context.Background(), metadata) assert.Nil(t, err) - assert.Equal(t, pubsub.Single, pubsubRabbitMQ.(*rabbitMQ).metadata.Concurrency) + assert.Equal(t, pubsub.Single, pubsubRabbitMQ.metadata.Concurrency) }) t.Run("default", func(t *testing.T) { @@ -147,51 +147,91 @@ func TestConcurrencyMode(t *testing.T) { }} err := pubsubRabbitMQ.Init(context.Background(), metadata) assert.Nil(t, err) - assert.Equal(t, pubsub.Parallel, pubsubRabbitMQ.(*rabbitMQ).metadata.Concurrency) + assert.Equal(t, pubsub.Parallel, pubsubRabbitMQ.metadata.Concurrency) }) } func TestPublishAndSubscribe(t *testing.T) { - broker := newBroker() - pubsubRabbitMQ := newRabbitMQTest(broker) - metadata := pubsub.Metadata{Base: mdata.Base{ - Properties: map[string]string{ - metadataHostnameKey: "anyhost", - metadataConsumerIDKey: "consumer", + tests := []struct { + name string + componentMetadata map[string]string + subscribeMetadata map[string]string + topic string + declaredQueues []string + }{ + { + name: "only consumer id", + componentMetadata: map[string]string{ + metadataHostnameKey: "anyhost", + metadataConsumerIDKey: "consumer", + }, + topic: "mytopic", + declaredQueues: []string{"consumer-mytopic"}, + }, + { + name: "only queue name", + componentMetadata: map[string]string{ + metadataHostnameKey: "anyhost", + }, + subscribeMetadata: map[string]string{ + metadataQueueNameKey: "myqueue", + }, + topic: "mytopic", + declaredQueues: []string{"myqueue"}, + }, + { + name: "queue name takes precedence over consumer id", + componentMetadata: map[string]string{ + metadataHostnameKey: "anyhost", + metadataConsumerIDKey: "consumer", + }, + subscribeMetadata: map[string]string{ + metadataQueueNameKey: "myqueue", + }, + topic: "mytopic", + declaredQueues: []string{"myqueue"}, }, - }} - err := pubsubRabbitMQ.Init(context.Background(), metadata) - assert.Nil(t, err) - assert.Equal(t, int32(1), broker.connectCount.Load()) - assert.Equal(t, int32(0), broker.closeCount.Load()) - - topic := "mytopic" - - messageCount := 0 - lastMessage := "" - processed := make(chan bool) - handler := func(ctx context.Context, msg *pubsub.NewMessage) error { - messageCount++ - lastMessage = string(msg.Data) - processed <- true - - return nil } - - err = pubsubRabbitMQ.Subscribe(context.Background(), pubsub.SubscribeRequest{Topic: topic}, handler) - assert.Nil(t, err) - - err = pubsubRabbitMQ.Publish(context.Background(), &pubsub.PublishRequest{Topic: topic, Data: []byte("hello world")}) - assert.Nil(t, err) - <-processed - assert.Equal(t, 1, messageCount) - assert.Equal(t, "hello world", lastMessage) - - err = pubsubRabbitMQ.Publish(context.Background(), &pubsub.PublishRequest{Topic: topic, Data: []byte("foo bar")}) - assert.Nil(t, err) - <-processed - assert.Equal(t, 2, messageCount) - assert.Equal(t, "foo bar", lastMessage) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + broker := newBroker() + pubsubRabbitMQ := newRabbitMQTest(broker) + metadata := pubsub.Metadata{Base: mdata.Base{ + Properties: test.componentMetadata, + }} + err := pubsubRabbitMQ.Init(context.Background(), metadata) + assert.Nil(t, err) + assert.Equal(t, int32(1), broker.connectCount.Load()) + assert.Equal(t, int32(0), broker.closeCount.Load()) + + messageCount := 0 + lastMessage := "" + processed := make(chan bool) + handler := func(ctx context.Context, msg *pubsub.NewMessage) error { + messageCount++ + lastMessage = string(msg.Data) + processed <- true + return nil + } + + err = pubsubRabbitMQ.Subscribe(context.Background(), pubsub.SubscribeRequest{Topic: test.topic, Metadata: test.subscribeMetadata}, handler) + assert.NoError(t, err) + assert.True(t, pubsubRabbitMQ.declaredExchanges[test.topic]) + assert.ElementsMatch(t, test.declaredQueues, broker.declaredQueues) + + err = pubsubRabbitMQ.Publish(context.Background(), &pubsub.PublishRequest{Topic: test.topic, Data: []byte("hello world")}) + assert.NoError(t, err) + <-processed + assert.Equal(t, 1, messageCount) + assert.Equal(t, "hello world", lastMessage) + + err = pubsubRabbitMQ.Publish(context.Background(), &pubsub.PublishRequest{Topic: test.topic, Data: []byte("foo bar")}) + assert.NoError(t, err) + <-processed + assert.Equal(t, 2, messageCount) + assert.Equal(t, "foo bar", lastMessage) + }) + } } func TestPublishReconnect(t *testing.T) { @@ -385,10 +425,10 @@ func createAMQPMessage(body []byte) amqp.Delivery { } type rabbitMQInMemoryBroker struct { - buffer chan amqp.Delivery - - connectCount atomic.Int32 - closeCount atomic.Int32 + buffer chan amqp.Delivery + declaredQueues []string + connectCount atomic.Int32 + closeCount atomic.Int32 } func (r *rabbitMQInMemoryBroker) Qos(prefetchCount, prefetchSize int, global bool) error { @@ -412,6 +452,7 @@ func (r *rabbitMQInMemoryBroker) PublishWithDeferredConfirmWithContext(ctx conte } func (r *rabbitMQInMemoryBroker) QueueDeclare(name string, durable bool, autoDelete bool, exclusive bool, noWait bool, args amqp.Table) (amqp.Queue, error) { + r.declaredQueues = append(r.declaredQueues, name) return amqp.Queue{Name: name}, nil } From 6d6cb2af0a3a48b256c87a4f8412be3f4d38a442 Mon Sep 17 00:00:00 2001 From: Phillip LeBlanc Date: Mon, 10 Jul 2023 15:16:57 +0900 Subject: [PATCH 08/19] Update Cloudflare Queues docs link (#2974) Signed-off-by: Phillip LeBlanc --- bindings/cloudflare/queues/cfqueues.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/cloudflare/queues/cfqueues.go b/bindings/cloudflare/queues/cfqueues.go index ad32503bf9..1b3d04a106 100644 --- a/bindings/cloudflare/queues/cfqueues.go +++ b/bindings/cloudflare/queues/cfqueues.go @@ -31,7 +31,7 @@ import ( ) // Link to the documentation for the component -const componentDocsURL = "https://docs.dapr.io/reference/components-reference/supported-bindings/cfqueues/" +const componentDocsURL = "https://docs.dapr.io/reference/components-reference/supported-bindings/cloudflare-queues/" // CFQueues is a binding for publishing messages on Cloudflare Queues type CFQueues struct { From cef854fab68b3d324ce1b934e1757bc846eb913f Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 10 Jul 2023 18:53:58 -0700 Subject: [PATCH 09/19] Some linting in metadata.yaml files (#2977) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- bindings/azure/blobstorage/metadata.yaml | 8 +++--- bindings/azure/cosmosdb/cosmosdb.go | 2 -- bindings/azure/cosmosdb/metadata.yaml | 2 +- .../azure/cosmosdbgremlinapi/metadata.yaml | 2 +- bindings/azure/eventgrid/metadata.yaml | 2 +- bindings/azure/eventhubs/metadata.yaml | 27 ++++++++++++------- bindings/http/metadata.yaml | 18 ++++++------- bindings/postgres/metadata.yaml | 4 +-- bindings/redis/metadata.yaml | 10 +++---- configuration/azure/appconfig/metadata.yaml | 12 ++++----- pubsub/azure/eventhubs/metadata.yaml | 8 ++++-- pubsub/azure/servicebus/queues/metadata.yaml | 3 ++- pubsub/azure/servicebus/topics/metadata.yaml | 3 ++- state/azure/blobstorage/metadata.yaml | 10 +++---- state/sqlserver/metadata.yaml | 6 +++-- 15 files changed, 65 insertions(+), 52 deletions(-) diff --git a/bindings/azure/blobstorage/metadata.yaml b/bindings/azure/blobstorage/metadata.yaml index 500c273b7b..047d4fa0bb 100644 --- a/bindings/azure/blobstorage/metadata.yaml +++ b/bindings/azure/blobstorage/metadata.yaml @@ -12,13 +12,13 @@ binding: output: true operations: - name: create - description: "Create blob." + description: "Create blob" - name: get - description: "Get blob." + description: "Get blob" - name: delete - description: "Delete blob." + description: "Delete blob" - name: list - description: "List blob." + description: "List blob" capabilities: [] builtinAuthenticationProfiles: - name: "azuread" diff --git a/bindings/azure/cosmosdb/cosmosdb.go b/bindings/azure/cosmosdb/cosmosdb.go index c0c39c2857..785514f74f 100644 --- a/bindings/azure/cosmosdb/cosmosdb.go +++ b/bindings/azure/cosmosdb/cosmosdb.go @@ -174,8 +174,6 @@ func (c *CosmosDB) lookup(m map[string]interface{}, ks []string) (val interface{ return nil, fmt.Errorf("needs at least one key") } - c.logger.Infof("%s, %s", ks[0], m[ks[0]]) - if val, ok = m[ks[0]]; !ok { return nil, fmt.Errorf("key not found %v", ks[0]) } diff --git a/bindings/azure/cosmosdb/metadata.yaml b/bindings/azure/cosmosdb/metadata.yaml index 4ec191dc3f..ce33d40b60 100644 --- a/bindings/azure/cosmosdb/metadata.yaml +++ b/bindings/azure/cosmosdb/metadata.yaml @@ -12,7 +12,7 @@ binding: output: true operations: - name: create - description: "Create an item." + description: "Create an item" capabilities: [] builtinAuthenticationProfiles: - name: "azuread" diff --git a/bindings/azure/cosmosdbgremlinapi/metadata.yaml b/bindings/azure/cosmosdbgremlinapi/metadata.yaml index 3c2988f05f..609bd1aea9 100644 --- a/bindings/azure/cosmosdbgremlinapi/metadata.yaml +++ b/bindings/azure/cosmosdbgremlinapi/metadata.yaml @@ -12,7 +12,7 @@ binding: output: true operations: - name: query - description: "Perform a query." + description: "Perform a query" capabilities: [] authenticationProfiles: - title: "Master key" diff --git a/bindings/azure/eventgrid/metadata.yaml b/bindings/azure/eventgrid/metadata.yaml index bd2ee8b6dc..7c99fc44fa 100644 --- a/bindings/azure/eventgrid/metadata.yaml +++ b/bindings/azure/eventgrid/metadata.yaml @@ -13,7 +13,7 @@ binding: output: true operations: - name: create - description: "Create an event subscription." + description: "Create an event subscription" capabilities: [] builtinAuthenticationProfiles: - name: "azuread" diff --git a/bindings/azure/eventhubs/metadata.yaml b/bindings/azure/eventhubs/metadata.yaml index 40a7b83c48..77c221cfa2 100644 --- a/bindings/azure/eventhubs/metadata.yaml +++ b/bindings/azure/eventhubs/metadata.yaml @@ -13,7 +13,7 @@ binding: output: true operations: - name: create - description: "Create an event subscription." + description: "Create an event subscription" capabilities: [] authenticationProfiles: - title: "Connection string" @@ -24,12 +24,15 @@ authenticationProfiles: sensitive: true description: | Connection string for the Event Hub or the Event Hub namespace. - example: '"Endpoint=sb://{EventHubNamespace}.servicebus.windows.net/;SharedAccessKeyName={PolicyName};SharedAccessKey={Key};EntityPath={EventHub}"' + example: | + "Endpoint=sb://{EventHubNamespace}.servicebus.windows.net/;SharedAccessKeyName={PolicyName};SharedAccessKey={Key};EntityPath={EventHub}" - name: eventHub type: string description: | - The name of the Event Hubs hub (“topic”). Required if the connection string doesn’t contain an EntityPath value. + The name of the Event Hubs hub ("topic"). Required if the connection string doesn't contain an EntityPath value. required: false # Optional when a connectionString is provided + example: | + mytopic builtinAuthenticationProfiles: - name: "azuread" metadata: @@ -50,6 +53,7 @@ builtinAuthenticationProfiles: type: bool required: false default: "false" + example: "false" description: | Allow management of the Event Hub namespace and storage account. - name: resourceGroupName @@ -62,7 +66,7 @@ builtinAuthenticationProfiles: - name: subscriptionId type: string required: false - bindings: + binding: input: true output: false description: | @@ -92,12 +96,13 @@ metadata: description: | DEPRECATED. deprecated: true + example: "" # Input-only metadata # consumerGroup is an alias for consumerId, if both are defined consumerId takes precedence. - name: consumerId type: string required: true # consumerGroup is an alias for this field, let's promote this to default - bindings: + binding: input: true output: false description: | @@ -106,7 +111,7 @@ metadata: - name: consumerGroup type: string required: false - bindings: + binding: input: true output: false description: | @@ -117,7 +122,7 @@ metadata: - name: storageAccountKey type: string required: false - bindings: + binding: input: true output: false description: | @@ -129,17 +134,19 @@ metadata: - name: storageConnectionString type: string required: false - bindings: + binding: input: true output: false description: | Connection string for the checkpoint store, alternative to specifying storageAccountKey. Property "storageAccountKey" is ignored when "storageConnectionString" is present + example: | + "BlobEndpoint=https://storagesample.blob.core.windows.net;..." - name: storageAccountName type: string required: true - bindings: + binding: input: true output: false description: | @@ -148,7 +155,7 @@ metadata: - name: storageContainerName type: string required: true - bindings: + binding: input: true output: false description: | diff --git a/bindings/http/metadata.yaml b/bindings/http/metadata.yaml index de0ac80f4b..23309ea0a7 100644 --- a/bindings/http/metadata.yaml +++ b/bindings/http/metadata.yaml @@ -13,23 +13,23 @@ binding: input: false operations: - name: create - description: "Alias for \"post\", for backwards-compatibility." + description: "Alias for \"post\", for backwards-compatibility" - name: get - description: "Read data/records." + description: "Read data/records" - name: head - description: "Identical to get except that the server does not return a response body." + description: "Identical to get except that the server does not return a response body" - name: post - description: "Typically used to create records or send commands." + description: "Typically used to create records or send commands" - name: put - description: "Update data/records." + description: "Update data/records" - name: patch - description: "Sometimes used to update a subset of fields of a record." + description: "Sometimes used to update a subset of fields of a record" - name: delete - description: "Delete a data/record." + description: "Delete a data/record" - name: options - description: "Requests for information about the communication options available (not commonly used)." + description: "Requests for information about the communication options available (not commonly used)" - name: trace - description: "Used to invoke a remote, application-layer loop-back of the request message (not commonly used)." + description: "Used to invoke a remote, application-layer loop-back of the request message (not commonly used)" capabilities: [] metadata: - name: url diff --git a/bindings/postgres/metadata.yaml b/bindings/postgres/metadata.yaml index 321744d039..1d771523a3 100644 --- a/bindings/postgres/metadata.yaml +++ b/bindings/postgres/metadata.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=../../../component-metadata-schema.json +# yaml-language-server: $schema=../../component-metadata-schema.json schemaVersion: v1 type: bindings name: postgres @@ -26,7 +26,7 @@ authenticationProfiles: - name: url required: true sensitive: true - bindings: + binding: input: false output: true description: "Connection string for PostgreSQL." diff --git a/bindings/redis/metadata.yaml b/bindings/redis/metadata.yaml index fd20b1191c..d26b0e695a 100644 --- a/bindings/redis/metadata.yaml +++ b/bindings/redis/metadata.yaml @@ -14,16 +14,16 @@ binding: input: false operations: - name: create - description: "Create item." + description: "Create item" - name: get - description: "Get item." + description: "Get item" - name: delete - description: "Delete item." + description: "Delete item" - name: increment description: "Increment a key" authenticationProfiles: - title: "Username and password" - description: "Authenticate using username and password." + description: "Authenticate using username and password" metadata: - name: redisUsername type: string @@ -40,7 +40,7 @@ authenticationProfiles: description: | Password for Redis host. Use secretKeyRef for secret reference - example: "KeFg23!" + example: "KeFg23!" default: "" metadata: - name: redisHost diff --git a/configuration/azure/appconfig/metadata.yaml b/configuration/azure/appconfig/metadata.yaml index dc2e619e70..040b72fb5d 100644 --- a/configuration/azure/appconfig/metadata.yaml +++ b/configuration/azure/appconfig/metadata.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=../../component-metadata-schema.json +# yaml-language-server: $schema=../../../component-metadata-schema.json schemaVersion: v1 type: configuration name: azure.appconfig @@ -17,7 +17,7 @@ authenticationProfiles: required: true sensitive: true description: "The Azure App Configuration connection string." - example: 'Endpoint=https://foo.azconfig.io;Id=osOX-l9-s0:sig;Secret=00000000000000000000000000000000000000000000"' + example: 'Endpoint=https://foo.azconfig.io;Id=osOX-l9-s0:sig;Secret=xxx"' # If omitted, uses the same values as ".binding" binding: output: true @@ -35,22 +35,22 @@ metadata: default: '3' example: '10' - name: retryDelay - description: "Specifies the initial amount of delay to use before retrying an operation. The delay increases exponentially with each retry up to the maximum specified by MaxRetryDelay. Defaults to 4 seconds. -1 disables delay between retries." + description: "Specifies the initial amount of delay to use before retrying an operation, in nanoseconds. The delay increases exponentially with each retry up to the maximum specified by MaxRetryDelay. Defaults to 4 seconds. -1 disables delay between retries." type: number default: '4000000000' example: '5000000000' - name: maxRetryDelay - description: "Specifies the maximum delay allowed before retrying an operation. Typically the value is greater than or equal to the value specified in RetryDelay. Defaults to 120 seconds. -1 disables the limit." + description: "Specifies the maximum delay allowed before retrying an operation, in nanoseconds. Typically the value is greater than or equal to the value specified in RetryDelay. Defaults to 120 seconds. -1 disables the limit." type: number default: '120000000000' example: '180000000000' - name: subscribePollInterval - description: "Specifies the poll interval for polling the subscribed keys for any changes. Default polling interval is set to 24 hours." + description: "Specifies the poll interval for polling the subscribed keys for any changes, in nanoseconds. Default polling interval is set to 24 hours." type: number default: '86400000000000' example: '240000000000' - name: requesttimeout - description: "Specifies the time allowed to pass until a request is failed. Default timeout is set to 15 seconds." + description: "Specifies the time allowed to pass until a request is failed, in nanoseconds. Default timeout is set to 15 seconds." type: number default: '15000000000' example: '30000000000' \ No newline at end of file diff --git a/pubsub/azure/eventhubs/metadata.yaml b/pubsub/azure/eventhubs/metadata.yaml index 9cbf4edb6f..b9cd436f24 100644 --- a/pubsub/azure/eventhubs/metadata.yaml +++ b/pubsub/azure/eventhubs/metadata.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=../../../../component-metadata-schema.json +# yaml-language-server: $schema=../../../component-metadata-schema.json schemaVersion: v1 type: pubsub name: azure.eventhubs @@ -17,7 +17,8 @@ authenticationProfiles: sensitive: true description: | Connection string for the Event Hub or the Event Hub namespace. - example: '"Endpoint=sb://{EventHubNamespace}.servicebus.windows.net/;SharedAccessKeyName={PolicyName};SharedAccessKey={Key};EntityPath={EventHub}"' + example: | + "Endpoint=sb://{EventHubNamespace}.servicebus.windows.net/;SharedAccessKeyName={PolicyName};SharedAccessKey={Key};EntityPath={EventHub}" builtinAuthenticationProfiles: - name: "azuread" metadata: @@ -31,6 +32,7 @@ builtinAuthenticationProfiles: type: bool required: false default: "false" + example: "false" description: | Allow management of the Event Hub namespace and storage account. @@ -81,6 +83,8 @@ metadata: Connection string for the checkpoint store, alternative to specifying storageAccountKey. Property "storageAccountKey" is ignored when "storageConnectionString" is present + example: | + "BlobEndpoint=https://storagesample.blob.core.windows.net;..." - name: storageAccountName type: string required: true diff --git a/pubsub/azure/servicebus/queues/metadata.yaml b/pubsub/azure/servicebus/queues/metadata.yaml index 613edcf7fd..b931d95a40 100644 --- a/pubsub/azure/servicebus/queues/metadata.yaml +++ b/pubsub/azure/servicebus/queues/metadata.yaml @@ -18,7 +18,8 @@ authenticationProfiles: required: true sensitive: true description: "Shared access policy connection string for the Service Bus." - example: '"Endpoint=sb://{ServiceBusNamespace}.servicebus.windows.net/;SharedAccessKeyName={PolicyName};SharedAccessKey={Key};EntityPath={ServiceBus}"' + example: | + "Endpoint=sb://{ServiceBusNamespace}.servicebus.windows.net/;SharedAccessKeyName={PolicyName};SharedAccessKey={Key};EntityPath={ServiceBus}" # If omitted, uses the same values as ".binding" binding: output: true diff --git a/pubsub/azure/servicebus/topics/metadata.yaml b/pubsub/azure/servicebus/topics/metadata.yaml index daf2d01c24..e54e2b9fe8 100644 --- a/pubsub/azure/servicebus/topics/metadata.yaml +++ b/pubsub/azure/servicebus/topics/metadata.yaml @@ -18,7 +18,8 @@ authenticationProfiles: required: true sensitive: true description: "Shared access policy connection string for the Service Bus." - example: '"Endpoint=sb://{ServiceBusNamespace}.servicebus.windows.net/;SharedAccessKeyName={PolicyName};SharedAccessKey={Key};EntityPath={ServiceBus}"' + example: | + "Endpoint=sb://{ServiceBusNamespace}.servicebus.windows.net/;SharedAccessKeyName={PolicyName};SharedAccessKey={Key};EntityPath={ServiceBus}" # If omitted, uses the same values as ".binding" binding: output: true diff --git a/state/azure/blobstorage/metadata.yaml b/state/azure/blobstorage/metadata.yaml index 9e796c1f2b..f90198900d 100644 --- a/state/azure/blobstorage/metadata.yaml +++ b/state/azure/blobstorage/metadata.yaml @@ -14,11 +14,11 @@ capabilities: builtinAuthenticationProfiles: - name: "azuread" metadata: - - name: accountName - required: true - sensitive: false - description: "The storage account name" - example: '"mystorageaccount"' + - name: accountName + required: true + sensitive: false + description: "The storage account name" + example: '"mystorageaccount"' authenticationProfiles: - title: "Connection string" description: "Authenticate using a connection string." diff --git a/state/sqlserver/metadata.yaml b/state/sqlserver/metadata.yaml index 713631017e..f5129bbb25 100644 --- a/state/sqlserver/metadata.yaml +++ b/state/sqlserver/metadata.yaml @@ -1,10 +1,11 @@ -# yaml-language-server: $schema=../../../component-metadata-schema.json +# yaml-language-server: $schema=../../component-metadata-schema.json schemaVersion: "v1" type: "state" name: "sqlserver" version: "v1" 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/" @@ -17,7 +18,7 @@ capabilities: authenticationProfiles: - title: "Connection string" description: | - Authenticates using a connection string. + Authenticates using a connection string metadata: - name: connectionString required: true @@ -36,6 +37,7 @@ builtinAuthenticationProfiles: 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 From 651834e9de50616be9374a933a90be69e5fcc2cc Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Tue, 11 Jul 2023 22:37:19 +0800 Subject: [PATCH 10/19] wasm: implements strictSandbox for output binding and HTTP middleware (#2884) Signed-off-by: Adrian Cole --- bindings/wasm/Makefile | 2 +- bindings/wasm/output.go | 32 +++---- bindings/wasm/testdata/args/main.wasm | Bin 8665 -> 9070 bytes bindings/wasm/testdata/example/main.wasm | Bin 7682 -> 8280 bytes bindings/wasm/testdata/loop/main.wasm | Bin 6028 -> 6082 bytes go.mod | 4 +- go.sum | 8 +- internal/wasm/Makefile | 9 ++ internal/wasm/testdata/args/main.wasm | Bin 8665 -> 9070 bytes internal/wasm/testdata/strict/main.go | 32 +++++++ internal/wasm/testdata/strict/main.wasm | Bin 0 -> 11356 bytes internal/wasm/wasm.go | 50 +++++++++- internal/wasm/wasm_test.go | 88 +++++++++++++++++- middleware/http/wasm/example/go.mod | 2 +- middleware/http/wasm/example/go.sum | 4 +- middleware/http/wasm/example/router.wasm | Bin 9256 -> 7985 bytes middleware/http/wasm/httpwasm.go | 15 +-- .../http/wasm/internal/e2e-guests/go.mod | 2 +- .../http/wasm/internal/e2e-guests/go.sum | 4 +- .../wasm/internal/e2e-guests/output/main.wasm | Bin 124070 -> 116531 bytes .../internal/e2e-guests/rewrite/main.wasm | Bin 8528 -> 7540 bytes 21 files changed, 203 insertions(+), 49 deletions(-) create mode 100644 internal/wasm/Makefile create mode 100644 internal/wasm/testdata/strict/main.go create mode 100755 internal/wasm/testdata/strict/main.wasm diff --git a/bindings/wasm/Makefile b/bindings/wasm/Makefile index 671c185e59..6bb0e7134c 100644 --- a/bindings/wasm/Makefile +++ b/bindings/wasm/Makefile @@ -1,3 +1,3 @@ .PHONY: build build: - @$(MAKE) -C example + @$(MAKE) -C testdata diff --git a/bindings/wasm/output.go b/bindings/wasm/output.go index f29e7a591f..9d80d5511e 100644 --- a/bindings/wasm/output.go +++ b/bindings/wasm/output.go @@ -16,7 +16,6 @@ package wasm import ( "bytes" "context" - "crypto/rand" "fmt" "io" "reflect" @@ -40,11 +39,10 @@ const ExecuteOperation bindings.OperationKind = "execute" type outputBinding struct { logger logger.Logger runtimeConfig wazero.RuntimeConfig - moduleConfig wazero.ModuleConfig - guestName string - runtime wazero.Runtime - module wazero.CompiledModule + meta *wasm.InitMetadata + runtime wazero.Runtime + module wazero.CompiledModule instanceCounter atomic.Uint64 } @@ -61,28 +59,19 @@ func NewWasmOutput(logger logger.Logger) bindings.OutputBinding { // The below ensures context cancels in-flight wasm functions. runtimeConfig: wazero.NewRuntimeConfig(). WithCloseOnContextDone(true), - - // The below violate sand-boxing, but allow code to behave as expected. - moduleConfig: wazero.NewModuleConfig(). - WithRandSource(rand.Reader). - WithSysWalltime(). - WithSysNanosleep(), } } func (out *outputBinding) Init(ctx context.Context, metadata bindings.Metadata) (err error) { - meta, err := wasm.GetInitMetadata(ctx, metadata.Base) - if err != nil { + if out.meta, err = wasm.GetInitMetadata(metadata.Base); err != nil { return fmt.Errorf("wasm: failed to parse metadata: %w", err) } - out.guestName = meta.GuestName - // Create the runtime, which when closed releases any resources associated with it. out.runtime = wazero.NewRuntimeWithConfig(ctx, out.runtimeConfig) // Compile the module, which reduces execution time of Invoke - out.module, err = out.runtime.CompileModule(ctx, meta.Guest) + out.module, err = out.runtime.CompileModule(ctx, out.meta.Guest) if err != nil { _ = out.runtime.Close(context.Background()) return fmt.Errorf("wasm: error compiling binary: %w", err) @@ -101,11 +90,16 @@ func (out *outputBinding) Init(ctx context.Context, metadata bindings.Metadata) } func (out *outputBinding) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { + guestName := out.meta.GuestName + if guestName == "" { + guestName = out.module.Name() + } + // Currently, concurrent modules can conflict on name. Make sure we have // a unique one. instanceNum := out.instanceCounter.Add(1) - instanceName := out.guestName + "-" + strconv.FormatUint(instanceNum, 10) - moduleConfig := out.moduleConfig.WithName(instanceName) + instanceName := guestName + "-" + strconv.FormatUint(instanceNum, 10) + moduleConfig := wasm.NewModuleConfig(out.meta).WithName(instanceName) // Only assign STDIN if it is present in the request. if len(req.Data) > 0 { @@ -117,7 +111,7 @@ func (out *outputBinding) Invoke(ctx context.Context, req *bindings.InvokeReques moduleConfig = moduleConfig.WithStdout(&stdout) // Set the program name to the binary name - argsSlice := []string{out.guestName} + argsSlice := []string{guestName} // Get any remaining args from configuration if args := req.Metadata["args"]; args != "" { diff --git a/bindings/wasm/testdata/args/main.wasm b/bindings/wasm/testdata/args/main.wasm index 1b0ad29652717092db030428430448961b07285c..4a164b6f0ac8c75706d3c99812f9408def23c16e 100755 GIT binary patch literal 9070 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 F{}<+!fmHwi delta 4603 zcmaJ_Yiv}<6`t39?A~2`!4DQc*tyq^yCepjCahf&i5(ha7ehc5O7eq9Nefa~I~2o) zQWa?zYztGNis&K*sr*0{KlnkbRBfeH%?~0)EmBBDC0bFHs#ZlSSGC$yZJMY-qTiW& z9V+! ziE&q$(@gU)XYyg51pzbfpGx+NtON9mcYc4NfBx~mEd~!~9i{-iG zsvLA%^UAd5liTtUac?g9M2_U{&Cyw~Cr_!zg0t8s^$qxER7A%vsD2_pQriTs`=?q@ zq}z$&1qHJW)VUupEui`mt}T)i4U0TX=aLtlO4ow=LJ|8Wy=adeW9R3mmY zn~H>X$PINkiUhfn|67;-RYr!Z5vynn2}P;bEF{5+vL}RG%S%c=jM~I`OgWEpZlf0^JIkP;Jakym^ zxVhTUbwsh71>pF9yixE+!E?#cTz@mW$;C~;qlKb&iLBfa%j_0|Ti_UAFSS8cg^%Er zj>nGS+Ubcrt>D7TcDHV1!xZY+Z{qjLtU{1tEH#gTr99a)GhL5_sL5sx<^b7+l!RK0 z6*)j4u~!4DKdx$`riGUE*rz~y>@G>AHp)9g6Rb(z|FiK$pQ}oQmi6;O4DANhiiaRa+dWH zH@ZH+f*xkhB)`iK_WWDm(u{=$d^+r;K=ua%D}hxs|6=fMa=(Tn3FRb6%eEl&alv95 z$6w&aCMBl;O0dn$6IC$;GF&&z+~kx$jI<$8<*TA%HDjhyhfD(7Ho|-?F$>$lnIb<% zoS!7cs!cHruaT4Z6l8sRf;56-Qn@%iDiDQFf16qY>R0V9GUT!yKvNt6luIemAQ~0T zl`*O3aPe^pBG69LJiw2fEDEUO7|{rW*sF@CiM@nU$tLU$$1?n10~5$I znp0>!EkfCiQzU{z1nG;ZBq3xmDW(AhMylEY&SPUjOG@y_m9?gJ6-o8TfAGo+7=!Fh z$u7+!MOMQ{(n5v>x<9a-bTzKTGyrVSHIN5Waf+feg{_(Uv`6ybCgUU7vN#+2nH&9<`<85*{I$wPR_OS4s)U|66?>6{gSd84G(gl2TL6%~K;^WJS;Vc!u!4dT zNu3;STRn+HKBC=lJ(51dn~Qr14h@(Lasv6`qJZF7oWsbsBQdoT*I0%YPwC0zW?Q*A zM>8t|RcV3aFb+`($)S!3otx~rpJ(78(FG7V*$08s2@-@P}tMAg1j`}-=#5}a3iDFGx`JQrn&a48<}})7O=XBJ@a;2{vqu) zv4Y;>z*Uo2tRKupZm}_jSkDiRA5E(Zwjj_8wPsDKJc>Mj2NJN6mNmbQ`!be_C{Vf} zpaBqM6}*ceqq3EtHbzwu!iC5f>QA4XwwULp1I>_s&j8{GJ9YJrZ0UX#e3O0WS}opGMNiMm^<7?sh(9`!-Z0 zvxcc}JAip`wZIE2-8wb-v(#vn8KrK0!**-x{1%I=s$w{^d6;YlPHP%{1cS^((?>~8 zKr^m(8@n;Fu7i;lrUR7M6R9ESt<)H;!+NQO)>aJkshy^dL?6cphlW7FUjYH@00_7c z5$hZ3;C56%Z!mMH75Jpt<2AHA1|0BYV(r>y2sQ{SfS|Y3W4V>}W-_d=SKfdt4VmHz z{#_|g0m*_}_hu(B3#wutbpm`)i2*%EEgJh!^#wfVP}fpB$kzR4BXWYEkZTyGbEdqz zCn}(R68CgUPTEJLz4WCdDD|v6Kq83;YP_N)N%VAKPf-9}Ys<)H)H-y`jH8h1V6n*>g@6R;~?)Op}EjO=rkUaELXRz!tPqHWm8P>jq+ z!^lxTpya}7k^sIA`cr^{IT8{>4ZPoI&9{#y8gCGV5!_=(s1^k(k9c|XZe<-Qt3@5& zdvt7oP}iG|D=~ViPRBzOWr7%djwWQ|gPvoNhhAgf(U*yW4c#Pj1%il{lDPsW(QhO= zQD+ZhBMwa8vY(8-C_~v7c17CTq@ztY;8VB>tpb`b<#HDrr zSOyRMToXyYJowl}3~AvAmhm;_XJ89BYsX4E*4x>vs}8^X>OuaV)1MqHpX=;6`r6^f np;r%X%IMyg4|3)$&S2$0;8-FQT-@o@?q6HR` diff --git a/bindings/wasm/testdata/example/main.wasm b/bindings/wasm/testdata/example/main.wasm index e9ca99eb683e7875ddf71b467e30f60558658f1b..119a962f3d601fc560d1d01ef349c97098e0e324 100755 GIT binary patch literal 8280 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 YYyPoQOCQdh^(QBG?U~q_@7p~4A2Mt;_5c6? delta 1836 zcmY*ZO=w(I6n^*KH}Acf_a-w<$7IS($a$}$jFLY@UP>%DS4=C`Viqn04K%ieW=w;L zQ52L(sDem@mg~ZWqJoPGE-LM!i&m<15u}SQTv!lPaG@Yn!G&T$zjN;^L5ATw_nmXk zJ?A?=Ur%pOpM6%2wa+cK&b_v%-o3Z|#jVdm(KctIQ~s|~l+EqvfkB|R&-f&A>5-s- z!PD2a{I2ver7v4Sdv%#g68e(+?&mThshh84B`v7bN<;EV>FZY8*J?v2edq`H?QHpz z9z1R6!Ame1Khj!T>8DcmR1eR|Tn-Y4b&~T{p}kaDozl@^zbAVnN%gY`J}5j~y&GAI zfb|U*OgWbgoNl^PDVI$u%~ut9q{v?(+2>$B=ZWIPFBlalPinmX+Y*Lgfvy@P`!9V85qlUda58N51a%wZ}~Pk9xC;@FC1)!Z*NL z8rrMAj{4Swpotsb%`7$_C8HCHf)$uD2LZ(t?Q#HOsj@-=%GGxk=9}b^ z0ixv$XZlFAq`kxO`9vg~{HDB{{e!uD2qp(Wg5D8#Vzzg=XG6K1Dbk41Wbqc~U=n=bAXolNEOad#l*>oF&{GepK@Aj@VJc9RREdz`SrhMjdC|N&tZt{Y=eyZL>7g?r^c7c(SmsNBr7?JJoTD zbNRISWT@GD294$yA=%PChtk`{P|R+3VNsBvJ-?E z!9{d?moIL!7aS%s%;H%=Wgb87|H1=Q4b~@Ap)xzMLpHz*_!x1$J(lm6$t)hSQezg@ zkS4wMoY!!cwi}vG?r=A>o!rf^waCd6Tq`a;oW93lf%*3tVg3OlOh0Cf^0xex1te!3 zpDzx!|2{YMJ*`n&OII9N*$Q94y2JF2U~B#>*0RvfaJW0e;s0lt*BRFr{}}(MnCFoD z`dfYw_+5e584>n1Mud5V5xsVq5m{Zr(-8jH`B9e-jjPLkVskN+{f2J~eY}xFdcyZw=>DfAJjo96i5+wl*+zq)k#q`2UZnZ?Ev_cYdDUfozeeR8V6g{9Ty d;}^edTovZSk%z@Q=GI782qQj$K)l#Rs0MzSIzu@VoFg0vecNPh5x9~=oOBoGM& z9;}e>ySJ*%7^6JIR?YpXbI(2JJ3seSRf|TlB!v+2(8w8o9slI|8M!XbDBBaZBQS$$ zdt03G*8hwnfy)EEP(PLaJuftp*6F0zSWeFM)6>g?c&!z`u&=bxJpIC;mBvDR2gTTY zw?E%Gowk6;_?_cU_m6%`s!_6Cinp72-N#mb5>d$F0jg_jM z5!IB5)<9WRB#M0fUt@TqJ1MU-hSzs7dpsc*>vSkd*D3sw*^F@p;;M~EIrH-CbOFjEstOmRC0gWRZrJU#Jn7%sO-7@Zv+oJ1f)Ln` znXe!*uA#&gKh%I=h3t-w0>o&n;)Ry^wTz`vYwcQ~1MF?+H!?NBN@~`V72peE$REKy zDk?Nk3?sLYVt0(!J2e_a<2x(Z-_b#B>`7w*1rZ8vCaIgDy)s3d55vbtB}B4n81ERM zlTkDz*}?^Jf!(8V=yG%vKxTk&5jOB3o%S3faPe$V**W%@89lmNgX4ZPxJD71nHT}2#>GA1BW(QW}_ zBhn?{BNS((WF`<=o`RX-8iG!)$m#b-!6Jk9lR^A*ca<+TF{^a=V8oq__Trm=k9Gcn#tDgs6hG4azFS zL^ulM4F;L>hN_6}9&!exqH}XZz-BW14<$?M@b|fP&7VS+gRn-J#|asjFr2Y{w9VLP z0XWDMKeUj`4#I!@a8H zwf$Ue=cUg1tg$r@Qzxz3buS?zbfWn!E=^aBY6U1$%m74!4k$j|rETOY~R zWCpGgi|ao@bpKlPIyKse;fBe~C7DBY=E{Hxd?8W}6K-Lm9lj~yL9n}q8DV!=u&%ae z`LF7+L-pHq0Gx+)6#-QN!wVx$Fe_*gg-rJo=%U%hnYt>u25U1|*i^u&(njvM$0k^E z7}S-Y!(=;JI*fca!fX-VKzQVji8TZU zP@Jq54b@aptqLz?G!Y+yW?@oDok&fdz2%vb%WaNht)x9v8Eqh0D>!qYUTN3Lbe`gI z1c)?myHj}tmc;0i8$sX+9UL9?fNnvT^oS0&3Ah0Aw9sXn^$~oz;Ea1|-gHY*0d;eQ zytJL|Zmq2hlm)Tb#Ho!7@dh^o1eHP_=&WkAnpP!bS9GJhkay`;7AR-_a8j5rk(7%p zPQZeH{Jcb|qtI|BS5w%>p3IqNLIlui*^PEsme!+`HFkJiNoV%%`}>6l!nkNQafqx>001QDi59?uqFu1*ZAl0 zWMLvzDQo~Vo+P!#nFG`C4ow5ooG4~I}t#j&G{bOh}R+`2l$z3S>Y*F39bwcz{(CpJTf2Q=aqh@pZhWRr#Aq`K22 z;`ZB=I57}A1ykr$3y=bxHvVuHj#Vq6;?pJb@++IA&WhPUd!F0#%q!TPu_K1I_Q5)s zj%kA42jSrkRPuRyyrbYzro^)pMOUXn3 z003M4Yl-v-iy|P8*(As~t*g`;tSW(p&(ej@e28HSj6=jk@R?#=o!7;sc)S^KY9`%g58)V|Hq!o2$`obfvDv6rcTgTr6AA$Ivh zsyXd3Uw!l{9MN3e^zGs1WOxfPItzr#)Ai>zlrPj6#(P+7gN)Yy>I% zNP`CwT!pFb5|?36vXsyH?0>PRB&&RqML!ORavsN>C5XJE=6{)NI@09U5E z$CJDu^IbuP=M@A)ULBO}LFWTl?&@pA5d=l_^4zvK`HbiY0@;AYg9NQ$9UO8gmH__uu!eTE2udn z<3n78ivMeGg%5}3Drz(qnbLeDj!lBAS*Ip&azCt+oK>>*P-z}q#SwdD(_g6(4J2ZZ zq>58;KkM-YGNj+i6#%iFk4Wz$94w({zK>)}>MGu{h2}%vpk?i01Tf%gsK@^eMW1~; z_0LL{snT9#Ym_q7))m}7>I4=Jn6sw~ZozUxO9!`E8Yf4I7RG~N&DO_=m|JCcnaX*% zXdG6@$d-b-(SEMZw%$>xA*Xe9!B*Mu+zGNoaB;GTA_51iy%pk+1T)y&KEsz;ep||k zT7|Wa?eD6=pi{bZbd>er2)v-8OSejIr2z|g(+T)`1}=pY2$)$9V(>%KX**U%IMR@> z;OGffdwT@?sOkV5&c5T}eoN+Pxd74YrSNpKZtx~;Mr^MMzCeJ_KX`Hy*6X&zpH*aq z_`ii((iz}jU40*5?`0`@O|q2CpLc5G5Gccanh|DQ2E`9Lj(x%0&+`RamrS4w=~Kid z3slZd*|O2WqZx^h=TJeHLpg+i?SSjU{(I4b(@dcOJ)s5ZE;OLZ&BKUx_M?YzZrJZtNlp?CCW0Ni?>M@(zpbju(=|Ed=$jfOPavI+_Xs9dew=R$v74 z{|Js%q;uhdS*ih?%(~svaJxJYp{A*;XN`2Yl~Y)_Y#w}SvN~$bUsp}H<~;otUC8(x z%UZlDw8_Y$S7Dldf_u+%AFf3Ij6I>?ZlkB{%NgW2y6`u(DR$djbq-Dqrz>U)m)-ge zWCZf*f)8GEAX#zS+6Y~p@&{ZKz!5=4rYKKFK${0fif7DFm+hfy2}BLy@)BI6phF&h zK4O6q)v&5;gUYQ%y4#e95wNq1Z`((c4QeUNbBd2OsFy|vgCo>Ppo}oA2X`Mbj3C&e zYW7Bw(!wV+7oxVZgQ*(!M5R=Aj}C4*h>%msVMLe7;UOD?!aWcG7x=gKcPsxONNlfi z5G&9_fYEy_*^}W=m@uGS6Jwp zWu@_s4=J`5!t-=6sEtk50LdqoJ?x7iM7hpk1;)Y<65fyy$1eD0RvxP>6hL;nUGyVV zL7~gbqX?8S9RjZGmBsf-te&h|hFd$&69z+x@DR`7(u7m)BX|3x9hb4rRsLgI~s6!Bot zA0)H3c_tmSdW#~P6a7Ra7ZQGk!uMEClso7+N#50gpULcp${jz;_2YXCDE- zMR5i9Cvd;Nn(F?7c3%_I3(F89N#a4O7aEYLx!mZr<`3(v8LamB9aLMb59?m5n{TL_ z@gQEn)tir7y=Ht~XN(7p-eR0|Tl4Xjnbls>Scqqz;f*~Gzr(s4_qGrv7T09<$ZD$# z{ac+l4w6RcWp#Z7(o zf(Bx%@nx((g!Oxx@!FmwZ9;7E2N-{WdD@4z`UtHL+jD#qWBb7vMa!q6gs;!i&OHn~ z`^oHTuXX+yfIHFXHM=p_3tybeXl6l^dB*I~!Jya2{f{w6ulN|ew%a{?aR@CB@X(xp z=f3~Gff4_`>q+tJS6tu6xPr@#ZnX+?aN`+5CgJyeFU8`T<-G=eK_F_J$nt58VE62Hq8~tTwuZv7G0*Msn_v{lVS& zxZCe{R+pcqw-tvQWH;@sNlSO-#MYhcY7pC+UCPUmxr^GEo>fA=iDnWypC&Trq~s$+<{@u7Z;aLf~NV>udfwjbP| z0^znhFi7I`88W|X$N-UK^|||jTnwa-`PD9n$k#NRgBW3%Y`+7y1;iVOnme*z0^Bo^ z^B~AejpZY)USn|KnK-@I{zPEn&i9YU>3x%TT=`_&jnjDN&~~SnX{O158LmISEB~Fr z{n^h99@sVgz1t#p_bj6B!Vk{H^PLADxWnB);x90su@S8AoBmc9?Vhy+)u97qib3R> zzFp+i`A(b?uw(tf2>|fW?W+Jp@h&OGPxDt1_+>|yGRr$XQJfw0n~k&~3$tug(KfUD z#3fl?#;w))c#x=47ddUUu^3D52ai1_D!4OG>ojN1UQmU3-u;t(kIwF$-K(e1rRj2V zc+Z~2R(fvrEMQpL6DRobNs_b{_H?^zOEXyAj^|U|*n4okvCwQhw(r2+`LoT(;zthd zf8^2meUI+nfA+xs`oRPH_wQ@&ZyeYiJlpDBINlHT&h9@1(SAIR-*({VB&QeRM!E{l z{=?%-t7+qGH=bGS_RluD$-{Re3#Nm{@-mL8nT6$)hs49-+0|}m#wGrTOG#_7Hxr*v F{}<+!fmHwi delta 4603 zcmaJ_Yiv}<6`t39?A~2`!4DQc*tyq^yCepjCahf&i5(ha7ehc5O7eq9Nefa~I~2o) zQWa?zYztGNis&K*sr*0{KlnkbRBfeH%?~0)EmBBDC0bFHs#ZlSSGC$yZJMY-qTiW& z9V+! ziE&q$(@gU)XYyg51pzbfpGx+NtON9mcYc4NfBx~mEd~!~9i{-iG zsvLA%^UAd5liTtUac?g9M2_U{&Cyw~Cr_!zg0t8s^$qxER7A%vsD2_pQriTs`=?q@ zq}z$&1qHJW)VUupEui`mt}T)i4U0TX=aLtlO4ow=LJ|8Wy=adeW9R3mmY zn~H>X$PINkiUhfn|67;-RYr!Z5vynn2}P;bEF{5+vL}RG%S%c=jM~I`OgWEpZlf0^JIkP;Jakym^ zxVhTUbwsh71>pF9yixE+!E?#cTz@mW$;C~;qlKb&iLBfa%j_0|Ti_UAFSS8cg^%Er zj>nGS+Ubcrt>D7TcDHV1!xZY+Z{qjLtU{1tEH#gTr99a)GhL5_sL5sx<^b7+l!RK0 z6*)j4u~!4DKdx$`riGUE*rz~y>@G>AHp)9g6Rb(z|FiK$pQ}oQmi6;O4DANhiiaRa+dWH zH@ZH+f*xkhB)`iK_WWDm(u{=$d^+r;K=ua%D}hxs|6=fMa=(Tn3FRb6%eEl&alv95 z$6w&aCMBl;O0dn$6IC$;GF&&z+~kx$jI<$8<*TA%HDjhyhfD(7Ho|-?F$>$lnIb<% zoS!7cs!cHruaT4Z6l8sRf;56-Qn@%iDiDQFf16qY>R0V9GUT!yKvNt6luIemAQ~0T zl`*O3aPe^pBG69LJiw2fEDEUO7|{rW*sF@CiM@nU$tLU$$1?n10~5$I znp0>!EkfCiQzU{z1nG;ZBq3xmDW(AhMylEY&SPUjOG@y_m9?gJ6-o8TfAGo+7=!Fh z$u7+!MOMQ{(n5v>x<9a-bTzKTGyrVSHIN5Waf+feg{_(Uv`6ybCgUU7vN#+2nH&9<`<85*{I$wPR_OS4s)U|66?>6{gSd84G(gl2TL6%~K;^WJS;Vc!u!4dT zNu3;STRn+HKBC=lJ(51dn~Qr14h@(Lasv6`qJZF7oWsbsBQdoT*I0%YPwC0zW?Q*A zM>8t|RcV3aFb+`($)S!3otx~rpJ(78(FG7V*$08s2@-@P}tMAg1j`}-=#5}a3iDFGx`JQrn&a48<}})7O=XBJ@a;2{vqu) zv4Y;>z*Uo2tRKupZm}_jSkDiRA5E(Zwjj_8wPsDKJc>Mj2NJN6mNmbQ`!be_C{Vf} zpaBqM6}*ceqq3EtHbzwu!iC5f>QA4XwwULp1I>_s&j8{GJ9YJrZ0UX#e3O0WS}opGMNiMm^<7?sh(9`!-Z0 zvxcc}JAip`wZIE2-8wb-v(#vn8KrK0!**-x{1%I=s$w{^d6;YlPHP%{1cS^((?>~8 zKr^m(8@n;Fu7i;lrUR7M6R9ESt<)H;!+NQO)>aJkshy^dL?6cphlW7FUjYH@00_7c z5$hZ3;C56%Z!mMH75Jpt<2AHA1|0BYV(r>y2sQ{SfS|Y3W4V>}W-_d=SKfdt4VmHz z{#_|g0m*_}_hu(B3#wutbpm`)i2*%EEgJh!^#wfVP}fpB$kzR4BXWYEkZTyGbEdqz zCn}(R68CgUPTEJLz4WCdDD|v6Kq83;YP_N)N%VAKPf-9}Ys<)H)H-y`jH8h1V6n*>g@6R;~?)Op}EjO=rkUaELXRz!tPqHWm8P>jq+ z!^lxTpya}7k^sIA`cr^{IT8{>4ZPoI&9{#y8gCGV5!_=(s1^k(k9c|XZe<-Qt3@5& zdvt7oP}iG|D=~ViPRBzOWr7%djwWQ|gPvoNhhAgf(U*yW4c#Pj1%il{lDPsW(QhO= zQD+ZhBMwa8vY(8-C_~v7c17CTq@ztY;8VB>tpb`b<#HDrr zSOyRMToXyYJowl}3~AvAmhm;_XJ89BYsX4E*4x>vs}8^X>OuaV)1MqHpX=;6`r6^f np;r%X%IMyg4|3)$&S2$0;8-FQT-@o@?q6HR` diff --git a/internal/wasm/testdata/strict/main.go b/internal/wasm/testdata/strict/main.go new file mode 100644 index 0000000000..b4fa2f4b8d --- /dev/null +++ b/internal/wasm/testdata/strict/main.go @@ -0,0 +1,32 @@ +/* +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 implieout. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "crypto/rand" + "encoding/hex" + "time" +) + +func main() { + t := time.Now() // should use walltime and nanotime + println(t.Nanosecond()) + println(time.Since(t)) // should use nanotime + time.Sleep(50 * time.Millisecond) // uses nanosleep + b := make([]byte, 5) + if _, err := rand.Read(b); err != nil { // uses randSource + panic(err) + } + println(hex.EncodeToString(b)) +} diff --git a/internal/wasm/testdata/strict/main.wasm b/internal/wasm/testdata/strict/main.wasm new file mode 100755 index 0000000000000000000000000000000000000000..c757fc6b83956b5dc8330f5088f5fb07e980bca3 GIT binary patch literal 11356 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 literal 0 HcmV?d00001 diff --git a/internal/wasm/wasm.go b/internal/wasm/wasm.go index 2ae1e04065..c008e5b056 100644 --- a/internal/wasm/wasm.go +++ b/internal/wasm/wasm.go @@ -1,13 +1,18 @@ package wasm import ( - "context" + "crypto/rand" "errors" "fmt" "net/url" "os" "path" "strings" + "sync/atomic" + "time" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/sys" "github.com/dapr/components-contrib/metadata" ) @@ -24,6 +29,19 @@ type InitMetadata struct { // retrieved via HTTP. In these cases, no filesystem will be mounted. URL string `mapstructure:"url"` + // StrictSandbox when true uses fake sources to avoid vulnerabilities such + // as timing attacks. + // + // # Affected configuration + // + // - sys.Walltime increments with a constant value when read, initially a + // second resolution of the current system time. + // - sys.Nanotime increments with a constant value when read, initially + // zero. + // - sys.Nanosleep returns immediately. + // - Random number generators are seeded with a deterministic source. + StrictSandbox bool `mapstructure:"strictSandbox"` + // Guest is WebAssembly binary implementing the guest, loaded from URL. Guest []byte `mapstructure:"-"` @@ -32,7 +50,7 @@ type InitMetadata struct { } // GetInitMetadata returns InitMetadata from the input metadata. -func GetInitMetadata(ctx context.Context, md metadata.Base) (*InitMetadata, error) { +func GetInitMetadata(md metadata.Base) (*InitMetadata, error) { // Note: the ctx will be used for other schemes such as HTTP and OCI. var m InitMetadata @@ -75,3 +93,31 @@ func GetInitMetadata(ctx context.Context, md metadata.Base) (*InitMetadata, erro return &m, nil } + +// NewModuleConfig returns a new module config appropriate for the initialized +// metadata. +func NewModuleConfig(m *InitMetadata) wazero.ModuleConfig { + if !m.StrictSandbox { + // The below violate sand-boxing, but allow code to behave as expected. + return wazero.NewModuleConfig(). + WithRandSource(rand.Reader). + WithSysNanotime(). + WithSysWalltime(). + WithSysNanosleep() + } + + // wazero's default is strict as defined here, except walltime. wazero + // does not return a real clock reading by default for performance and + // determinism reasons. + // See https://github.com/tetratelabs/wazero/blob/main/RATIONALE.md#syswalltime-and-nanotime + return wazero.NewModuleConfig(). + WithWalltime(newFakeWalltime(), sys.ClockResolution(time.Millisecond)) +} + +func newFakeWalltime() sys.Walltime { + t := time.Now().Unix() * int64(time.Second) + return func() (sec int64, nsec int32) { + wt := atomic.AddInt64(&t, int64(time.Millisecond)) + return wt / 1e9, int32(wt % 1e9) + } +} diff --git a/internal/wasm/wasm_test.go b/internal/wasm/wasm_test.go index ebfb657bcf..9c07dc0c84 100644 --- a/internal/wasm/wasm_test.go +++ b/internal/wasm/wasm_test.go @@ -1,11 +1,15 @@ package wasm import ( + "bytes" "context" _ "embed" "testing" + "time" "github.com/stretchr/testify/require" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/dapr/components-contrib/metadata" ) @@ -16,7 +20,7 @@ const ( ) //go:embed testdata/args/main.wasm -var urlArgsBin []byte +var binArgs []byte func TestGetInitMetadata(t *testing.T) { type testCase struct { @@ -34,10 +38,23 @@ func TestGetInitMetadata(t *testing.T) { }}, expected: &InitMetadata{ URL: urlArgsFile, - Guest: urlArgsBin, + Guest: binArgs, GuestName: "main", }, }, + { + name: "file valid - strictSandbox", + metadata: metadata.Base{Properties: map[string]string{ + "url": urlArgsFile, + "strictSandbox": "true", + }}, + expected: &InitMetadata{ + URL: urlArgsFile, + Guest: binArgs, + GuestName: "main", + StrictSandbox: true, + }, + }, { name: "empty url", metadata: metadata.Base{Properties: map[string]string{}}, @@ -105,8 +122,7 @@ func TestGetInitMetadata(t *testing.T) { for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - md, err := GetInitMetadata(ctx, tc.metadata) + md, err := GetInitMetadata(tc.metadata) if tc.expectedErr == "" { require.NoError(t, err) require.Equal(t, tc.expected, md) @@ -117,3 +133,67 @@ func TestGetInitMetadata(t *testing.T) { }) } } + +//go:embed testdata/strict/main.wasm +var binStrict []byte + +func TestNewModuleConfig(t *testing.T) { + type testCase struct { + name string + metadata *InitMetadata + minDuration, maxDuration time.Duration + } + + tests := []testCase{ + { + name: "strictSandbox = false", + metadata: &InitMetadata{Guest: binStrict}, + // In CI, Nanosleep(50ms) returned after 197ms. + // As we can't control the platform clock, we have to be lenient + minDuration: 50 * time.Millisecond, + maxDuration: 50 * time.Millisecond * 5, + }, + { + name: "strictSandbox = true", + metadata: &InitMetadata{StrictSandbox: true, Guest: binStrict}, + minDuration: 10 * time.Microsecond, + maxDuration: 1 * time.Millisecond, + }, + } + + ctx := context.Background() + rt := wazero.NewRuntime(ctx) + defer rt.Close(ctx) + wasi_snapshot_preview1.MustInstantiate(ctx, rt) + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + var out bytes.Buffer + + cfg := NewModuleConfig(tc.metadata). + WithStdout(&out).WithStderr(&out). + WithStartFunctions() // don't include instantiation in duration + mod, err := rt.InstantiateWithConfig(ctx, tc.metadata.Guest, cfg) + require.NoError(t, err) + + start := time.Now() + _, err = mod.ExportedFunction("_start").Call(ctx) + require.NoError(t, err) + duration := time.Since(start) + + // TODO: TinyGo doesn't seem to use monotonic time. Track below: + // https://github.com/tinygo-org/tinygo/issues/3776 + deterministicOut := `2000000 +1000000 +6393cff83a +` + if tc.metadata.StrictSandbox { + require.Equal(t, deterministicOut, out.String()) + } else { + require.NotEqual(t, deterministicOut, out.String()) + } + require.True(t, duration > tc.minDuration && duration < tc.maxDuration, duration) + }) + } +} diff --git a/middleware/http/wasm/example/go.mod b/middleware/http/wasm/example/go.mod index b8651c468e..ec15f4f775 100644 --- a/middleware/http/wasm/example/go.mod +++ b/middleware/http/wasm/example/go.mod @@ -2,4 +2,4 @@ module github.com/dapr/components-contrib/middleware/wasm/example go 1.20 -require github.com/http-wasm/http-wasm-guest-tinygo v0.1.0 +require github.com/http-wasm/http-wasm-guest-tinygo v0.3.0 diff --git a/middleware/http/wasm/example/go.sum b/middleware/http/wasm/example/go.sum index 6a27987f7e..cb03367741 100644 --- a/middleware/http/wasm/example/go.sum +++ b/middleware/http/wasm/example/go.sum @@ -1,2 +1,2 @@ -github.com/http-wasm/http-wasm-guest-tinygo v0.1.0 h1:vcYHJkbfQ2G0bD/zupIzHe/h1LZQJiVGdn5eZZTJM88= -github.com/http-wasm/http-wasm-guest-tinygo v0.1.0/go.mod h1:/3UO8OXP9nxe7d2qJ5ifTVkqM7KjaXxUZLoqBsDXpy0= +github.com/http-wasm/http-wasm-guest-tinygo v0.3.0 h1:J11RX1ajUC6fhVtv3ZU5k66SL4EB4DhThHmz4Ilwevw= +github.com/http-wasm/http-wasm-guest-tinygo v0.3.0/go.mod h1:zcKr7h/t5ha2ZWIMwV4iOqhfC/qno/tNPYgybVkn/MQ= diff --git a/middleware/http/wasm/example/router.wasm b/middleware/http/wasm/example/router.wasm index 212b339da34012bd0a76b802b5e42e858c8fb316..da96de20795f8f18bdecbe2c9e7251ba178328da 100755 GIT binary patch literal 7985 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 literal 9256 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 V9u_Wj2bAl>g?{65djWak{{dcN1H=FT diff --git a/middleware/http/wasm/httpwasm.go b/middleware/http/wasm/httpwasm.go index d8b110a70e..aa024f6a55 100644 --- a/middleware/http/wasm/httpwasm.go +++ b/middleware/http/wasm/httpwasm.go @@ -3,7 +3,6 @@ package wasm import ( "bytes" "context" - "crypto/rand" "fmt" "net/http" "reflect" @@ -12,7 +11,6 @@ import ( "github.com/http-wasm/http-wasm-host-go/api" "github.com/http-wasm/http-wasm-host-go/handler" wasmnethttp "github.com/http-wasm/http-wasm-host-go/handler/nethttp" - "github.com/tetratelabs/wazero" "github.com/dapr/components-contrib/internal/wasm" mdutils "github.com/dapr/components-contrib/metadata" @@ -38,7 +36,7 @@ func (m *middleware) GetHandler(ctx context.Context, metadata dapr.Metadata) (fu // getHandler is extracted for unit testing. func (m *middleware) getHandler(ctx context.Context, metadata dapr.Metadata) (*requestHandler, error) { - meta, err := wasm.GetInitMetadata(ctx, metadata.Base) + meta, err := wasm.GetInitMetadata(metadata.Base) if err != nil { return nil, fmt.Errorf("wasm: failed to parse metadata: %w", err) } @@ -46,15 +44,10 @@ func (m *middleware) getHandler(ctx context.Context, metadata dapr.Metadata) (*r var stdout, stderr bytes.Buffer mw, err := wasmnethttp.NewMiddleware(ctx, meta.Guest, handler.Logger(m), - handler.ModuleConfig(wazero.NewModuleConfig(). + handler.ModuleConfig(wasm.NewModuleConfig(meta). WithName(meta.GuestName). - WithStdout(&stdout). // reset per request - WithStderr(&stderr). // reset per request - // The below violate sand-boxing, but allow code to behave as expected. - WithRandSource(rand.Reader). - WithSysNanosleep(). - WithSysWalltime(). - WithSysNanosleep())) + WithStdout(&stdout). // reset per request + WithStderr(&stderr))) // reset per request if err != nil { return nil, err } diff --git a/middleware/http/wasm/internal/e2e-guests/go.mod b/middleware/http/wasm/internal/e2e-guests/go.mod index 42277c93d1..e24ba6af60 100644 --- a/middleware/http/wasm/internal/e2e-guests/go.mod +++ b/middleware/http/wasm/internal/e2e-guests/go.mod @@ -2,4 +2,4 @@ module github.com/dapr/components-contrib/middleware/wasm/internal go 1.20 -require github.com/http-wasm/http-wasm-guest-tinygo v0.1.0 +require github.com/http-wasm/http-wasm-guest-tinygo v0.3.0 diff --git a/middleware/http/wasm/internal/e2e-guests/go.sum b/middleware/http/wasm/internal/e2e-guests/go.sum index 6a27987f7e..cb03367741 100644 --- a/middleware/http/wasm/internal/e2e-guests/go.sum +++ b/middleware/http/wasm/internal/e2e-guests/go.sum @@ -1,2 +1,2 @@ -github.com/http-wasm/http-wasm-guest-tinygo v0.1.0 h1:vcYHJkbfQ2G0bD/zupIzHe/h1LZQJiVGdn5eZZTJM88= -github.com/http-wasm/http-wasm-guest-tinygo v0.1.0/go.mod h1:/3UO8OXP9nxe7d2qJ5ifTVkqM7KjaXxUZLoqBsDXpy0= +github.com/http-wasm/http-wasm-guest-tinygo v0.3.0 h1:J11RX1ajUC6fhVtv3ZU5k66SL4EB4DhThHmz4Ilwevw= +github.com/http-wasm/http-wasm-guest-tinygo v0.3.0/go.mod h1:zcKr7h/t5ha2ZWIMwV4iOqhfC/qno/tNPYgybVkn/MQ= diff --git a/middleware/http/wasm/internal/e2e-guests/output/main.wasm b/middleware/http/wasm/internal/e2e-guests/output/main.wasm index 28b3e3f0613d7b896f362218416b1ce7cd42358f..1b2f1e0fe9aca1cdfaeec36049df34b5d6de2d8d 100755 GIT binary patch literal 116531 zcmcG%51f|QRqy-!d;iS5@60^F1ST*I&--?2CoMEs(}dErnT;WkK$>`)YSef_EKxc` z6CfjvN@3E_hE}RnY0J3>>)czkw9+HqLzT9%a%`$-(W0i6>%}Xc%lXK?CsjP3sOZTt z_xoFWKkvLVLsIqJn|y}nd7o$Rwbx#2?X}ikd+q&14?q5%IEte9-zTG+lf8SR&FNnK z8}E(xZqB&n-Y)!%xu*zsD#N9s_dKlHm76HqT=Vz*utIyvmlTiq%l7WsWAF5DvUiW5 z@A1v<@lEoJaykbMMTrD1i1a5^J;~ z6ev~&oFP-$ySseg%kh9GLe?%(yeIi&Hn8};4?q6yhaP|Q;q8xaeQfff?K>aY_3lUB z`_?;cWhylud34vicRu#$D?(mS7W!u( zLAUAGi67bW_~gTruh?ck#h~T4Ew`wv+?IFmeCW}K-}6XRdzrRt6yEvB z!&_dKCo1sxBaci(gD(T2XZypGTOWFC`y-D=*S&0!osYfip+}y0_heK_Psd-5E0tOj zMfD`^T^!fyjU-7T`-^xI_4X#UdbL)orS&MSrE#>R*{sFYIF6&Z7WbuTmNlYEmPJt| zN-F)8sF&)MYNcB3t;A7QO|zj&B}w9eB$0(dhCBHAN)g?XM2(u>^+%0UwK&_Gj3xXPi7W2O#){abJJuD+ zx}AM-5f$;uC|VOy+Qs9%ipSfjfA8Xnc7;O8QeP@w6H(qJDwWKXN@kZz)u<=E2U00-b@;5t4Ln#6-EP%B2fXoZL~AcYR7al87nF-zl)w! zT1hi*=+{I$arq8j7Fm(rm_)ZGk-w|on$(NT<>M`0=t8@#3V?yo7YRc8GIazezctA{ zoS|EjA>b4q4j18HsM&#yeNjUI>pNyQ@9MrVmsIbw@P0HAhX6*ug_OFl_epqvQxv_0 z?q^Wh-gB|{@s4$Hj2b1!1{yHdv$o=jdqo*&0}m(q)0ENDP+?j#Xf%|JkH$2e=&`Gg z-xx!tDz^*ATe0hJ>9$`*T3n%f#)~E`dmxe#)r-eRBY+qpt}=dO1ds~P7YIn70qIi_ zfD9Or1!8NmS6uQ8Y9&P~wgD~lL@h+|Es+7eF{<1e(Rz{I6!pC&>ht9W)kk&q^w8;$s`6Eu5BC1)YBOD4;h&AA2JT-KlGTzt}@Yz83NpB$m%H)gIMo< zoL)Zp{(bd{9d3&26KcL;CDk%5n%INMMB}q@obG*f@-7)o@^*Ynvcf3rKJ#Se;v&}L!TdU1>Wm03 zP03HFB?=ESf9u(u=`m&{srtHwv7#!GQr6|Ne)!qp+n!(5VElED(? zN8UtCNJpM5fGnFuFtFkjrR-hytZN|0*FSUN^YlPZZ{uar#Y7H#aK z<3$a8_3%-Y+26(6#=%Gu%b=P-pJc6=ZZR%YoTmsLFxvv-%Sq~Z8yD$BzQj|j-lnIZ z4@sJ1u?U_36$nsS%K#qalHQs$O9IqA4B8WzN>RThsne}Q z3N~e^WUZQwVEvByo%r9dxyZ)bHBS?O7DgJo;@aWQj(5-oXQHxeR zx_^_!HHLvmfA!ZYn7E6-R?**$DHT4q`_?pamwCb;Pu9kIz~C4dciFC1#XV;-*oIx= zi~k?WX6q$-Mlmr|&k=!_;vM}s6S1L$=q`my*!KMU^shVp#Y+Bt>6rV6U#n=iflt&* z_{;cX1PO*aZ_O3)Mz4zvZk#`0kh3(3lWGHr{eE`^jQQ(e+|0!Gc!$8P>x1L@_oLPf z__+YmCA-Gk)uKwOd5EN#-h zFGsg&6EF<;X4HTU^g?}e@&-aj%s2w;-o6-=tQ#)9Q#{Xquv26f6luv;oc{oQK_N9t zf~XP=q%!cS2LzNT;67T))xEedd#IA@7di>^_x23H@Ddyko7fp^Hf8j@udWH=l=UkX{(Zq zC1a2z+7v8CyXt=V$!r{fLitqP-{SAp-6lLl$%dGNyM<;ds)!{DT0nN}Y*iTep+Id% zkw{Y|F1bq*vNDR>vZ(1Ucf*c$>VEktgDw@Ay5s&wQPa=58fnEbrQ%3 zZsrwxYVcM>2~d7>@7-48CsZR<4OOECH3xRM=HyOQP0OlTsG5SjuNrsiG*zdz!!4ZL zX^_;cDrXuCXdJJEU|KfNkS4`e#NKg!DKgL5jBJ6A8@X8Cse5U_o_hj_5@0qJ?=)AL zU)5diw0<#UkvnlAH6BcDr`U<~PCLUrxhn?L ze$hnPCXP3+n==mp0hZW)(OgXEE}5$2#S#z>VRyaM6X08^9|{*E{C61(S<44878v;Z zOJh+B#^PKQtOZ*9zGMuAHWizfUSw;cjq=uH{wnTk(Kw?S`C>4?s=!Cckv}d?X#g|v z2mu~K7O0Ca*g^arbVSx6>n5Pd2jFrx08L7ug$F9f0HDK?TG7Ai5i99W8zucg*V8|1 zRV9Vp&E9LjO2X5uN1`O>+wDOY;X%`_o{bpl-S>9PBevQjcAIai+UcQSRXeOvQ`K(P z2=eG`$WYQlUgaT=ddMr}y@DtV^EMW?m=s7=At+t05e7AqYw*qB8Ze!z!|l49BGYuz zkG;lbGt~8>7g$iT;vE16eTz9N(-Aq>1^Tv!*+>xwF9BPH!-IZW)~cn9di|Je)hOG8 zU^WUSXhn4!$`cIhMJi9Y22)W=+ss3t0B@Q+)FTNGMDec-Cm}b!*Snp)|8?Ba{2J{oK$gH9YDs zE8O#+CW?@^f^Qb6BJY`L$!SeXy!+_sl9x1qLQ)R^6E*5k1BKcVpDuD6DXb^!tTQ{i zdgGSdDZjzvh}(+$?*8mUGdKcPs5agC5AaGjih3>Ms&^FZ{h%)vh?k2FP2(J#{kLh7 z?2Y%huVH^{`8$1Iy5|DYZk@S6IejVT0^p>%K;$m|uN8BJu#=d~GoQqaxRv?$u0?*|$7pTjj~ch!rRe}3uQY$m zKRX(pmA_B^+YYF*+Qk4p-|nkS1uRF!7lgr3gk(DmZH5x$%D&?G6Y78P_`FE&ATz(os3>*KyOWe{fiDUdq;!b?<8WPtT72x>Kkhq&-DFmBx?GL&H zk8+MKDfI@;b@YapIK>wH$b?FoeEHEYC(i#5n(2-{)JQAQgHjzR`m?k~WHZV9KS`Fw zS;DWhk`b`f^dMm#;BqdN2EI?i?89Mv-$nH8_HL4 zL8cvJ;F6;4m!2KiOq|5-f zr3CkYz&&r^9t0fm(8TRmlshl-MINX9vG{XEP^RK>_o5+1Xm>O@(4o3zH(O}2rw zChPzk75x3Gb%G9br0ayCc0kXMf&NDsD{{c7d$NPtF)MRWWu{eEZ;#JGf7;3%vR02; zK&R}$VbwcqM0<@V+2L3{IHKnV4PNNMkxq*TWA$O$pdE_o1o8Gbe|0-m(4>d)^iLl| zcJwsGB=C3lvR2NOF}VN@)NmhXy!ECCwfxMZa@m_jZb1QeC4+xOGr!RzGgT2`o~0>u z>q@3r+$sIy>51@kU!{#&KOcTw1-0Y4r{pnRdG}nnp5!{3?60(X=-lOO+k8m zLRNCzHC|MqLn#q?=Ja)a-Oms#+}cE_Zh)BXD|ZWE_7NsXC=<6 z#2HHT7EnvrJ=ZC5CTl~UbJoV$tko!bOo3K1oxCM;`%XZUZxp-LS~damDx}<*rzkX;k5h=Q9ok}6uiV6QSP#7F`A6-MhVqd zL=i1VZ|6Q$(5nSgTYa)9j67Pa7d`n>!8(w&`&_)<$W2EzHCwOsD9DaXW$v3#5es6f zli1wGP`Yd~`h2yhOX+312wx1`+1D!&R}`?|`g|-*>B%4YV8fc|CN-0r`P2W&Pm8^*(Dg@Zzd?p}Fep{{dQ?=&wa_ zC9Niz*@~;Ib!P6&d{MlbXmm^&)usaKFDeG^O^j&ces6xY{i*48t9CF{J3x$!Tf5+tl9yqb|O?eu4=@(82eW3-27^%tlB}Vb~;ph zM%66De8#H%(fn#>tlA-~b}m#qt7HC-0e9+jI>A4u zBf{^Z#*`0F5yKL77*$4&Kb=q1%ht}xnj4lY!srTZJx^Pu0zXw_oJ1S6EajF%0-~_@ zF8eX{v|l|1e$hXtXXn)(?W$*URMvTG@fpEnP2U>57#h8x4n{=Psd>gQK0hDhGgkjt zjcJJKhI66v*)EKaixXgsJqF`ccz0urA()5psh7w2l(l%mL`I8mkm+=2^qCoq6;nE` zL9Lq?=$Ecnt3^-Ir`Rl)34Dcm(Kn8lqfX|pvyPrZDWPrM{-^OuOP}DBxkZXIvvZ>U$C0b#x1RE$TPY~J8YE+0`J_ydrAcNHK&H z7eFf#Hlxv+5TdToJX2A6rS;rM@c>(G?#!mUBTy`h_$XZ&gF-R9d<=@Nhu9miMWPTl z^7C!T$cjgIaO(Q5PyxnGg#P5y)?2Ghh)F>5YX#7{BTz47al>Z8gw<_{$t_mXsYTG3 z&S(m3Q2@ncngVFkYPb+4z|2<*!umV3ZcxrFm#&w;hojvVe@3|FGU_jdB$t4M!tp$Z zH|eC30*1!b0%5upJm#`Wo(Ohy9eN>lgX0Ld7CVXk#;w?~-kFZcYVxkiq>~3)v8K}% z@vL1tjZ^?^5>x7mFl^?Y%YFWGbI~IdwHUzc zw&)4yJ-|KaZS|McG>pe-7*&g2pEy%1>L6~*f+&bs2(=hs5JW2$cG>1Hw^6l5wam;4 z04nHL7kc5@jg1d$fgdu2(ozWc85MfKy(Z{VV zCasuz7#?&tf*wo@N62xK@E3j9I#$zW5lsF-O9??zT9O=o z(Fbx7XyIX(;Tbv1R}x5z+(&;pEIIxLF`*E}%?Rsea`}e2fLbT=>EkZSxe>lBqeu{I zt<;^|N6<)e@UQuwPHCNe*|Lb}Nmi-W>OH-UW?%n6zF^^^!Rr<;8M=O{qEe~*lA3Aq zBjv|$+K=n~o8Ph@Lk(Bq&2QU}CH}|P?8jpNd{TTFb{&)MasNt#r_&@E( zLjU8E{aE0C{6F?1Z@4RV!xG-#+f9GN{gd7FHJHz)?gPX^$Vq9qUVi+v{b+ayhwTU9 zl78NN(th-Kd=x)P2yqyI6ZQiEQ&+!WKZww&chA@l;&l4)AMFRxI+5qM?FTVC)%iX9 zLHtb}`hEMsz|3sSohI0kc_~LYmBseQA0YXR!HDEHu}=1qoF5YbNjgBKaA3G3-77jD zT+e(!6H&TM^n8g{uetveV)Yu`kD$cS?@|pA>pEmtDwOE&b*OztB*C*r<7m=ZLGa42 z)Zfy7UVEDoM*+BQ?H$(KZSa_hQBq?vMtjx@y0DbTvMsk@qQ56a)XBZb60Py(h4-2j zvsXMkQu>uC2dNv#g9=|qfRI;1-cg=kl2C;o<&;ECqCG5v2;t=HvaHJbiV@n1S%fry z!D3)0% zJ0&5Y&Wxq-4?Fe(+SKBd|Dpuf-b{YvQ<<1TNqb@Hk&JtwhR|HDJ`L+u3srWJ#Y!|A zYmy2*F1=GU(#;8_B>U6V=f;I!HUl~z|k8|7co;$!E%-Ab)v0=0bTOX(IrzUH!fXp~fk+B!eW z02|g#Yb4>wYpUkoDa7tKSy27_jl6{#h&JzSF*ll7`@d^_|0w5S+X=JdK*JwUh+E^L6y{<4{p`A?ZLh2|m;Wpp9jJA;F z$#%p;s$%4KG(I|aRL>gA3hyD^oi&aymT9gtoa=r#haufPm@N>W_zoypWFf)#IF919 zlEg6FMr)+?D7-hRuh`GSV_0a}7 zrheW0A^f4F_G%ho~CSJGa6@-#>in?sBuW`1g3j1e{H zGBX~j=`t4a&IaiiBcIXy4TB~7gjn4yf46kf<&FB)Txmm-1~?XOOcFX8Y;dAKi6hgw zrlnC>=l~@aGcc7@z=iBkGe_QiMMo&Bj8|beHFYaxCrxJBON^zav??#KmU_R!KgxoQ z)T2>7lFG6Ih5v=u?fyw&Pr1cesRwT6CDOUN=+XmpPF|Z|e(%x)-FA2WsLohhVoLSj zkb8M=GifcMyx65Uc%%99mKNCbb!6195#b(sB#WO$oOlBoOc$J69@ZLit4FQVE zK-*$w$AK~4^k-2VCv+vu?}$JxRVeg2;^cH$Qv;Dkw&IGP518_6DCpXx1un{{=7Qsq zkFW&bGo#@m>O&H&p7B~;uvO|qL0ysY;>T#IuH^LjE@sG(&+#C0K^ZTE54yB^)q62P zt79!YDh$LET&~dFIifeHnX9}FAE=GIW-{rJ0?S8ShLQtKOqGo9h_ZQ$6q82(bU0`H>I%&TG!;TODaKF=65k_ z7vn2BN{gC=rR%_53Weq8P^`Y5A8@sfmto9>gQyF0FDNXNqfmrEK=vVW$zY>Ez_}R^ z6cCs5o6r#!uGDUzgdqdXu55Yk?dz*d69rP+;h2oe6u9Gp91pn>pa61Z3sd~=G9K*M2l-o)I_ z_s6R}g>$sDuUJh?S+UxWBGWC2uk{-uMEtKHMEA6Aw+UqMd0by9e@PffqJzvp{N*pH z?=e)E<%|h^MBo(%myLWfhG%GEj^2}OOe9zAU(Cln9^-j4Q$0?`I{JVWr)nA}Sv9hi zBRtlu^j+66dgAXSwLiRBC%5gp;>a%+d54Y8iUtfMdi|!vwgJTCm5CUy*xR5={&t=x z_qln5BMJKddlGaAkW8?HlR!{Kv2eQ5mzMpLhFs;lAAjz*x}Uz$6CwWGSIW7(C&11B z0s^2a&@Ap3=Mmued;+YSZ2;EBucY}I2r)TRH_r^FFnqod>X`DH-bc2y=|)f{xLoJu3TAPn=$Z(!#5`ydim6IN9I&5X%W*Mj%r=sY3Xb z_oLj~=N}h_C{7)l*G#7E)MO{cve6hlXWHR@*)wws3yzrC_B%q7k$rL7R657(Yf^t7I_0Gi&gIfh8t1L+>&JA~Wd6kelCXk~78Ctx#1UM>y$Mw1hEv}f|!$Jj4 zgcfbqanf2Gu@*`2*H9hoZgEJWeM%q^c(rJZ99E0kPGGeMZA6H+&jiqg9F5beMaCG_ za5U7%5nt^AU+qD*5Yl(5J!=_{i96IS7CxCv{KgqqYuknV8EbK}6eI$Bmf|~G5YHkj zDh*h^pYS;E?sWAz6+Fxu zq6&VUf*mm;Z3{%t8=^<79ZTO5Ml9@ESIFCftQQ1AX>uZv(e;oPBCW)JD{)aJrYK== z55(?|X-KJxpgslay6%fRbi5SkNdD|KsB&4rNNRIeS@FDqMoV%C$_Z=lc@-hQ&9`?l zb|2r*sziy?G3(eBYxQ_svf!C(Hui!bowvTJnNH4@b&gyhD0Gz~!oik=UR3#WRvv6F zFgflF_dtUjux`%IxD_fK9;n+Yc|)BKiRYR_ZdPX1U|Dt;KZOS?&5rFoi{_?y(EQ z0HqI|#i!B~`o$+Rg$mMO%KjVqtGGKD*+Aa92&g45L!;?F_%UtoyKcsCfh7IKGIJvn zBpWSDmZFewY(H6zd*s=kV*{KwKLn=^sh1k1ky|m#`mow}u(UqpKLQSaE@9n~#Yfv( zN48zNqFg&!a1_pAL896HOxZ2}!k!+{Q;ldk!X4u+{|Nvwd3plR2SlgH7GRJ~xL??7 z8{bBg-7JY_$_~-et41*Ua}bBOTRwBj-A115W-l4jC>H}5Vx{hRbbFdVfp4|Ur#j1U zBV0e}g=-a>*XVY$&rr)OtrLs%+8>)H8aF1(LAX2i?ed@4~Y!X8jv8DF7u zU_LrqB@oZ55GEKpP<>mz#N+|)nHX#-?GEqXF6kM=P21Cp0T68Ln9)TPe~_d>B3XCguW*F z89~CRscSzoziYV&bBr>+Ci@>dHGf)9Rqdy#MweNg!G3qM$yWN0gRi0_y(DdNQP&9l zo7t&*%>3r=hNXTepXCEH!`09HY`7xwUBmTSyJQ#@Hb)Gp)?t_??i;KYGPkjrV;H6u zI>C$e!&L5U7h4iPOxNRv7l=KCmvJLaAydvNtCc*V?{CNJgSQYs<2c`76C-;>St1B` z*#df_@n2&fJnd+oGJf}MmLGj^%P?h$%a8j=EG`qF{uvaA0e3a*xP%uN?zpJ&x$q-sLz3LCX; zdcr^~vT(SHRB>CO9us40j*ML?J@k@JUm;>9k8NH%O>Jtnb|T*nh}chUcxhW7p$aE$ zButP5hNqhO({sosv4Hz(2mM9?pFbgz{b_Gn-<$4EG+95#sWeqWVO2MbL!*o_%p0ow ziY=QOCxsh&{=C=aRe1#fcYAe)gWJcxk;wi{^LtE?qGZhf9~NR}eljof%M;~$kX4vW z`*0eO{)_EDom`Yusy&t7dT(#7*P=)mvD@1VL{>O=dwbzK`ud6m#lm-@>`1{=ACvrT zodR!bFTB5xOtE6&_fnv#rA~AXrE;eXzEO6GFm5i=U7XsMdX4W4?DF-Hyo5`!hpR%v zjFY!(){Skapdjfdz%A)Yt-rN35YT&TtG^g127b7&r|2*Gf7oo073A?2^02wQi<)$- zD#b2fZz-y;Psf13Wi{2Snji?Fw-|`Nx>HUrFGt&K$?%Z#cd@UfzsR@Hjx|?TfmLm_ z+1Dffs3pA(4YKKkwm@~PZQ}|`jcwwFYV?%3od7HxY?JpfD{SU_HOZlYfggus#pn1$47Ahc5GDWwaG#TQ&P>j4nDA z?r&p{nCM!C`;_T*1*w)kCQ+Ud-CHyZkY5=+kl)P$HG5@=g9r`l?@dC5AUj^d3@WQU zuzl>rxB0Dso{EJG6JbE@=kzG*usZB10XCt|q_AO%YgM)jn%Rly% z@N#UVeJ=L2B@^Bfm8<5}dc@yE`;0J~$Xa8k4O`~8Byi&}1r(k|DKag#XZyMOtuHoI zIuMS9vP={RJ~B}yliwzr2KOWoro*!%iH+x)^H+Q)^}^_fn-@q5T=DK`tbxQN)P7CJ z@+X*|LcL=+xnP3h7vw0^IAr?>m}puJCQ_((=BFBKnnqzTRz`>8{BDTBqH9$aJ|a$I zrE@iQT3~V5zVcI7Xk*W#qHAAun zcx1&{sZi5*x7nj!(cO5P$oT|S!lreM+a{lOQVZxq;zADj-pA!m%iU=As4|9Ft#B`r zO0(8BQIHLfoDj0f(n-0Q03Y6P0Gbh!QD|ki80n3?@hlkwUCzvbQNojU zgq>^jJ0Jym3g#%cmpzWz)`vc>JW!qG8zPt6qW#mjs%N?qJR(_HG(vWE-)liO}e+ z1$(72B+p!M8~uuG?9N&gS?|a|^&rFqst9Afw!4`MXF3(w$=$~^EtF7-L}Uj6NjGST z`QQOtA(LBb%rQqS_RM)3a)1I!Ez!t$NS5?cuuC1)F1K}r(=aIoq%ZDJFOZ%5;x(~_)!t(?C0e_vx52eIz~o6RT6L4nD5Sg zo*fn85JqxiUGI8c|2^xQN`ZJDS5)Ot<(H3XpV-u|SMq47fEeGEK9Bdi*{9Eg*S*wA zL^E1Ubo!AFe<_cHy4UXQOel8HCH^QT91Gm1e^dT+DGcJl4@wA1!ZvQet2S}rAxBY( zm*AvBsCMB2H(p2kvkEYlw8HIhD`%fo)!4vzxjh=g*IwU-cDF~Z$el3?wG!P>A)i!_ zeS57a|Dg_KG(2IzLqiy*`GYQQ#il02G7Sru1TQ$Z zQKlKSGwCNP92V7zBtEDDf7X@l%8nyKUtWMk5FIC_c`~KjpgMsRR{XyW1RX-w{nGvbfA|`MRd833R<_x z>h#_A>Oi8XG+3C;i6mHkcwY6BWt6HCj=YGyb{>lv?2?0+swN4OI_ikiR=g+#rnc4p zDV;+0AEQSnq@C<|F&+jnkVuBAAnl`mF zIy5l-h6GR~EJqzpmbrv;3v9;jRjJ09e@6;1OF9hr&GU4fjx(?=K1?MwMWg}?TE{fH z-3xZ1opElpSVJopj-e0>%LQ`AMz=+-c&zNfLmCcIX z0sjkxIZ#pgAsRxQHdIs>WQYL-!^ttj#Goq?*LX)Q`l^_lZ6!z8;}TjojwcRDq_De+ z9_3F=izgtymXZB{^bVVHR^)}log1A$HsEHhs9rP4Om(Zu8$d^(BWc)G{)`H{$?5T zAEKVO0h)kq5@nTk2o;3zgjiWtpt2usxS}qVI7%QBL=qKNAJ2RIDakU`7=Nu{XkW`n zq}f6W^?Nux}w6W(VB(n5;U@k_OFLq&u9Po2UVeDxd}lI$b(eRG=Uptt1)RY*$-8+n>u8v!gV=P*D}Es@^dkgu^z34;!n{4e9oz z#LD97Tt#T0VxwHBw4z`Ql_|ZdOtP2si@^~*z>NZ~?jScp(HD6Fl5q;oj~eCLF_%$s zExIwy*G~QQlP~q`UOUBq#r@0NsV~^%3bXtvZdOnfAK%(l2;$0TXei&x)5CgwTlpHE z2pUn>-8?$*SsGU2P!Rb(DB4eB)ikOssgmlLDB^<06(yGjA zX`)nj(b~&34bbtN2}DE?IAxZqJX46UcIwc7xsdK{r*^_Cf|#zIdj8O-ntOJ?P3FjJ z*}sqd>Aq!_4T{Q7!IYa2?sQ?fP-d~Cfc+VUT6qz=oYbpnSDOEh1#(zK>G0d@Q<*bX zEWKJ~tQJL83d9xpXAxx0A?xn%qWlYJlU5aZ^~}wfKztUP>O}^wj&L8-6GSM>Kc+V< z*;uqk`!TtJgL8y_+?W_?dSg`spljKYr`3MceZ|9D@hoVX`ph* zq|v?0`{kdn|lXQh0I0W!kMuY80XZ-^}%m;;BG|fZnJ+{Xe+>jCE4Kf1O9$UB)s3>BTe^lN2NQsP=1UHMQ`Im zsnuM7sx1ue60RA4rczyPvPd6Ck;l9Iwa5SVv;!yuLqL_$i^5?!YE49*znCs?fr6Zh zNbbAbfO4IWjWH0&ps0G`p=|M;)w-8zYcxoi`>yi~mTp7lpb6?p@{_*@hg73p!*@*K z_U-=u4v~bDX5~UCex<_vQEG+$89|)n&gR>K#^+g~#-P2H*G5P$^mi*bkz02wqnp=6 zgmhHfR>p_?{cUOp=dXi6fqaZ8a!J8E6!Pbly7&Ya@ZZga8dEqwBL6;JDYRc#`fTge ztgXY8VA2kvlENGXlBf)DiWKDaMFL@|F$>5;6zm$bO$|`~YW(4@x9VX4gUV6CuZo(- zbO##Kx`M_LYuWN8X2y6j=3kx&g-?b;tYS0z6{j%Nn;iYp8U4hrNjk9SHvFp$CE}%~ z8T!cvGf+jJ>40|c%80qh&URy&IC#duGvfJ)WS-VfEi~~H44;ib$0{_t)CL;mY-ny+ zn&%uE!z+$HtbU{DolzY9C{u{|3W@_-K8L3n_AuB+M^!M93<8ZbYv#%0y7_66Sjc%n zIB>ljuIKG4Z3xVZ`hhC}7u)hPkr7)7)G~a{W&_gmC2H`vsTEe^J;I9>kb60+P0X}` zv&wQ`JC)Iz3XBUvx5~2>#c3bIOD zlIO>E6#4Onm_5L-09gHl-rQkZHDbXv+ z1+0*x?%%$#LSs++$!K^)^14adGqKuoR&cbe75xLoUVX9*qj&&w4tXZXvrH+r50k3n zyNOi)hnXpN%2R*5hRVs$>v9I`ZV?laMHX^Sm^Q#DodZJ7=_YKgB7Eo?0zyopumFm} z(;!BB5~0L`>{YBOVWq$(s@R*cs6PPL=8f5(ySpLVNuwnOd$q_+^)syhiH2S9>g zAMW{F{%GT!gc+c=nCgD@7f9){6J^~hj#0fz+pccNXwrt#A10OyaNzt2>$(Y_omkc# zjaFdO2lVa&A@=EvVRcxAktfQ(Mqs+Lj}s?K%tFUJdOL_jy#W4JYLljc43MT_ zC|R1ucEQrA<)9=oc7VvCF!6i6FI@;HI>X5ErA6$S(xa57B^FE!w&h|!q0*+F5+{~e z4@e4}A&=-L3XLYKB>l{%J!~<~N|WFruto~ujbq{q+Ii-Q6KAd9*aUW(of4)WLWr!k7t z{>X*QO^03VSQt`tzff7fOqv+a3kkyIIYt6Xy!M9u_GyOYPpzGL>8ro-XYp=y8WGxe zEL%JESHJVjU&ilijj9UC3w4G)N!*_i1>Msh&sx{Hw6z$r>A43iKM;*~LPgEqrZ=Xe z#ppY+ylm*WFJ|eNt#&cGcIs#U@|%C5_)N6B zbz>2K+Q({kw3L7_;BI?Qdw8PtDwX^E^JTfUQ-3@1{!c3=@hIj5zyILJiIUs^Ee_~r zx#vV{$i0>&^3%nA9$^$mUf z+%eH%dUkIgSyePa1MXXt>Zeql#d^ZKJNxwIow~lQR;L7bBSX#R0!>tjzV{WCyGb(x z{9S#Eh*PBQ?+%tJ!t20A7Cz;~p>|aVh~hNd1-!bexqqNo%`%DQ;i)BSsH%p93JpD( z_=e~MKMho$Ct#n3eO1{0MKocSZ|Plq{h%Z;i-Zke-1}9Z4VJ<{sBW;{gXltF3Dv1i zHZwQ`RLRwtjcV0BMX~BDqxyk=8LD7E8`Td6RE5=ty0Btzu)4G`bcqRoNa2>yu>pzB z3^So&hnY52FlK(IQ3s&`h{)ezsvveT73tu(s0%-xY4Os48)&EoF~n-*1TNnT1%-kr zziK@%0CEq{BpG`4M*pl@ip~NGbaV8l)Xn~vN4(jQ!Z&G7)P=7gb>Zs{NC*SRMI#bV zbxNh8k&O~E4lr2HI$~ssqDwb? zSip%qOKJm=q#Wk4#0yk-)Wy5Rkc%8`@VRnCv+Ipsd8a#+Th@qqQ~j0ZXq z1oZdmlqPJQ4)p3rSA!oDpjyejS$Ye`p|=hqDRcCeV025$4c#DmxDvAMCFx1ys8_-EUhLGRria4n4{I^Vo?|fBdXT~ut6G}Ln<8~ zWCDQS3lf&MHu7RNC6)uAdQD;qM9uwsK+GZLToBiHgP283f#`9c2gDp=&IK{t4Pq8C z1)}1<42U_zoC{*88^kPP3Pja?1rT$HITyq-1JOlHQawaW+$M|^Rt6qr?&(wJuc_ho68Gji+av3WB`9f~;Uf_IZCuuRO?i)t z@*TJ>xO~YNu1UT44iZPWyhfRpOG<^_ey!}SB10VCsTHNy@>NE&x4j%AD@U(FJc~cT z+34$xD^dd)6D+>Esi_w^jnKW?wTqEuJMlJ1G3LK`Cp~!s^Hq)et`Vq+M!y>L-ieQ0 zDJRK$VW>L=lH_5Yz{9Meu6l^DK@7GJS@85kRjxNDMdN0ks>&bd{Q7DOC1UQ}6ybMG zbDM4|;k=jyoG)%^jX2)GyAe2SeWA*6MLt?&8;)|Ydy6ygNaU@y7V0`(ZQ=d^fHNP9 zdZ$rk!9Sod~x83M^-0@i_UZ?o#v^ z36pz-@BY|RZeOfTJ)?wqNkF|pb(O6g>S8jj@ZnIHNQRgKpLLC4p5s zad?R^K4>+>=0#=q4u`^rLg9ldytqMu9_?RXg<(zfCz`IFu)3N16NFlv3Dh(;CK_QJ zvkh%e1$ZZcCyaT)$=NVNn@3dc{zkHdL<$kS_@2pzG-X`G$a83_!jyAIL=ma@2ra4nQu=^!J$h zOKy7@6@w+I&xfMVg)VRmb*;Hb{hdl@3a4Sbr7h1yzI`UTPU+b4g~r5A;k+E2heP2* zp)kk4*EA!8>|KSMkD*|gKrl)6Z5#{Lj)rQ}s>b{XW?!>fV8y9W?PRFN@$WTeA5=Tt zsdjzY#+gv~*--ej3geJMp{~LK^7&Bsxlov+-fgQA+~^cuT0-U&f1}W)Q23$>vuz3f zcNGqhuZF@egu+);82gpGrZySC0={5Sb6-8A6&cR_Emg&SJ{2PJa~`W_gSIbHkFjJG zerIll!=VBJ1h4v30mrt}(6rV2-MRISg?dLrz3EU7URZ^%&8=`MR5%$boKOXZ+yI7X zXo54Xf))EyRvP#M3Cs~3uFuUDXksq`(z5~5=>Vy)IFnnsCi-L5|KloIIK``4^`hPC zmv5c9*RP5AKnSd~R~+eG4DgC0WfmJvFWOP=*O^^eTvGIMC9qB3L4mRtE8q>Znby88 zkY6ub(-OF~^apzU2W3n0e1?3KeTd*|d8ZW~ix}x}w4)Y6(*(9JgyljkEh|{Xo7Obv z)$7~+>!4V^%e};>m+I?^W$SiwP7F=G!L5)@muqs@vUX!l^mRq+rl6jv_;mulbJXBe z)yLy_dxf3@1GxcdjAxZo>UuQj^y^AR>hd_y_>4uM#tQ_2@T=;!8M`1N*h7@M=;dHC zdM_n(f(CTlER1 z2IM0BuxMnsasqs2o5r!`d=JEMwJ#4nuL9Vv%Uu0NafKF*#Y2AW`ij6l zP4witm)6^@bgwTGMI+ZlzYjqKWvdoxUr>uWErT^A_xn(m7c3!h>_xYEc|b+4)`(~( zYj1zL9e<#R-`9?P!p>AX33)n2Ow3SqHxs$z@*oZ)g{BUE>5Jd|+?Bo`Fmx3TmWRs9 z$}II7s2Z8?v1L+{VGYIo|0i9e^oL1=^2?BQlSGXWV^R1G+eUC zHf53M+`6vOJjl!k_p0&bf zG+Do6o20+AG+}g{47%ckbcOl))7Qpm+A!j~3x?5gHFB`T=%_V9UiDmz4%&D;Nnt+A z5n4Q?7MpI_1fjC(Mi&=-leif>)&-@yW=-@*FxpKS8hrL+woVl!z8%G|J!Me&=99M~ zKn>%WuURy8H7TOctqj5SUJs> zXIQm)o$M&=s2=Wv;gT9EMSwh5a^{hTAOG$Oc0Fu zB=JZn{C$jpp83VpO6O--xPfoMP_Y&3G!4OvJ=Ti27|00&i4}(w6cjsAk6Mr;GlDp_ zwt8n4m@#7TF&E+#4Y?`Xk!dbkC*;DCu|$)W_|qB2x&>_(5ery~OT~^hrfSb{A_g@&C;2`K6lfS#Gen>?+`fWShvH} zZj3A!xFM>KKys6loa`=hGoYI*+-SrLKe@XR9p(2?b(o*ix&ofeH%rJptScxTiiM$j z|Hp-uqH8uJcm+obIw(0>b7wnRX5K6WLmmrgvCNMhjUsoG+)Puj;)VGgK%c`2lCDUo zGvYU;I?1xLA9WFi6uU}?6QU_Jo6C>_AvOxvb;&)UIYwrN!Xl&IjkGKsiNLOY4SEhX zS#a%QopHiQDNc~8=Q&|?7blGF;)F3toFHe^bHey1PAJ*_WjGNSz~zkkNtI`HmD$#n zhDi;y4=a&A$Z&BlAin(;vS;L#EbL|tU*Zt@CLxk3@ml;+Wfty3c(@O!@*MT_{AZNO zQ;vkfvi*$Y!x{;s4%&|)(VwJ34p!Gl2r|w6eEOT95BYJCd#ZB6+zkCiDo=kK1jBw1 z40^sbSp=U;e^JRsf>BZP1~?m>fp?s^obEsx4VsR5#cf5ReoQ%82Rm6QGKc!uY4lPoT6;KTr~hd}L}^-eD0n z{%bfoU73g8*@4uXC_om?9jYMB>~lmCzbOfn=r4nb?0!oF-oP!DRw)ecpO3qvocE?L zLayQ%FKxgn;F`@$pOA(fX>-Jr;$Qk4lHc~1xtvh)_z0uw7xwYJIWvw$yhd!l=lk{82Tj`@@hg;$r%9 zc~Cv5y{aRW%tX{@d>YxMmf_jq@?0JcGZaG6I_%6&)?@L#?&O3T2ygeCh90T^<^L$j zS2&M{ltliE?Y|`9M4n2eUg@d!^wevpb8B=g(9$A^Xd)3=4EZ`0FG*|&zL zZ++NDNbJBJD)@tM*uwbww9x4{(!W6UkLwQ;0pd;munyY68?Wj>Hq~;>dfENEN~&NQ z;*fSlMpRYvQ`HWy%}ioKK$gc8T9H2iAm+aqN9)4P0&olfszz6C4(gn@cT$p%3E@H@ zO1|_>MY^e7vomcp`L>4$LpT&>lk$u(5{B6ZO$;#-_)7Pg1B72;P0W@1^u!6ryBvr`QZlm*(tbN`5oJv` zSUBNu7YoXGVuu>LMRW9-Rd_-fyd-3svj$6G+sbGv&fg0+IAG!DIb>ki94p`Gn|(j> zH*-MC<8w}AVM${|C$enx>7jIIy?MRVbe#hU)-g7N8;z$*yLbfVHlQ+4G2WACkz*te zJ8KN`()KbitiJ#*3x&(Mlw;BoygT_3%Fc`0HP#!%D*hBrl9HwX8RTRr(?UBw%_b6k z{MW^{f0lY2aKb{S+q}b#;gIOLeclqfxI|x2ROYj)5|>~r{1oTo>)Gu)sKXIj)k*SY zc}hu_fOh65J{7lphub_C9_uwT@O0eJ3_Sa!;H);Pv;laU;RwAZ&EWH1NvUyy!w+n$ zCk3qWT#cul#=E*uaZl`U+t<^T>@KuWr!O;Y>zQx6-oZ!D?4t!;_~;pZHl&(Ds>Vld zCU-JFFf4Z(B2yXbxG&wl5yO&sbmVA2V(CZFlHrieM0l{+60#Jg%C1DRx%I}cgyRy} zC|(~S-PhZ?9 znlCGW0FyAmetf+heIVYmonuu{z@Qn`{U=FRriIV$aG*gkfWpOv2 ziaWdbGX|YSteRYK75G>Yg0Fy{(yogpP34h;B0^wMe3^u8=|F zReyKcMsMr6ygI<*N}2Z2FOP=b!u{73--@V}I*Ms7v z=w1uxDL`gNqddw;UB&)}2Ps@}yKFndDvq7_A)3R=s?`gjRVY)oYI`cTtDW38*EHb( zLZ()J<_{vj7-P+7ou7|!)gHBm_|Oc+dLL!qOz(Db?AJs;z>}K$ySUT#4_c8+(W4$k zU+Vk81gBpJ$ufxyD7z9NJHVPfVw$8{|A#$7**e7o?3maZ6s65aaq7F$Ihg!R*lVNJ z6O@STTq>N1HPQFc7|w#)w0!`xNk1ym@1_Pefjtz;?Y5>@_`?p2?2;P}T&$Qh={QPD z8DV3tvYE&b@gDF#wSeuHqLi$GU)eFXY1c^7js7{1u-IS4yKQ-Yczhnp7i>-s0M|Ne>bGf(%QD^m}A-|rA|ILj)wot zl9m%}OyJO2`Zx0gPg+gn&>>7zwD%?@1c^~qP)9mbdEQmc9j$~>admGE$UMM+V`i>FERqx|K2ncQ=WA<*{acD3=8ZNU}#8KHBb=m(w>Qew)qAkz!IIui&JHhQMnL`iWD%?o9Uom zY|3nTR)qDB*OG;MGo5WP4RI=1`ie~`Vub3a;J0n-g1LunY!h!d5EC2YN&=VmBV5H= z@arJNBpEHvkv&T9Q)wueKsug3(I$Ph*MnjuEB0Yz-f1I zm;}NGV=Zw5MY612MLERoo8(3CLqOY!d&_#>t{}$|9f2Yh`#6yiG0H!}l65O}j5p!B ziIzwtu>ig%8`d84PwlLw!8?33oxDom$kdSgtw=UW=L%JlE3e5j`wC>`*bafj_@)Tc ztEsY?6>3JMMiEC3y6|lcYvO`zpi8(6GNQTU?1do?*1fxhm1)usc?a)ITNH!EqGBOw zdU?^C|4iJjA8+Z39CZOvd|oofbp8>8VOJ z5Z>5-_THZAB_L8~(LUlt-PUG_HuszJMyOxE=ks?Vi`DI_JeQWdPhoch7<|GuhTLN6Av4&2pezsTrV~+FKguj zA-$ryEW4@WZ|)a2&(!yKi2pbUD>G>7V;L-M2FuPKRo@2?lX01Grk(P-m|WW+>*nu5UlZm}o=}%)lUqvaYw3n)%`x* zDwF7A3qG(23-z5gcPczVLBbZ9`ve7NcIz;^g3Y|mKOhRw|Lx+hk%J%G$m*^K`6_cx<2tH4Fwldz!pnP|znmAp7%bO$M=vI53+nYMs{Zw{yI$ zwfoeSD~zQ+)V)pBOd_W6f!Apt2G9(F59C6isu>WF*gS;ovtV)Wg!ilK!i}s==8<<{ z=IYW7+)oHAtv(O)G?802JX2w~C7%mGs&>O&eVHP3+|87@p{1>Olwix5D^!Ae;Ze+# zDU`QTb+7ZyzD~`)&YH~``EM9+F9csO!cG(mNf5$GSILS#I92C;(O{+7XfdWxK1YhP zqX?UA^8bXk?K7a*F)#t;F$`J)$9q$xkSPIkQ_~>>rp;E5yCaB>j%(};9Y4kN1Iv$| z+uSLWukNwM$>9~IV!i;=Ercz+ws|mzjfbJPf}sb;4yiof*YVSvb!?Gi3TqLHdOpC2 z)M5rz8tw*W9g4HqLFE-9MTmniuN8Sm97YJ>s{wjlNVgmzxsUJpNn(umA1U;4%<4^1 z-Cvd}0Z?-qWAP1L6g1_2T)Bb3@?8k}Sq~DdnJYvjP&*Uj9cCi+p@$ zU@(+8ncQ%*u*k^-5wTMSXZ4?{);0R?o`KNz!Ge;SHb3Iv^FYnaQ}eWt_%_r;?ig(h zuwEqvlV*S?2%Fr$qX~W5$WZhf5Y7tRF?$agumfGjxTwMxL$ z)$w|_ugfw40Ivk_oC1$ry`Wz_y{s$0pXVxHUoktKMw?P!xuD+hC-?))dU&dciXrrb zR1aUJqSjRG-O$}F@|8`iyeQ-uiMHe1Q#=Dnv@oG8@lW$qb$)*n^ilVyNNU~fJK4+w)x9Sk1y7+ju%0oG{V@bb1_`Z&@?cv+4!Y4P6Ei8*Vk zdWzRvN9hgr!QP&-woauG1@#va6)@dT|G;z#;oU$s% z6Ng!i1Vd0X=SWptEzvQ}obhlG#^xK1;Y7(z=F>zT%laycs^VYt6aS(}`1eBM zD%*5(F>%#x;|375q%(EK`9@r)rw3`|nREuZI|&|=?V*Jd7A?y*0*(jf;kdsWN7*wV zi81h;Z^yBSi9$2}5y~^HSZ=Ruv(|MQlFI1$RXY^%wEen~Z)o1Cuh*HR>=%^FKp_+u zs$u>^waamP36jh&`(_-gDKT$+Mv* zb{y6koC4Hjzo4xNged+PaWZ`P!3!p=O|6~!htRR91yKKO5-qDzB$+4&8ny_lcFCGO z#LQX=_B=R%4%GEvxL#&QgCtvL-V*a=t&#V=MA9&TD3rr0#`S2p?hn`J$q^-}V0n?l zHqUJq!+zw*NFrIucqx^{u`o1bLNgI=T8xft2pL4_GXE_bU;e{3(w?;P6w!jYi`K7+ z@U|>Q)?Mny6_7zJ3#iEUv(w^W}v-HP;Q=6Ti+Wkq~@m0iV2y5Oznd>=; z)jnNNDPARG*X;62bKapZ8NQ2~X$orH$}t`KyGnSz|S=gjT^OcTc0^9!)mGj6PFbT6!uAuw=>GmC=KIn@R^4R^LRPX7h7vix2K=Ehh-WgpEL) z#yN48<(PDN>V2Av${pdBe7obVrlz*K zgDkKQ(#xErV+8LiEchyM3v+IFyoC^)O{1lb;`_OWIq2m=dYJ&4POZtc4)P9tq}z*< zJ+dWrzhIws1-^9LW?B@trla_QT3<#6+p!|{+DT+fB04dXMzrF}MR9lS`>=36?EpX= zVy-E}Lo|r{q9~SYLBli=C+ec_5x~ z7R!N*1md4UE&doqnm7~e$IwBWtmyaQd)6`-5FnI?_&XY7I%tX`j2gcn#r(4Z=ja~7 zTEWUO_m$eB0nxlx42;4)9Zzl6pKkOi*NdJu_1z-HqAcN?IuiwZ+eHP&(P|m^9Lcea zY$ARfxG$;EdOV_`H43wWCOoS_6B#;Yb71xpw-#vA8e4&N#Bik;Qh$26Sz=sBwVTWD z;Zasar5N-q7}UBH`FKcSu~a2ZO@UvfxXzcj&X>^AMX{{Agk*yb2Mr^Wtgw&y)tF}Q z>)~q2t;6M$ueZ?qdI+%C%g#f*yB8|SBJbUC7O#){6yx5t(G)n`=`uu9_=kgK9)aI= zCr$~TB}>X_^f5$sRoDc?W{K29PVhGf#S>v79ig9egv0(|^VB~X7?ExRwI-^jpMgg> zUdvW~N)2z@2$@ooVO}y;9+chyXgDB!A!ekWjwOs9J`O?<9ThauCaHv#oJEL3yUB6d zn5cVTKZPvh=RsPysR}7M?+_Goy|pzF2_}uU2aB36*J)_7U^$NEEu~FukZFsaYY@A? zZh~$9Mc$|L25g}`wZ$(S5o$`}- zI_I}q;(7Uwf%TAei~<;O9nou-5iYzN^_K#+)on=+iRTgISOKXm=ZZIW|-UjoDLgbtJu?On^xHgu9y&=F;wn6R(77$ zCKu&>)W{5kgI@h;t<%hA$pt^XtlIZr8iOQ8!Qu4ojMJ${qlhKkM&qL~XOPQK7BNEw z6N&8mZ?q!PrikU*qKMH%2poz;xN2RNF`e8+4G5l^^4NX>?k$9iKoS(2XBmg_CSWugFvq<<*KnN&P-+>`{NR5l^$Z0snh zlh^7RayvB1%etM5Qrodp_q*KGKczdk8``yVC)P#na{4p~nn3vDNB>QC18r-&s%bbr}zk++4d_-U&a%P~*b0&E< zsx)AhnPmYW0-{??olkP7`wOWU<#Q`Ab$O|SRl8}tg)9PY}o)X(<>7G*BsTFBko)`!N}h9Y$gA^C2Sfs<`) zSBhjKnicoUH_a!Qq!Ll%W%fDZlqm{@mTBRzM&37M+T^7H*^oj(Hkw?>{L8^fx}@>9 zVM9QNjEFA2Eewz-m$ZC69}|m`7|3+eaPWWW^%$w&g~c6FL&xI$ATqo%Vx^g0 zYaYaj&x@LvH86rcmqxpxK9)LqaI9Evlb`K|IO{AmgVZBbSw`io9 z5*vShG%_fqspWofL00rt4yLq>%4d=!*;B#BteyG~Pd;_iR6AY!^$))nTdl-~sofkd zKxV`gdLsLOiF^Mzuc!3?|M~hculZTM^aHDGuNhf`()|8Wq31}yORLz{VjDH-_teak zA{t5tAuGg&L6j6B78}Nj4WT6#i;azKSIB!q$okwL*Ll8PGu7_<^Znkwe|*EgU7;S!f5BE(P-6A`W2_R+;;Rjx;~RW!VKJ|ACh2wr|h zNg8b>Bb>^t7z{3Q!%VBX;fEi8?4WF`kWD#Ypo#Y7vN({db&`zu0`~%Ypr6)4rLJt) zuh;HFEbI=2t+6yD^FrnmLipj z7pY8G0|ZbqF+c%k!t+zI=@lFAL`>)=)rMIwVru%UE5V|vb=yP;)fEae)P^lOgaHj2 zDg@LE8l%}PJmL3MK$dWD8Eqj>>Yk~MpHu4}F%8S=SwD+=52idzRT>sBEQyz#4*w87 znTH25 z@bGZ|NPpfWCszD8Tmvv+PIh=N=5QPi=`+=zJ%R>c(b;568=BMP`ePP8v|a27b%oYh zZ2-`8|9QT;gkRd zLnRz!wzrhEmN${czF0D3FyXsw6d3U%BJhfHyld(xxNBnFEM3oU854@RE zf$5uE_{ZPmew*3kcb4NwXXVRWN^mc!8Kj|gNF@iot1}X5qZmgy^IhO5uZek@4r)(J z9n>zXOO;|_$R?Kb@i;{_puYQfo%~MOc|=ukQoOvZwr|sxv+}8yZ2_+k=OU*A#+Od| zuN7BAp@lP87J`C|TCL=`u;g4@v*DcT zh$cRZ_CNEQ89;LYQj0+lH4FtsMNBRBN!rYrQG;M*^zmxc1pWwH8e(NLGDY*F|NGh1 zh!BgdoLG&f1UwTbL16&Xdgyw1P-aV-&Zs8Jn1<>%XH;t@O@XWb?My*B6rF1AuJn%ya-tqa_ZQ|1AONRt!05&nxR`x}5sgpKF7;fEpW8vVOKMob6 zjrlCh;&>w69aeO(|AO3&3{xAKfM{t54g60gQ63+BhI*WqwwSuzP8MrXCU<}s5Lm?F zuXB4Q4^i0~R?|0;FP{}=#FTVfm@x)lGqi%4+dF1<=eEWCJvS(7HQi^VNY=GJtS;FM z3}(@=d@RZ4!jlz_sIPS?`N{yk!H{5OpJfpz3=R+n^XbMia&U^9C2_TbR2;s(-uk65>Q#$+ri#(JBr>oe-Fco z0~m?KuuN&3$SH7^CyOVw#>hjKvxeA_@C}1z`iNh5|cmjuCl_d@T$D@n8%vuPe37 z7}R)Y75hr?)Nz@v+ss=&piS%$t1%!|e6 zNhfQ=6eeg_igX?rUEuH9kcRTMV{iRE8`82Fc@@{l;DW*6hBQVtwTceS$X*(C_EYOl zn@)B+7;`NY@Rt2|+MxR?+z?3@x#7%a&-jJJ6aZuZlI>+9ka5WX_nR?wFdr4q>F?vj zD#8vFY)RXkMiesB+7#1$UJPR9YirPNLUbW6JIve_8@AKMdYD|YRU&c8r#de#HfjtW zxSh%_-=fRK;HYs|J~&TI<+%KhQVF`8q^$(u!PHF|(AXp>Rf*fZMdqaFXii|Gsl;7G zZjvl%c!hQKJSu2(LH70jR%Xh3pvJBtwL(bsD{w>~?ye;xcdGTDmK>Ka2equSVTHj?D~| zZbc(Y!PLpH?EL+rviUUAfTv0EslrW6OlfY!f8OnjWOn5uJHg(fIl*?WmgDmJc(uIZ zkq|CZJZj$;iv6aaoUqy=$T&sy(4q9Sv<(T`Muw!yHVG_^fb@2v6BS!*SGcDK|GB-H z95fZ0Wpi+w=DA2`b-isr7*4j(Ac197R9mE3>!NnFi&k(T{#y%K>(i{z6p*2W*C>eK zHpdZ1OuVLfZbaNCmE^y7cY{bMHWf8Kfc^5^6vQg3chmL}2Y~KjI&W}-!8$d+d!pJG-KEKet zAyd+sejb6FHv}xN-GE|@HhsJ`eAf0fBRmn4T56z~>HZq>^1hq?B$6dWWAbQmmORkd z6fo7pD0b~2`?HzZK^>0DkdH!hcxt&%fWY@>=1jGu0?L5y+-7u#&b zHe0c+K`{b^LULoLTS!vKfA!#Egd%|?fvC`6dr<5>6~m{H?knhtsPco0?X+Tqd{Jz7 zP;8fqF&3s<#8t?D{orDItr#(46l)A9zE8!>L!?lzkiYHVVoeqbM|cy(mIlR^&{0m` z{|MVg{lv(CWfdFZ6w3kqF;RtXH> z2MgwQl{VQ<_gA^X!}T#~S6R*q(U~a~@B2Gm=w}Tu$!lC6(LH)7NZDV0V@qTze_A}Y z{>gG|Y>W<(wdqDUxY@ep-Gd3W+~~U}?6dD$H8lraL%>!DtR#K&fc$m_oztcYw8q~E!{pqphPf2zg(HCzY zjHn&&x--l+Ni1i0At*b)6-@$)nWhdl`3FHpagaR_&3lEW+jNoc-+M5clSFhPdFXJh z2~yKcNBe03j;6omTtsmP9!JyKX3t4+;48o`y&)2K9upXyA$NWRzDjK__`uVSZO>oF zIOn13ht9J5a6EOI`gB_)Hw@`fIwNhBN#6C@3EZX;{|zqrmdUB!B*8Pd`N}l;eiGi0 zJa*qWMa2)N@couGY5v1l&+%JA_!J+CNKc0ROLTnyLx3{9+@M@jdWV2v=CM0tB2;Gu zln1SPU>?R7#$?Rmhp=a9i-W%~OCY@g3LfddiK^q2k+uzxvlpnU zSIrcnZ9fmOWiXNO#~;%H;;(%qjdK-}b$?(7s~f6(Xj^x#{J1Pc;)WkuP8rr`S zTMY|=1n%aKD8d^?(Gbrg32D&V_-{jY5W8xCgYeygJg(71h&}o6dRv@g2jK)ytMLj^ z3vj}-CJqj8lBXOB1*l24B;$$BX1=I=vW*Niwb=Lor@Z(89>PCsEoaOJ7sE?452`fW z!={EIgVl(ov@(|Mc;HWAu5)$ZK$+rml*I^Os~haAi&~%Zfo_D6tc$Xy!Ahn8!%#>) ztkzMlh@ev+%#gF(`;j@UH4s%VI;(?yaTlzoa|Hwf*EVxtU6^fQ+yN%gn%+%yerS%Z z=rA?6PFLzWopX4@j=;wA7O0mnZ+?o-gqqC)#Xu71#r-;$J5%XfSkc;|J~o;JFoUu| z^CVT&knAeD2*NhUvcHdpvNXZp&ibestW^X*j|uG3T)RvirOReCsbs=wncw-(vJ}V5 z4l-z;l|frY-4}xhZiiW{)$)zI$XM5g22pn~whrCq0;wsvcukY}C~Avt!M3 zGW@v;C(w2+4qQezw-6mHk7d>rJeg>Q7+lE_D9mG4ETHc~4S5~VYJTHo!IZ?qT}Q{c zV(kfEYV#$7FTr&+Sho$*hB5z-A!%V%D9+5Zl6qF8ltiGlyfH^~Re&c)#;8$A95wKD zuuW=!W`7zj5t+djC0A^N&a+kh1p5|Bz9B&ht!f}xIgV@`a}Tu$E@u~Se^8AJGtbNu-usg$5B z{=@Vnq_!)6Ge>rnikj7Ru-VDkh^_Qt8$`&Xe1n-2i8+X}cW9NkiV9ps6{;xP!UYa0 zO=GBwOG{C{XkG@!0M`KyM1Kcjutpu0EcpDvlFBD0ITrFAR zoGrXm+M#5Rc-a9J3oM;IuG2P8b|0YR3FT_?eWa(mnbt*2;xzcrjv4OM|%uJ!V*`UmIKpG4NYba=>JO%q@ z59S^UKa7nbGN|{`64_d_&60Ql7`w#a(ue5K6bZ%&^i~5s$$_4b8_h=TTI;vT7m_Ut zKT9i*Yn7t;4bQa)<2g#e-WHoMv=}vYTnB9)6AUy!&BR*)7}1uE(OHg5v;6xHhSpz! zn%k8}cSPXL!6hqCFI+pA+alKJ3lgFgo;8%FngRMVIG6-k4;Ki=4st4_1!CxI4vbAW zA1`xSJ<&vfv)hcUfvd^MH`zG%>8D_?Hq0Uc!vi4$Ek_b{EkxN#3xv1=KnAFaqhDm+ zDP**dcdy{sd0{Z14)I#W3k$L$x%WWWhdbDv9c=tCf-RUj<4fS~7C7Ng1)<=GDuCW& zuU*aV6tI=52F;`<+#OT{4~X>%`*PLjXk?xeRKxNo>eu4G6@Y0P-BTw?dWkbM<5wd- zjA}dnzm7WTU9TY$>CNetznBpH_!+e~3SUp669hF?u4rc>l7bEJf$#W3fc zGXRc&Kn#D7#lThw>>@@n#rv_tLY@Ev-_NFq2D=H*pJLcDB@#18%;%M9{!UTbDHsEn za+Jcn4qSl)r<2b-O~4JvR1z%^i9(-}8YxSjArg|Ukam1TkyJI(Z@i0z zhD+1pxnA~=aERy?HE)zDbz3Py_aXb*0TV(;hfYPkDhdg6+q;+#kUEs`s?r+sysC>X z`8o4a2BhrIBY+AERMhVz!;$!@2EXM8YrnQg@P*c3k^71jM!P5=B~&%%616pU7A(Oe zmPVwV4i%|!Rh`X0;0~y{t}3*UfVZ&{GwFTi`4rEE_8zaOOtzj)55umesvVw;e|N-N z2bBx0%8Qt799;+gTC|iUzs`lF37s9D@Zk)i`Me7;k4S_@(GI3<=;6~eVYX#3dUut~ z;0Sj{q>O*C8AD#>l!lo)qBT2D^jE7n56XDGMMVeMX6|%pn;Lp~%$WyT!x&j|3?jK* z%Rx<3avo7wm?Uu~+({ha=m5vq!8o=-E|u><E`j4T7;&m}Oyq=Qin)Dgw?~=AJCWvqSagOoSe&7g>ngfoSd!?Z`3xI3 zgZEkp;(ce-NrtU;2k&!obHZ@6{?qpiceREFBb@-PDQQNWF?Zd_;}kPNubCNv0Sx)?)@ru}at*o ze*DS>D16?E$GbJ z<;3r>Ir?l=hpz|mI@1o9g*fIFC1sZu7Org$V-OY}az$Gr%lRg#g3*}i0xZd5tZx^z zldz~#hs{Lo28s4-gQ7CnhQ%-s78EZ+Gm>j?KsWI(GGrExFzZ?RkjsY>A!ZHGd* z>sdVD)&Myz!YQXkP1Nj*y+*H!M|X$Z(yDG$z!JcV#P*&6B$0B?CXM>X^zq7*eu3cJVi&j} zEug)u7TF-Xpj;*^lUirG;g7PN5sK%qNC~oN9h*O#fvP1Sqf28;Bv=B{w4L>#Azl4r zSXO7v*y=8$8j6_71y;^hiuz?1)zeL%60Slsfk22BxJ6g{5iEIN1p!90E?DoXkTbe5 zeb~AHjd7%OBVi~??W}au?+B_=5E)D*NFF3ddL*w@M6^3>;JUzs3;Z#G4G*$Y%FS~4 zfk3cjiar4k!qk&e1@)H&)CU(6D2EG!R2y7e_@{zl1r@$a;)3|~@i?l{p#<#1`3-l< z>0}(mpNuxtp&;dxSV>IVU=?nI)x{|S{l7yhz=YNd6Euw2T1YSsS%k1~|G%M-eF&6- zlc1Iw8($a7z9r^%n>3=xVssf1X!2y)(ahwuaFWzsLHVkz6FA=^xZw;Iw0l^vU3AJ_ zCh(J|1Atv;v~S=M;`AHxq81w%VrAToRP?6vr@DvkD0{ zqcOvUPH)w?tZ^G6GdBQc2DsTl(j&=7bb4HlCp+m`Ln2Nm{1EuNY)*u~SMj|78mC{1 z${^`46^i1};jIkd`lo&35#Z|OB#6V7P&~=}pu)^k6WQ8m2J}Q9Zv+XmIMww^P?u8$ zR#(32vQtttd!w$N0n*ki%>sv}upR~oj2A(A>0qomi&tm70wUocaW86sbpv?W+7Nvl zO=L4)F}DIw;RIHIo1asSQ5AvVxMFn=Q&sULmXh=YMv4uJ)^{fF1cs}V`0YAL+i#TJ zdPy}vZn(nT1)s?RvwE_RH^jYX5#BvB6LdC=J7WES&d5-+<|f@fv`%w85_b1HqZD;v zd~HraJ8A=E_F}Jay9F6`Tx~2FYgLT1PbS({j_)J>?%-1copvLJDJqqszJzo3mkIn= zybt>=p4JV*J|~`q5~0U7uFLJ{uU^A7kv`4_BkohY*`POwfSIF9e~fDCVDhesPK!93R$h6k#)|8T zfh&|y>2ill9}VQoUeDb)8S_8#AR80w_KI2Dr0rHVpSGKURv8{);gROAqO00?XDmRB z7<4>HZ5WAMNDLfFdA zt%sGhovLG1XmM^w%|7W+ZKg%936Qoj<%OrN%j?v{F$5hGg6jMb>@{1M=*-N0Q3`6t znd{~!IxWs?ttYWilkC7OrH-atE*Ver?}j@G*mImCzA@h1r%3Jp$}Ejg_P;SpV^qx2 zXhdq_EP5kM)G-TNo39uB$gqeE(zV1mr_thmO{*Bf%%l1n-uSxMTQw}DCh<6SDo#BE zx;r9FxoL2TdI4w*XEoAwD~RC0Y#J5(CE-<_2yP z;OHjfiKU=73NS=znl)FYPBriHeD}4M^vNURXlGQpjkQs^3`G@kZ zR5gqBqvbk1hWU#f8)fO^qP29UO#c;8LgFavhS0|F;=E2(;YGQf{Yzv+MRT(Z zeN**;Ud;%W8Kdm4V%U)j!!0$2Ln^^+#wlrjahPvjmL2EoD?FO8~W^TD{Pt= zW>f?9%%P5LtCovCOnZzz6h%m4P!O~+I5k!8xQYwRMJQ2z1=Q6|^?^c!!gl&ox)w0_ zeSRL(FP+JtF^yo#1{o>zKa(`Bc!O`l{@ zzwX2GI&{sA+Yri$2n?Yex+bz`!Ci!0)aSC~pUYw|DuIX(sqjRH29@^5oz^vvGh<~X zXAF|L>}JfEhvetc&6(@S5Z81$TX(F*{EkE0v1;?5$m-ZFMMWr+q+&**n4QkVflA{}SC=_r!fF&y z51Ip_jtrfpo!LH2@V$K6+nr(MjI2iUZ`<$9?l)yS%;9(|Q2+HXbpq!isaTV94}#V+ z3x-i?J4810bKr`K()69JPH<#Eq$6VSyNz_sIMAkMv;GiONUqBoUuT^SEN@U9bAdK_ z!$DpGYSa9+I&&En(;~G^x{1_pN$Dy7@zhVLto4YJoifyMl*9|;e_%Tlp3XJm(k1f@-qEW=|E&DeUPa^g7WT*q3dkVCn{hnxr~X;^_l6H5cnoq zVGKZO(Te5LdiI_u>T z=}slbMp|4YEbD=@aB!bDS{=tm*1I~I`HXOU;T z!R!RiL^DvvMg^85XHj9ypQH$=E({F|>KUZ&g461FPu2RJQO5v6J^F2<4$6{iZ8YkH zWj51sD9dnHQyqESHDNlllj%(6WSwP+=e4U-A1xeZDl^fpt}8$eOQ>+yT+O}?4SpX^ zO^0c!*g0|N!d%V;ld|RDt9mV=T@2}AGd5VDeZ%Ty=8u{Lq?^_~>`WM&PV1C74E?9| zdOk=gcCu-vl%f_0_tTnzp#yM$bsp7~zew7wu3U@(L8y+IJ{pnH|1d@OE>9Hf~HmQKxyXYtKNU zM;-){cBL~0r}+==Z_x(vCKhG#0Qs!Wh)`T1klG*)X8r?|0-KdsI`Ctp?W}QWMAP+V zE4e(dfS0i$91+wqkajrMV-P@0YfXqV~whG*SujA=*|5?c6(TX0crjk zjV!PV+D8){ZU1&8*sd+qJnyiTV(vpXdrqKj6J~lUhQumRbfPUyd~Mg95ks<8^^#f9 zD=vn_>fL=ny)@L-%XW4nJ=@r=-aS^YKqPOlTkjxJ4Rtw?5AWYNs(r*|Qh-aFsvqL5 zBKxh$Hki_6xXe|}W_zo;QJd^(@}dZbN`O6@eLX~1%HB`+V_Wdz?I7JM;&KJ37qED& zmlxs?Djikg!I_wZcEf-vO^N+fs&eye>9v@g28;<-$9rkv{y3k^xvJroOH3FhHR>Ec zh(`}2ng<)ea58Env5J)?MBl&a4T4xNdTu8doBdJgyDjluwLZ#$t9 z8AGh|>e`7DI0YBh0e>A!lJW!)tdB!R`x)tEbCPAK1#&~D8(a+HW0S#I!E=M#RO;vlf=t0~F+q z;O;LiL|VW+2<2v2F>P-(*M}_j43jqx9&pV7L7I^#q$8MX_7yh`G6JFaF@omdCzztm zy-1fP+0K2N>EC~;?dQO^`6Z_WD})AO^I65{wDehd*;5AV#xa(W0E5(6@Q%osCvS3O zj{j5bQo4Vk!BLD{#9!+kbiQ}kmzK};{}7t0+g*UliV^F^snhaU*A-saBtyw8y-aK$ zL&SM?6s&FpYZTBU=tNsDjF3AVzb{1ZrE49Ey_xP;G8a9llfGQ8 z;6NffE=NQl9yzJL#d+r151>;{_j9>&Zh?YMq5Et-G5gbSC__OhsndfF4}4W&zH9C$ zYbQT&Wrg)3&I?ux4(Rf5(B(Esb6t+3Ns14C6RWyb%KZk>^@1f@PM+3k0x^ERh;0;m8!xMP@I(5uN1WqTmq znL3^`rt>fT0zZLNnuN(C&GHcYL{Kgvs)v@E>KJ#qW&dDVKd2p&=4-)Hl0>su+mkL} zB~|r!ZD_33{!~~W%R|i>;Y@c}6ERC!$WT^QxSUfmU9>l2h@w)lsraCfe$QkB$-zKo zI>gz(#B+F~XR%=8Y2Aj3Q4BO@lZ(%>HB$@t5d#Eor84M%&Qpvz8D%u_)U3?BZ#p}d z(X<2nk)w@%3=nj!t9So3SlC(q2Qm6BOiSNLqU>61>BxY<2dc1K4bVB#Ybo0y zWnVCC;FFn}D=&ZB|6scCRKNQL?K4827mj)!TN-R0u={p%v`F#Fh;dg!g{s116ko(U z#5Z;t9v*a`i=1=rbSNrcKO%}cm&_apAY%;N)e67{1_Qw5;fin;fXM#_AU*~LX$1f< zVL(K}s?-9cU_}2n7zFj z5UJ}Bqq@!vXY0~bO^aFSRwYaZtT&=!j%2Qg<-$taKaX}nN?PUDC4N}oli2V|pEw1K zl@fX`>c8g6#<1^QQjxe(I1GcIEI-=)B@CPfgR@p9XwG_ z+Xm!8vHzfl1y)FQ)8MU`E&ZT?bO$f()M|V3#9PkTFrD5lFWrVz#W5w zow1qXHw8_xNbE5*3j{}5GigtbjH#$mn^KHodv7P6R3$kT&M1(t0Pewj#B>A_Q4cOp z@_RzxhbQ6n5IkdDi?J34UJ3((5ES)c7>rGp=&TYh*wFgghTaIRRA)DZIOmzM7lVq( zu7K_ufI?37s<9jy)0yT+<%>U@jQVdjvC_>lbt=$#h8JJV>+_@@1a)K2m3RV^?)-q0@7QH8LANkSB)Zv zAaj_g2i_@i=xQCM2?-bv(?tTt-Zbo6TM**sF!7~@QTFmY|1YovG=tifyP}$N2BxIe&Y>iBtQ}b4 z-sVJ@YDpRw$%E%S!H9)k5KaCAQ()Hw8_)Gl3YZ7i5-@eS#IrV$K0 z&B_1lq7(V>_2mm#MWMXiU7qF+u{0bdN;059GiXvS%ndD^O(SDQh;s0DcC*4k=_lzW z7jwqcKhx6B z=^&n>`(a>^_&k=ZvdYxTF<<~&Qo$Avt7$Kj!}$^P9Nw0o=Mo@ex>!0a zzQGL?LN?!(?%{gY4+A>>oDnJHZ@()|<3GI02uuA>2k+P4oqWI6M^@s02;Of^<=_AP z?&Z+{4zZW}mDtgogd|DRf^I9cm8oz0zu$06TWD=dY){p*e+gCtgZkkju@V!R_@*S% zscKRm$)Rx7J&08-{!Eas#2bL z){*JlXXCf$ObxgCM1F?PaGbDWpfmqPTL>8A8q*9-gK_}I(_j*w2JkWxe*v96OGcZd zlH%Z;;_%dGAgQ0nl7N$hLL)^oZ=2)IWF%)ZzYWCh-Epxf-EOJGtTkkFZozgto@=!`pRarwI-ZJHqmO zd!VR^Fcuz9%cxh6OC!9F22DZ?o}f4#|!uWfAXIKA)$yLnbodc zBBX(>`>gg|J@$$5zli-WnifmXXx~2Gu0x0RnOVB?Qv&Px-@zV-p+a&x$>u_QS@myK zEIEWb;TOa0uLeNC*v%?oI|x%HxH0eeNWyPagD8RNX(c$6)kG%@WEFxuB1RHc)_Xj^r^aQdZy>e@AUKoNKwBY6n@n z3;{U^9EKIPC=cl-rBt)cj=tU+yxpi2qHPY+7FIcMBKU2HNOz)u7m@H#dvS4T!zAd=$)Hc^l@lRCtRO_-h{T}tdY+WRa zYR!wfbp)^$$sC*&3h>gX5v0wQ#($~~UkMYO%H+o3%$!R&`x)XG!(nYuX0=iXx*>R8 z&wi93^P#ebVt#XKFzzE#tq!w8jMR?YCT-@-am~vOlgF9q38R)ET_2=Ng0wM6cLlX= z3u<2xyx$Z&FV{0zEe+Bg!Taq&`gV|RmI>yLmQnVtdQJ6P>~wm#qPMQPNH&Ny;zKlZ zOZU|>KSy3znPaxOfWvdlG}8ppe%S(#h=mxuC%B}G0(ABT)$a|`odjehEC!J3<9VY0 zZhcCRch~Pd^hLbK5rQi%L}t}umwX!K3>AC~O#)B4EK}?sXPFK!vksHQnPt~ z-zwG?w1t4IW@G|?t;&(E4$@{~RAj$$5CI2qS~Cp@XgDoMQ&uW3=>}3wX7=K{(_N-` zD&)7bZySR=o0O-;*Z_LRjSV&jY+%Q!!v-w@8#rO>orhUB7|$%YWf8_d>wTe*1R%y^=-w`NB!oygDw0Tw<5VbsCQXX*Y1a; zGbXGx%|XT$LeLKKw6QVOolA_&s{;f%&=Uk#wn1>IJVys3_`X>sraWP!$1$_m$3ip@ z66VfyO?D$&DWrQ4nuNkEv_@}($cnHgXtosz0on)jnEitQu|hK$gy3AFMn=xc>(s|P7C2-glFL$dDOkEq@Oi&ANVfo! zDGMQWnRrNYwQsx!SAb%dwNGT_YLkPpGw`*QKyd{)-cEcxNNc^x#n&rF?5-r2_|DE-X)O~Mu~C92b&_XwJ}R%r-3fLa^* zhPy_}&LDFrgM&O%cyuF(#Rzvu5-d+m0>{TiN;yx3`z;xXg9nBuHl}3an{eZQ>a#Ld7a^14_An^a#r6h)W#bU^0e$jq1Hk#W0|><>4} zfx!kVFLVIfq6c0Q(q}*)XMUr}4H8XTS*>NdgGB2na|X8j@Hm={3Y?S;VHxkBJg)_S z;4Mqp3Soc<-m>SN`c6dzbwSfUp5^D6BWZ2#EE>jcer7NtVuca>Nb*t9C=GV+sZc!V z{~daOjNOFyb{@2i4jrg%bdr3caByHI4qL0nfod;rG^J_6BBtn) zHR(Bt5UN@Ng~cH8A_KxErbI~f3#TS$tBEg>O5&9)ZKN@|A%r7DPNHWsEZEzIx;TMp z!NR_tGvb4MPQD{6fl-6aMhi9MMv}z|v4UdqxbFug3vn5ig2Q66Izzjb1d`SC_^_Zo z;M4>S)eD}~$`T>Q(x#Y+7+)+x#gr$^-F@Qyo-T8?a(&&*+o#5;#kM zfmSOwnsKp`Vf@$>a|>B*5AedHM|a}@#2qN-QqqWBE#(6bZ_q+Z%7`_-VF}{^Ble^A z?!u?-ETB2ZIZAh>CQSGl@FLnFU!LsK`Me7jH|W6Tz!&c~ho%S*PwE&;!ar^bgMn-s zI}9}3Usj9_P6&kzt86Wlh)uP)6Jv1)>5w}m+@ELj-D20OOR(n$vdaW($X0od*6ho% zhiDc6h0k4-P1+C&Sa6FPt$i6hPBrHZV6|SGg%6U7vv8HEIi9Mc3-aWVO@sMj@P3e$ zWJJ3RS4Hw~5)s&yNYYq17Kz0|9(cgJ39gWg#$ukCa8jS@H{-OQ5&Dfz?c*Ki%n&{v zBWuAZdK?Kae&!gEU&qpF69D51Evo_iL(kT?{GV{eTqtNZp~Fl~!yyk)QRyr8ZqVCu zohf2(wfc|61tR+V71&pNXbS1?5||EmjNPb{;r!RfhI7j8s~pyg1cr1i^oJ>rQ5JV~ zI|%pnj-gS6%hH^l{9;9sU&l*WY+;9TguvJXQBCk4Re}NvjI|{{l$7omZUkG9)wLZU zD1V8jWFe>@i$4a0ULKAS3ubDfRM2D`?Ko2CEG-RM2WejKc;h(D2HeF&EceGZ0z+qv z%XI=U?zZaV_24&(v>+g`-O~d37vdduE+^ezz&rs(#7aF1h&T{jK5X0-;;x@~o2i~- zX!qi-ns-Ne#RQQZl%`Xq@OML?-M-ml8{xN&pCW=R1z=i3qb5 zhs;OI2>1M>rN(J;Hr9n(k!Z6HobJ|vza=n3=qR70-U=R^?NhMMOi?i{V3UJ%0cZRr zbpcL?$9a;kA1au)^grr?!yyuUdTh8`pbIFtNMJ}eCUpT>{;Mv4SNGEelxtCW8x7F~ zo1i)!7Dd<$S1?h7c=-OuIXeai- zk-P0Ajad!`rzm1;=^Tn0Ra}sk&utOb&JulFxLRao3B;xgZT;L!XkMOLCwJ@7PCtaf z-#KJ0G(usFW|Se3c8=!vZ7-8|AtroJg^=139rm(|8pf)Tp$E+2G?--%Sxky1aKCNk zvU3k|>@UtT;UO(}U?_Mzl>K!qoSwR$ln@`(LSl+y*7S>rEVL!R z^nwE<F1CMt%98`Eg=Y3(w#xWao*`a{ytxrT}6Azofai$$F2 z^AP?-FwoNVpcCFDy+AcfHpBl4>pI6D4vEC@OyW|({29dUvr!4Niv3DVS=c6$$Nu;r zSf6~!V1355RIq+KmB{g@0FRh`t0U8oSoxk_*y8i`{X%``E*q=U=Xzh@_w*v}yU#r~ z;uAGrv{n~+=w#?3si6BM5Nm^F)Zo?g?iF?$Zf(oO?X+xn3&K~3e{+R-krRJd>n%1Z zVSk8p8iK3IE0MUy_D)OL!e9a*aDP5vw>Fy+VozUlniPY2LtZ=r8H34B!nQyKHN}uO zCo481*7iDten|+am!MFZZH&-{A8!*Il;(FBfw-eF8~jT*M`m@^dXmBd+DP~@%n9gT z*&)6~Xd&;3h##I#_^uYP-RE!WQfqOa*ft?PU$APovUP`E46} z@Nba*g2raG%SzAUf1#{sKmrjZ!i8S&V8nluhiH0cX4Ka7O?RCNPljgMHgMCx=oZC7 zI7aYsMYL*hMLe{5zay#LU9f86cn2Cl#O2|OQhr;FrR`pyfB2$2$VC>L3 z2mPQX+r1(*4#yd!(4~ZAnqs?J!IdMvMG#IhepJ?$vU5o$gyK3x-b&0`wYY_P2xHZN#0LSLZYJUgZf0;A-N6`mOUT<_`O-ONCPs*r zP4l3L8Xp*8N#hVkMXn##XyJZtXghEak!psnx*PdaS3lbJO;E5E!WYhE zrL3lUcxgR-*Kf@sl}W$j`mKo$WS|{xYV&0*oaWBSm-p5Mrgz4-uD)4n-rKDf_?!w# zhrUhDZyOf+fC|&{Ufu$f7?j}}K%PVXHx{-WM54Q6;2FAbt^=5crvlWM$)5wgfx^2l@#&NW(WQVq1Z1v&SCe{SQRC zag?X+XFD)sNMPzw2_xNLGMQZxji15?nsm69J#eYeW*-gNTHPX|<1`oqG-=4X8;0qzw>NSgIvt>p=n$67DHK z&?pJJ@lr~JzW2l)Y=1K4;h{Hpg>qoY-YJ30euj9@MVqQ606A=EX0}+Qsw~!L3dUKxeQ@<+I51_L(IEA+RLOVH=Au^!DA2BgAiBslQ-wNT$>OhJ#m% zu8t(ULQf-{M*BphDzSKxBj&qdk9`5UUd;wqacDn!p#Rh)hlb=jbVC+o6lXzOckO)$ zZK_t1B{i7!PatgSejB`Y-^aO6cT3e2Oo^i^0$@Z8BtqPSqSM*i79Q>j_v9ravV^-+ z->|~@Z3`z{;dpsu)P9AzDc-SpNL>pKXd?d;2y-ae5>wQ_WC%oK9Pd!jrhB5eCHN)< z4dYJYH^~IO$dUS`d&!p;pX}I3B0+P5CSxj#0{itYpo!qXsEXhO%--@yDA&^zBm2?F z#OV!QUCqR4HRBt+n!38$x_Ym+-ph>ge&Dp~^Xt9IwY?|Sd(rr*b#>nPlN%<@KJSF_ zwbOe~YG|0*>w@a~HotnEKf9*Bp;yD?8S~Ds?LDb_#)N4#b(9=lJ+F3l!>QA1rhC!D z&#ay{yT*Rk&#NC_J#Cs7%@|ePFp0!Ic@2}N*Lcyyh?@HP>ht-PF>LaT3El}_uY%qG zKmYH`|Ih!k?H>R2Pbaq##Qi-g5-GvpAJC@G#%G$JX}n|UGmY1I{woWk*^i8zUei$R z6%-Z~mz0*3S5#J?H@;?~=Xn?MZ{O&Umk|zmQ7@G2-xD0|)ybZB7I}IG-*fmK=AZZT z5P}%;nos8+<=*6#U`(Ddr+V7t3Fp<;PMbV~?ro^xUtt-4ii`MDRx+Ev`d6Vpg=P9% zT%^D5)69mtiPLJUd1pyk2@EuITFuMACeEHQetPvx3e6thFjWkqHt(X%`wnRHn0Yg6U|luY+I%?e z6HPLB4)HJ9X0_Fo9Z}OTsdj>Q3Y8C-TsLy|^nsIWrcIbVW3q6bUISti`XuTnUs#i< zotS6cE0ukG^^6&{4T3?Gkxvk_&?4NJi zG=t@Y{sa-|ghcHOYO-FoQdM+y)x3t9`cqs_Nnni(1J#eII;}(gGl~PE2jJ&A_&;X` zznSFEBORjl4w=!AA5`L)_Sh46RZ}NE=#!W|qh{{R+B*7y{HrNma4dNpp#c`s_Y|gLrTeJlIIt9JtHr(-SNiZT#(ca7FOFEf22bop?}XJ+GdBVy*GD z6X@@#VTmkGG>v>-^#toAXqk{CRo6k3`tg%$Cd@|RsxcELTO%ga&zmt`^qN*XeyXr_ zziK8VM$B%gnM)m>_Zjv5lyu_cX*G%JX>~Q#6Xqpq=1#6}sJB-WYHJ*E?A?C3#!suQ zubD8hu7>K9%rnzTE@Bl6d0l`Dz-d%}P>z{=Kb-FYXHT!0URyWMwFW%^roG&QI>TBk zawMwj>uc&75)-SzcEU`w1Av31ZZ@iEdQHNp-6t_)^0Y*NOkzR}{l>o;<7*BUC&$BW z1uRfP#zDPZzx7E>tC_K%6!xB7U)P(4RZr`Eel2y*s7-jVqL<0P_9)Xh|2p!oGtZ-E z&zMj>Zw#X&$uDXq%&;G0Ce5zXi-C2M`8&G0VRoIKYR;>hU0pYCL^bNqpIJ9~+KB3T zLt*)$v!~7TXP-a2zF{=Fux9#sHFc+rZ>S|1Sv$ub2h@zW6sme&cmDO|pY*(X`eyz; z%D-*=`xpNX0j~o7jpW~S{i~ifv9@kHykS(luyzJwF+;Da8z$Gzu;$iJg4KHKlfLwB z^KRPY^A32gH%6YC-eb<{J+yiT|F?G4&KOZEa*P-~rl_ExtXDyK0skZEQ&7PF?b$u? z_bm67#C^4A_oTmxKX`xRtM>fAiarH@+!Dd&zU$pULcFc%cz;#z$ewjwmcVq40!)-s*h;Pcg*_7fzn37aGbmo5#+)^IqQ3V=@Mh0+ zn%xwelre}U;6h4B+11s}p4pHfuLIL?;e_gj>e(}<&Zxa$MjOV3<7ygEPeBv);FwV; zSj!;;8a6%T#i0Je43}QWb2`tLl4g=#MJgTrn;_rv;CUYTm9IH?UQH^#Sr?=alSX)c zlr&2EM36p3s`{QI75puvg7XHc>iJ8MPN<$XX83@aqi3H#zTxD$3+B52C;B9!-f1(v z-VkEe?+QQh^;6_mdKqO7 zKP}`P6Y#a*WZI}eK1>;Gc^K3&aeTsLjJ1iieEBsTTs?Cp+&RLADsPLjQ;%e09O()mQ=Es#A5@`=O9P=K7q6w@;Zf7FdBLHGU-NAJu7b?Qsvpf@03idTgAqF zJA!`&?cBHi{1%PX#tVR1!TXt{D@m^-)%!XmlWo7RK z=R)2o-%S3c@Cd|R<&^g<(iOq?IKPv07cT3m`x4S|q*nxW36|G*BFdK%mw9B{&z_`w|99|pZHI0TL%0Iqe9+qgZl0c())vY1^0rw$qhB7MWuyB zrDa8>#UzFLQC`8X;>rSk7FOEp%JMROD588uf?`*?xS+JK z+`%XWb_MekWi2l&sw}Q7F>u8dg(YQ`d2P$yQoYUg1bkLc_|KP#;-bQeqOwW|QCQ-Lq(+yQl@(ML7MDPhg7P9quOeDs3?NMbi;Kac3=GsOg(ao* z7c{4uvNBMqC@6DeEH5fA1>v$ns8?K4R9ab3T71MbN(pAcds`j%7-qLrHmYX+cFr z8GTU*l+yAfBNY{u7njq+r6ncB&;y!7G>~&0QCL`9R-qm*FQSLaAV1UtcEEO}WfdjG zG@+ubpo0GjVVcUaipt7>8H*}PiVA5*3AEwA0@$deqNJpgVIH1W#vWmn3F+7ucQQahrJMvVv$7Ui;H21ia>q}3kwTi{(_5a zX(x;+)gTUr8xY+J1gN|MO;Q|4aUog+^1!BWlQA?}3I$PuNEKJ2We^u9+!g#PBfA*O z*t=YeSOzN;P!H7(fEE&so3c?D4nW@cPsFORqls zU%T>#e!jeTUt`4;pBFVe@Imj@_rG`SGbi`GGj{BnlMgxTt-il`YxJJa)1U9L@|Q3D zap>NfnywQ!jJWc=)7h7Qk@4_lojxgToZEBVQG+%< zGV%DUKPcStVD6}w`wpM8_KbqW_lI76;xk>3>+$r^zus`z)P6gkcyC?12k$CbH~q*z z&wc;$Vdw1}{m!IyANOA}p>FZiHN~|j&f0dx9}e&S^9O%uKPh&j_wtf!UcB#rKA3oP z*QYv$2S3^J!xy{zZu`Zsao-d?HY_x~VfnvroO92jUguVnK6Ur)e$(|QUD9{ktM@!H zd+}v!hsRF;XX&fw485}IwHq>*{pr&$PN{$R!{!TLz46LR_hx_l{3oAwj(0y|$WN-i z|MbqUM_zEj_MaWG?cQ;}y`$rI6JOepcgBs&*C(#rT5;K+n;-XnSYLX^IlrE<;f%}1 zKeXiB`o!%=E?iUc&6T;Ip11M+-epTyKfmLsH$$^`{o|gCj(_v_r)=o>&5W|6b6+fe zcu=ptHD|0@KlRqEMKdPZl8Zh|F{9l|q ziq4?D{HNo{{###*Qa_cFgR?jVDYheqq4S#otW5`I?=xe$)Kv zk-6KypYr)57v6oz*upD*HQ#0Ya z7`&CfT{WQX~T}L9q{_rzg_rk z(XOmhzCHhMBQJP<%EI$r-Syj(SG{t>H~xyeH|Lz%EqrUhnI%j7QD5>5`rYEEpXqts z%P$-yc%nx2!+;oh@Izn||8g?pb)%_9rI&xZ=7F z{U7<$i+N{X*JIt-op*Ns!xNbq@uQx;@s@YWrj=iR)vBWWoUCp4_xk?F>jn(E{LyJY zKe_0-oEH!M_S+vH>2k{_AN}n&=P!6?)rk-E-M3)ry6#)AS#g2?*Vhht<(8q}-?3-; zF#{f2_~s#hKIP2*{g3?akA({gZ+yS*=&2L`S(-oi^nKkHd~wpnSH1Rmnpbm5(Yq7- z?l|}UYrCzxY)X3nleRs*wExiC%FcXv{M*;x72SF7fD6u=G&1j_*{7X!McK<~cWv#` zWmfp3HD`_fTf6%rCtdN^ z<9|D5LGx{)ucue-dwD>^yC;0Vec5S?!q484G5Y&|pK-|b-&OwP!tqC+QrPm+F;8A{ z{~4w8n(i38@$(x_-2K7WlHOOJ@$9fi$N#Q$_WYmSbZBkp`_I-qbo*WXj$ZhiuIqpQ z-b>HUyRXZ+17E)S^P?`vTk+2qfBKVMSHALH_OB0{J?E+$@BGb06T6?ktMS`c%g*W8 z`Lpka{O*YUe{VeI!{1%DXwRX)Ts^Pb$ML46>mT3s@Sk2E`#^c-mcr}0tl9A1C#$+I zPXFST%d=1Z_rtet`|yVC*RMMN#VOwTufF=!U$^&|es<5_Z{Ge?)i}?;^R257pV3tR z%|E^yJAL%r{?`xL^1$s^P8l_OW#xl|C*8F4t6wd6H*!P45*t_riI%T-UU) z?;H1Cl=1QAxtD(R_MaC#vSvb7)6!dq{`C8|J}i3em0yp0b3yEkTbn=spR<4V%7Qz$ z_dWaapAPsPy`z4WgYH;-Pp@VL1@zA&n= z?wE|H3Kl(;KcPqIna}K9`P9h5p}YEhyz7>y%RapEg05GT{ps8XIt}cw?Bp4XetFRu zALTE){OenmAJ*xK*RK4y_q+c*qWI&VHMCsv{xANXJLijYHow067R8N|MXe0?0YtDSeE}n$6pTF`NsFtzx?BfS3mvm?00^D|DSu!n()+)uUG6I zSA5MQudTUh#Jp>s+I3sWrk<6J>zfyzv9I~hyLX-cy#Lh&gPPwPQ1|F&JoQUrSo5({?l)M zdHc(Qs>W?xdE$K^G%n2eq+$2vPrq~e+)vwGGy2f#l~;HF&9q}qSbF}M6MsH)=db3y z^4RFV-2Ha9C%v!#S~PIhJ5w81=D)tB{mPw<|GN2^n_qctaq}w=#~M4nl2^U^;s<|d zS-5)hrN7Qd{GxH{ylLljJ@KKHuYb3`Z(2iH!QWmUe?;L6pWpl3iWOJI4jHiTFHi3q z_tXVzmhO3OK(E1F&iMW}pZ9%YZI?GUe7R?6w-LR%yx(>9^Z&ZKSK1+m-Ez+O=DBBW zJMZj9>G(gdy<_p%*O&j}wfB!1cz104MX@bMjk#dq zkJned`Osm*H}^dvb-F{m5TG`BLJg&o8K-a$@%#D?Y!h?xGEsj5+D6 z~-Jjd@?WHe#5PkoRYx-|o z_`xOr`nICqA^omCb=%;FM%~}zrgLXj47$2Y_5;7j{O$Wa-dfjp&XU+qK7aR`ci#HA zG3(Fw4FCAW!&bhwZS~$OhIh|>W%z4%oV)SJYa&^1e0^W{^Cx?W=LXF=zGmj4Emt&b ztKaT>d*0b~Y~Pb#c>S;2HZNK__H=JneA1rG*Dmvu`ySiPzWA`B$@7&wB z<%ihoU#@s*{Jlp`KXmAIONSOb)im&g0YCivilXSqYkzs`2`A0zvTMcA+kZIr-Ks7f z#(j0u-*-Ru_26w?8a}w^kiT?%{Natc>n4=^>i_iiCh$}}|KtC;+3&TJT~rF$m#mc~ zgd&kr*}3+RB}izzFf1ls)_kaBJ zcs8%u=ggTiXXf0Q*BJ1#?P7UxhTG}w7sao4u~$qbElCg7RJC4qKdYTHths%M)y8S{ z5!bpXy$bdn>)5Vll`>A@-KlSiTYN0=sT@51X>?2X$GZ0{e81v$vA7bQR}Sqto9^3n zw)gu@NwfM*_gr0ME22%FR`87VPIIU=@n~zU@HO`d_uYOjW-5h#@>0T+{o4({H(}85I-zhl_)LD{| z%lH0D&$;@0hx(Bx)2lvp`5L99-q-)Qk>mKtvC1#KPup!b-TU#{hRMRI;A-yL^K>+m zr{!)wDQ6wlNPcAMeZ)X7;Mzm5jdLH6&T^b> zK4uo;S=rK5?Av%XxOHaZAqN+JyleHOF}-CrW5!)tsZA?)WZQ9Fw8xLPZ8z}LuQ=`6 z&oaedKlNy&{%Y5Wy=%YKzd1Yceq5l}f3Kc+@FG1k<72Pn z7HpoZ6fO>MTE3#cGd=mUG`nE8A7?Ax_Obj(q=@yI%3UHH3FnV8RfNsu=N8?%#d1~6hqmSMDn19}7lDu-V(d@y~6pwq6X$d!_*~$%z%5x9V)ifRW ztP@}x+~Jt^(#N34u~T3R+bP2k4;mZ$sJ06)H!}65bVylUXlneamRDbAe415o&M-*v zSWsZ;iD#9nJ1o}uyls7G`)%t(R;38BXyS0G@wvL$ilG&=iJPzSoMLM_H}UiavDKaH@xiq7ssQ5koQmfewkLl%IE*s3N+c~v6Zn7Y`dsWKWEsn&U;{y{?H-GKAb5`PT;GXKk z;@%m?Acc%56 z!?sNS#8AKJHk-VOO1^iS?p}|45YnoD>s^=Y1sA*XtwsAjPxCLkuT!1#FN1rfZNoX?2luL?2C$thwhw6rD$1%|)NkYutj39R zsBGG@+F4|1CGk4mr!0Mv@F9}%V69T3>!y9DnfvG`S})|ro%PnaP0f5mmR*ogR#%tc zTM?7=sZQU^T4JoRIp9_P&>R&G=#!h(X%Jog?tl~)#HelVd-opG9Xo}8>gZ4XrkUNjV|4WWi(%>0 zw09GZFQ0a|J)Zo+NY8vD&;7DPzC#xyE7oq<_U>H%wiW)Fd)vkC=Q_!5z5Hrp$6I28 z(E)0OAL5#`u~fNw{RKx3wKu-kIIqxN?L%#v@+R3PH6pMpu0~U5f824WP01m@YO{7d z^&QDI%nahdKJ^r(ridPN$W1b}VzS_=wKd;!?{JA(2zym_DXZs-(Te=*2i~N)@OKV; z_HLo(NWR&^L|0(H^1#}JRZKGt=MLF!e9~-Q<;qlC=4KL79-3MaL!&OS?mnw!pZ1NS zgjn10fDhFBQiD(4j(xUbkMa$bQFDRDZ<@SA&Nrf+WE$S+dAMIymOf+i8TWR1`bPDw zg{xOLzQ0qcE@yX&kDdu{eeP>P0KZ4x$xzj6D-Gjg)>nT@Hec0bHq_npYNzz;0Ie4$ zhyDF6cXAF2L=W6v~>9B;WC~&eld_>!hfF_WByBqUW2~2qWs>_Ewh~ z+P^-x;cG-}$K%11MI~SDdZyZ|w~A(u86M3V;(6m-nW(B;w4$V~v(-B)v28Hy`|r(L zC2zJKJ-y5*FDQIXhTLyX>t8n7S7bM*b6E6wGHg;$UH{|P)0W?6(LA1MEvJ}Tu<)^4 zQ8S^6=53BwtON89_ns+#;&& z%g?FNSqUb@4kw(gi+VnFd-g7aL20Ive11U1&AbyqE_bE3?R#f*|88Q^M^^mLtvKa0 zmRE(vYQI7R;}c`XFWrlXFk^XX$*{g#r1Z8|G3{6uQH+DRbgMRp^t)%e(mLf)8j}yB zO}rQ$-HFlfR9&9s8ux29U*sWS?#p$4qv*ZNI=;8=b_`Xw&VKBYl}(z6#=FR!%PLjU zvhhB#F{DySf~juicdK0lTfn1&ZTzm(2jk^8-q8;3??|k9!BTRs&LCAI8(!?!X{wbY z%>;cEy2IL7R!3~%-%{9+lbdO*$zfOjOoGV!-h#XQ$-r7U`a*%dj_Pl>pFZcw9)8t( zBffLVe~S~5~{U+OmN7HV3 zOXL=4n9yA}eDY5e^H<0b}O^y7$^}F6i{3O{mk8H4H^H57BaHNv#te3amTu%4k zQ0uu`lKpw)3gIsOD!#hvQDhI(;pF%p&2-D;{J~^oPd9(XE%(LbeWnKI9b~63XtSDG z)6W;Xo%ai}_qSy1y{p{TUh!lk0j!#rHX42_qM@P@Z7e7UdpphJi~D(-Ha8ZRC-;N> z%4+ObpO@v}p;r0Bagf_d$*?Our^fsEj@4M*#4zC^Wnn^xr|hK+dztmU7^e7W)%Uf! zx^I?crEOdpgTY2vi4yPR7fG9$X-n;ER*P z)#DKt2nHD2f7m3yt|_RBvG-!6ZuW~BWhKP+uzbXC)hH#x+p6TMGQb@WC^6n&YH`4WBk zyZIxBfa8n`>f6pbM+8K<#tUyf{4jdT z|J*LM*KG%04xQt^dRJg**mUF9=Jw68#sk$oTZbKT^K-U|PTzkncg11IZNzU=Ud!jW z34v)58(r%}9SQbWvzhldmHJeN%U<~3<|<1*kb9@gR>!J2&vDzcPmkqh^#;sO+%Z%W z9K6fY7q4}n)&z`Z9rGxHjU;H@W8Lx(pcCpU5yJT^+TaD0Dd!_KyT!4<#1o=o?) zEkAtv;a2g@Hd$-bbBmf|RETei?_L_`C}$oqV1G4rJ#uz=kMia~4|&)1o&`<6r8sM( zPEzkoraMw!XOKL3NcENU?LyjtLl3O|6uPH>y^e|t{?vK(z|h7jv2@p&*Plzz7mh|U zwz@D4%G2M!!?t==ZD(O_Jgz=IQv?4);zhx_Vy2f*r+R~4H#)Bk>LM2K#nLy0tL))9 zQ-Sl|ZK|;=qok!GGt$fY*4ky;hSs(fFj&ugd|XPiPtM}#_ML%tgvPzr#bGO(N;Y1z zb8vm38XNGk$HNuIZ5*f09~QRx zT;*2da{h4k%KDc>8kasFeD9_^QdhHrb#UBxpsnEw=Ya+5EoeWYx*7{6t;PhWUxa*>g_u91^_E>!4&z5vG zO7I8|U@Vq7@N?SmEZ`l^dB$kWQ`w7A4Pt}`$Pw6YPu0EkvEwbgShH~nyl((_F z%Ot;^`8s6riKPMiaQNJfp%2Q=WzOO<@63MIUa;Qy)^GS2_Ov&s+T;6qiQ{K3`kJa$I0}WVv2= zSA_1IdyVXQvuuY{XCjXy`$w)hy^)Y3pW6=C+G@ldez@J!CT97y?ca{3jjFIHSl|6% z5HON@{_6fK4U%-vYJL=13YZt*!WbDI{`}S4B;)(a*I^ykvh?i_XQonnx@SH)iyeM# zguBmn`BDXL`OcYlM{fx3$iS_Cq1U)pn=db9?ny5mrUQs#2u0T)n-*| z-8X?<_&eY3y!K`TdXLGFx|-MMB0L52pjZk2}(e6CR^ok(zT;7;p`qIP}L z#r#@#s-R4JgH^=SJYA|62?-{h6EAM$=~gDJK1rx+xUerFa$n>M&Q6zrLXTMa@?&F4 zE@L@sUomb@>ZFsOb6kC0XW7V;3cAk*)gN_^2nih=ap3Pu?(L!WhJlrrM!Ih0+f9-lZT`Ff{;L7n5?e)m-?SA}_0m$T+y=o*_Tenab?w=TV@ zus<>JSc2C^tK{Yev-QoB+(MPAs{mK3?2zBy#GV`J=cX)6*Cq8$4yUpmm`r>d zy-^jG<5gN!=lnzl@^>~ur-o%PdT(ZpZntx zi43kXLPh$_@xyzE64Tz=95lH2fzCJLE=QaNXK3A*>474x*ygx3*IwT6+#_)FW3VId z(~_$3Yp-H$TD(5kq@S<8toLT81(W{mLvPj$t5b))SpD^++cnnI9KK!G2MlS1A4%pE zQNK+nM>%}B=ch_ho<$bn1 zbMC~2Xzek7E-9IWsb_4jD`(f+CApR}NQYQm2#H9FcRd__dusEV7}YMtx(iuV->Izo zoF@~dN9fq=ZY*p25m;9_+{-T*`a@^y?dx_&<+*)b*S)fL?3U-cl*3<{G27inO|Mo^WLJ!<8A)rOsnF%gKS|HU z8!S+F&AmZ)qv-BSJBv>4yPR^>C$;hC*2uu0?{^j|op;d4juPIG+LOao%I~&2;>d=! zxID3kwH-SIGOpi|)`C`If`*@?lhu{~Xs@nJ*{J_7AZ!w%#4sX;#x+ zK9#jUx7md&?x?iLIbs9VP&U{3YTKD``X)E$ zq3>qztB$*wi>jXf<=q?m^--YT{-m?^NfnzbQuShPh+^lSGzY6xRc7yW(z3XINv`%c z&7t$OZm#VEH9MrcsDvh}KYSI^d9SrtMpT9kkrs;T>9T8VJAy(?~guUu+qe_lxbP+x=eT4(uuZ#(&rM zZIIXK8+$fJ4o7c+;VVPA5{0_E9`>C4bZS%Mljp}16O_0+B{n7Pe<$6lOlz2PTba7& zQQ7IK=JS>J8lnW)K3`dd*Gx#cD45Q9zUz4j{>;__#`dziRBM{|9AP)KHo7JFf+fQ7 znvS%ZtkAjcTj}qg_Mdn<`*qM#UqrNl3CnN^LHI8%dGRhU4u(c|4aWCu3jCD#Yuw`GPbo=`UZQ4G; zR$g0e_YjHBd*gwv1L4=6h6zouNACN%?S?G3cAv;zx(v~>bIxN5y>EVBG!?uiYw)1t zYA9|*Glgq|PEO{lkPFksk;Ar0VnVT5CqL8DOKYXQ7hQL|axD9sanTj}lR;#J4ObCH~CGhj&Io~*mQ$y=NfmBJfkx|B^9f0 z^aZ|+-TAAo>_j^s-$l)`k=TM$CE~YyMG7M_gzMX{7WgDzTj6&lUaGVI!RPV&F}qkc zyiSNF28U$J96rD^&7FLL7+pR%o5CMo)pPpIWZLwX!}-LlO-EMwtuvR~6?l9qdqszh zXjEKMv|7iT_3mda#T1S!e{&hHTzBH+E-JkW-?1av$E7wGyZTE=Nk;6>W*re~_x&x- zY%i{U@xqOt&TcR1+JySVu5Uy?W9oI0Ha>X}wrBa@^fCxvx0quh_8V^}<`QguE zRu6SPXL@Ef%{-g0X(&&vqV%0M>$3x=)q_hN?cJMb8aJ2dx4sYAQ+lWoPm>rMS)SPQ z`Y3<5R|en2+He8h(y@>hF}X7X{h6#6Mz1;u*~tjZ_=Ns&5NdsX?`zBkXH|8FJ8j)# z8**@lV;nD!_fpZ^?ZCBgN78F~p5rmjd8rdAwe{GYv>d}H2Gy5VK4E_GX`<(f#PJkpO0^ZiC`!*1$)a8CnN_`MWpZ%q+Xp@DGnP zQxD`-iaYSXyLtPB&g1IKQ@Rd2#lNfDzY-QJOwugc62u*nr|_+%l0{s;(@Ou*qk`BW zgW4B&pXcp466v>hu4dPmi=}tWumPv6P%fL?oYzr&Ik7UL|Yu^qi^LafvOjPHQS=U#`F9*1Y5P?C8&w?IkAn;-VBC zj!!*PxV$U1=85Oim$#o|)@Ls^V_Uw4$wcDmwtr?gy1t@*s@6i(u1fTAv%UScciP$C zF9x2g5a>_|s}8Mhdo=JVrG8iSM+f|tUdf(9D$Tx?HGYL2y}ut>@C0(EP@7l{4mW?< z%(x;kMdOy4@DLHS5ur=ugRT=%0M~LI2=j&L9KJ z?zlM}YnhAAv9CG$cMu;vmGsqkv0gnKcjJkLb@t&SbQ9a#wo>8yj=nkeYfAL`b?WIe zb}ZVF(rl+3ZcTnm?P>`6se1OQXmCf-omGL?j+!d-_|dC0mAo{3b!$0+>CD+xar!jj z0$xq6QLDO)O#D^8+(>P_`oS=$F7L|@yUYaIY#XhLn2TxL3eOdu-O1*^-_#{G-g&fc z_;LhB?-9T1)@}Cj5u;VUgH2f&K1$Jjl}qP{`@2Dwe$_Tjr{`Gs@{EuEA0CZ~%S0S) zKf-YD!(IbhGl4L#?`r)S_upj7xG3{hm8gc^{Zk0U(V*}UiZ&!E9|XTpmCD@=5r_d)MS|Ctd5Ty4ZCW7HYSG}sS&I>kU0wW{y%dvh82+*_yC6f;mHQ(NHIT?HaEb=CsDFP zdL%idrZx-70ev|o{d@O=hGe#+qAhB$!Gf?8w#s4H=Y#s($RvF)1vA~pmE55k^94-cZVorka% zX@?4F@2ZR*X-gYP7PfcwGlPx1wi-B07E&{|GnO*&bdmrTH&ttCZ`RS(^9l0T^YgOP za}6XqD#*jWGf3G{H~{v;3wuHqey#_J!rnea2S^3OZbEuuVEzx0P*~wj!m6b3pCLTD z8ng$M=;`I}>;n6XojqNhTw#BkA1bAd^e-+;E?*V(N!NJ5UKu(rNlU(lqE*@+^ zn2$)x6~jy*K4`(j{!A5iRC)zKUa((OP8My{TDmuq6f+*;Nl@Y~N%emlu^Hk?LOj^- z2kX#f(YpNgvP!TV9~Pg(s&ZIcFQ))2x?#@&>^)FHn>pm=Ko150KW@!;*h_Sl$m?C18cUGA#2~g)JOty9wH?0PDwLow^Dv>6V3s z^ROfy7U9E&3|LeS8wOxEfE?_HP=GxOu+E>fR2}w+z+MMff)1<7*Ten;Sl$o23ZRH+ zeLrk;fP`RKIyhO{S=w7VS$bOfSo&EWv<$TLw=`dDIItVv5w_!NO@z}Bngr03uoCv= zAQQ0)WYp%7ECMoW)yOWoL|z9n+P{O`r9nmy4GO2QL{5Cc1v} zCGr7~1u5>aAftWNC`=*9Xg@BhBN%)x$b%pYQ{3@vww*N^$oF8TD1D-cy#ywIGXA+y_8j2Qs=J z92{gBwOk{>1h4|afGGgOLAwU%00lr9xCo?EXa{o!2m?J07lCx39q0w70SxpNtOle2 zL%;+G2Eu@Ppc&`@&}Qp2ARWj9ih*aqOJEwnU|+i^APYDEUKB9ccdi8cEHwdhz!q=@ zFxb;c1JDC1zy=B$U|s@pfa^dn@B^3z_+jsy5TFWZPzVFF96);s+kmG4EgqgBU_GD$ zr~w*)K41b^0JgwopceQDd;xv}80^Db52ym>fCbH?d z0JXpvFa?Oh9zV1%QWda;XRQb*21)=7+Px?s3+w@kfrmgZ@DsqGtr`KQfERECxCgWY zpMgG_4SfM12uJ|3fChl^;4pO{9T)@V09sz?V<|9#sRF12dVmSA2S^8UfPA16cn$Oc z7$00SU;po*%#!@C3Yoc3=n?1%3dS zJCqkN0Rn(fAP&&<#9^rb<^|6;upNj3;(*IQFVF{!0^b148;AXh0y7$(`xr1!!!rtC z#n8W&K>C0w;0QPa-oW$?@V^6lhyiaP1PBA7fq39Na1po+VC_&|0DA)cBOng=0Ed9j z0QMBF1Ly_D0qhwL+XdJHfdKX#(gChe$O1DP$OG^l@QwuZ07D?_1rEyv9(LleR^ZfY z*pCZb2e4jvHv#;>IskhIV27ocJ9jJW|&kZm~0sD-@?gLH03jiB{X9h3< z_5jv^4RGKq4l4z4Ly#_j4a2(w5Czl$Z3Km@RHcpiR2eFI;CQQ$X#{la0s zKp5~6=mow2a3&!}09b)7fC;b%umz%kc%TR<21)>I5{H>hfj_Vx@CN*VKp+?h10sQV zAQvbG%79v+4rm8n0N6ARYXL@qNdS$~da_}pma_~~EMJCc0)iFGFkv90bs1I+^nL;V z32>i6BRVR~kOM|-IA&}FPy?5XW>AmYP+HKR?e~-R_9pLTPo%iTgAW?VTm*Zj5tIqc z3ZT{$$_1uAxM>1tKY4{Q*}P9NpHR#miuoB#>g~`sgNgQzqkNUXjDa$km@JmT-rosS zr+^0|sA>QMR90Zk5EjKj`D2`9Z=gog0N`c{YkiPWX6M;4xFRZ135joYKS)lJc2mSDe0km0+98hfgdUp zN)zQN3-M7o(0CPr?tu!pAyb!Pno-Ps6w{t!x=~CYiWx*PkAjKvAm<<8>SzKQF+c$o z~v_R9L@%09MNial5`io%XjeK{(K!@ZF0xP5l_YWln zuQ5=Ogs!?1@+*dX(S6VX8Rd`a2Mh5bdifwtIuIw+*~`mO%f}g%V`11~k^&x3!UfIa z2xy}4LW;aFEDRUmW9JR}C!jCTd^$V4`}_YEGvioTEoxir|E*(h^K`yO0}tbeqBSpxBXnFxa2NBM<{J z2PEO;D~*(}kW^FDQrytAh#F`Hy84U~((2N-Cuq|2tbqq3m~ z%mnO4lyaar4d7-0Ch6M2ya#p{iXD}Cza8j}F(WBU0UsDmfE$|r&AUT;CAmj0b>Hme z>FebIiej9xI`FNaq=%ZT5Ags}a=34eqZK7t{9jIx;vY_niA}+THxsTE-8U(?8nRXu zA8DjT(UhDn)CUb#_#eagdPB+JiN>%&2v-W>Q1hUudqMS2Qo@3!VfVpL2fANMogzQ% zE9Bn|?zT{W6nD^S7x>40`wVgWAPj0Zs4Jgut0X0ikZ&-ihkXZs)O|1YC+S%O|0!@q z-9GBNm-_qI1t3Lp7)qZJ{Qpj$^b(QZL zSF1OdhOiNiiku6)$osQ>FM4L+g}4u%jALXO#Xv7;vW@(rMq!XU9b6q1ru8z9h9IsC zCEx_Ntze?w!2wM40pL62jII*JuY>Re&_kY2LttYyC%dDe&115AF1WwLqpvUMOV3d# zlp>hu@x%R|n-j(kV;NWgRWQ{NstBrGG<#@H(wwGgqG_iYq#2?4PD4w}PrHJaiH?=d zkj{v%gszruif)EZie8p}AH6lb8+|%`6a6##7=}28c!skKHyFwoZZq6rxX;kU(9H0V zp_QQ%{<;}HFnnZ~Wx&Bf9V~cOJO^GCzX7j}--6f2Z^awocfj8syfxkle-M8Ze;a=X z{}|s6f4%q*_>cH+_(?n?0hH1bI0&kQ4Fny+7J@!uE5V3hPS{PbBsdV92myp+gp-6= zLK5LT;WFVWA(N0x$R}JQ+$P*5)Dap9j|uJY*9m_g2~z}&QI&BcqY?3;eY)y=D5y zG{Q8-^qmREtj4UtypQ=Pb2W1<^JC_A=2y(!%x{@777vyKEU#HG)+W}+@Yl|2#BRcF z#cs=fnmwNV7JCJI1A8<3atH>$eiQx> z{xJS={@?t{0_p+=0!9MS0`UTs0@VUd0__4*0+`@(As!(?Az>k1A$=huArqlcp)jFm zLNA0ygvNwsg;<3}g=K}Ug}sFj2>S_N6iyee5pEYA6~ zEIlSOKC>~iJyRlEGy7LImQ#~cm(!B-BS$hSBk-~K1biO81Ye48#JAuv0yROLAV*LnSQ8=$ zX9yPvX@my^CPp?!aYkK61IFEqry0*M<}sEsHZVS5e9Jh>$i&3Pqzh*|g)v1i-C(-I z)WOur6wZ8{`4n>|b2f7Wa}%>ZOA1Rh%VU;CRs;6k?0eWxu$Qpcu-{|vWM|_D<6`38 z&wYSfh}VX1O3*@RuaKpXwa_i02BB`DA3|}$$r3V>Hj-QnX6gXv-PsEvqwRBV%aaTQ*(%O$Z;rgsBu_wa`JNXD)BN1C=28Y z&RItjW8#t9|}dWqP$?RB?zH-Db?yy$u5^E=P)Kd0?j-XYkb)}hg%+p(h~ z{WV?hs@^rd)WgEVtB1vg6^2!Y)rR$l4TrZ4n+%%`TMX|Vwj8z|wjFjDb|3Z~4jc|0 zJ~DiAICD6ExOTLCG-&eCAxj`RnUA+8kU>b7Jl^7srL-ai8>f_~! zibAXK-v?hPKi#_jeI%$GlJ-)1ng1%RlcTSnogXR@gY1750WM}f27dkjQ4ARU+Mt_B z5dW{DAw`dj|6NdDBGH{m;(wp2H>eG?@dA~8%>PYbA1?`MLiHpzApwK;Tag^3TdBrr<)xBz3Z6j}_cv>*mMLl<}{) zG}5Fj^lu(S@DS$uml&YbcqQ}vb=i<6+p7fsB^)T3CHi;-L8Yzc{}*qVtCKW1i~L*W zXmq@W_g`X>D`YL-zj(R&8bdpE6y;dBMre!V@O#Dn?Lyp%LE`^bGN>C;$=0#YUnQv$ zNjFbo`GO*7GzAftCTAeI&_}_PnG{fpXFf7neJ@FSFUd!mb0G=REJa)zr6j}gw=2}M z>|$<|paw`CwH(|0C4%;AN0N^`=e*N?J70G#Pe(HsS0_J(f5t~@sTBqP&g&0l)a(CI zK-5T-{*e}`l9UPa85@rkQ(_= zNo{x>|1GJ_Fe}z^^;z)xGaZU=s3a<%oe#=!Atbpx~?I2PUqa;ge_-g+LiE9TLLq-2lC|^<^ z7W$vdCAqYw4Cb}k&)4uPio1^0i+ zQW8C*{~-YK3YCP%b-{nJD3WCD{S==?_rGd`94eF&`oFG3aHk*e|v8C_xrc>7M=g&HIr|PiMun>3;b6a^F>DQfxo=aF%EF{ zLZ~FPYjuq&5DpL<+Ol9q>n!TsnjG-N}+Z5#L&=~^lcQ?1IZ6w*eF?D zJjv^Tzn33TZ@JhXU!*8Owy^ucC|BAQKF~qOKQwIjWaGVD%T2|@hK%}D_8sS81L%JqsdeYMeO5R-<(Qj~iq?-2dbj_w(Xd-Hd zbV&mcJt8_tfjIw2kzXzUH`7-;Z&&I0#O7b7|Ig`2LLI=^#L0EBA6@#&M}4gdyxP3x zz5m~e59w?p9wd5Dx)VxYeTb9+xwhtCvH#!X^G9a|U6GeFr7v(>cyuYZ9jd`y`0u9R z@8C}KLk$s@=;}km^7uOzG_d^(Pa7pAV>lbZbHS0+Fi^dX@v^EL-^$ivK}>mdDD z>uBKVcZ5K)yZUZ}ZYc~0v(>+@lRULQiX*?0{eq!qJBDK;1wzxa`3ecgu`Ial40eTI z6eDo_l%y6zp>por3#Vk*9mg?{e9@PWNF2Ek@EOO>(KCwT0~M?ojpLvc2%S7(uw7gl z1v&8#S9@1y#gqSZg`a^>;m8+EDmWTF#^6{<8Iv5gla2>Dy)=v|oaz&cVww z!4JBB{x~Quy&D}5kc49uTSxh|m*SL+ zxB9p`yO6`AEjp49u|X5gOE}K?kYq<4SLo7xFE6GCuNU3)KdgEgIPUpKXrcU)1@BiD zug=l+psO#MZfXa?(tM)N`m4C*^RXyd5glCN^_hv|ns@(m*?rdHg~Gl8a>23}-5`Vg zUXF4^)f^nhd;$yM6y$Sp0`sm*?*Xc3bRY6iQ$(*$@^T(=Y0{VBeDrkv!xeoIDEL<& zlv*LW|0oV1j{egQEW)w<>11Xfcx^tz`Cz08z;zt+l91kZj>Yr&kb;p4Wdf&g!91e` zw|q%Z@P@8|=uGs1qn-bB-*FRK>tcyu`i;IA!XY_mwgTrpFY$v^%x?W%ezU-``LcR@ zi%Y{dZ+Lv*RHbqpX{%;KhzIF0 zaHM?0x%XFC(w(ULD=ejU>;H15OuFtbtszL6T)7SNcN1MH8uvBM-%aE|Ufn>R%lbBv zFVvG5K)P_EPcx1H=}<;;P7iQu{})3ms5e(X5xEu|GdV9C%1VrfIC`|a1O49c2*-6^~!YLYA;7%P}s>@=S}e0`sD*$g(J}XI+$)*cN4F_C;BRV^LP+ zT#)5umoLh4T#K?i_oA%8vnVU_F3Riq7G)*=MOj&3QC1P8kw&$-f{tWJ%L!2gd0~p6 zfTiFVyrIth9f&@@%jiAe)INVW@e@b2SydHd1Rn>O;1M0A?1PrUZwuhNPj0=mthB7K z7&?2@S4~D{Nt+@1qMuxRU7ch+JPz)cg4k|wqN%W*tg?fhlcOCvzrn%YQI!Y>xhSl6 zK!>8(qjNmqybn1?c{@dM+MRG}=@u_qS!sC{X*tZ81?E!lzPE9rOed%%SoZt-q3?}S y&K_R&u<%TR!xPTFf+cR=(9cLYdHbQ0?<5HJ{vPg9Fo%TG>?G*XS5YY#9sMs)5cO{W literal 124070 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 b?<3H)nQ$#j+q#p`2^>i}bcww*j8y**aiuL3 diff --git a/middleware/http/wasm/internal/e2e-guests/rewrite/main.wasm b/middleware/http/wasm/internal/e2e-guests/rewrite/main.wasm index f3bc7e41bf904df726a83392b4a373416ba98225..382b68f63ec5097b12b046b73769bff6f0be3ec3 100755 GIT binary patch literal 7540 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 Udu_8%y*^#&HCH;bjaT~r2TjZ= Date: Tue, 11 Jul 2023 11:21:29 -0400 Subject: [PATCH 11/19] [Component Metadata Schema] Adds Common/Shared Authentication Profiles for AWS (#2958) Signed-off-by: Roberto Rojas Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> --- .../builtin-authentication-profiles.go | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.build-tools/pkg/metadataschema/builtin-authentication-profiles.go b/.build-tools/pkg/metadataschema/builtin-authentication-profiles.go index b49dd3b8a8..d2fb6983b8 100644 --- a/.build-tools/pkg/metadataschema/builtin-authentication-profiles.go +++ b/.build-tools/pkg/metadataschema/builtin-authentication-profiles.go @@ -20,6 +20,34 @@ import ( // ParseBuiltinAuthenticationProfile returns an AuthenticationProfile(s) from a given BuiltinAuthenticationProfile. func ParseBuiltinAuthenticationProfile(bi BuiltinAuthenticationProfile) ([]AuthenticationProfile, error) { switch bi.Name { + case "aws": + return []AuthenticationProfile{ + { + Title: "AWS: Access Key ID and Secret Access Key", + Description: "Authenticate using an Access Key ID and Secret Access Key included in the metadata", + Metadata: []Metadata{ + { + Name: "accessKey", + Required: true, + Sensitive: true, + Description: "AWS access key associated with an IAM account", + Example: `"AKIAIOSFODNN7EXAMPLE"`, + }, + { + Name: "secretKey", + Required: true, + Sensitive: true, + Description: "The secret key associated with the access key", + Example: `"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"`, + }, + }, + }, + { + Title: "AWS: Credentials from Environment Variables", + Description: "Use AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from the environment", + Metadata: []Metadata{}, + }, + }, nil case "azuread": azureEnvironmentMetadata := Metadata{ Name: "azureEnvironment", From 760948a533106a8e8e4769781700dc67e9687016 Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:50:03 -0700 Subject: [PATCH 12/19] Local storage binding: disable access to other system folders for security reasons (#2947) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- bindings/localstorage/localstorage.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bindings/localstorage/localstorage.go b/bindings/localstorage/localstorage.go index a038fd4ce4..654bc54346 100644 --- a/bindings/localstorage/localstorage.go +++ b/bindings/localstorage/localstorage.go @@ -40,6 +40,9 @@ const ( // List of root paths that are disallowed var disallowedRootPaths = []string{ + filepath.Clean("/proc"), + filepath.Clean("/sys"), + filepath.Clean("/boot"), // See: https://github.com/dapr/components-contrib/issues/2444 filepath.Clean("/var/run/secrets"), } From fd8e3a208674713572dd94fa0282ed1fe27be064 Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:51:58 -0700 Subject: [PATCH 13/19] Fixes in HTTP binding (#2981) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Bernd Verst --- bindings/http/http.go | 47 +++++++++++++------------------------- bindings/http/http_test.go | 16 ------------- 2 files changed, 16 insertions(+), 47 deletions(-) diff --git a/bindings/http/http.go b/bindings/http/http.go index 3acc9dff2d..63fe4ee4f6 100644 --- a/bindings/http/http.go +++ b/bindings/http/http.go @@ -29,7 +29,6 @@ import ( "strconv" "strings" "time" - "unicode" "github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/internal/utils" @@ -102,11 +101,11 @@ func (h *HTTPSource) Init(_ context.Context, meta bindings.Metadata) error { // See guidance on proper HTTP client settings here: // https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779 dialer := &net.Dialer{ - Timeout: 5 * time.Second, + Timeout: 15 * time.Second, } netTransport := &http.Transport{ Dial: dialer.Dial, - TLSHandshakeTimeout: 5 * time.Second, + TLSHandshakeTimeout: 15 * time.Second, TLSClientConfig: tlsConfig, } @@ -150,17 +149,11 @@ func (h *HTTPSource) readMTLSClientCertificates(tlsConfig *tls.Config) error { func (h *HTTPSource) setTLSRenegotiation(tlsConfig *tls.Config) error { switch h.metadata.MTLSRenegotiation { case "RenegotiateNever": - { - tlsConfig.Renegotiation = tls.RenegotiateNever - } + tlsConfig.Renegotiation = tls.RenegotiateNever case "RenegotiateOnceAsClient": - { - tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient - } + tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient case "RenegotiateFreelyAsClient": - { - tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient - } + tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient default: return fmt.Errorf("invalid renegotiation value: %s", h.metadata.MTLSRenegotiation) } @@ -231,23 +224,18 @@ func (h *HTTPSource) Invoke(parentCtx context.Context, req *bindings.InvokeReque errorIfNot2XX := h.errorIfNot2XX // Default to the component config (default is true) - if req.Metadata != nil { - if path, ok := req.Metadata["path"]; ok { - // Simplicity and no "../../.." type exploits. - u = fmt.Sprintf("%s/%s", strings.TrimRight(u, "/"), strings.TrimLeft(path, "/")) - if strings.Contains(u, "..") { - return nil, fmt.Errorf("invalid path: %s", path) - } - } - - if _, ok := req.Metadata["errorIfNot2XX"]; ok { - errorIfNot2XX = utils.IsTruthy(req.Metadata["errorIfNot2XX"]) - } - } else { + if req.Metadata == nil { // Prevent things below from failing if req.Metadata is nil. req.Metadata = make(map[string]string) } + if req.Metadata["path"] != "" { + u = strings.TrimRight(u, "/") + "/" + strings.TrimLeft(req.Metadata["path"], "/") + } + if req.Metadata["errorIfNot2XX"] != "" { + errorIfNot2XX = utils.IsTruthy(req.Metadata["errorIfNot2XX"]) + } + var body io.Reader method := strings.ToUpper(string(req.Operation)) // For backward compatibility @@ -262,10 +250,8 @@ func (h *HTTPSource) Invoke(parentCtx context.Context, req *bindings.InvokeReque return nil, fmt.Errorf("invalid operation: %s", req.Operation) } - var ctx context.Context - if h.metadata.ResponseTimeout == nil { - ctx = parentCtx - } else { + ctx := parentCtx + if h.metadata.ResponseTimeout != nil { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(parentCtx, *h.metadata.ResponseTimeout) defer cancel() @@ -294,8 +280,7 @@ func (h *HTTPSource) Invoke(parentCtx context.Context, req *bindings.InvokeReque // Any metadata keys that start with a capital letter // are treated as request headers for mdKey, mdValue := range req.Metadata { - keyAsRunes := []rune(mdKey) - if len(keyAsRunes) > 0 && unicode.IsUpper(keyAsRunes[0]) { + if len(mdKey) > 0 && (mdKey[0] >= 'A' && mdKey[0] <= 'Z') { request.Header.Set(mdKey, mdValue) } } diff --git a/bindings/http/http_test.go b/bindings/http/http_test.go index 04e53cdfd0..164b1ef328 100644 --- a/bindings/http/http_test.go +++ b/bindings/http/http_test.go @@ -553,14 +553,6 @@ func verifyDefaultBehaviors(t *testing.T, hs bindings.OutputBinding, handler *HT err: "", statusCode: 200, }, - "invalid path": { - input: "expected", - operation: "POST", - metadata: map[string]string{"path": "/../test"}, - path: "", - err: "invalid path: /../test", - statusCode: 400, - }, "invalid operation": { input: "notvalid", operation: "notvalid", @@ -665,14 +657,6 @@ func verifyNon2XXErrorsSuppressed(t *testing.T, hs bindings.OutputBinding, handl err: "", statusCode: 200, }, - "invalid path": { - input: "expected", - operation: "POST", - metadata: map[string]string{"path": "/../test"}, - path: "", - err: "invalid path: /../test", - statusCode: 400, - }, } for name, tc := range tests { From 1349fca858369cc067a93576be0a19d0c05df58f Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Wed, 12 Jul 2023 14:16:35 -0700 Subject: [PATCH 14/19] MySQL binding: allow passing parameters for queries (#2975) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- bindings/mysql/metadata.yaml | 13 +- bindings/mysql/mysql.go | 129 +++++++++++------ bindings/mysql/mysql_integration_test.go | 176 +++++++++++++---------- bindings/mysql/mysql_test.go | 8 +- 4 files changed, 192 insertions(+), 134 deletions(-) diff --git a/bindings/mysql/metadata.yaml b/bindings/mysql/metadata.yaml index e305696d11..005b2e7637 100644 --- a/bindings/mysql/metadata.yaml +++ b/bindings/mysql/metadata.yaml @@ -17,17 +17,17 @@ binding: - name: query description: "The query operation is used for SELECT statements, which returns the metadata along with data in a form of an array of row values." - name: close - description: "The close operation can be used to explicitly close the DB connection and return it to the pool. This operation doesn’t have any response." + description: "The close operation can be used to explicitly close the DB connection and return it to the pool. This operation doesn't have any response." metadata: - name: url required: true - description: "Represent a DB connection in Data Source Name (DNS) format." - example: "user:password@tcp(localhost:3306)/dbname" + description: "Represent a DB connection in Data Source Name (DNS) format" + example: '"user:password@tcp(localhost:3306)/dbname"' type: string - name: pemPath required: false description: "Path to the PEM file. Used with SSL connection" - example: "path/to/pem/file" + example: '"path/to/pem/file"' type: string - name: maxIdleConns required: false @@ -49,8 +49,3 @@ metadata: description: "The max connection idel time." example: "12s" type: duration - - name: maxRetries - required: false - description: "MaxRetries is the maximum number of retries for a query." - example: "5" - type: number diff --git a/bindings/mysql/mysql.go b/bindings/mysql/mysql.go index 1c53dc8df4..2127caf0f3 100644 --- a/bindings/mysql/mysql.go +++ b/bindings/mysql/mysql.go @@ -25,6 +25,7 @@ import ( "os" "reflect" "strconv" + "sync/atomic" "time" "github.com/go-sql-driver/mysql" @@ -52,7 +53,8 @@ const ( // "%s:%s@tcp(%s:3306)/%s?allowNativePasswords=true&tls=custom",'myadmin@mydemoserver', 'yourpassword', 'mydemoserver.mysql.database.azure.com', 'targetdb'. // keys from request's metadata. - commandSQLKey = "sql" + commandSQLKey = "sql" + commandParamsKey = "params" // keys from response's metadata. respOpKey = "operation" @@ -67,6 +69,7 @@ const ( type Mysql struct { db *sql.DB logger logger.Logger + closed atomic.Bool } type mysqlMetadata struct { @@ -87,21 +90,22 @@ type mysqlMetadata struct { // ConnMaxIdleTime is the maximum amount of time a connection may be idle. ConnMaxIdleTime time.Duration `mapstructure:"connMaxIdleTime"` - - // MaxRetries is the maximum number of retries for a query. - MaxRetries int `mapstructure:"maxRetries"` } // NewMysql returns a new MySQL output binding. func NewMysql(logger logger.Logger) bindings.OutputBinding { - return &Mysql{logger: logger} + return &Mysql{ + logger: logger, + } } // Init initializes the MySQL binding. func (m *Mysql) Init(ctx context.Context, md bindings.Metadata) error { - m.logger.Debug("Initializing MySql binding") + if m.closed.Load() { + return errors.New("cannot initialize a previously-closed component") + } - // parse metadata + // Parse metadata meta := mysqlMetadata{} err := metadata.DecodeMetadata(md.Properties, &meta) if err != nil { @@ -112,23 +116,29 @@ func (m *Mysql) Init(ctx context.Context, md bindings.Metadata) error { return fmt.Errorf("missing MySql connection string") } - db, err := initDB(meta.URL, meta.PemPath) + m.db, err = initDB(meta.URL, meta.PemPath) if err != nil { return err } - db.SetMaxIdleConns(meta.MaxIdleConns) - db.SetMaxOpenConns(meta.MaxOpenConns) - db.SetConnMaxIdleTime(meta.ConnMaxIdleTime) - db.SetConnMaxLifetime(meta.ConnMaxLifetime) + if meta.MaxIdleConns > 0 { + m.db.SetMaxIdleConns(meta.MaxIdleConns) + } + if meta.MaxOpenConns > 0 { + m.db.SetMaxOpenConns(meta.MaxOpenConns) + } + if meta.ConnMaxIdleTime > 0 { + m.db.SetConnMaxIdleTime(meta.ConnMaxIdleTime) + } + if meta.ConnMaxLifetime > 0 { + m.db.SetConnMaxLifetime(meta.ConnMaxLifetime) + } - err = db.PingContext(ctx) + err = m.db.PingContext(ctx) if err != nil { return fmt.Errorf("unable to ping the DB: %w", err) } - m.db = db - return nil } @@ -138,22 +148,38 @@ func (m *Mysql) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindi return nil, errors.New("invoke request required") } + // We let the "close" operation here succeed even if the component has been closed already if req.Operation == closeOperation { - return nil, m.db.Close() + return nil, m.Close() + } + + if m.closed.Load() { + return nil, errors.New("component is closed") } if req.Metadata == nil { return nil, errors.New("metadata required") } - m.logger.Debugf("operation: %v", req.Operation) - s, ok := req.Metadata[commandSQLKey] - if !ok || s == "" { + s := req.Metadata[commandSQLKey] + if s == "" { return nil, fmt.Errorf("required metadata not set: %s", commandSQLKey) } - startTime := time.Now() + // Metadata property "params" contains JSON-encoded parameters, and it's optional + // If present, it must be unserializable into a []any object + var ( + params []any + err error + ) + if paramsStr := req.Metadata[commandParamsKey]; paramsStr != "" { + err = json.Unmarshal([]byte(paramsStr), ¶ms) + if err != nil { + return nil, fmt.Errorf("invalid metadata property %s: failed to unserialize into an array: %w", commandParamsKey, err) + } + } + startTime := time.Now().UTC() resp := &bindings.InvokeResponse{ Metadata: map[string]string{ respOpKey: string(req.Operation), @@ -162,16 +188,16 @@ func (m *Mysql) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindi }, } - switch req.Operation { //nolint:exhaustive + switch req.Operation { case execOperation: - r, err := m.exec(ctx, s) + r, err := m.exec(ctx, s, params...) if err != nil { return nil, err } resp.Metadata[respRowsAffectedKey] = strconv.FormatInt(r, 10) case queryOperation: - d, err := m.query(ctx, s) + d, err := m.query(ctx, s, params...) if err != nil { return nil, err } @@ -182,7 +208,7 @@ func (m *Mysql) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindi req.Operation, execOperation, queryOperation, closeOperation) } - endTime := time.Now() + endTime := time.Now().UTC() resp.Metadata[respEndTimeKey] = endTime.Format(time.RFC3339Nano) resp.Metadata[respDurationKey] = endTime.Sub(startTime).String() @@ -200,23 +226,26 @@ func (m *Mysql) Operations() []bindings.OperationKind { // Close will close the DB. func (m *Mysql) Close() error { + if !m.closed.CompareAndSwap(false, true) { + // If this failed, the component has already been closed + // We allow multiple calls to close + return nil + } + if m.db != nil { - return m.db.Close() + m.db.Close() + m.db = nil } return nil } -func (m *Mysql) query(ctx context.Context, sql string) ([]byte, error) { - rows, err := m.db.QueryContext(ctx, sql) +func (m *Mysql) query(ctx context.Context, sql string, params ...any) ([]byte, error) { + rows, err := m.db.QueryContext(ctx, sql, params...) if err != nil { return nil, fmt.Errorf("error executing query: %w", err) } - - defer func() { - _ = rows.Close() - _ = rows.Err() - }() + defer rows.Close() result, err := m.jsonify(rows) if err != nil { @@ -226,10 +255,8 @@ func (m *Mysql) query(ctx context.Context, sql string) ([]byte, error) { return result, nil } -func (m *Mysql) exec(ctx context.Context, sql string) (int64, error) { - m.logger.Debugf("exec: %s", sql) - - res, err := m.db.ExecContext(ctx, sql) +func (m *Mysql) exec(ctx context.Context, sql string, params ...any) (int64, error) { + res, err := m.db.ExecContext(ctx, sql, params...) if err != nil { return 0, fmt.Errorf("error executing query: %w", err) } @@ -238,13 +265,15 @@ func (m *Mysql) exec(ctx context.Context, sql string) (int64, error) { } func initDB(url, pemPath string) (*sql.DB, error) { - if _, err := mysql.ParseDSN(url); err != nil { + conf, err := mysql.ParseDSN(url) + if err != nil { return nil, fmt.Errorf("illegal Data Source Name (DSN) specified by %s", connectionURLKey) } if pemPath != "" { + var pem []byte rootCertPool := x509.NewCertPool() - pem, err := os.ReadFile(pemPath) + pem, err = os.ReadFile(pemPath) if err != nil { return nil, fmt.Errorf("error reading PEM file from %s: %w", pemPath, err) } @@ -254,17 +283,25 @@ func initDB(url, pemPath string) (*sql.DB, error) { return nil, fmt.Errorf("failed to append PEM") } - err = mysql.RegisterTLSConfig("custom", &tls.Config{RootCAs: rootCertPool, MinVersion: tls.VersionTLS12}) + err = mysql.RegisterTLSConfig("custom", &tls.Config{ + RootCAs: rootCertPool, + MinVersion: tls.VersionTLS12, + }) if err != nil { return nil, fmt.Errorf("error register TLS config: %w", err) } } - db, err := sql.Open("mysql", url) + // Required to correctly parse time columns + // See: https://stackoverflow.com/a/45040724 + conf.ParseTime = true + + connector, err := mysql.NewConnector(conf) if err != nil { return nil, fmt.Errorf("error opening DB connection: %w", err) } + db := sql.OpenDB(connector) return db, nil } @@ -274,7 +311,7 @@ func (m *Mysql) jsonify(rows *sql.Rows) ([]byte, error) { return nil, err } - var ret []interface{} + var ret []any for rows.Next() { values := prepareValues(columnTypes) err := rows.Scan(values...) @@ -289,13 +326,13 @@ func (m *Mysql) jsonify(rows *sql.Rows) ([]byte, error) { return json.Marshal(ret) } -func prepareValues(columnTypes []*sql.ColumnType) []interface{} { +func prepareValues(columnTypes []*sql.ColumnType) []any { types := make([]reflect.Type, len(columnTypes)) for i, tp := range columnTypes { types[i] = tp.ScanType() } - values := make([]interface{}, len(columnTypes)) + values := make([]any, len(columnTypes)) for i := range values { values[i] = reflect.New(types[i]).Interface() } @@ -303,8 +340,8 @@ func prepareValues(columnTypes []*sql.ColumnType) []interface{} { return values } -func (m *Mysql) convert(columnTypes []*sql.ColumnType, values []interface{}) map[string]interface{} { - r := map[string]interface{}{} +func (m *Mysql) convert(columnTypes []*sql.ColumnType, values []any) map[string]any { + r := map[string]any{} for i, ct := range columnTypes { value := values[i] @@ -312,7 +349,7 @@ func (m *Mysql) convert(columnTypes []*sql.ColumnType, values []interface{}) map switch v := values[i].(type) { case driver.Valuer: if vv, err := v.Value(); err == nil { - value = interface{}(vv) + value = any(vv) } else { m.logger.Warnf("error to convert value: %v", err) } diff --git a/bindings/mysql/mysql_integration_test.go b/bindings/mysql/mysql_integration_test.go index fcc173526f..d5df16dda3 100644 --- a/bindings/mysql/mysql_integration_test.go +++ b/bindings/mysql/mysql_integration_test.go @@ -22,36 +22,20 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" ) -const ( - // MySQL doesn't accept RFC3339 formatted time, rejects trailing 'Z' for UTC indicator. - mySQLDateTimeFormat = "2006-01-02 15:04:05" - - testCreateTable = `CREATE TABLE IF NOT EXISTS foo ( - id bigint NOT NULL, - v1 character varying(50) NOT NULL, - b BOOLEAN, - ts TIMESTAMP, - data LONGTEXT)` - testDropTable = `DROP TABLE foo` - testInsert = "INSERT INTO foo (id, v1, b, ts, data) VALUES (%d, 'test-%d', %t, '%v', '%s')" - testDelete = "DELETE FROM foo" - testUpdate = "UPDATE foo SET ts = '%v' WHERE id = %d" - testSelect = "SELECT * FROM foo WHERE id < 3" - testSelectJSONExtract = "SELECT JSON_EXTRACT(data, '$.key') AS `key` FROM foo WHERE id < 3" -) +// MySQL doesn't accept RFC3339 formatted time, rejects trailing 'Z' for UTC indicator. +const mySQLDateTimeFormat = "2006-01-02 15:04:05" func TestOperations(t *testing.T) { - t.Parallel() t.Run("Get operation list", func(t *testing.T) { - t.Parallel() - b := NewMysql(nil) - assert.NotNil(t, b) + b := NewMysql(logger.NewLogger("test")) + require.NotNil(t, b) l := b.Operations() assert.Equal(t, 3, len(l)) assert.Contains(t, l, execOperation) @@ -70,123 +54,165 @@ func TestOperations(t *testing.T) { func TestMysqlIntegration(t *testing.T) { url := os.Getenv("MYSQL_TEST_CONN_URL") if url == "" { - t.SkipNow() + t.Skip("Skipping because env var MYSQL_TEST_CONN_URL is empty") } b := NewMysql(logger.NewLogger("test")).(*Mysql) m := bindings.Metadata{Base: metadata.Base{Properties: map[string]string{connectionURLKey: url}}} - if err := b.Init(context.Background(), m); err != nil { - t.Fatal(err) - } - defer b.Close() + err := b.Init(context.Background(), m) + require.NoError(t, err) - req := &bindings.InvokeRequest{Metadata: map[string]string{}} + defer b.Close() t.Run("Invoke create table", func(t *testing.T) { - req.Operation = execOperation - req.Metadata[commandSQLKey] = testCreateTable - res, err := b.Invoke(context.TODO(), req) + res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{ + Operation: execOperation, + Metadata: map[string]string{ + commandSQLKey: `CREATE TABLE IF NOT EXISTS foo ( + id bigint NOT NULL, + v1 character varying(50) NOT NULL, + b BOOLEAN, + ts TIMESTAMP, + data LONGTEXT)`, + }, + }) assertResponse(t, res, err) }) t.Run("Invoke delete", func(t *testing.T) { - req.Operation = execOperation - req.Metadata[commandSQLKey] = testDelete - res, err := b.Invoke(context.TODO(), req) + res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{ + Operation: execOperation, + Metadata: map[string]string{ + commandSQLKey: "DELETE FROM foo", + }, + }) assertResponse(t, res, err) }) t.Run("Invoke insert", func(t *testing.T) { - req.Operation = execOperation for i := 0; i < 10; i++ { - req.Metadata[commandSQLKey] = fmt.Sprintf(testInsert, i, i, true, time.Now().Format(mySQLDateTimeFormat), "{\"key\":\"val\"}") - res, err := b.Invoke(context.TODO(), req) + res, err := b.Invoke(context.Background(), &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"}`), + }, + }) assertResponse(t, res, err) } }) t.Run("Invoke update", func(t *testing.T) { - req.Operation = execOperation + date := time.Now().Add(time.Hour) + for i := 0; i < 10; i++ { + res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{ + Operation: execOperation, + Metadata: map[string]string{ + commandSQLKey: fmt.Sprintf( + "UPDATE foo SET ts = '%v' WHERE id = %d", + date.Add(10*time.Duration(i)*time.Second).Format(mySQLDateTimeFormat), i), + }, + }) + assertResponse(t, res, err) + assert.Equal(t, "1", res.Metadata[respRowsAffectedKey]) + } + }) + + t.Run("Invoke update with parameters", func(t *testing.T) { + date := time.Now().Add(2 * time.Hour) for i := 0; i < 10; i++ { - req.Metadata[commandSQLKey] = fmt.Sprintf(testUpdate, time.Now().Format(mySQLDateTimeFormat), i) - res, err := b.Invoke(context.TODO(), req) + res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{ + Operation: execOperation, + Metadata: map[string]string{ + commandSQLKey: "UPDATE foo SET ts = ? WHERE id = ?", + commandParamsKey: fmt.Sprintf(`[%q,%d]`, date.Add(10*time.Duration(i)*time.Second).Format(mySQLDateTimeFormat), i), + }, + }) assertResponse(t, res, err) + assert.Equal(t, "1", res.Metadata[respRowsAffectedKey]) } }) t.Run("Invoke select", func(t *testing.T) { - req.Operation = queryOperation - req.Metadata[commandSQLKey] = testSelect - res, err := b.Invoke(context.TODO(), req) + res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{ + Operation: queryOperation, + Metadata: map[string]string{ + commandSQLKey: "SELECT * FROM foo WHERE id < 3", + }, + }) assertResponse(t, res, err) t.Logf("received result: %s", res.Data) // verify number, boolean and string - assert.Contains(t, string(res.Data), "\"id\":1") - assert.Contains(t, string(res.Data), "\"b\":1") - assert.Contains(t, string(res.Data), "\"v1\":\"test-1\"") - assert.Contains(t, string(res.Data), "\"data\":\"{\\\"key\\\":\\\"val\\\"}\"") + assert.Contains(t, string(res.Data), `"id":1`) + assert.Contains(t, string(res.Data), `"b":1`) + assert.Contains(t, string(res.Data), `"v1":"test-1"`) + assert.Contains(t, string(res.Data), `"data":"{\"key\":\"val\"}"`) - result := make([]interface{}, 0) + result := make([]any, 0) err = json.Unmarshal(res.Data, &result) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, 3, len(result)) // verify timestamp - ts, ok := result[0].(map[string]interface{})["ts"].(string) + ts, ok := result[0].(map[string]any)["ts"].(string) assert.True(t, ok) // have to use custom layout to parse timestamp, see this: https://github.com/dapr/components-contrib/pull/615 var tt time.Time tt, err = time.Parse("2006-01-02T15:04:05Z", ts) - assert.Nil(t, err) + require.NoError(t, err) t.Logf("time stamp is: %v", tt) }) - t.Run("Invoke select JSON_EXTRACT", func(t *testing.T) { - req.Operation = queryOperation - req.Metadata[commandSQLKey] = testSelectJSONExtract - res, err := b.Invoke(context.TODO(), req) + t.Run("Invoke select with parameters", func(t *testing.T) { + res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{ + Operation: queryOperation, + Metadata: map[string]string{ + commandSQLKey: "SELECT * FROM foo WHERE id = ?", + commandParamsKey: `[1]`, + }, + }) assertResponse(t, res, err) t.Logf("received result: %s", res.Data) - // verify json extract number - assert.Contains(t, string(res.Data), "{\"key\":\"\\\"val\\\"\"}") + // verify number, boolean and string + assert.Contains(t, string(res.Data), `"id":1`) + assert.Contains(t, string(res.Data), `"b":1`) + assert.Contains(t, string(res.Data), `"v1":"test-1"`) + assert.Contains(t, string(res.Data), `"data":"{\"key\":\"val\"}"`) - result := make([]interface{}, 0) + result := make([]any, 0) err = json.Unmarshal(res.Data, &result) - assert.Nil(t, err) - assert.Equal(t, 3, len(result)) - }) - - t.Run("Invoke delete", func(t *testing.T) { - req.Operation = execOperation - req.Metadata[commandSQLKey] = testDelete - req.Data = nil - res, err := b.Invoke(context.TODO(), req) - assertResponse(t, res, err) + require.NoError(t, err) + assert.Equal(t, 1, len(result)) }) t.Run("Invoke drop", func(t *testing.T) { - req.Operation = execOperation - req.Metadata[commandSQLKey] = testDropTable - res, err := b.Invoke(context.TODO(), req) + res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{ + Operation: execOperation, + Metadata: map[string]string{ + commandSQLKey: "DROP TABLE foo", + }, + }) assertResponse(t, res, err) }) t.Run("Invoke close", func(t *testing.T) { - req.Operation = closeOperation - req.Metadata = nil - req.Data = nil - _, err := b.Invoke(context.TODO(), req) + _, err := b.Invoke(context.Background(), &bindings.InvokeRequest{ + Operation: closeOperation, + }) assert.NoError(t, err) }) } func assertResponse(t *testing.T, res *bindings.InvokeResponse, err error) { + t.Helper() + assert.NoError(t, err) assert.NotNil(t, res) if res != nil { - assert.NotNil(t, res.Metadata) + assert.NotEmpty(t, res.Metadata) } } diff --git a/bindings/mysql/mysql_test.go b/bindings/mysql/mysql_test.go index a17c151b12..37c8f8ce5e 100644 --- a/bindings/mysql/mysql_test.go +++ b/bindings/mysql/mysql_test.go @@ -42,7 +42,7 @@ func TestQuery(t *testing.T) { assert.Nil(t, err) t.Logf("query result: %s", ret) assert.Contains(t, string(ret), "\"id\":1") - var result []interface{} + var result []any err = json.Unmarshal(ret, &result) assert.Nil(t, err) assert.Equal(t, 3, len(result)) @@ -65,13 +65,13 @@ func TestQuery(t *testing.T) { assert.Contains(t, string(ret), "\"id\":1") assert.Contains(t, string(ret), "\"value\":2.2") - var result []interface{} + var result []any err = json.Unmarshal(ret, &result) assert.Nil(t, err) assert.Equal(t, 3, len(result)) // verify timestamp - ts, ok := result[0].(map[string]interface{})["timestamp"].(string) + ts, ok := result[0].(map[string]any)["timestamp"].(string) assert.True(t, ok) var tt time.Time tt, err = time.Parse(time.RFC3339, ts) @@ -134,7 +134,7 @@ func TestInvoke(t *testing.T) { } resp, err := m.Invoke(context.Background(), req) assert.Nil(t, err) - var data []interface{} + var data []any err = json.Unmarshal(resp.Data, &data) assert.Nil(t, err) assert.Equal(t, 1, len(data)) From a4012953ea92db109c8721203c095856f86c98af Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:03:18 -0700 Subject: [PATCH 15/19] Add Azure AD support to Postgres configuration store and bindings (#2971) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Signed-off-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> --- .../azure/setup-azure-conf-test.sh | 12 ++ .../docker-compose-postgresql.yml | 2 +- .github/scripts/test-info.mjs | 56 ++++++++- bindings/postgres/metadata.go | 52 ++++++++ bindings/postgres/metadata.yaml | 54 ++++++++- bindings/postgres/postgres.go | 69 ++++++----- bindings/postgres/postgres_test.go | 4 +- configuration/postgres/metadata.go | 51 +++++++- configuration/postgres/metadata.yaml | 59 +++++++-- configuration/postgres/postgres.go | 57 ++------- configuration/postgres/postgres_test.go | 18 +-- .../authentication/postgresql/metadata.go | 113 ++++++++++++++++++ internal/component/postgresql/metadata.go | 113 +++--------------- .../component/postgresql/metadata_test.go | 28 ++--- .../component/postgresql/postgresdbaccess.go | 23 ++-- state/postgresql/metadata.yaml | 2 +- .../components/standard/postgres.yaml | 6 +- .../bindings/postgres/docker-compose.yml | 4 +- .../bindings/postgres/postgres_test.go | 38 +++--- .../configuration/postgres/docker-compose.yml | 2 +- tests/certification/go.mod | 1 - tests/certification/go.sum | 2 - .../state/postgresql/docker-compose.yml | 2 +- tests/config/bindings/postgres/bindings.yml | 10 -- .../bindings/postgresql/azure/bindings.yml | 18 +++ .../bindings/postgresql/docker/bindings.yml | 11 ++ tests/config/bindings/tests.yml | 4 +- .../postgresql/azure/configstore.yml | 20 ++++ .../docker}/configstore.yml | 3 +- tests/config/configuration/tests.yml | 4 +- .../state/postgresql/azure/statestore.yml | 6 + tests/conformance/common.go | 11 +- .../configuration/configuration.go | 17 ++- .../utils/configupdater/postgres/postgres.go | 32 +++-- 34 files changed, 599 insertions(+), 305 deletions(-) create mode 100644 bindings/postgres/metadata.go create mode 100644 internal/authentication/postgresql/metadata.go delete mode 100644 tests/config/bindings/postgres/bindings.yml create mode 100644 tests/config/bindings/postgresql/azure/bindings.yml create mode 100644 tests/config/bindings/postgresql/docker/bindings.yml create mode 100644 tests/config/configuration/postgresql/azure/configstore.yml rename tests/config/configuration/{postgres => postgresql/docker}/configstore.yml (85%) diff --git a/.github/infrastructure/conformance/azure/setup-azure-conf-test.sh b/.github/infrastructure/conformance/azure/setup-azure-conf-test.sh index 8220f57fcd..ca553026bd 100755 --- a/.github/infrastructure/conformance/azure/setup-azure-conf-test.sh +++ b/.github/infrastructure/conformance/azure/setup-azure-conf-test.sh @@ -230,6 +230,9 @@ SQL_SERVER_DB_NAME_VAR_NAME="AzureSqlServerDbName" SQL_SERVER_CONNECTION_STRING_VAR_NAME="AzureSqlServerConnectionString" AZURE_DB_POSTGRES_CONNSTRING_VAR_NAME="AzureDBPostgresConnectionString" +AZURE_DB_POSTGRES_CLIENT_ID_VAR_NAME="AzureDBPostgresClientId" +AZURE_DB_POSTGRES_CLIENT_SECRET_VAR_NAME="AzureDBPostgresClientSecret" +AZURE_DB_POSTGRES_TENANT_ID_VAR_NAME="AzureDBPostgresTenantId" STORAGE_ACCESS_KEY_VAR_NAME="AzureBlobStorageAccessKey" STORAGE_ACCOUNT_VAR_NAME="AzureBlobStorageAccount" @@ -693,6 +696,15 @@ AZURE_DB_POSTGRES_CONNSTRING="host=${PREFIX}-conf-test-pg.postgres.database.azur echo export ${AZURE_DB_POSTGRES_CONNSTRING_VAR_NAME}=\"${AZURE_DB_POSTGRES_CONNSTRING}\" >> "${ENV_CONFIG_FILENAME}" az keyvault secret set --name "${AZURE_DB_POSTGRES_CONNSTRING_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${AZURE_DB_POSTGRES_CONNSTRING}" +echo export ${AZURE_DB_POSTGRES_CLIENT_ID_VAR_NAME}=\"${SDK_AUTH_SP_APPID}\" >> "${ENV_CONFIG_FILENAME}" +az keyvault secret set --name "${AZURE_DB_POSTGRES_CLIENT_ID_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${SDK_AUTH_SP_APPID}" + +echo export ${AZURE_DB_POSTGRES_CLIENT_SECRET_VAR_NAME}=\"${SDK_AUTH_SP_CLIENT_SECRET}\" >> "${ENV_CONFIG_FILENAME}" +az keyvault secret set --name "${AZURE_DB_POSTGRES_CLIENT_SECRET_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${SDK_AUTH_SP_CLIENT_SECRET}" + +echo export ${AZURE_DB_POSTGRES_TENANT_ID_VAR_NAME}=\"${TENANT_ID}\" >> "${ENV_CONFIG_FILENAME}" +az keyvault secret set --name "${AZURE_DB_POSTGRES_TENANT_ID_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${TENANT_ID}" + # ---------------------------------- # Populate Event Hubs test settings # ---------------------------------- diff --git a/.github/infrastructure/docker-compose-postgresql.yml b/.github/infrastructure/docker-compose-postgresql.yml index 6819464d45..d11b2d537f 100644 --- a/.github/infrastructure/docker-compose-postgresql.yml +++ b/.github/infrastructure/docker-compose-postgresql.yml @@ -1,7 +1,7 @@ version: '2' services: db: - image: postgres:15 + image: postgres:15-alpine restart: always ports: - "5432:5432" diff --git a/.github/scripts/test-info.mjs b/.github/scripts/test-info.mjs index 8bfeb0187f..1c4468af26 100644 --- a/.github/scripts/test-info.mjs +++ b/.github/scripts/test-info.mjs @@ -167,9 +167,28 @@ const components = { sourcePkg: ['bindings/mqtt3'], }, 'bindings.postgres': { - conformance: true, certification: true, + }, + 'bindings.postgresql.docker': { + conformance: true, conformanceSetup: 'docker-compose.sh postgresql', + sourcePkg: [ + 'bindings/postgresql', + 'internal/authentication/postgresql', + ], + }, + 'bindings.postgresql.azure': { + conformance: true, + requiredSecrets: [ + 'AzureDBPostgresConnectionString', + 'AzureDBPostgresClientId', + 'AzureDBPostgresClientSecret', + 'AzureDBPostgresTenantId', + ], + sourcePkg: [ + 'bindings/postgresql', + 'internal/authentication/postgresql', + ], }, 'bindings.rabbitmq': { conformance: true, @@ -191,9 +210,32 @@ const components = { sourcePkg: ['bindings/redis', 'internal/component/redis'], }, 'configuration.postgres': { - conformance: true, certification: true, + sourcePkg: [ + 'configuration/postgresql', + 'internal/authentication/postgresql', + ], + }, + 'configuration.postgresql.docker': { + conformance: true, conformanceSetup: 'docker-compose.sh postgresql', + sourcePkg: [ + 'configuration/postgresql', + 'internal/authentication/postgresql', + ], + }, + 'configuration.postgresql.azure': { + conformance: true, + requiredSecrets: [ + 'AzureDBPostgresConnectionString', + 'AzureDBPostgresClientId', + 'AzureDBPostgresClientSecret', + 'AzureDBPostgresTenantId', + ], + sourcePkg: [ + 'configuration/postgresql', + 'internal/authentication/postgresql', + ], }, 'configuration.redis.v6': { conformance: true, @@ -585,6 +627,7 @@ const components = { certification: true, sourcePkg: [ 'state/postgresql', + 'internal/authentication/postgresql', 'internal/component/postgresql', 'internal/component/sql', ], @@ -594,15 +637,22 @@ const components = { conformanceSetup: 'docker-compose.sh postgresql', sourcePkg: [ 'state/postgresql', + 'internal/authentication/postgresql', 'internal/component/postgresql', 'internal/component/sql', ], }, 'state.postgresql.azure': { conformance: true, - requiredSecrets: ['AzureDBPostgresConnectionString'], + requiredSecrets: [ + 'AzureDBPostgresConnectionString', + 'AzureDBPostgresClientId', + 'AzureDBPostgresClientSecret', + 'AzureDBPostgresTenantId', + ], sourcePkg: [ 'state/postgresql', + 'internal/authentication/postgresql', 'internal/component/postgresql', 'internal/component/sql', ], diff --git a/bindings/postgres/metadata.go b/bindings/postgres/metadata.go new file mode 100644 index 0000000000..281b2c593d --- /dev/null +++ b/bindings/postgres/metadata.go @@ -0,0 +1,52 @@ +/* +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 postgres + +import ( + pgauth "github.com/dapr/components-contrib/internal/authentication/postgresql" + contribMetadata "github.com/dapr/components-contrib/metadata" +) + +type psqlMetadata struct { + pgauth.PostgresAuthMetadata `mapstructure:",squash"` + + // URL is the connection string to connect to the database. + // Deprecated alias: use connectionString instead. + URL string `mapstructure:"url"` +} + +func (m *psqlMetadata) InitWithMetadata(meta map[string]string) error { + // Reset the object + m.PostgresAuthMetadata.Reset() + m.URL = "" + + err := contribMetadata.DecodeMetadata(meta, &m) + if err != nil { + return err + } + + // Legacy options + if m.ConnectionString == "" && m.URL != "" { + m.ConnectionString = m.URL + } + + // Validate and sanitize input + // Azure AD auth is supported for this component + err = m.PostgresAuthMetadata.InitWithMetadata(meta, true) + if err != nil { + return err + } + + return nil +} diff --git a/bindings/postgres/metadata.yaml b/bindings/postgres/metadata.yaml index 1d771523a3..01341e690b 100644 --- a/bindings/postgres/metadata.yaml +++ b/bindings/postgres/metadata.yaml @@ -19,17 +19,33 @@ binding: description: "The query operation is used for SELECT statements, which return both the metadata and the retrieved data in a form of an array of row values." - name: close description: "The close operation can be used to explicitly close the DB connection and return it to the pool. This operation doesn't have any response." +builtinAuthenticationProfiles: + - name: "azuread" + metadata: + - name: useAzureAD + required: true + type: bool + example: '"true"' + description: | + Must be set to `true` to enable the component to retrieve access tokens from Azure AD. + This authentication method only works with Azure Database for PostgreSQL databases. + - name: connectionString + required: true + sensitive: true + description: | + The connection string for the PostgreSQL database + This must contain the user, which corresponds to the name of the user created inside PostgreSQL that maps to the Azure AD identity; this is often the name of the corresponding principal (e.g. the name of the Azure AD application). This connection string should not contain any password. + example: | + "host=mydb.postgres.database.azure.com user=myapplication port=5432 database=dapr_test sslmode=require" + type: string authenticationProfiles: - title: "Connection string" - description: "Authenticate using a Connection String." + description: "Authenticate using a Connection String" metadata: - - name: url + - name: connectionString required: true sensitive: true - binding: - input: false - output: true - description: "Connection string for PostgreSQL." + description: "The connection string for the PostgreSQL database" url: title: More details url: https://docs.dapr.io/reference/components-reference/supported-bindings/postgres/#url-format @@ -37,3 +53,29 @@ authenticationProfiles: "user=dapr password=secret host=dapr.example.com port=5432 dbname=dapr sslmode=verify-ca" or "postgres://dapr:secret@dapr.example.com:5432/dapr?sslmode=verify-ca" type: string +metadata: + - name: maxConns + required: false + description: | + Maximum number of connections pooled by this component. + Set to 0 or lower to use the default value, which is the greater of 4 or the number of CPUs. + example: "4" + default: "0" + type: number + - name: connectionMaxIdleTime + required: false + description: | + Max idle time before unused connections are automatically closed in the + connection pool. By default, there's no value and this is left to the + database driver to choose. + example: "5m" + type: duration + - name: url + deprecated: true + required: false + description: | + Deprecated alias for "connectionString" + type: string + sensitive: true + example: | + "user=dapr password=secret host=dapr.example.com port=5432 dbname=dapr sslmode=verify-ca" diff --git a/bindings/postgres/postgres.go b/bindings/postgres/postgres.go index 7c03e3bb7a..9919f9b1d0 100644 --- a/bindings/postgres/postgres.go +++ b/bindings/postgres/postgres.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +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 @@ -20,6 +20,7 @@ import ( "fmt" "reflect" "strconv" + "sync/atomic" "time" "github.com/jackc/pgx/v5/pgxpool" @@ -35,38 +36,36 @@ const ( queryOperation bindings.OperationKind = "query" closeOperation bindings.OperationKind = "close" - connectionURLKey = "url" - commandSQLKey = "sql" + commandSQLKey = "sql" ) // Postgres represents PostgreSQL output binding. type Postgres struct { logger logger.Logger db *pgxpool.Pool -} - -type psqlMetadata struct { - // ConnectionURL is the connection string to connect to the database. - ConnectionURL string `mapstructure:"url"` + closed atomic.Bool } // NewPostgres returns a new PostgreSQL output binding. func NewPostgres(logger logger.Logger) bindings.OutputBinding { - return &Postgres{logger: logger} + return &Postgres{ + logger: logger, + } } // Init initializes the PostgreSql binding. func (p *Postgres) Init(ctx context.Context, meta bindings.Metadata) error { + if p.closed.Load() { + return errors.New("cannot initialize a previously-closed component") + } + m := psqlMetadata{} - err := metadata.DecodeMetadata(meta.Properties, &m) + err := m.InitWithMetadata(meta.Properties) if err != nil { return err } - if m.ConnectionURL == "" { - return fmt.Errorf("required metadata not set: %s", connectionURLKey) - } - poolConfig, err := pgxpool.ParseConfig(m.ConnectionURL) + poolConfig, err := m.GetPgxPoolConfig() if err != nil { return fmt.Errorf("error opening DB connection: %w", err) } @@ -75,7 +74,7 @@ func (p *Postgres) Init(ctx context.Context, meta bindings.Metadata) error { // only scoped to postgres creating resources at init. p.db, err = pgxpool.NewWithConfig(ctx, poolConfig) if err != nil { - return fmt.Errorf("unable to ping the DB: %w", err) + return fmt.Errorf("unable to connect to the DB: %w", err) } return nil @@ -96,16 +95,19 @@ func (p *Postgres) Invoke(ctx context.Context, req *bindings.InvokeRequest) (res return nil, errors.New("invoke request required") } + // We let the "close" operation here succeed even if the component has been closed already if req.Operation == closeOperation { - p.db.Close() + err = p.Close() + return nil, err + } - return nil, nil + if p.closed.Load() { + return nil, errors.New("component is closed") } if req.Metadata == nil { return nil, errors.New("metadata required") } - p.logger.Debugf("operation: %v", req.Operation) sql, ok := req.Metadata[commandSQLKey] if !ok || sql == "" { @@ -125,14 +127,14 @@ func (p *Postgres) Invoke(ctx context.Context, req *bindings.InvokeRequest) (res case execOperation: r, err := p.exec(ctx, sql) if err != nil { - return nil, fmt.Errorf("error executing %s: %w", sql, err) + return nil, err } resp.Metadata["rows-affected"] = strconv.FormatInt(r, 10) // 0 if error case queryOperation: d, err := p.query(ctx, sql) if err != nil { - return nil, fmt.Errorf("error executing %s: %w", sql, err) + return nil, err } resp.Data = d @@ -152,17 +154,21 @@ func (p *Postgres) Invoke(ctx context.Context, req *bindings.InvokeRequest) (res // Close close PostgreSql instance. func (p *Postgres) Close() error { - if p.db == nil { + if !p.closed.CompareAndSwap(false, true) { + // If this failed, the component has already been closed + // We allow multiple calls to close return nil } - p.db.Close() + + if p.db != nil { + p.db.Close() + } + p.db = nil return nil } func (p *Postgres) query(ctx context.Context, sql string) (result []byte, err error) { - p.logger.Debugf("query: %s", sql) - rows, err := p.db.Query(ctx, sql) if err != nil { return nil, fmt.Errorf("error executing query: %w", err) @@ -172,29 +178,26 @@ func (p *Postgres) query(ctx context.Context, sql string) (result []byte, err er for rows.Next() { val, rowErr := rows.Values() if rowErr != nil { - return nil, fmt.Errorf("error parsing result '%v': %w", rows.Err(), rowErr) + return nil, fmt.Errorf("error reading result '%v': %w", rows.Err(), rowErr) } rs = append(rs, val) //nolint:asasalint } - if result, err = json.Marshal(rs); err != nil { - err = fmt.Errorf("error serializing results: %w", err) + result, err = json.Marshal(rs) + if err != nil { + return nil, fmt.Errorf("error serializing results: %w", err) } - return + return result, nil } func (p *Postgres) exec(ctx context.Context, sql string) (result int64, err error) { - p.logger.Debugf("exec: %s", sql) - res, err := p.db.Exec(ctx, sql) if err != nil { return 0, fmt.Errorf("error executing query: %w", err) } - result = res.RowsAffected() - - return + return res.RowsAffected(), nil } // GetComponentMetadata returns the metadata of the component. diff --git a/bindings/postgres/postgres_test.go b/bindings/postgres/postgres_test.go index 78adb43ab0..392fe2a6ad 100644 --- a/bindings/postgres/postgres_test.go +++ b/bindings/postgres/postgres_test.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +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 @@ -63,7 +63,7 @@ func TestPostgresIntegration(t *testing.T) { // live DB test b := NewPostgres(logger.NewLogger("test")).(*Postgres) - m := bindings.Metadata{Base: metadata.Base{Properties: map[string]string{connectionURLKey: url}}} + m := bindings.Metadata{Base: metadata.Base{Properties: map[string]string{"connectionString": url}}} if err := b.Init(context.Background(), m); err != nil { t.Fatal(err) } diff --git a/configuration/postgres/metadata.go b/configuration/postgres/metadata.go index 0bf10cb742..b53a5b2d3c 100644 --- a/configuration/postgres/metadata.go +++ b/configuration/postgres/metadata.go @@ -13,10 +13,53 @@ limitations under the License. package postgres -import "time" +import ( + "fmt" + "time" + + pgauth "github.com/dapr/components-contrib/internal/authentication/postgresql" + contribMetadata "github.com/dapr/components-contrib/metadata" +) type metadata struct { - MaxIdleTimeout time.Duration `mapstructure:"connMaxIdleTime"` - ConnectionString string `mapstructure:"connectionString"` - ConfigTable string `mapstructure:"table"` + pgauth.PostgresAuthMetadata `mapstructure:",squash"` + + ConfigTable string `mapstructure:"table"` + MaxIdleTimeoutOld time.Duration `mapstructure:"connMaxIdleTime"` // Deprecated alias for "connectionMaxIdleTime" +} + +func (m *metadata) InitWithMetadata(meta map[string]string) error { + // Reset the object + m.PostgresAuthMetadata.Reset() + m.ConfigTable = "" + m.MaxIdleTimeoutOld = 0 + + err := contribMetadata.DecodeMetadata(meta, &m) + if err != nil { + return err + } + + // Legacy options + if m.ConnectionMaxIdleTime == 0 && m.MaxIdleTimeoutOld > 0 { + m.ConnectionMaxIdleTime = m.MaxIdleTimeoutOld + } + + // Validate and sanitize input + if m.ConfigTable == "" { + return fmt.Errorf("missing postgreSQL configuration table name") + } + if len(m.ConfigTable) > maxIdentifierLength { + return fmt.Errorf("table name is too long - tableName : '%s'. max allowed field length is %d", m.ConfigTable, maxIdentifierLength) + } + if !allowedTableNameChars.MatchString(m.ConfigTable) { + return fmt.Errorf("invalid table name '%s'. non-alphanumerics or upper cased table names are not supported", m.ConfigTable) + } + + // Azure AD auth is supported for this component + err = m.PostgresAuthMetadata.InitWithMetadata(meta, true) + if err != nil { + return err + } + + return nil } diff --git a/configuration/postgres/metadata.yaml b/configuration/postgres/metadata.yaml index 16e411c8af..47fba4a0f4 100644 --- a/configuration/postgres/metadata.yaml +++ b/configuration/postgres/metadata.yaml @@ -1,14 +1,33 @@ # yaml-language-server: $schema=../../component-metadata-schema.json schemaVersion: v1 type: configuration -name: postgres +name: postgresql version: v1 -status: alpha -title: "Postgres" +status: stable +title: "PostgreSQL" urls: - title: Reference - url: https://docs.dapr.io/reference/components-reference/supported-configuration-stores/postgres-configuration-store/ + url: https://docs.dapr.io/reference/components-reference/supported-configuration-stores/postgresql-configuration-store/ capabilities: [] +builtinAuthenticationProfiles: + - name: "azuread" + metadata: + - name: useAzureAD + required: true + type: bool + example: '"true"' + description: | + Must be set to `true` to enable the component to retrieve access tokens from Azure AD. + This authentication method only works with Azure Database for PostgreSQL databases. + - name: connectionString + required: true + sensitive: true + description: | + The connection string for the PostgreSQL database + This must contain the user, which corresponds to the name of the user created inside PostgreSQL that maps to the Azure AD identity; this is often the name of the corresponding principal (e.g. the name of the Azure AD application). This connection string should not contain any password. + example: | + "host=mydb.postgres.database.azure.com user=myapplication port=5432 database=dapr_test sslmode=require" + type: string authenticationProfiles: - title: "Connection string" description: "Authenticate using a Connection String." @@ -16,10 +35,9 @@ authenticationProfiles: - name: connectionString required: true sensitive: true - description: | - The connection string for PostgreSQL, as a URL or DSN. - Note: the default value for `pool_max_conns` is 5. - example: "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test pool_max_conns=10" + description: The connection string for the PostgreSQL database + example: | + "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" type: string metadata: - name: table @@ -27,9 +45,26 @@ metadata: description: The table name for configuration information. example: "configTable" type: string - - name: connMaxIdleTime + - name: connectionMaxIdleTime required: false - description: The maximum amount of time a connection may be idle. - example: "15s" - default: "30s" + description: | + Max idle time before unused connections are automatically closed in the + connection pool. By default, there's no value and this is left to the + database driver to choose. + example: "5m" type: duration + - name: maxConns + required: false + description: | + Maximum number of connections pooled by this component. + Set to 0 or lower to use the default value, which is the greater of 4 or the number of CPUs. + example: "4" + default: "0" + type: number + - name: connMaxIdleTime + deprecated: true + required: false + description: | + Deprecated alias for 'connectionMaxIdleTime'. + example: "5m" + type: duration \ No newline at end of file diff --git a/configuration/postgres/postgres.go b/configuration/postgres/postgres.go index f948ac0ed7..3a072da851 100644 --- a/configuration/postgres/postgres.go +++ b/configuration/postgres/postgres.go @@ -23,13 +23,12 @@ import ( "strconv" "strings" "sync" - "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" - "k8s.io/utils/strings/slices" + "golang.org/x/exp/slices" "github.com/dapr/components-contrib/configuration" contribMetadata "github.com/dapr/components-contrib/metadata" @@ -62,34 +61,29 @@ const ( ) var ( - allowedChars = regexp.MustCompile(`^[a-zA-Z0-9./_]*$`) - allowedTableNameChars = regexp.MustCompile(`^[a-z0-9./_]*$`) - defaultMaxConnIdleTime = time.Second * 30 + allowedChars = regexp.MustCompile(`^[a-zA-Z0-9./_]*$`) + allowedTableNameChars = regexp.MustCompile(`^[a-z0-9./_]*$`) ) func NewPostgresConfigurationStore(logger logger.Logger) configuration.Store { - logger.Debug("Instantiating PostgreSQL configuration store") return &ConfigurationStore{ logger: logger, subscribeStopChanMap: make(map[string]chan struct{}), } } -func (p *ConfigurationStore) Init(parentCtx context.Context, metadata configuration.Metadata) error { - if m, err := parseMetadata(metadata); err != nil { +func (p *ConfigurationStore) Init(ctx context.Context, metadata configuration.Metadata) error { + err := p.metadata.InitWithMetadata(metadata.Properties) + if err != nil { p.logger.Error(err) return err - } else { - p.metadata = m } + p.ActiveSubscriptions = make(map[string]*subscription) - ctx, cancel := context.WithTimeout(parentCtx, p.metadata.MaxIdleTimeout) - defer cancel() - client, err := Connect(ctx, p.metadata.ConnectionString, p.metadata.MaxIdleTimeout) + p.client, err = p.connectDB(ctx) if err != nil { return fmt.Errorf("error connecting to configuration store: '%w'", err) } - p.client = client err = p.client.Ping(ctx) if err != nil { return fmt.Errorf("unable to connect to configuration store: '%w'", err) @@ -180,7 +174,7 @@ func (p *ConfigurationStore) Subscribe(ctx context.Context, req *configuration.S } } if pgNotifyChannel == "" { - return "", fmt.Errorf("unable to subscribe to '%s'.pgNotifyChannel attribute cannot be empty", p.metadata.ConfigTable) + return "", fmt.Errorf("unable to subscribe to '%s'. pgNotifyChannel attribute cannot be empty", p.metadata.ConfigTable) } return p.subscribeToChannel(ctx, pgNotifyChannel, req, handler) } @@ -290,37 +284,8 @@ func (p *ConfigurationStore) handleSubscribedChange(ctx context.Context, handler } } -func parseMetadata(cmetadata configuration.Metadata) (metadata, error) { - m := metadata{ - MaxIdleTimeout: defaultMaxConnIdleTime, - } - decodeErr := contribMetadata.DecodeMetadata(cmetadata.Properties, &m) - if decodeErr != nil { - return m, decodeErr - } - - if m.ConnectionString == "" { - return m, fmt.Errorf("missing postgreSQL connection string") - } - - if m.ConfigTable != "" { - if !allowedTableNameChars.MatchString(m.ConfigTable) { - return m, fmt.Errorf("invalid table name '%s'. non-alphanumerics or upper cased table names are not supported", m.ConfigTable) - } - if len(m.ConfigTable) > maxIdentifierLength { - return m, fmt.Errorf("table name is too long - tableName : '%s'. max allowed field length is %d", m.ConfigTable, maxIdentifierLength) - } - } else { - return m, fmt.Errorf("missing postgreSQL configuration table name") - } - if m.MaxIdleTimeout <= 0 { - m.MaxIdleTimeout = defaultMaxConnIdleTime - } - return m, nil -} - -func Connect(ctx context.Context, conn string, maxTimeout time.Duration) (*pgxpool.Pool, error) { - config, err := pgxpool.ParseConfig(conn) +func (p *ConfigurationStore) connectDB(ctx context.Context) (*pgxpool.Pool, error) { + config, err := p.metadata.GetPgxPoolConfig() if err != nil { return nil, fmt.Errorf("postgres configuration store connection error : %w", err) } diff --git a/configuration/postgres/postgres_test.go b/configuration/postgres/postgres_test.go index 4a9666582e..74fb36a787 100644 --- a/configuration/postgres/postgres_test.go +++ b/configuration/postgres/postgres_test.go @@ -20,8 +20,10 @@ import ( "github.com/pashagolub/pgxmock/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/dapr/components-contrib/configuration" + pgauth "github.com/dapr/components-contrib/internal/authentication/postgresql" ) func TestSelectAllQuery(t *testing.T) { @@ -43,7 +45,7 @@ func TestSelectAllQuery(t *testing.T) { if err != nil { t.Errorf("Error building query: %v ", err) } - assert.Nil(t, err, "Error building query: %v ", err) + assert.NoError(t, err, "Error building query: %v ", err) assert.Equal(t, expected, query, "did not get expected result. Got: '%v' , Expected: '%v'", query, expected) } @@ -57,7 +59,7 @@ func TestPostgresbuildQuery(t *testing.T) { query, params, err := buildQuery(g, "cfgtbl") _ = params - assert.Nil(t, err, "Error building query: %v ", err) + assert.NoError(t, err, "Error building query: %v ", err) expected := "SELECT * FROM cfgtbl WHERE KEY IN ($1) AND $2 = $3" assert.Equal(t, expected, query, "did not get expected result. Got: '%v' , Expected: '%v'", query, expected) i := 0 @@ -80,12 +82,14 @@ func TestPostgresbuildQuery(t *testing.T) { func TestConnectAndQuery(t *testing.T) { m := metadata{ - ConnectionString: "mockConnectionString", - ConfigTable: "mockConfigTable", + PostgresAuthMetadata: pgauth.PostgresAuthMetadata{ + ConnectionString: "mockConnectionString", + }, + ConfigTable: "mockConfigTable", } mock, err := pgxmock.NewPool() - assert.Nil(t, err) + require.NoError(t, err) defer mock.Close() query := "SELECT EXISTS (SELECT FROM pg_tables where tablename = '" + m.ConfigTable + "'" @@ -97,9 +101,9 @@ func TestConnectAndQuery(t *testing.T) { rows := mock.QueryRow(context.Background(), query) var id string err = rows.Scan(&id) - assert.Nil(t, err, "error in scan") + assert.NoError(t, err, "error in scan") err = mock.ExpectationsWereMet() - assert.Nil(t, err, "pgxmock error in expectations were met") + assert.NoError(t, err, "pgxmock error in expectations were met") } func TestValidateInput(t *testing.T) { diff --git a/internal/authentication/postgresql/metadata.go b/internal/authentication/postgresql/metadata.go new file mode 100644 index 0000000000..66f67fef1f --- /dev/null +++ b/internal/authentication/postgresql/metadata.go @@ -0,0 +1,113 @@ +/* +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 postgresql + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/dapr/components-contrib/internal/authentication/azure" +) + +// PostgresAuthMetadata contains authentication metadata for PostgreSQL components. +type PostgresAuthMetadata struct { + ConnectionString string `mapstructure:"connectionString"` + ConnectionMaxIdleTime time.Duration `mapstructure:"connectionMaxIdleTime"` + MaxConns int `mapstructure:"maxConns"` + UseAzureAD bool `mapstructure:"useAzureAD"` + + azureEnv azure.EnvironmentSettings +} + +// Reset the object. +func (m *PostgresAuthMetadata) Reset() { + m.ConnectionString = "" + m.ConnectionMaxIdleTime = 0 + m.MaxConns = 0 + m.UseAzureAD = false +} + +// InitWithMetadata inits the object with metadata from the user. +// Set azureADEnabled to true if the component can support authentication with Azure AD. +// This is different from the "useAzureAD" property from the user, which is provided by the user and instructs the component to authenticate using Azure AD. +func (m *PostgresAuthMetadata) InitWithMetadata(meta map[string]string, azureADEnabled bool) (err error) { + // Validate input + if m.ConnectionString == "" { + return errors.New("missing connection string") + } + + // Populate the Azure environment if using Azure AD + if azureADEnabled && m.UseAzureAD { + m.azureEnv, err = azure.NewEnvironmentSettings(meta) + if err != nil { + return err + } + } else { + // Make sure this is false + m.UseAzureAD = false + } + + return nil +} + +// GetPgxPoolConfig returns the pgxpool.Config object that contains the credentials for connecting to PostgreSQL. +func (m *PostgresAuthMetadata) GetPgxPoolConfig() (*pgxpool.Config, error) { + // Get the config from the connection string + config, err := pgxpool.ParseConfig(m.ConnectionString) + if err != nil { + return nil, fmt.Errorf("failed to parse connection string: %w", err) + } + if m.ConnectionMaxIdleTime > 0 { + config.MaxConnIdleTime = m.ConnectionMaxIdleTime + } + if m.MaxConns > 1 { + config.MaxConns = int32(m.MaxConns) + } + + // Check if we should use Azure AD + if m.UseAzureAD { + tokenCred, errToken := m.azureEnv.GetTokenCredential() + if errToken != nil { + return nil, errToken + } + + // Reset the password + config.ConnConfig.Password = "" + + // We need to retrieve the token every time we attempt a new connection + // This is because tokens expire, and connections can drop and need to be re-established at any time + // Fortunately, we can do this with the "BeforeConnect" hook + config.BeforeConnect = func(ctx context.Context, cc *pgx.ConnConfig) error { + at, err := tokenCred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{ + m.azureEnv.Cloud.Services[azure.ServiceOSSRDBMS].Audience + "/.default", + }, + }) + if err != nil { + return err + } + + cc.Password = at.Token + return nil + } + } + + return config, nil +} diff --git a/internal/component/postgresql/metadata.go b/internal/component/postgresql/metadata.go index fb10bed6a0..a4dd06f076 100644 --- a/internal/component/postgresql/metadata.go +++ b/internal/component/postgresql/metadata.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +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 @@ -14,15 +14,10 @@ limitations under the License. package postgresql import ( - "context" "fmt" "time" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/dapr/components-contrib/internal/authentication/azure" + pgauth "github.com/dapr/components-contrib/internal/authentication/postgresql" "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/state" "github.com/dapr/kit/ptr" @@ -39,24 +34,17 @@ const ( ) type postgresMetadataStruct struct { - ConnectionString string `mapstructure:"connectionString"` - ConnectionMaxIdleTime time.Duration `mapstructure:"connectionMaxIdleTime"` - TableName string `mapstructure:"tableName"` // Could be in the format "schema.table" or just "table" - MetadataTableName string `mapstructure:"metadataTableName"` // Could be in the format "schema.table" or just "table" - Timeout time.Duration `mapstructure:"timeoutInSeconds"` - CleanupInterval *time.Duration `mapstructure:"cleanupIntervalInSeconds"` - MaxConns int `mapstructure:"maxConns"` - UseAzureAD bool `mapstructure:"useAzureAD"` + pgauth.PostgresAuthMetadata `mapstructure:",squash"` - // Set to true if the component can support authentication with Azure AD. - // This is different from the "useAzureAD" property above, which is provided by the user and instructs the component to authenticate using Azure AD. - azureADEnabled bool - azureEnv azure.EnvironmentSettings + TableName string `mapstructure:"tableName"` // Could be in the format "schema.table" or just "table" + MetadataTableName string `mapstructure:"metadataTableName"` // Could be in the format "schema.table" or just "table" + Timeout time.Duration `mapstructure:"timeoutInSeconds"` + CleanupInterval *time.Duration `mapstructure:"cleanupIntervalInSeconds"` } -func (m *postgresMetadataStruct) InitWithMetadata(meta state.Metadata) error { +func (m *postgresMetadataStruct) InitWithMetadata(meta state.Metadata, azureADEnabled bool) error { // Reset the object - m.ConnectionString = "" + m.PostgresAuthMetadata.Reset() m.TableName = defaultTableName m.MetadataTableName = defaultMetadataTableName m.CleanupInterval = ptr.Of(defaultCleanupInternal * time.Second) @@ -69,8 +57,9 @@ func (m *postgresMetadataStruct) InitWithMetadata(meta state.Metadata) error { } // Validate and sanitize input - if m.ConnectionString == "" { - return errMissingConnectionString + err = m.PostgresAuthMetadata.InitWithMetadata(meta.Properties, azureADEnabled) + if err != nil { + return err } // Timeout @@ -79,79 +68,15 @@ func (m *postgresMetadataStruct) InitWithMetadata(meta state.Metadata) error { } // Cleanup interval - if m.CleanupInterval != nil { - // Non-positive value from meta means disable auto cleanup. - if *m.CleanupInterval <= 0 { - if meta.Properties[cleanupIntervalKey] == "" { - // unfortunately the mapstructure decoder decodes an empty string to 0, a missing key would be nil however - m.CleanupInterval = ptr.Of(defaultCleanupInternal * time.Second) - } else { - m.CleanupInterval = nil - } - } - } - - // Populate the Azure environment if using Azure AD - if m.azureADEnabled && m.UseAzureAD { - m.azureEnv, err = azure.NewEnvironmentSettings(meta.Properties) - if err != nil { - return err + // Non-positive value from meta means disable auto cleanup. + if m.CleanupInterval != nil && *m.CleanupInterval <= 0 { + if meta.Properties[cleanupIntervalKey] == "" { + // Unfortunately the mapstructure decoder decodes an empty string to 0, a missing key would be nil however + m.CleanupInterval = ptr.Of(defaultCleanupInternal * time.Second) + } else { + m.CleanupInterval = nil } } return nil } - -// GetPgxPoolConfig returns the pgxpool.Config object that contains the credentials for connecting to Postgres. -func (m *postgresMetadataStruct) GetPgxPoolConfig() (*pgxpool.Config, error) { - // Get the config from the connection string - config, err := pgxpool.ParseConfig(m.ConnectionString) - if err != nil { - return nil, fmt.Errorf("failed to parse connection string: %w", err) - } - if m.ConnectionMaxIdleTime > 0 { - config.MaxConnIdleTime = m.ConnectionMaxIdleTime - } - if m.MaxConns > 1 { - config.MaxConns = int32(m.MaxConns) - } - - // Check if we should use Azure AD - if m.azureADEnabled && m.UseAzureAD { - tokenCred, errToken := m.azureEnv.GetTokenCredential() - if errToken != nil { - return nil, errToken - } - - // Reset the password - config.ConnConfig.Password = "" - - /*// For Azure AD, using SSL is required - // If not already enabled, configure TLS without certificate validation - if config.ConnConfig.TLSConfig == nil { - config.ConnConfig.TLSConfig = &tls.Config{ - //nolint:gosec - InsecureSkipVerify: true, - } - }*/ - - // We need to retrieve the token every time we attempt a new connection - // This is because tokens expire, and connections can drop and need to be re-established at any time - // Fortunately, we can do this with the "BeforeConnect" hook - config.BeforeConnect = func(ctx context.Context, cc *pgx.ConnConfig) error { - at, err := tokenCred.GetToken(ctx, policy.TokenRequestOptions{ - Scopes: []string{ - m.azureEnv.Cloud.Services[azure.ServiceOSSRDBMS].Audience + "/.default", - }, - }) - if err != nil { - return err - } - - cc.Password = at.Token - return nil - } - } - - return config, nil -} diff --git a/internal/component/postgresql/metadata_test.go b/internal/component/postgresql/metadata_test.go index 9d56f0ab34..bb4b612f18 100644 --- a/internal/component/postgresql/metadata_test.go +++ b/internal/component/postgresql/metadata_test.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Dapr Authors +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 @@ -28,9 +28,9 @@ func TestMetadata(t *testing.T) { m := postgresMetadataStruct{} props := map[string]string{} - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.Error(t, err) - assert.ErrorIs(t, err, errMissingConnectionString) + assert.ErrorContains(t, err, "connection string") }) t.Run("has connection string", func(t *testing.T) { @@ -39,7 +39,7 @@ func TestMetadata(t *testing.T) { "connectionString": "foo", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.NoError(t, err) }) @@ -49,7 +49,7 @@ func TestMetadata(t *testing.T) { "connectionString": "foo", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.NoError(t, err) assert.Equal(t, m.TableName, defaultTableName) }) @@ -61,7 +61,7 @@ func TestMetadata(t *testing.T) { "tableName": "mytable", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.NoError(t, err) assert.Equal(t, m.TableName, "mytable") }) @@ -72,7 +72,7 @@ func TestMetadata(t *testing.T) { "connectionString": "foo", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.NoError(t, err) assert.Equal(t, defaultTimeout*time.Second, m.Timeout) }) @@ -84,7 +84,7 @@ func TestMetadata(t *testing.T) { "timeoutInSeconds": "NaN", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.Error(t, err) }) @@ -95,7 +95,7 @@ func TestMetadata(t *testing.T) { "timeoutInSeconds": "42", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.NoError(t, err) assert.Equal(t, 42*time.Second, m.Timeout) }) @@ -107,7 +107,7 @@ func TestMetadata(t *testing.T) { "timeoutInSeconds": "0", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.Error(t, err) }) @@ -117,7 +117,7 @@ func TestMetadata(t *testing.T) { "connectionString": "foo", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.NoError(t, err) _ = assert.NotNil(t, m.CleanupInterval) && assert.Equal(t, defaultCleanupInternal*time.Second, *m.CleanupInterval) @@ -130,7 +130,7 @@ func TestMetadata(t *testing.T) { "cleanupIntervalInSeconds": "NaN", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.Error(t, err) }) @@ -141,7 +141,7 @@ func TestMetadata(t *testing.T) { "cleanupIntervalInSeconds": "42", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.NoError(t, err) _ = assert.NotNil(t, m.CleanupInterval) && assert.Equal(t, 42*time.Second, *m.CleanupInterval) @@ -154,7 +154,7 @@ func TestMetadata(t *testing.T) { "cleanupIntervalInSeconds": "0", } - err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}) + err := m.InitWithMetadata(state.Metadata{Base: metadata.Base{Properties: props}}, false) assert.NoError(t, err) assert.Nil(t, m.CleanupInterval) }) diff --git a/internal/component/postgresql/postgresdbaccess.go b/internal/component/postgresql/postgresdbaccess.go index 5a5ec16e1b..431279569c 100644 --- a/internal/component/postgresql/postgresdbaccess.go +++ b/internal/component/postgresql/postgresdbaccess.go @@ -35,8 +35,6 @@ import ( "github.com/dapr/kit/ptr" ) -var errMissingConnectionString = errors.New("missing connection string") - // Interface that applies to *pgxpool.Pool. // We need this to be able to mock the connection in tests. type PGXPoolConn interface { @@ -57,9 +55,10 @@ type PostgresDBAccess struct { gc internalsql.GarbageCollector - migrateFn func(context.Context, PGXPoolConn, MigrateOptions) error - setQueryFn func(*state.SetRequest, SetQueryOptions) string - etagColumn string + migrateFn func(context.Context, PGXPoolConn, MigrateOptions) error + setQueryFn func(*state.SetRequest, SetQueryOptions) string + etagColumn string + enableAzureAD bool } // newPostgresDBAccess creates a new instance of postgresAccess. @@ -67,13 +66,11 @@ func newPostgresDBAccess(logger logger.Logger, opts Options) *PostgresDBAccess { logger.Debug("Instantiating new Postgres state store") return &PostgresDBAccess{ - logger: logger, - metadata: postgresMetadataStruct{ - azureADEnabled: opts.EnableAzureAD, - }, - migrateFn: opts.MigrateFn, - setQueryFn: opts.SetQueryFn, - etagColumn: opts.ETagColumn, + logger: logger, + migrateFn: opts.MigrateFn, + setQueryFn: opts.SetQueryFn, + etagColumn: opts.ETagColumn, + enableAzureAD: opts.EnableAzureAD, } } @@ -81,7 +78,7 @@ func newPostgresDBAccess(logger logger.Logger, opts Options) *PostgresDBAccess { func (p *PostgresDBAccess) Init(ctx context.Context, meta state.Metadata) error { p.logger.Debug("Initializing Postgres state store") - err := p.metadata.InitWithMetadata(meta) + err := p.metadata.InitWithMetadata(meta, p.enableAzureAD) if err != nil { p.logger.Errorf("Failed to parse metadata: %v", err) return err diff --git a/state/postgresql/metadata.yaml b/state/postgresql/metadata.yaml index 2d88cf6b6b..e8eee6fbb1 100644 --- a/state/postgresql/metadata.yaml +++ b/state/postgresql/metadata.yaml @@ -37,7 +37,7 @@ builtinAuthenticationProfiles: type: string authenticationProfiles: - title: "Connection string" - description: "Authenticate using a Connection String." + description: "Authenticate using a Connection String" metadata: - name: connectionString required: true diff --git a/tests/certification/bindings/postgres/components/standard/postgres.yaml b/tests/certification/bindings/postgres/components/standard/postgres.yaml index 0c9f368f74..ebfc129b32 100644 --- a/tests/certification/bindings/postgres/components/standard/postgres.yaml +++ b/tests/certification/bindings/postgres/components/standard/postgres.yaml @@ -3,8 +3,8 @@ kind: Component metadata: name: standard-binding spec: - type: bindings.postgres + type: bindings.postgresql version: v1 metadata: - - name: url - value: "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" \ No newline at end of file + - name: connectionString + value: "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" diff --git a/tests/certification/bindings/postgres/docker-compose.yml b/tests/certification/bindings/postgres/docker-compose.yml index 48a388d3f9..2d62bd1b27 100644 --- a/tests/certification/bindings/postgres/docker-compose.yml +++ b/tests/certification/bindings/postgres/docker-compose.yml @@ -1,11 +1,11 @@ version: '2' services: db: - image: postgres + image: postgres:15-alpine restart: always ports: - "5432:5432" environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: example - POSTGRES_DB: dapr_test \ No newline at end of file + POSTGRES_DB: dapr_test diff --git a/tests/certification/bindings/postgres/postgres_test.go b/tests/certification/bindings/postgres/postgres_test.go index ac825be467..6cc0052f98 100644 --- a/tests/certification/bindings/postgres/postgres_test.go +++ b/tests/certification/bindings/postgres/postgres_test.go @@ -19,7 +19,8 @@ import ( "testing" "time" - _ "github.com/lib/pq" + // PGX driver for database/sql + _ "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -46,6 +47,7 @@ const ( ) func TestPostgres(t *testing.T) { + const tableName = "dapr_test_table" ports, _ := dapr_testing.GetFreePorts(3) grpcPort := ports[0] @@ -59,7 +61,7 @@ func TestPostgres(t *testing.T) { ctx.Log("Invoking output binding for exec operation!") req := &daprClient.InvokeBindingRequest{Name: "standard-binding", Operation: "exec", Metadata: metadata} - req.Metadata["sql"] = "INSERT INTO dapr_test_table (id, c1, ts) VALUES (1, 'demo', '2020-09-24T11:45:05Z07:00');" + req.Metadata["sql"] = "INSERT INTO " + tableName + " (id, c1, ts) VALUES (1, 'demo', '2020-09-24T11:45:05Z07:00');" errBinding := client.InvokeOutputBinding(ctx, req) require.NoError(ctx, errBinding, "error in output binding - exec") @@ -74,7 +76,7 @@ func TestPostgres(t *testing.T) { ctx.Log("Invoking output binding for query operation!") req := &daprClient.InvokeBindingRequest{Name: "standard-binding", Operation: "query", Metadata: metadata} - req.Metadata["sql"] = "SELECT * FROM dapr_test_table WHERE id = 1;" + req.Metadata["sql"] = "SELECT * FROM " + tableName + " WHERE id = 1;" resp, errBinding := client.InvokeBinding(ctx, req) assert.Contains(t, string(resp.Data), "1,\"demo\",\"2020-09-24T11:45:05Z07:00\"") require.NoError(ctx, errBinding, "error in output binding - query") @@ -84,17 +86,18 @@ func TestPostgres(t *testing.T) { testClose := func(ctx flow.Context) error { client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) - require.NoError(t, err, "Could not initialize dapr client.") + require.NoError(ctx, err, "Could not initialize dapr client.") metadata := make(map[string]string) - ctx.Log("Invoking output binding for query operation!") + 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 dapr_test_table WHERE id = 1;" + req.Metadata["sql"] = "SELECT * FROM " + tableName + " WHERE id = 1;" errBinding = client.InvokeOutputBinding(ctx, req) require.Error(ctx, errBinding, "error in output binding - query") @@ -102,9 +105,9 @@ func TestPostgres(t *testing.T) { } createTable := func(ctx flow.Context) error { - db, err := sql.Open("postgres", dockerConnectionString) + db, err := sql.Open("pgx", dockerConnectionString) assert.NoError(t, err) - _, err = db.Exec("CREATE TABLE dapr_test_table(id INT, c1 TEXT, ts TEXT);") + _, err = db.Exec("CREATE TABLE " + tableName + " (id INT, c1 TEXT, ts TEXT);") assert.NoError(t, err) db.Close() return nil @@ -114,7 +117,6 @@ func TestPostgres(t *testing.T) { Step(dockercompose.Run("db", dockerComposeYAML)). Step("wait for component to start", flow.Sleep(10*time.Second)). Step("Creating table", createTable). - Step("wait for component to start", flow.Sleep(10*time.Second)). Step(sidecar.Run("standardSidecar", embedded.WithoutApp(), embedded.WithDaprGRPCPort(grpcPort), @@ -124,14 +126,13 @@ func TestPostgres(t *testing.T) { )). Step("Run exec test", testExec). Step("Run query test", testQuery). - Step("wait for DB operations to complete", flow.Sleep(10*time.Second)). Step("Run close test", testClose). Step("stop postgresql", dockercompose.Stop("db", dockerComposeYAML, "db")). - Step("wait for component to start", flow.Sleep(10*time.Second)). Run() } func TestPostgresNetworkError(t *testing.T) { + const tableName = "dapr_test_table_network" ports, _ := dapr_testing.GetFreePorts(3) grpcPort := ports[0] @@ -145,7 +146,7 @@ func TestPostgresNetworkError(t *testing.T) { ctx.Log("Invoking output binding for exec operation!") req := &daprClient.InvokeBindingRequest{Name: "standard-binding", Operation: "exec", Metadata: metadata} - req.Metadata["sql"] = "INSERT INTO dapr_test_table (id, c1, ts) VALUES (1, 'demo', '2020-09-24T11:45:05Z07:00');" + req.Metadata["sql"] = "INSERT INTO " + tableName + " (id, c1, ts) VALUES (1, 'demo', '2020-09-24T11:45:05Z07:00');" errBinding := client.InvokeOutputBinding(ctx, req) require.NoError(ctx, errBinding, "error in output binding - exec") @@ -160,7 +161,7 @@ func TestPostgresNetworkError(t *testing.T) { ctx.Log("Invoking output binding for query operation!") req := &daprClient.InvokeBindingRequest{Name: "standard-binding", Operation: "query", Metadata: metadata} - req.Metadata["sql"] = "SELECT * FROM dapr_test_table WHERE id = 1;" + req.Metadata["sql"] = "SELECT * FROM " + tableName + " WHERE id = 1;" errBinding := client.InvokeOutputBinding(ctx, req) require.NoError(ctx, errBinding, "error in output binding - query") @@ -168,9 +169,9 @@ func TestPostgresNetworkError(t *testing.T) { } createTable := func(ctx flow.Context) error { - db, err := sql.Open("postgres", dockerConnectionString) + db, err := sql.Open("pgx", dockerConnectionString) assert.NoError(t, err) - _, err = db.Exec("CREATE TABLE dapr_test_table(id INT, c1 TEXT, ts TEXT);") + _, err = db.Exec("CREATE TABLE " + tableName + "(id INT, c1 TEXT, ts TEXT);") assert.NoError(t, err) db.Close() return nil @@ -180,7 +181,6 @@ func TestPostgresNetworkError(t *testing.T) { Step(dockercompose.Run("db", dockerComposeYAML)). Step("wait for component to start", flow.Sleep(10*time.Second)). Step("Creating table", createTable). - Step("wait for component to start", flow.Sleep(10*time.Second)). Step(sidecar.Run("standardSidecar", embedded.WithoutApp(), embedded.WithDaprGRPCPort(grpcPort), @@ -190,8 +190,8 @@ func TestPostgresNetworkError(t *testing.T) { )). Step("Run exec test", testExec). Step("Run query test", testQuery). - Step("wait for DB operations to complete", flow.Sleep(10*time.Second)). - Step("interrupt network", network.InterruptNetwork(30*time.Second, nil, nil, "5432")). + Step("wait for DB operations to complete", flow.Sleep(5*time.Second)). + Step("interrupt network", network.InterruptNetwork(20*time.Second, nil, nil, "5432")). Step("wait for component to recover", flow.Sleep(10*time.Second)). Step("Run query test", testQuery). Run() @@ -204,7 +204,7 @@ func componentRuntimeOptions() []runtime.Option { bindingsRegistry.Logger = log bindingsRegistry.RegisterOutputBinding(func(l logger.Logger) bindings.OutputBinding { return binding_postgres.NewPostgres(l) - }, "postgres") + }, "postgresql") return []runtime.Option{ runtime.WithBindings(bindingsRegistry), diff --git a/tests/certification/configuration/postgres/docker-compose.yml b/tests/certification/configuration/postgres/docker-compose.yml index 560a5d111c..dd46ec9693 100644 --- a/tests/certification/configuration/postgres/docker-compose.yml +++ b/tests/certification/configuration/postgres/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: db: - image: postgres:15 + image: postgres:15-alpine restart: always ports: - "5432:5432" diff --git a/tests/certification/go.mod b/tests/certification/go.mod index 8896bf8d2f..6c16649bd7 100644 --- a/tests/certification/go.mod +++ b/tests/certification/go.mod @@ -28,7 +28,6 @@ require ( github.com/google/uuid v1.3.0 github.com/jackc/pgx/v5 v5.3.1 github.com/lestrrat-go/jwx/v2 v2.0.11 - github.com/lib/pq v1.10.7 github.com/nacos-group/nacos-sdk-go/v2 v2.1.3 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/rabbitmq/amqp091-go v1.8.1 diff --git a/tests/certification/go.sum b/tests/certification/go.sum index 3bdfe28575..9f6d9eb0f8 100644 --- a/tests/certification/go.sum +++ b/tests/certification/go.sum @@ -893,8 +893,6 @@ github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570/go.mod h1:BLt8L9ld7wVsvEWQbuLrUZnCMnUmLZ+CGDzKtclrTlE= github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f/go.mod h1:UGmTpUd3rjbtfIpwAPrcfmGf/Z1HS95TATB+m57TPB8= github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042/go.mod h1:TPpsiPUEh0zFL1Snz4crhMlBe60PYxRHr5oFF3rRYg0= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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.9.8 h1:jN50elxBsGBDGVDEKqUlDuU1cFwJ11K/yrJCBMe/7Wg= diff --git a/tests/certification/state/postgresql/docker-compose.yml b/tests/certification/state/postgresql/docker-compose.yml index 48a388d3f9..dd46ec9693 100644 --- a/tests/certification/state/postgresql/docker-compose.yml +++ b/tests/certification/state/postgresql/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: db: - image: postgres + image: postgres:15-alpine restart: always ports: - "5432:5432" diff --git a/tests/config/bindings/postgres/bindings.yml b/tests/config/bindings/postgres/bindings.yml deleted file mode 100644 index be68503baa..0000000000 --- a/tests/config/bindings/postgres/bindings.yml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: postgres-binding -spec: - type: bindings.postgres - version: v1 - metadata: - - name: url # Required - value: "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" diff --git a/tests/config/bindings/postgresql/azure/bindings.yml b/tests/config/bindings/postgresql/azure/bindings.yml new file mode 100644 index 0000000000..7d66ed5021 --- /dev/null +++ b/tests/config/bindings/postgresql/azure/bindings.yml @@ -0,0 +1,18 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: postgres-binding +spec: + type: bindings.postgresql + version: v1 + metadata: + - name: connectionString + value: "${{AzureDBPostgresConnectionString}}" + - name: azureClientId + value: "${{AzureDBPostgresClientId}}" + - name: azureClientSecret + value: "${{AzureDBPostgresClientSecret}}" + - name: azureTenantId + value: "${{AzureDBPostgresTenantId}}" + - name: useAzureAD + value: "true" \ No newline at end of file diff --git a/tests/config/bindings/postgresql/docker/bindings.yml b/tests/config/bindings/postgresql/docker/bindings.yml new file mode 100644 index 0000000000..f6b9679791 --- /dev/null +++ b/tests/config/bindings/postgresql/docker/bindings.yml @@ -0,0 +1,11 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: postgres-binding +spec: + type: bindings.postgresql + version: v1 + metadata: + # "url" is the old name for "connectionString" and is kept here to test for backwards-compatibility + - name: url + value: "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" diff --git a/tests/config/bindings/tests.yml b/tests/config/bindings/tests.yml index b8efdcf7a8..62c83c0c81 100644 --- a/tests/config/bindings/tests.yml +++ b/tests/config/bindings/tests.yml @@ -73,7 +73,9 @@ components: checkInOrderProcessing: false - component: kubemq operations: [ "create", "operations", "read" ] - - component: postgres + - component: postgresql.docker + operations: [ "exec", "query", "close", "operations" ] + - component: postgresql.azure operations: [ "exec", "query", "close", "operations" ] - component: aws.s3.docker operations: ["create", "operations", "get", "list"] diff --git a/tests/config/configuration/postgresql/azure/configstore.yml b/tests/config/configuration/postgresql/azure/configstore.yml new file mode 100644 index 0000000000..375f4f5391 --- /dev/null +++ b/tests/config/configuration/postgresql/azure/configstore.yml @@ -0,0 +1,20 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: configstore +spec: + type: configuration.postgresql + version: v1 + metadata: + - name: connectionString + value: "${{AzureDBPostgresConnectionString}}" + - name: azureClientId + value: "${{AzureDBPostgresClientId}}" + - name: azureClientSecret + value: "${{AzureDBPostgresClientSecret}}" + - name: azureTenantId + value: "${{AzureDBPostgresTenantId}}" + - name: useAzureAD + value: "true" + - name: table + value: configtable \ No newline at end of file diff --git a/tests/config/configuration/postgres/configstore.yml b/tests/config/configuration/postgresql/docker/configstore.yml similarity index 85% rename from tests/config/configuration/postgres/configstore.yml rename to tests/config/configuration/postgresql/docker/configstore.yml index b2d313707e..5f408c6f4d 100644 --- a/tests/config/configuration/postgres/configstore.yml +++ b/tests/config/configuration/postgresql/docker/configstore.yml @@ -3,7 +3,8 @@ kind: Component metadata: name: configstore spec: - type: configuration.postgres + type: configuration.postgresql + version: v1 metadata: - name: connectionString value: "host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test" diff --git a/tests/config/configuration/tests.yml b/tests/config/configuration/tests.yml index 17dd6354a0..6713453afa 100644 --- a/tests/config/configuration/tests.yml +++ b/tests/config/configuration/tests.yml @@ -5,5 +5,7 @@ components: operations: [] - component: redis.v7 operations: [] - - component: postgres + - component: postgresql.azure + operations: [] + - component: postgresql.docker operations: [] diff --git a/tests/config/state/postgresql/azure/statestore.yml b/tests/config/state/postgresql/azure/statestore.yml index 1788537e8e..b58a92c49c 100644 --- a/tests/config/state/postgresql/azure/statestore.yml +++ b/tests/config/state/postgresql/azure/statestore.yml @@ -8,5 +8,11 @@ spec: metadata: - name: connectionString value: "${{AzureDBPostgresConnectionString}}" + - name: azureClientId + value: "${{AzureDBPostgresClientId}}" + - name: azureClientSecret + value: "${{AzureDBPostgresClientSecret}}" + - name: azureTenantId + value: "${{AzureDBPostgresTenantId}}" - name: useAzureAD value: "true" \ No newline at end of file diff --git a/tests/conformance/common.go b/tests/conformance/common.go index 7ba46cd7e5..bfd4850992 100644 --- a/tests/conformance/common.go +++ b/tests/conformance/common.go @@ -431,13 +431,10 @@ func loadConfigurationStore(tc TestComponent) (configuration.Store, configupdate var store configuration.Store var updater configupdater.Updater switch tc.Component { - case redisv6: - store = c_redis.NewRedisConfigurationStore(testLogger) - updater = cu_redis.NewRedisConfigUpdater(testLogger) - case redisv7: + case redisv6, redisv7: store = c_redis.NewRedisConfigurationStore(testLogger) updater = cu_redis.NewRedisConfigUpdater(testLogger) - case "postgres": + case "postgresql.docker", "postgresql.azure": store = c_postgres.NewPostgresConfigurationStore(testLogger) updater = cu_postgres.NewPostgresConfigUpdater(testLogger) default: @@ -624,7 +621,9 @@ func loadOutputBindings(tc TestComponent) bindings.OutputBinding { binding = b_rabbitmq.NewRabbitMQ(testLogger) case "kubemq": binding = b_kubemq.NewKubeMQ(testLogger) - case "postgres": + case "postgresql.docker": + binding = b_postgres.NewPostgres(testLogger) + case "postgresql.azure": binding = b_postgres.NewPostgres(testLogger) case "aws.s3.docker": binding = b_aws_s3.NewAWSS3(testLogger) diff --git a/tests/conformance/configuration/configuration.go b/tests/conformance/configuration/configuration.go index 4dd76bac70..988e6192c5 100644 --- a/tests/conformance/configuration/configuration.go +++ b/tests/conformance/configuration/configuration.go @@ -37,7 +37,7 @@ const ( v1 = "1.0.0" defaultMaxReadDuration = 30 * time.Second defaultWaitDuration = 5 * time.Second - postgresComponent = "postgres" + postgresComponent = "postgresql" pgNotifyChannelKey = "pgNotifyChannel" pgNotifyChannel = "config" ) @@ -152,7 +152,7 @@ func ConformanceTests(t *testing.T, props map[string]string, store configuration require.NoError(t, err) // Creating trigger for postgres config updater - if component == postgresComponent { + if strings.HasPrefix(component, postgresComponent) { err = updater.(*postgres_updater.ConfigUpdater).CreateTrigger(pgNotifyChannel) require.NoError(t, err) } @@ -223,7 +223,7 @@ func ConformanceTests(t *testing.T, props map[string]string, store configuration t.Run("subscribe", func(t *testing.T) { subscribeMetadata := make(map[string]string) - if component == postgresComponent { + if strings.HasPrefix(component, postgresComponent) { subscribeMetadata[pgNotifyChannelKey] = pgNotifyChannel } t.Run("subscriber 1 with non-empty key list", func(t *testing.T) { @@ -307,7 +307,7 @@ func ConformanceTests(t *testing.T, props map[string]string, store configuration // Delete initValues2 errDelete := updater.DeleteKey(getKeys(initValues2)) assert.NoError(t, errDelete, "expected no error on updating keys") - if component != postgresComponent { + if !strings.HasPrefix(component, postgresComponent) { for k := range initValues2 { initValues2[k] = &configuration.Item{} } @@ -323,10 +323,9 @@ func ConformanceTests(t *testing.T, props map[string]string, store configuration t.Run("unsubscribe", func(t *testing.T) { t.Run("unsubscribe subscriber 1", func(t *testing.T) { - ID1 := subscribeIDs[0] err := store.Unsubscribe(context.Background(), &configuration.UnsubscribeRequest{ - ID: ID1, + ID: subscribeIDs[0], }, ) assert.NoError(t, err, "expected no error in unsubscribe") @@ -346,10 +345,9 @@ func ConformanceTests(t *testing.T, props map[string]string, store configuration }) t.Run("unsubscribe subscriber 2", func(t *testing.T) { - ID2 := subscribeIDs[1] err := store.Unsubscribe(context.Background(), &configuration.UnsubscribeRequest{ - ID: ID2, + ID: subscribeIDs[1], }, ) assert.NoError(t, err, "expected no error in unsubscribe") @@ -367,10 +365,9 @@ func ConformanceTests(t *testing.T, props map[string]string, store configuration }) t.Run("unsubscribe subscriber 3", func(t *testing.T) { - ID3 := subscribeIDs[2] err := store.Unsubscribe(context.Background(), &configuration.UnsubscribeRequest{ - ID: ID3, + ID: subscribeIDs[2], }, ) assert.NoError(t, err, "expected no error in unsubscribe") diff --git a/tests/utils/configupdater/postgres/postgres.go b/tests/utils/configupdater/postgres/postgres.go index 609bdd469b..ce5b02bb2b 100644 --- a/tests/utils/configupdater/postgres/postgres.go +++ b/tests/utils/configupdater/postgres/postgres.go @@ -5,10 +5,13 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/dapr/components-contrib/configuration" + pgauth "github.com/dapr/components-contrib/internal/authentication/postgresql" + "github.com/dapr/components-contrib/internal/utils" "github.com/dapr/components-contrib/tests/utils/configupdater" "github.com/dapr/kit/logger" ) @@ -72,7 +75,7 @@ func (r *ConfigUpdater) CreateTrigger(channel string) error { return fmt.Errorf("error creating config event function : %w", err) } triggerName := "configTrigger_" + channel - createTriggerSQL := "CREATE TRIGGER " + triggerName + " AFTER INSERT OR UPDATE OR DELETE ON " + r.configTable + " FOR EACH ROW EXECUTE PROCEDURE " + procedureName + "();" + createTriggerSQL := "CREATE OR REPLACE TRIGGER " + triggerName + " AFTER INSERT OR UPDATE OR DELETE ON " + r.configTable + " FOR EACH ROW EXECUTE PROCEDURE " + procedureName + "();" _, err = r.client.Exec(ctx, createTriggerSQL) if err != nil { return fmt.Errorf("error creating config trigger : %w", err) @@ -81,31 +84,38 @@ func (r *ConfigUpdater) CreateTrigger(channel string) error { } func (r *ConfigUpdater) Init(props map[string]string) error { - var conn string - ctx := context.Background() - if val, ok := props["connectionString"]; ok && val != "" { - conn = val - } else { - return fmt.Errorf("missing postgreSQL connection string") + md := pgauth.PostgresAuthMetadata{ + ConnectionString: props["connectionString"], + UseAzureAD: utils.IsTruthy(props["useAzureAD"]), + } + err := md.InitWithMetadata(props, true) + if err != nil { + return err } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + if tbl, ok := props["table"]; ok && tbl != "" { r.configTable = tbl } else { return fmt.Errorf("missing postgreSQL configuration table name") } - config, err := pgxpool.ParseConfig(conn) + + config, err := md.GetPgxPoolConfig() if err != nil { return fmt.Errorf("postgres configuration store connection error : %w", err) } - pool, err := pgxpool.NewWithConfig(ctx, config) + + r.client, err = pgxpool.NewWithConfig(ctx, config) if err != nil { return fmt.Errorf("postgres configuration store connection error : %w", err) } - err = pool.Ping(ctx) + err = r.client.Ping(ctx) if err != nil { return fmt.Errorf("postgres configuration store ping error : %w", err) } - r.client = pool + err = createAndSetTable(ctx, r.client, r.configTable) if err != nil { return fmt.Errorf("postgres configuration store table creation error : %w", err) From 1ab15ef04ba571ff7c7c51b299d77afaa4565071 Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:34:14 -0700 Subject: [PATCH 16/19] Postgres binding: support parametrized queries (#2972) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> --- bindings/postgres/postgres.go | 30 ++++-- .../certification/bindings/postgres/README.md | 1 + .../bindings/postgres/postgres_test.go | 99 ++++++++++++------- 3 files changed, 87 insertions(+), 43 deletions(-) diff --git a/bindings/postgres/postgres.go b/bindings/postgres/postgres.go index 9919f9b1d0..637b88d344 100644 --- a/bindings/postgres/postgres.go +++ b/bindings/postgres/postgres.go @@ -36,7 +36,8 @@ const ( queryOperation bindings.OperationKind = "query" closeOperation bindings.OperationKind = "close" - commandSQLKey = "sql" + commandSQLKey = "sql" + commandArgsKey = "params" ) // Postgres represents PostgreSQL output binding. @@ -109,11 +110,22 @@ func (p *Postgres) Invoke(ctx context.Context, req *bindings.InvokeRequest) (res return nil, errors.New("metadata required") } - sql, ok := req.Metadata[commandSQLKey] - if !ok || sql == "" { + // Metadata property "sql" contains the query to execute + sql := req.Metadata[commandSQLKey] + if sql == "" { return nil, fmt.Errorf("required metadata not set: %s", commandSQLKey) } + // Metadata property "params" contains JSON-encoded parameters, and it's optional + // If present, it must be unserializable into a []any object + var args []any + if argsStr := req.Metadata[commandArgsKey]; argsStr != "" { + err = json.Unmarshal([]byte(argsStr), &args) + if err != nil { + return nil, fmt.Errorf("invalid metadata property %s: failed to unserialize into an array: %w", commandArgsKey, err) + } + } + startTime := time.Now().UTC() resp = &bindings.InvokeResponse{ Metadata: map[string]string{ @@ -125,14 +137,14 @@ func (p *Postgres) Invoke(ctx context.Context, req *bindings.InvokeRequest) (res switch req.Operation { //nolint:exhaustive case execOperation: - r, err := p.exec(ctx, sql) + r, err := p.exec(ctx, sql, args...) if err != nil { return nil, err } resp.Metadata["rows-affected"] = strconv.FormatInt(r, 10) // 0 if error case queryOperation: - d, err := p.query(ctx, sql) + d, err := p.query(ctx, sql, args...) if err != nil { return nil, err } @@ -168,8 +180,8 @@ func (p *Postgres) Close() error { return nil } -func (p *Postgres) query(ctx context.Context, sql string) (result []byte, err error) { - rows, err := p.db.Query(ctx, sql) +func (p *Postgres) query(ctx context.Context, sql string, args ...any) (result []byte, err error) { + rows, err := p.db.Query(ctx, sql, args...) if err != nil { return nil, fmt.Errorf("error executing query: %w", err) } @@ -191,8 +203,8 @@ func (p *Postgres) query(ctx context.Context, sql string) (result []byte, err er return result, nil } -func (p *Postgres) exec(ctx context.Context, sql string) (result int64, err error) { - res, err := p.db.Exec(ctx, sql) +func (p *Postgres) exec(ctx context.Context, sql string, args ...any) (result int64, err error) { + res, err := p.db.Exec(ctx, sql, args...) if err != nil { return 0, fmt.Errorf("error executing query: %w", err) } diff --git a/tests/certification/bindings/postgres/README.md b/tests/certification/bindings/postgres/README.md index 1759d74259..f30909c863 100644 --- a/tests/certification/bindings/postgres/README.md +++ b/tests/certification/bindings/postgres/README.md @@ -17,6 +17,7 @@ The purpose of this module is to provide tests that certify the PostgreSQL Outpu * Run dapr application with component to store data in postgres as output binding. * Read stored data from postgres. * Ensure that read data is same as the data that was stored. + * Verify the ability to use named paramters in queries. * Verify reconnection to postgres for output binding. * Simulate a network error before sending any messages. * Run dapr application with the component. diff --git a/tests/certification/bindings/postgres/postgres_test.go b/tests/certification/bindings/postgres/postgres_test.go index 6cc0052f98..deddaaccb2 100644 --- a/tests/certification/bindings/postgres/postgres_test.go +++ b/tests/certification/bindings/postgres/postgres_test.go @@ -55,31 +55,58 @@ func TestPostgres(t *testing.T) { testExec := func(ctx flow.Context) error { client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) - require.NoError(t, err, "Could not initialize dapr client.") - - metadata := make(map[string]string) - - ctx.Log("Invoking output binding for exec operation!") - req := &daprClient.InvokeBindingRequest{Name: "standard-binding", Operation: "exec", Metadata: metadata} - req.Metadata["sql"] = "INSERT INTO " + tableName + " (id, c1, ts) VALUES (1, 'demo', '2020-09-24T11:45:05Z07:00');" - errBinding := client.InvokeOutputBinding(ctx, req) - require.NoError(ctx, errBinding, "error in output binding - exec") + 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": "INSERT INTO " + tableName + " (id, c1, ts) VALUES (1, 'demo', '2020-09-24T11:45:05+07:00');", + }, + }) + 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": "INSERT INTO " + tableName + " (id, c1, ts) VALUES ($1, $2, $3);", + "params": `[2, "demo2", "2021-03-19T11:45:05+07:00"]`, + }, + }) + 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.") - - metadata := make(map[string]string) - - 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;" - resp, errBinding := client.InvokeBinding(ctx, req) - assert.Contains(t, string(resp.Data), "1,\"demo\",\"2020-09-24T11:45:05Z07:00\"") - require.NoError(ctx, errBinding, "error in output binding - query") + 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;", + }, + }) + assert.Equal(t, `[[1,"demo","2020-09-24T11:45:05Z"]]`, string(resp.Data)) + require.NoError(ctx, err, "error in output binding - query") + + 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 = ANY($1);", + "params": `[[1, 2]]`, + }, + }) + assert.Equal(t, `[[1,"demo","2020-09-24T11:45:05Z"],[2,"demo2","2021-03-19T11:45:05Z"]]`, string(resp.Data)) + require.NoError(ctx, err, "error in output binding - query") return nil } @@ -107,8 +134,8 @@ func TestPostgres(t *testing.T) { createTable := func(ctx flow.Context) error { db, err := sql.Open("pgx", dockerConnectionString) assert.NoError(t, err) - _, err = db.Exec("CREATE TABLE " + tableName + " (id INT, c1 TEXT, ts TEXT);") - assert.NoError(t, err) + _, err = db.Exec("CREATE TABLE " + tableName + " (id INT, c1 TEXT, ts TIMESTAMP);") + require.NoError(t, err) db.Close() return nil } @@ -140,14 +167,16 @@ func TestPostgresNetworkError(t *testing.T) { testExec := func(ctx flow.Context) error { client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) - require.NoError(t, err, "Could not initialize dapr client.") - - metadata := make(map[string]string) + require.NoError(t, err, "Could not initialize dapr client") ctx.Log("Invoking output binding for exec operation!") - req := &daprClient.InvokeBindingRequest{Name: "standard-binding", Operation: "exec", Metadata: metadata} - req.Metadata["sql"] = "INSERT INTO " + tableName + " (id, c1, ts) VALUES (1, 'demo', '2020-09-24T11:45:05Z07:00');" - errBinding := client.InvokeOutputBinding(ctx, req) + errBinding := client.InvokeOutputBinding(ctx, &daprClient.InvokeBindingRequest{ + Name: "standard-binding", + Operation: "exec", + Metadata: map[string]string{ + "sql": "INSERT INTO " + tableName + " (id, c1, ts) VALUES (1, 'demo', '2020-09-24T11:45:05+07:00');", + }, + }) require.NoError(ctx, errBinding, "error in output binding - exec") return nil @@ -155,14 +184,16 @@ func TestPostgresNetworkError(t *testing.T) { testQuery := func(ctx flow.Context) error { client, err := daprClient.NewClientWithPort(fmt.Sprintf("%d", grpcPort)) - require.NoError(t, err, "Could not initialize dapr client.") - - metadata := make(map[string]string) + require.NoError(t, err, "Could not initialize dapr client") 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) + errBinding := client.InvokeOutputBinding(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 @@ -171,7 +202,7 @@ func TestPostgresNetworkError(t *testing.T) { createTable := func(ctx flow.Context) error { db, err := sql.Open("pgx", dockerConnectionString) assert.NoError(t, err) - _, err = db.Exec("CREATE TABLE " + tableName + "(id INT, c1 TEXT, ts TEXT);") + _, err = db.Exec("CREATE TABLE " + tableName + " (id INT, c1 TEXT, ts TIMESTAMP);") assert.NoError(t, err) db.Close() return nil From 95045c4dfeeff85a9792955b87004a1e1a401c63 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Singh Date: Fri, 14 Jul 2023 02:57:42 +0530 Subject: [PATCH 17/19] Add output binding for OpenAI (#2965) Signed-off-by: Shivam Kumar Singh Signed-off-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> --- bindings/azure/openai/metadata.yaml | 38 ++++ bindings/azure/openai/openai.go | 326 ++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 6 +- tests/certification/go.mod | 2 +- tests/certification/go.sum | 4 +- 6 files changed, 373 insertions(+), 6 deletions(-) create mode 100644 bindings/azure/openai/metadata.yaml create mode 100644 bindings/azure/openai/openai.go diff --git a/bindings/azure/openai/metadata.yaml b/bindings/azure/openai/metadata.yaml new file mode 100644 index 0000000000..f579916940 --- /dev/null +++ b/bindings/azure/openai/metadata.yaml @@ -0,0 +1,38 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: bindings +name: azure.openai +version: v1 +status: alpha +title: "Azure OpenAI" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-bindings/azure-openai/ +binding: + output: true + input: false + operations: + - name: completion + description: "Text completion" + - name: chat-completion + description: "Chat completion" +builtinAuthenticationProfiles: + - name: "azuread" +authenticationProfiles: + - title: "API Key" + description: "Authenticate using an API key" + metadata: + - name: apiKey + required: true + sensitive: true + description: "API Key" + example: '"1234567890abcdef"' +metadata: + - name: endpoint + required: true + description: "Endpoint of the Azure OpenAI service" + example: '"https://myopenai.openai.azure.com"' + - name: deploymentID + required: true + description: "ID of the model deployment in the Azure OpenAI service" + example: '"my-model"' diff --git a/bindings/azure/openai/openai.go b/bindings/azure/openai/openai.go new file mode 100644 index 0000000000..83d189203a --- /dev/null +++ b/bindings/azure/openai/openai.go @@ -0,0 +1,326 @@ +/* +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 openai + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/cognitiveservices/azopenai" + + "github.com/dapr/components-contrib/bindings" + azauth "github.com/dapr/components-contrib/internal/authentication/azure" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/kit/config" + "github.com/dapr/kit/logger" +) + +// List of operations. +const ( + CompletionOperation bindings.OperationKind = "completion" + ChatCompletionOperation bindings.OperationKind = "chat-completion" + + APIKey = "apiKey" + DeploymentID = "deploymentID" + Endpoint = "endpoint" + MessagesKey = "messages" + Temperature = "temperature" + MaxTokens = "maxTokens" + TopP = "topP" + N = "n" + Stop = "stop" + FrequencyPenalty = "frequencyPenalty" + LogitBias = "logitBias" + User = "user" +) + +// AzOpenAI represents OpenAI output binding. +type AzOpenAI struct { + logger logger.Logger + client *azopenai.Client +} + +type openAIMetadata struct { + // APIKey is the API key for the Azure OpenAI API. + APIKey string `mapstructure:"apiKey"` + // DeploymentID is the deployment ID for the Azure OpenAI API. + DeploymentID string `mapstructure:"deploymentID"` + // Endpoint is the endpoint for the Azure OpenAI API. + Endpoint string `mapstructure:"endpoint"` +} + +type ChatSettings struct { + Temperature float32 `mapstructure:"temperature"` + MaxTokens int32 `mapstructure:"maxTokens"` + TopP float32 `mapstructure:"topP"` + N int32 `mapstructure:"n"` + PresencePenalty float32 `mapstructure:"presencePenalty"` + FrequencyPenalty float32 `mapstructure:"frequencyPenalty"` +} + +// ChatMessages type for chat completion API. +type ChatMessages struct { + Messages []Message `json:"messages"` + Temperature float32 `json:"temperature"` + MaxTokens int32 `json:"maxTokens"` + TopP float32 `json:"topP"` + N int32 `json:"n"` + PresencePenalty float32 `json:"presencePenalty"` + FrequencyPenalty float32 `json:"frequencyPenalty"` +} + +// Message type stores the messages for bot conversation. +type Message struct { + Role string + Message string +} + +// Prompt type for completion API. +type Prompt struct { + Prompt string `json:"prompt"` + Temperature float32 `json:"temperature"` + MaxTokens int32 `json:"maxTokens"` + TopP float32 `json:"topP"` + N int32 `json:"n"` + PresencePenalty float32 `json:"presencePenalty"` + FrequencyPenalty float32 `json:"frequencyPenalty"` +} + +// NewOpenAI returns a new OpenAI output binding. +func NewOpenAI(logger logger.Logger) bindings.OutputBinding { + return &AzOpenAI{ + logger: logger, + } +} + +// Init initializes the OpenAI binding. +func (p *AzOpenAI) Init(ctx context.Context, meta bindings.Metadata) error { + m := openAIMetadata{} + err := metadata.DecodeMetadata(meta.Properties, &m) + if err != nil { + return fmt.Errorf("error decoding metadata: %w", err) + } + if m.Endpoint == "" { + return fmt.Errorf("required metadata not set: %s", Endpoint) + } + if m.DeploymentID == "" { + return fmt.Errorf("required metadata not set: %s", DeploymentID) + } + + if m.APIKey != "" { + // use API key authentication + var keyCredential azopenai.KeyCredential + keyCredential, err = azopenai.NewKeyCredential(m.APIKey) + if err != nil { + return fmt.Errorf("error getting credentials object: %w", err) + } + + p.client, err = azopenai.NewClientWithKeyCredential(m.Endpoint, keyCredential, m.DeploymentID, nil) + if err != nil { + return fmt.Errorf("error creating Azure OpenAI client: %w", err) + } + } else { + // fallback to Azure AD authentication + settings, innerErr := azauth.NewEnvironmentSettings(meta.Properties) + if innerErr != nil { + return fmt.Errorf("error creating environment settings: %w", innerErr) + } + + token, innerErr := settings.GetTokenCredential() + if innerErr != nil { + return fmt.Errorf("error getting token credential: %w", innerErr) + } + + p.client, err = azopenai.NewClient(m.Endpoint, token, m.DeploymentID, nil) + if err != nil { + return fmt.Errorf("error creating Azure OpenAI client: %w", err) + } + } + + return nil +} + +// Operations returns list of operations supported by OpenAI binding. +func (p *AzOpenAI) Operations() []bindings.OperationKind { + return []bindings.OperationKind{ + ChatCompletionOperation, + CompletionOperation, + } +} + +// Invoke handles all invoke operations. +func (p *AzOpenAI) Invoke(ctx context.Context, req *bindings.InvokeRequest) (resp *bindings.InvokeResponse, err error) { + if req == nil || len(req.Metadata) == 0 { + return nil, fmt.Errorf("invalid request: metadata is required") + } + + startTime := time.Now().UTC() + resp = &bindings.InvokeResponse{ + Metadata: map[string]string{ + "operation": string(req.Operation), + "start-time": startTime.Format(time.RFC3339Nano), + }, + } + + switch req.Operation { //nolint:exhaustive + case CompletionOperation: + response, err := p.completion(ctx, req.Data, req.Metadata) + if err != nil { + return nil, fmt.Errorf("error performing completion: %w", err) + } + responseAsBytes, _ := json.Marshal(response) + resp.Data = responseAsBytes + + case ChatCompletionOperation: + response, err := p.chatCompletion(ctx, req.Data, req.Metadata) + if err != nil { + return nil, fmt.Errorf("error performing chat completion: %w", err) + } + responseAsBytes, _ := json.Marshal(response) + resp.Data = responseAsBytes + + default: + return nil, fmt.Errorf( + "invalid operation type: %s. Expected %s, %s", + req.Operation, CompletionOperation, ChatCompletionOperation, + ) + } + + endTime := time.Now().UTC() + resp.Metadata["end-time"] = endTime.Format(time.RFC3339Nano) + resp.Metadata["duration"] = endTime.Sub(startTime).String() + + return resp, nil +} + +func (s *ChatSettings) Decode(in any) error { + return config.Decode(in, s) +} + +func (p *AzOpenAI) completion(ctx context.Context, message []byte, metadata map[string]string) (response []azopenai.Choice, err error) { + prompt := Prompt{ + Temperature: 1.0, + TopP: 1.0, + MaxTokens: 16, + N: 1, + PresencePenalty: 0.0, + FrequencyPenalty: 0.0, + } + err = json.Unmarshal(message, &prompt) + if err != nil { + return nil, fmt.Errorf("error unmarshalling the input object: %w", err) + } + + if prompt.Prompt == "" { + return nil, fmt.Errorf("prompt is required for completion operation") + } + + resp, err := p.client.GetCompletions(ctx, azopenai.CompletionsOptions{ + Prompt: []*string{&prompt.Prompt}, + MaxTokens: &prompt.MaxTokens, + Temperature: &prompt.Temperature, + TopP: &prompt.TopP, + N: &prompt.N, + }, nil) + if err != nil { + return nil, fmt.Errorf("error getting completion api: %w", err) + } + + // No choices returned + if len(resp.Completions.Choices) == 0 { + return []azopenai.Choice{}, nil + } + + choices := resp.Completions.Choices + response = make([]azopenai.Choice, len(choices)) + for i, c := range choices { + response[i] = *c + } + + return response, nil +} + +func (p *AzOpenAI) chatCompletion(ctx context.Context, messageRequest []byte, metadata map[string]string) (response []azopenai.ChatChoice, err error) { + messages := ChatMessages{ + Temperature: 1.0, + TopP: 1.0, + N: 1, + PresencePenalty: 0.0, + FrequencyPenalty: 0.0, + } + err = json.Unmarshal(messageRequest, &messages) + if err != nil { + return nil, fmt.Errorf("error unmarshalling the input object: %w", err) + } + + if len(messages.Messages) == 0 { + return nil, fmt.Errorf("messages are required for chat-completion operation") + } + + messageReq := make([]*azopenai.ChatMessage, len(messages.Messages)) + for i, m := range messages.Messages { + messageReq[i] = &azopenai.ChatMessage{ + Role: to.Ptr(azopenai.ChatRole(m.Role)), + Content: to.Ptr(m.Message), + } + } + + var maxTokens *int32 + if messages.MaxTokens != 0 { + maxTokens = &messages.MaxTokens + } + + res, err := p.client.GetChatCompletions(ctx, azopenai.ChatCompletionsOptions{ + MaxTokens: maxTokens, + Temperature: &messages.Temperature, + TopP: &messages.TopP, + N: &messages.N, + Messages: messageReq, + }, nil) + if err != nil { + return nil, fmt.Errorf("error getting chat completion api: %w", err) + } + + // No choices returned. + if len(res.ChatCompletions.Choices) == 0 { + return []azopenai.ChatChoice{}, nil + } + + choices := res.ChatCompletions.Choices + response = make([]azopenai.ChatChoice, len(choices)) + for i, c := range choices { + response[i] = *c + } + + return response, nil +} + +// Close Az OpenAI instance. +func (p *AzOpenAI) Close() error { + p.client = nil + + return nil +} + +// GetComponentMetadata returns the metadata of the component. +func (p *AzOpenAI) GetComponentMetadata() map[string]string { + metadataStruct := openAIMetadata{} + metadataInfo := map[string]string{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) + return metadataInfo +} diff --git a/go.mod b/go.mod index 937f84ebff..98002f8790 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,9 @@ require ( cloud.google.com/go/secretmanager v1.10.0 cloud.google.com/go/storage v1.30.1 dubbo.apache.org/dubbo-go/v3 v3.0.3-0.20230118042253-4f159a2b38f3 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/cognitiveservices/azopenai v0.0.0-20230705184009-934612c4f2b5 github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v0.5.0 github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.5 github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1 diff --git a/go.sum b/go.sum index 33ff6e88ca..d952c1ce42 100644 --- a/go.sum +++ b/go.sum @@ -420,11 +420,13 @@ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0 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.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 h1:8q4SaHjFsClSvuVne0ID/5Ka8u3fcIHyqkLjcFpNRHQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1/go.mod h1:gLa1CL2RNE4s7M3yopJ/p0iq5DdY6Yv5ZUt9MTRZOQM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= +github.com/Azure/azure-sdk-for-go/sdk/cognitiveservices/azopenai v0.0.0-20230705184009-934612c4f2b5 h1:DQCZXtoCPuwBMlAa2aC+B3CfpE6xz2xe1jqdqt8nIJY= +github.com/Azure/azure-sdk-for-go/sdk/cognitiveservices/azopenai v0.0.0-20230705184009-934612c4f2b5/go.mod h1:GQSjs1n073tbMa3e76+STZkyFb+NcEA4N7OB5vNvB3E= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v0.5.0 h1:OrKZybbyagpgJiREiIVzH5mV/z9oS4rXqdX7i31DSF0= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v0.5.0/go.mod h1:p74+tP95m8830ypJk53L93+BEsjTKY4SKQ75J2NmS5U= github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.5 h1:qS0Bp4do0cIvnuQgSGeO6ZCu/q/HlRKl4NPfv1eJ2p0= diff --git a/tests/certification/go.mod b/tests/certification/go.mod index 6c16649bd7..37efbf1384 100644 --- a/tests/certification/go.mod +++ b/tests/certification/go.mod @@ -52,7 +52,7 @@ require ( github.com/AdhityaRamadhanus/fasthttpcors v0.0.0-20170121111917-d4c07198763a // indirect github.com/AthenZ/athenz v1.10.39 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.5 // indirect github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1 // indirect diff --git a/tests/certification/go.sum b/tests/certification/go.sum index 9f6d9eb0f8..39ae5032fe 100644 --- a/tests/certification/go.sum +++ b/tests/certification/go.sum @@ -70,8 +70,8 @@ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0 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.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 h1:8q4SaHjFsClSvuVne0ID/5Ka8u3fcIHyqkLjcFpNRHQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1/go.mod h1:gLa1CL2RNE4s7M3yopJ/p0iq5DdY6Yv5ZUt9MTRZOQM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= From aa4d073cd5b372f062eb14e2bcca137dcb9b0e70 Mon Sep 17 00:00:00 2001 From: Roberto Rojas Date: Mon, 17 Jul 2023 17:38:58 -0400 Subject: [PATCH 18/19] [Bindings] Append Direction and Route as Built-in Metadata Properties. (#2945) Signed-off-by: Roberto Rojas Signed-off-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> --- .build-tools/pkg/metadataschema/validators.go | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/.build-tools/pkg/metadataschema/validators.go b/.build-tools/pkg/metadataschema/validators.go index 9527508fea..4cfc8b0efc 100644 --- a/.build-tools/pkg/metadataschema/validators.go +++ b/.build-tools/pkg/metadataschema/validators.go @@ -23,6 +23,13 @@ import ( mdutils "github.com/dapr/components-contrib/metadata" ) +const ( + bindingDirectionMetadataKey = "direction" + bindingDirectionInput = "input" + bindingDirectionOutput = "output" + bindingRouteMetadataKey = "route" +) + // IsValid performs additional validation and returns true if the object is valid. func (c *ComponentMetadata) IsValid() error { // Check valid component type @@ -135,6 +142,51 @@ func (c *ComponentMetadata) AppendBuiltin() error { }, }, ) + case mdutils.BindingType: + if c.Binding != nil { + if c.Metadata == nil { + c.Metadata = []Metadata{} + } + + if c.Binding.Input { + direction := bindingDirectionInput + allowedValues := []string{ + bindingDirectionInput, + } + + if c.Binding.Output { + direction = fmt.Sprintf("%s,%s", bindingDirectionInput, bindingDirectionOutput) + allowedValues = append(allowedValues, bindingDirectionOutput, direction) + } + + c.Metadata = append(c.Metadata, + Metadata{ + Name: bindingDirectionMetadataKey, + Type: "string", + Description: "Indicates the direction of the binding component.", + Example: `"`+direction+`"`, + URL: &URL{ + Title: "Documentation", + URL: "https://docs.dapr.io/reference/api/bindings_api/#binding-direction-optional", + }, + AllowedValues: allowedValues, + }, + ) + + c.Metadata = append(c.Metadata, + Metadata{ + Name: bindingRouteMetadataKey, + Type: "string", + Description: "Specifies a custom route for incoming events.", + Example: `"/custom-path"`, + URL: &URL{ + Title: "Documentation", + URL: "https://docs.dapr.io/developing-applications/building-blocks/bindings/howto-triggers/#specifying-a-custom-route", + }, + }, + ) + } + } } // Sanity check to ensure the data is in sync From ec05809ee63d5e7b1b695c47e91e2d860b52b86d Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Mon, 17 Jul 2023 15:17:22 -0700 Subject: [PATCH 19/19] [Metadata] Update validator and some other fixes (#2984) Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Bernd Verst --- .../builtin-authentication-profiles.yaml | 97 +++++++++++++ .build-tools/component-folders.json | 46 ------ .build-tools/component-folders.yaml | 43 ++++++ .build-tools/go.mod | 1 + .build-tools/main.go | 35 +++-- .../pkg/metadataanalyzer/analyzer.template | 33 +++-- .../builtin-authentication-profiles.go | 133 ++---------------- .build-tools/pkg/metadataschema/schema.go | 79 +++++------ .build-tools/pkg/metadataschema/validators.go | 12 ++ .golangci.yml | 1 + Makefile | 5 +- bindings/alicloud/dingtalk/webhook/webhook.go | 5 +- bindings/alicloud/oss/oss.go | 5 +- bindings/alicloud/sls/sls.go | 5 +- bindings/alicloud/tablestore/tablestore.go | 5 +- bindings/apns/apns.go | 5 +- bindings/aws/dynamodb/dynamodb.go | 5 +- bindings/aws/kinesis/kinesis.go | 5 +- bindings/aws/s3/s3.go | 11 +- bindings/aws/ses/ses.go | 5 +- bindings/aws/sns/sns.go | 5 +- bindings/aws/sqs/sqs.go | 5 +- bindings/azure/blobstorage/blobstorage.go | 5 +- bindings/azure/cosmosdb/cosmosdb.go | 5 +- .../cosmosdbgremlinapi/cosmosdbgremlinapi.go | 5 +- bindings/azure/eventgrid/eventgrid.go | 5 +- bindings/azure/eventgrid/metadata.yaml | 2 +- bindings/azure/eventhubs/eventhubs.go | 5 +- bindings/azure/eventhubs/metadata.yaml | 8 -- bindings/azure/openai/openai.go | 5 +- .../servicebusqueues/servicebusqueues.go | 5 +- bindings/azure/signalr/signalr.go | 5 +- bindings/azure/storagequeues/storagequeues.go | 5 +- bindings/cloudflare/queues/cfqueues.go | 5 +- bindings/commercetools/commercetools.go | 5 +- bindings/cron/cron.go | 5 +- bindings/dubbo/dubbo_output.go | 6 +- bindings/gcp/bucket/bucket.go | 11 +- bindings/gcp/pubsub/pubsub.go | 5 +- bindings/graphql/graphql.go | 5 +- bindings/http/http.go | 5 +- bindings/huawei/obs/obs.go | 5 +- bindings/influx/influx.go | 5 +- bindings/input_binding.go | 4 +- bindings/kafka/kafka.go | 5 +- bindings/kitex/kitex_output.go | 6 +- bindings/kubemq/kubemq.go | 5 +- bindings/kubernetes/kubernetes.go | 5 +- bindings/localstorage/localstorage.go | 5 +- bindings/mqtt3/mqtt.go | 5 +- bindings/mysql/mysql.go | 5 +- bindings/nacos/nacos.go | 5 +- bindings/output_binding.go | 4 +- bindings/postgres/postgres.go | 5 +- bindings/postmark/postmark.go | 5 +- bindings/rabbitmq/rabbitmq.go | 5 +- bindings/redis/redis.go | 5 +- bindings/rethinkdb/statechange/statechange.go | 5 +- bindings/smtp/smtp.go | 5 +- bindings/twilio/sendgrid/sendgrid.go | 5 +- bindings/twilio/sms/sms.go | 5 +- bindings/wasm/output.go | 5 +- bindings/zeebe/command/command.go | 5 +- bindings/zeebe/jobworker/jobworker.go | 5 +- component-metadata-schema.json | 2 +- configuration/azure/appconfig/appconfig.go | 5 +- configuration/postgres/postgres.go | 5 +- configuration/redis/redis.go | 5 +- configuration/store.go | 11 +- crypto/azure/keyvault/component.go | 5 +- crypto/jwks/component.go | 5 +- crypto/kubernetes/secrets/component.go | 9 +- crypto/localstorage/component.go | 5 +- crypto/subtlecrypto.go | 7 +- .../component/azure/blobstorage/metadata.go | 2 +- .../component/azure/eventhubs/metadata.go | 12 +- .../component/azure/servicebus/metadata.go | 2 +- internal/component/postgresql/postgresql.go | 5 +- internal/component/redis/settings.go | 18 +-- lock/redis/standalone.go | 5 +- lock/store.go | 11 +- metadata/componentmetadata.go | 23 +++ metadata/componentmetadata_metadata.go | 23 +++ metadata/utils.go | 62 ++++++-- metadata/utils_test.go | 60 +++++--- middleware/http/bearer/bearer_middleware.go | 5 +- middleware/http/oauth2/oauth2_middleware.go | 5 +- .../oauth2clientcredentials_middleware.go | 5 +- middleware/http/opa/middleware.go | 5 +- .../http/ratelimit/ratelimit_middleware.go | 5 +- .../routeralias/routeralias_middleware.go | 9 +- .../routerchecker/routerchecker_middleware.go | 5 +- middleware/http/sentinel/middleware.go | 5 +- middleware/http/wasm/httpwasm.go | 5 +- middleware/middleware.go | 1 - pubsub/aws/snssqs/snssqs.go | 5 +- pubsub/azure/eventhubs/eventhubs.go | 5 +- pubsub/azure/servicebus/queues/servicebus.go | 7 +- pubsub/azure/servicebus/topics/servicebus.go | 5 +- pubsub/gcp/pubsub/pubsub.go | 5 +- pubsub/in-memory/in-memory.go | 5 +- pubsub/jetstream/jetstream.go | 5 +- pubsub/kafka/kafka.go | 5 +- pubsub/kubemq/kubemq.go | 5 +- pubsub/mqtt3/mqtt.go | 5 +- pubsub/natsstreaming/natsstreaming.go | 5 +- pubsub/pubsub.go | 4 +- pubsub/pulsar/pulsar.go | 5 +- pubsub/rabbitmq/rabbitmq.go | 5 +- pubsub/redis/redis.go | 5 +- pubsub/rocketmq/rocketmq.go | 5 +- pubsub/solace/amqp/amqp.go | 5 +- .../alicloud/parameterstore/parameterstore.go | 5 +- .../aws/parameterstore/parameterstore.go | 5 +- .../aws/secretmanager/secretmanager.go | 5 +- secretstores/azure/keyvault/keyvault.go | 5 +- .../gcp/secretmanager/secretmanager.go | 5 +- secretstores/hashicorp/vault/vault.go | 5 +- secretstores/huaweicloud/csms/csms.go | 5 +- secretstores/kubernetes/kubernetes.go | 10 +- secretstores/local/env/envstore.go | 5 +- secretstores/local/file/filestore.go | 5 +- secretstores/secret_store.go | 5 +- secretstores/tencentcloud/ssm/ssm.go | 5 +- state/aerospike/aerospike.go | 5 +- state/alicloud/tablestore/tablestore.go | 5 +- state/aws/dynamodb/dynamodb.go | 5 +- state/azure/blobstorage/blobstorage.go | 5 +- state/azure/cosmosdb/cosmosdb.go | 5 +- state/azure/tablestorage/tablestorage.go | 5 +- state/bulk_test.go | 6 +- state/cassandra/cassandra.go | 5 +- state/cloudflare/workerskv/workerskv.go | 5 +- state/couchbase/couchbase.go | 5 +- state/etcd/etcd.go | 5 +- state/gcp/firestore/firestore.go | 5 +- state/hashicorp/consul/consul.go | 5 +- state/hazelcast/hazelcast.go | 5 +- state/in-memory/in_memory.go | 5 +- state/jetstream/jetstream.go | 5 +- state/memcached/memcached.go | 5 +- state/mongodb/mongodb.go | 5 +- state/mysql/mysql.go | 5 +- state/oci/objectstorage/objectstorage.go | 5 +- state/oracledatabase/oracledatabase.go | 5 +- state/redis/redis.go | 6 +- state/redis/redis_test.go | 2 - state/rethinkdb/rethinkdb.go | 5 +- state/sqlite/sqlite.go | 5 +- state/sqlserver/sqlserver.go | 8 +- state/store.go | 4 +- state/zookeeper/zk.go | 5 +- workflows/temporal/temporal.go | 5 +- workflows/workflow.go | 1 - 154 files changed, 703 insertions(+), 689 deletions(-) create mode 100644 .build-tools/builtin-authentication-profiles.yaml delete mode 100644 .build-tools/component-folders.json create mode 100644 .build-tools/component-folders.yaml create mode 100644 metadata/componentmetadata.go create mode 100644 metadata/componentmetadata_metadata.go diff --git a/.build-tools/builtin-authentication-profiles.yaml b/.build-tools/builtin-authentication-profiles.yaml new file mode 100644 index 0000000000..81e07c3635 --- /dev/null +++ b/.build-tools/builtin-authentication-profiles.yaml @@ -0,0 +1,97 @@ +aws: + - title: "AWS: Access Key ID and Secret Access Key" + description: | + Authenticate using an Access Key ID and Secret Access Key included in the metadata + metadata: + - name: accessKey + description: AWS access key associated with an IAM account + required: true + sensitive: true + example: '"AKIAIOSFODNN7EXAMPLE"' + - name: secretKey + description: The secret key associated with the access key + required: true + sensitive: true + example: '"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"' + - title: "AWS: Credentials from Environment Variables" + description: Use AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from the environment + +azuread: + - title: "Azure AD: Managed identity" + description: Authenticate using Azure AD and a managed identity. + metadata: + - name: azureClientId + description: | + Client ID (application ID). Required if the service has multiple identities assigned. + example: '"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"' + - name: azureEnvironment + description: | + Optional name for the Azure environment if using a different Azure cloud + default: AzurePublicCloud + example: '"AzurePublicCloud"' + allowedValues: + - AzurePublicCloud + - AzureChinaCloud + - AzureUSGovernmentCloud + - title: "Azure AD: Client credentials" + description: | + Authenticate using Azure AD with client credentials, also known as "service principals". + metadata: + - name: azureTenantId + description: ID of the Azure AD tenant + required: true + example: '"cd4b2887-304c-47e1-b4d5-65447fdd542a"' + - name: azureClientId + description: Client ID (application ID) + required: true + example: '"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"' + - name: azureClientSecret + description: Client secret (application password) + required: true + sensitive: true + example: '"Ecy3XG7zVZK3/vl/a2NSB+a1zXLa8RnMum/IgD0E"' + - name: azureEnvironment + description: | + Optional name for the Azure environment if using a different Azure cloud + default: AzurePublicCloud + example: '"AzurePublicCloud"' + allowedValues: + - AzurePublicCloud + - AzureChinaCloud + - AzureUSGovernmentCloud + - title: "Azure AD: Client certificate" + description: | + Authenticate using Azure AD with a client certificate. One of "azureCertificate" and "azureCertificateFile" is required. + metadata: + - name: azureTenantId + description: ID of the Azure AD tenant + required: true + example: '"cd4b2887-304c-47e1-b4d5-65447fdd542a"' + - name: azureClientId + description: Client ID (application ID) + required: true + example: '"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"' + - name: azureCertificate + description: | + Certificate and private key (in either a PEM file containing both the certificate and key, or in PFX/PKCS#12 format) + sensitive: true + example: | + "-----BEGIN PRIVATE KEY-----\n MIIEvgI... \n -----END PRIVATE KEY----- + \n -----BEGIN CERTIFICATE----- \n MIICoTC... \n -----END CERTIFICATE----- \n" + - name: azureCertificateFile + description: | + Path to PEM or PFX/PKCS#12 file on disk, containing the certificate and private key. + example: '"/path/to/file.pem"' + - name: azureCertificatePassword + description: Password for the certificate if encrypted. + sensitive: true + example: '"password"' + - name: azureEnvironment + description: | + Optional name for the Azure environment if using a different Azure cloud + default: AzurePublicCloud + example: '"AzurePublicCloud"' + allowedValues: + - AzurePublicCloud + - AzureChinaCloud + - AzureUSGovernmentCloud diff --git a/.build-tools/component-folders.json b/.build-tools/component-folders.json deleted file mode 100644 index ecea136ffb..0000000000 --- a/.build-tools/component-folders.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "componentFolders": [ - "bindings", - "configuration", - "crypto", - "lock", - "middleware/http", - "nameresolution", - "pubsub", - "secretstores", - "state", - "workflows" - ], - "excludeFolders": [ - "bindings/alicloud", - "bindings/aws", - "bindings/azure", - "bindings/gcp", - "bindings/huawei", - "bindings/rethinkdb", - "bindings/twilio", - "bindings/zeebe", - "configuration/azure", - "configuration/redis/internal", - "crypto/azure", - "crypto/kubernetes", - "pubsub/aws", - "pubsub/azure", - "pubsub/azure/servicebus", - "pubsub/gcp", - "secretstores/alicloud", - "secretstores/aws", - "secretstores/azure", - "secretstores/gcp", - "secretstores/hashicorp", - "secretstores/huaweicloud", - "secretstores/local", - "state/alicloud", - "state/aws", - "state/azure", - "state/gcp", - "state/hashicorp", - "state/oci", - "state/utils" - ] -} diff --git a/.build-tools/component-folders.yaml b/.build-tools/component-folders.yaml new file mode 100644 index 0000000000..13b0882176 --- /dev/null +++ b/.build-tools/component-folders.yaml @@ -0,0 +1,43 @@ +componentFolders: + - bindings + - configuration + - crypto + - lock + - middleware/http + - nameresolution + - pubsub + - secretstores + - state + - workflows + +excludeFolders: + - bindings/alicloud + - bindings/aws + - bindings/azure + - bindings/gcp + - bindings/huawei + - bindings/rethinkdb + - bindings/twilio + - bindings/zeebe + - configuration/azure + - configuration/redis/internal + - crypto/azure + - crypto/kubernetes + - pubsub/aws + - pubsub/azure + - pubsub/azure/servicebus + - pubsub/gcp + - secretstores/alicloud + - secretstores/aws + - secretstores/azure + - secretstores/gcp + - secretstores/hashicorp + - secretstores/huaweicloud + - secretstores/local + - state/alicloud + - state/aws + - state/azure + - state/gcp + - state/hashicorp + - state/oci + - state/utils diff --git a/.build-tools/go.mod b/.build-tools/go.mod index a75f66cde6..6b9370e53b 100644 --- a/.build-tools/go.mod +++ b/.build-tools/go.mod @@ -8,6 +8,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/xeipuuv/gojsonschema v1.2.1-0.20201027075954-b076d39a02e5 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 + gopkg.in/yaml.v3 v3.0.1 sigs.k8s.io/yaml v1.3.0 ) diff --git a/.build-tools/main.go b/.build-tools/main.go index fcfbf4adb3..ee602c3b3a 100644 --- a/.build-tools/main.go +++ b/.build-tools/main.go @@ -15,28 +15,39 @@ package main import ( _ "embed" - "encoding/json" + + "gopkg.in/yaml.v3" "github.com/dapr/components-contrib/build-tools/cmd" + "github.com/dapr/components-contrib/build-tools/pkg/metadataschema" ) -//go:embed component-folders.json -var componentFoldersJSON []byte +var ( + //go:embed component-folders.yaml + componentFoldersYAML []byte + //go:embed builtin-authentication-profiles.yaml + builtinAuthenticationProfilesYAML []byte +) -func init() { - parsed := struct { - ComponentFolders []string `json:"componentFolders"` - ExcludeFolders []string `json:"excludeFolders"` +func main() { + // Parse component-folders.json + parsedComponentFolders := struct { + ComponentFolders []string `json:"componentFolders" yaml:"componentFolders"` + ExcludeFolders []string `json:"excludeFolders" yaml:"excludeFolders"` }{} - err := json.Unmarshal(componentFoldersJSON, &parsed) + err := yaml.Unmarshal(componentFoldersYAML, &parsedComponentFolders) if err != nil { panic(err) } - cmd.ComponentFolders = parsed.ComponentFolders - cmd.ExcludeFolders = parsed.ExcludeFolders -} + cmd.ComponentFolders = parsedComponentFolders.ComponentFolders + cmd.ExcludeFolders = parsedComponentFolders.ExcludeFolders + + // Parse builtin-authentication-profiles.yaml + err = yaml.Unmarshal(builtinAuthenticationProfilesYAML, &metadataschema.BuiltinAuthenticationProfiles) + if err != nil { + panic(err) + } -func main() { cmd.Execute() } diff --git a/.build-tools/pkg/metadataanalyzer/analyzer.template b/.build-tools/pkg/metadataanalyzer/analyzer.template index b5fc62ecf3..6177c0050c 100644 --- a/.build-tools/pkg/metadataanalyzer/analyzer.template +++ b/.build-tools/pkg/metadataanalyzer/analyzer.template @@ -6,7 +6,7 @@ import ( "os" "strings" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/dapr/kit/logger" mdutils "github.com/dapr/components-contrib/metadata" @@ -17,24 +17,24 @@ import ( ) func main() { - if len(os.Args) < 2 { - fmt.Println("Please provide the path to the components-contrib root as an argument") - os.Exit(1) - } - basePath := os.Args[1] - log := logger.NewLogger("metadata") + if len(os.Args) < 2 { + fmt.Println("Please provide the path to the components-contrib root as an argument") + os.Exit(1) + } + basePath := os.Args[1] + log := logger.NewLogger("metadata") - var ( + var ( yamlMetadata *map[string]string - missing map[string]string + missing []string unexpected []string ) - missingByComponent := make(map[string]map[string]string) + missingByComponent := make(map[string][]string) unexpectedByComponent := make(map[string][]string) {{range $fullpkg, $val := .Pkgs}} instanceOf_{{index $val 0}} := {{index $val 0}}.{{index $val 1}}(log) - metadataFor_{{index $val 0}} := instanceOf_{{index $val 0}}.GetComponentMetadata() + metadataFor_{{index $val 0}} := instanceOf_{{index $val 0}}.(mdutils.ComponentWithMetadata).GetComponentMetadata() yamlMetadata = getYamlMetadata(basePath, "{{$fullpkg}}") missing = checkMissingMetadata(yamlMetadata, metadataFor_{{index $val 0}}) if len(missing) > 0 { @@ -127,14 +127,17 @@ func getYamlMetadata(basePath string, pkg string) *map[string]string { return &names } -func checkMissingMetadata(yamlMetadata *map[string]string, componentMetadata map[string]string) map[string]string { - missingMetadata := make(map[string]string) +func checkMissingMetadata(yamlMetadata *map[string]string, componentMetadata mdutils.MetadataMap) []string { + missingMetadata := make([]string, 0) // if there is no yaml metadata, then we are not missing anything yet if yamlMetadata != nil && len(*yamlMetadata) > 0 { - for key := range componentMetadata { + for key, md := range componentMetadata { + if md.Ignored { + continue + } lowerKey := strings.ToLower(key) if _, ok := (*yamlMetadata)[lowerKey]; !ok { - missingMetadata[lowerKey] = componentMetadata[key] + missingMetadata = append(missingMetadata, key) } // todo - check if the metadata is the same data type } diff --git a/.build-tools/pkg/metadataschema/builtin-authentication-profiles.go b/.build-tools/pkg/metadataschema/builtin-authentication-profiles.go index d2fb6983b8..bcb76d5ed0 100644 --- a/.build-tools/pkg/metadataschema/builtin-authentication-profiles.go +++ b/.build-tools/pkg/metadataschema/builtin-authentication-profiles.go @@ -17,131 +17,22 @@ import ( "fmt" ) +// Built-in authentication profiles +var BuiltinAuthenticationProfiles map[string][]AuthenticationProfile + // ParseBuiltinAuthenticationProfile returns an AuthenticationProfile(s) from a given BuiltinAuthenticationProfile. func ParseBuiltinAuthenticationProfile(bi BuiltinAuthenticationProfile) ([]AuthenticationProfile, error) { - switch bi.Name { - case "aws": - return []AuthenticationProfile{ - { - Title: "AWS: Access Key ID and Secret Access Key", - Description: "Authenticate using an Access Key ID and Secret Access Key included in the metadata", - Metadata: []Metadata{ - { - Name: "accessKey", - Required: true, - Sensitive: true, - Description: "AWS access key associated with an IAM account", - Example: `"AKIAIOSFODNN7EXAMPLE"`, - }, - { - Name: "secretKey", - Required: true, - Sensitive: true, - Description: "The secret key associated with the access key", - Example: `"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"`, - }, - }, - }, - { - Title: "AWS: Credentials from Environment Variables", - Description: "Use AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from the environment", - Metadata: []Metadata{}, - }, - }, nil - case "azuread": - azureEnvironmentMetadata := Metadata{ - Name: "azureEnvironment", - Required: false, - Description: "Optional name for the Azure environment if using a different Azure cloud", - Example: `"AzurePublicCloud"`, - Default: "AzurePublicCloud", - AllowedValues: []string{"AzurePublicCloud", "AzureChinaCloud", "AzureUSGovernmentCloud"}, - } - profiles := []AuthenticationProfile{ - { - Title: "Azure AD: Managed identity", - Description: "Authenticate using Azure AD and a managed identity.", - Metadata: mergedMetadata(bi.Metadata, - Metadata{ - Name: "azureClientId", - Description: "Client ID (application ID). Required if the service has multiple identities assigned.", - Example: `"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"`, - Required: false, - }, - azureEnvironmentMetadata, - ), - }, - { - Title: "Azure AD: Client credentials", - Description: "Authenticate using Azure AD with client credentials, also known as \"service principals\".", - Metadata: mergedMetadata(bi.Metadata, - Metadata{ - Name: "azureTenantId", - Description: "ID of the Azure AD tenant", - Example: `"cd4b2887-304c-47e1-b4d5-65447fdd542a"`, - Required: true, - }, - Metadata{ - Name: "azureClientId", - Description: "Client ID (application ID)", - Example: `"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"`, - Required: true, - }, - Metadata{ - Name: "azureClientSecret", - Description: "Client secret (application password)", - Example: `"Ecy3XG7zVZK3/vl/a2NSB+a1zXLa8RnMum/IgD0E"`, - Required: true, - Sensitive: true, - }, - azureEnvironmentMetadata, - ), - }, - { - Title: "Azure AD: Client certificate", - Description: `Authenticate using Azure AD with a client certificate. One of "azureCertificate" and "azureCertificateFile" is required.`, - Metadata: mergedMetadata(bi.Metadata, - Metadata{ - Name: "azureTenantId", - Description: "ID of the Azure AD tenant", - Example: `"cd4b2887-304c-47e1-b4d5-65447fdd542a"`, - Required: true, - }, - Metadata{ - Name: "azureClientId", - Description: "Client ID (application ID)", - Example: `"c7dd251f-811f-4ba2-a905-acd4d3f8f08b"`, - Required: true, - }, - Metadata{ - Name: "azureCertificate", - Description: "Certificate and private key (in either a PEM file containing both the certificate and key, or in PFX/PKCS#12 format)", - Example: `"-----BEGIN PRIVATE KEY-----\n MIIEvgI... \n -----END PRIVATE KEY----- \n -----BEGIN CERTIFICATE----- \n MIICoTC... \n -----END CERTIFICATE----- \n"`, - Required: false, - Sensitive: true, - }, - Metadata{ - Name: "azureCertificateFile", - Description: "Path to PEM or PFX/PKCS#12 file on disk, containing the certificate and private key.", - Example: `"/path/to/file.pem"`, - Required: false, - Sensitive: false, - }, - Metadata{ - Name: "azureCertificatePassword", - Description: "Password for the certificate if encrypted.", - Example: `"password"`, - Required: false, - Sensitive: true, - }, - azureEnvironmentMetadata, - ), - }, - } - return profiles, nil - default: + profiles, ok := BuiltinAuthenticationProfiles[bi.Name] + if !ok { return nil, fmt.Errorf("built-in authentication profile %s does not exist", bi.Name) } + + res := make([]AuthenticationProfile, len(profiles)) + for i, profile := range profiles { + res[i] = profile + res[i].Metadata = mergedMetadata(bi.Metadata, res[i].Metadata...) + } + return res, nil } func mergedMetadata(base []Metadata, add ...Metadata) []Metadata { diff --git a/.build-tools/pkg/metadataschema/schema.go b/.build-tools/pkg/metadataschema/schema.go index c807f0f65c..2e49338aec 100644 --- a/.build-tools/pkg/metadataschema/schema.go +++ b/.build-tools/pkg/metadataschema/schema.go @@ -18,116 +18,113 @@ package metadataschema // ComponentMetadata is the schema for the metadata.yaml / metadata.json files. type ComponentMetadata struct { // Version of the component metadata schema. - SchemaVersion string `json:"schemaVersion" jsonschema:"enum=v1"` + SchemaVersion string `json:"schemaVersion" yaml:"schemaVersion" jsonschema:"enum=v1"` // Component type, of one of the allowed values. - Type string `json:"type" jsonschema:"enum=bindings,enum=state,enum=secretstores,enum=pubsub,enum=workflows,enum=configuration,enum=lock,enum=middleware"` + Type string `json:"type" yaml:"type" jsonschema:"enum=bindings,enum=state,enum=secretstores,enum=pubsub,enum=workflows,enum=configuration,enum=lock,enum=middleware"` // Name of the component (without the inital type, e.g. "http" instead of "bindings.http"). - Name string `json:"name"` + Name string `json:"name" yaml:"name"` // Version of the component, with the leading "v", e.g. "v1". - Version string `json:"version"` + Version string `json:"version" yaml:"version"` // Component status. - Status string `json:"status" jsonschema:"enum=stable,enum=beta,enum=alpha,enum=development-only"` + Status string `json:"status" yaml:"status" jsonschema:"enum=stable,enum=beta,enum=alpha,enum=development-only"` // Title of the component, e.g. "HTTP". - Title string `json:"title"` + Title string `json:"title" yaml:"title"` // Additional description for the component, optional. - Description string `json:"description,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` // URLs with additional resources for the component, such as docs. - URLs []URL `json:"urls"` + URLs []URL `json:"urls" yaml:"urls"` // Properties for bindings only. // This should not present unless "type" is "bindings". - Binding *Binding `json:"binding,omitempty"` + Binding *Binding `json:"binding,omitempty" yaml:"binding,omitempty"` // Component capabilities. // For state stores, the presence of "actorStateStore" implies that the metadata property "actorStateStore" can be set. In that case, do not manually specify "actorStateStore" as metadata option. - Capabilities []string `json:"capabilities,omitempty"` + Capabilities []string `json:"capabilities,omitempty" yaml:"capabilities,omitempty"` // Authentication profiles for the component. - AuthenticationProfiles []AuthenticationProfile `json:"authenticationProfiles,omitempty"` + AuthenticationProfiles []AuthenticationProfile `json:"authenticationProfiles,omitempty" yaml:"authenticationProfiles,omitempty"` // Built-in authentication profiles to import. - BuiltInAuthenticationProfiles []BuiltinAuthenticationProfile `json:"builtinAuthenticationProfiles,omitempty"` + BuiltInAuthenticationProfiles []BuiltinAuthenticationProfile `json:"builtinAuthenticationProfiles,omitempty" yaml:"builtinAuthenticationProfiles,omitempty"` // Metadata options for the component. - Metadata []Metadata `json:"metadata,omitempty"` + Metadata []Metadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` } // URL represents one URL with additional resources. type URL struct { // Title of the URL. - Title string `json:"title"` + Title string `json:"title" yaml:"title"` // URL. - URL string `json:"url"` + URL string `json:"url" yaml:"url"` } // Binding represents properties that are specific to bindings type Binding struct { // If "true", the binding can be used as input binding. - Input bool `json:"input,omitempty"` + Input bool `json:"input,omitempty" yaml:"input,omitempty"` // If "true", the binding can be used as output binding. - Output bool `json:"output,omitempty"` + Output bool `json:"output,omitempty" yaml:"output,omitempty"` // List of operations that the output binding support. // Required in output bindings, and not allowed in input-only bindings. - Operations []BindingOperation `json:"operations"` + Operations []BindingOperation `json:"operations" yaml:"operations"` } // BindingOperation represents an operation offered by an output binding. type BindingOperation struct { // Name of the operation, such as "create", "post", "delete", etc. - Name string `json:"name"` + Name string `json:"name" yaml:"name"` // Descrption of the operation. - Description string `json:"description"` + Description string `json:"description" yaml:"description"` } // Metadata property. type Metadata struct { // Name of the metadata property. - Name string `json:"name"` + Name string `json:"name" yaml:"name"` // Description of the property. - Description string `json:"description"` + Description string `json:"description" yaml:"description"` // If "true", the property is required - Required bool `json:"required,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` // If "true", the property represents a sensitive value such as a password. - Sensitive bool `json:"sensitive,omitempty"` + Sensitive bool `json:"sensitive,omitempty" yaml:"sensitive,omitempty"` // Type of the property. // If this is empty, it's interpreted as "string". - Type string `json:"type,omitempty" jsonschema:"enum=string,enum=number,enum=bool,enum=duration"` + Type string `json:"type,omitempty" yaml:"type,omitempty" jsonschema:"enum=string,enum=number,enum=bool,enum=duration"` // Default value for the property. // If it's a string, don't forget to add quotes. - Default string `json:"default,omitempty"` + Default string `json:"default,omitempty" yaml:"default,omitempty"` // Example value. - Example string `json:"example"` + Example string `json:"example" yaml:"example"` // If set, forces the value to be one of those specified in this allowlist. - AllowedValues []string `json:"allowedValues,omitempty"` + AllowedValues []string `json:"allowedValues,omitempty" yaml:"allowedValues,omitempty"` // If set, specifies that the property is only applicable to bindings of the type specified below. // At least one of "input" and "output" must be "true". - Binding *MetadataBinding `json:"binding,omitempty"` + Binding *MetadataBinding `json:"binding,omitempty" yaml:"binding,omitempty"` // URL with additional information, such as docs. - URL *URL `json:"url,omitempty"` + URL *URL `json:"url,omitempty" yaml:"url,omitempty"` // If set, specifies that the property is deprecated and should not be used in new configurations. - Deprecated bool `json:"deprecated,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` } // MetadataBinding is the type for the "binding" property in the "metadata" object. type MetadataBinding struct { // If "true", the property can be used with the binding as input binding only. - Input bool `json:"input,omitempty"` + Input bool `json:"input,omitempty" yaml:"input,omitempty"` // If "true", the property can be used with the binding as output binding only. - Output bool `json:"output,omitempty"` + Output bool `json:"output,omitempty" yaml:"output,omitempty"` } // AuthenticationProfile is the type for an authentication profile. type AuthenticationProfile struct { // Title of the authentication profile. - Title string `json:"title"` + Title string `json:"title" yaml:"title"` // Additional description for the authentication profile, optional. - Description string `json:"description"` + Description string `json:"description" yaml:"description"` // Metadata options applicable when using this authentication profile. - Metadata []Metadata `json:"metadata,omitempty"` + Metadata []Metadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` } // BuiltinAuthenticationProfile is a reference to a built-in authentication profile. type BuiltinAuthenticationProfile struct { // Name of the built-in authentication profile. - // Currently supports: - // - // - `azuread` (Azure AD, including Managed Identity). - Name string `json:"name"` + Name string `json:"name" yaml:"name"` // Additional metadata options applicable when using this authentication profile. - Metadata []Metadata `json:"metadata,omitempty"` + Metadata []Metadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` } diff --git a/.build-tools/pkg/metadataschema/validators.go b/.build-tools/pkg/metadataschema/validators.go index 4cfc8b0efc..88b026e690 100644 --- a/.build-tools/pkg/metadataschema/validators.go +++ b/.build-tools/pkg/metadataschema/validators.go @@ -77,6 +77,18 @@ func (c *ComponentMetadata) IsValid() error { // Remove the property builtinAuthenticationProfiles now c.BuiltInAuthenticationProfiles = nil + // Trim newlines from all descriptions + c.Description = strings.TrimSpace(c.Description) + for i := range c.AuthenticationProfiles { + c.AuthenticationProfiles[i].Description = strings.TrimSpace(c.AuthenticationProfiles[i].Description) + for j := range c.AuthenticationProfiles[i].Metadata { + c.AuthenticationProfiles[i].Metadata[j].Description = strings.TrimSpace(c.AuthenticationProfiles[i].Metadata[j].Description) + } + } + for i := range c.Metadata { + c.Metadata[i].Description = strings.TrimSpace(c.Metadata[i].Description) + } + return nil } diff --git a/.golangci.yml b/.golangci.yml index 6b635ebc3c..73e3b28b38 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,7 @@ run: # list of build tags, all linters use it. Default is empty list. build-tags: - certtests + - metadata # which dirs to skip: they won't be analyzed; # can use regexp here: generated.*, regexp is applied on full path; diff --git a/Makefile b/Makefile index 4b4743b177..4028525320 100644 --- a/Makefile +++ b/Makefile @@ -109,14 +109,13 @@ verify-linter-version: ################################################################################ .PHONY: test test: - CGO_ENABLED=$(CGO) go test ./... $(COVERAGE_OPTS) $(BUILDMODE) --timeout=15m + CGO_ENABLED=$(CGO) go test ./... $(COVERAGE_OPTS) $(BUILDMODE) -tags metadata --timeout=15m ################################################################################ # Target: lint # ################################################################################ .PHONY: lint lint: verify-linter-installed verify-linter-version - # Due to https://github.com/golangci/golangci-lint/issues/580, we need to add --fix for windows $(GOLANGCI_LINT) run --timeout=20m ################################################################################ @@ -228,7 +227,7 @@ check-component-metadata: go get "github.com/dapr/components-contrib@master" && \ go mod edit -replace "github.com/dapr/components-contrib"="../" && \ go mod tidy && \ - go build . && \ + go build -tags metadata . && \ rm ./go.mod && rm ./go.sum && rm ./main.go && \ ./metadataanalyzer ../ diff --git a/bindings/alicloud/dingtalk/webhook/webhook.go b/bindings/alicloud/dingtalk/webhook/webhook.go index 30d5f10dec..14964f3443 100644 --- a/bindings/alicloud/dingtalk/webhook/webhook.go +++ b/bindings/alicloud/dingtalk/webhook/webhook.go @@ -207,11 +207,10 @@ func (t *DingTalkWebhook) sendMessage(ctx context.Context, req *bindings.InvokeR } // GetComponentMetadata returns the metadata of the component. -func (t *DingTalkWebhook) GetComponentMetadata() map[string]string { +func (t *DingTalkWebhook) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := Settings{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } func getPostURL(urlPath, secret string) (string, error) { diff --git a/bindings/alicloud/oss/oss.go b/bindings/alicloud/oss/oss.go index 4d43c1fb80..c124098f0d 100644 --- a/bindings/alicloud/oss/oss.go +++ b/bindings/alicloud/oss/oss.go @@ -108,9 +108,8 @@ func (s *AliCloudOSS) getClient(metadata *ossMetadata) (*oss.Client, error) { } // GetComponentMetadata returns the metadata of the component. -func (s *AliCloudOSS) GetComponentMetadata() map[string]string { +func (s *AliCloudOSS) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := ossMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/alicloud/sls/sls.go b/bindings/alicloud/sls/sls.go index 5b16fa494c..04c509b9b3 100644 --- a/bindings/alicloud/sls/sls.go +++ b/bindings/alicloud/sls/sls.go @@ -128,9 +128,8 @@ func (callback *Callback) Fail(result *producer.Result) { } // GetComponentMetadata returns the metadata of the component. -func (s *AliCloudSlsLogstorage) GetComponentMetadata() map[string]string { +func (s *AliCloudSlsLogstorage) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := SlsLogstorageMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/alicloud/tablestore/tablestore.go b/bindings/alicloud/tablestore/tablestore.go index 4d0cf6be6d..abb34a43be 100644 --- a/bindings/alicloud/tablestore/tablestore.go +++ b/bindings/alicloud/tablestore/tablestore.go @@ -347,9 +347,8 @@ func contains(arr []string, str string) bool { } // GetComponentMetadata returns the metadata of the component. -func (s *AliCloudTableStore) GetComponentMetadata() map[string]string { +func (s *AliCloudTableStore) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := tablestoreMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/apns/apns.go b/bindings/apns/apns.go index 273244cb98..df15daf86e 100644 --- a/bindings/apns/apns.go +++ b/bindings/apns/apns.go @@ -261,9 +261,8 @@ func makeErrorResponse(httpResponse *http.Response) (*bindings.InvokeResponse, e } // GetComponentMetadata returns the metadata of the component. -func (a *APNS) GetComponentMetadata() map[string]string { +func (a *APNS) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := APNSmetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/aws/dynamodb/dynamodb.go b/bindings/aws/dynamodb/dynamodb.go index db297d8164..d9ef0e53a4 100644 --- a/bindings/aws/dynamodb/dynamodb.go +++ b/bindings/aws/dynamodb/dynamodb.go @@ -115,9 +115,8 @@ func (d *DynamoDB) getClient(metadata *dynamoDBMetadata) (*dynamodb.DynamoDB, er } // GetComponentMetadata returns the metadata of the component. -func (d *DynamoDB) GetComponentMetadata() map[string]string { +func (d *DynamoDB) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := dynamoDBMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/aws/kinesis/kinesis.go b/bindings/aws/kinesis/kinesis.go index 5c646e4090..0f6547f232 100644 --- a/bindings/aws/kinesis/kinesis.go +++ b/bindings/aws/kinesis/kinesis.go @@ -418,9 +418,8 @@ func (p *recordProcessor) Shutdown(input *interfaces.ShutdownInput) { } // GetComponentMetadata returns the metadata of the component. -func (a *AWSKinesis) GetComponentMetadata() map[string]string { +func (a *AWSKinesis) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := &kinesisMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/aws/s3/s3.go b/bindings/aws/s3/s3.go index a7ba93d6f7..ff84145e74 100644 --- a/bindings/aws/s3/s3.go +++ b/bindings/aws/s3/s3.go @@ -62,10 +62,12 @@ type AWSS3 struct { } type s3Metadata struct { + // Ignored by metadata parser because included in built-in authentication profile + AccessKey string `json:"accessKey" mapstructure:"accessKey" mdignore:"true"` + SecretKey string `json:"secretKey" mapstructure:"secretKey" mdignore:"true"` + Region string `json:"region" mapstructure:"region"` Endpoint string `json:"endpoint" mapstructure:"endpoint"` - AccessKey string `json:"accessKey" mapstructure:"accessKey"` - SecretKey string `json:"secretKey" mapstructure:"secretKey"` SessionToken string `json:"sessionToken" mapstructure:"sessionToken"` Bucket string `json:"bucket" mapstructure:"bucket"` DecodeBase64 bool `json:"decodeBase64,string" mapstructure:"decodeBase64"` @@ -416,9 +418,8 @@ func (metadata s3Metadata) mergeWithRequestMetadata(req *bindings.InvokeRequest) } // GetComponentMetadata returns the metadata of the component. -func (s *AWSS3) GetComponentMetadata() map[string]string { +func (s *AWSS3) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := s3Metadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/aws/ses/ses.go b/bindings/aws/ses/ses.go index e609c1467c..03b48c6fd1 100644 --- a/bindings/aws/ses/ses.go +++ b/bindings/aws/ses/ses.go @@ -171,9 +171,8 @@ func (a *AWSSES) getClient(metadata *sesMetadata) (*ses.SES, error) { } // GetComponentMetadata returns the metadata of the component. -func (a *AWSSES) GetComponentMetadata() map[string]string { +func (a *AWSSES) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := sesMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/aws/sns/sns.go b/bindings/aws/sns/sns.go index e2d587fda9..7731401171 100644 --- a/bindings/aws/sns/sns.go +++ b/bindings/aws/sns/sns.go @@ -117,9 +117,8 @@ func (a *AWSSNS) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bind } // GetComponentMetadata returns the metadata of the component. -func (a *AWSSNS) GetComponentMetadata() map[string]string { +func (a *AWSSNS) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := snsMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/aws/sqs/sqs.go b/bindings/aws/sqs/sqs.go index 58c999b55d..231ac80151 100644 --- a/bindings/aws/sqs/sqs.go +++ b/bindings/aws/sqs/sqs.go @@ -187,9 +187,8 @@ func (a *AWSSQS) getClient(metadata *sqsMetadata) (*sqs.SQS, error) { } // GetComponentMetadata returns the metadata of the component. -func (a *AWSSQS) GetComponentMetadata() map[string]string { +func (a *AWSSQS) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := sqsMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/azure/blobstorage/blobstorage.go b/bindings/azure/blobstorage/blobstorage.go index 8779ec01e8..c28034edab 100644 --- a/bindings/azure/blobstorage/blobstorage.go +++ b/bindings/azure/blobstorage/blobstorage.go @@ -358,9 +358,8 @@ func (a *AzureBlobStorage) isValidDeleteSnapshotsOptionType(accessType azblob.De } // GetComponentMetadata returns the metadata of the component. -func (a *AzureBlobStorage) GetComponentMetadata() map[string]string { +func (a *AzureBlobStorage) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := storageinternal.BlobStorageMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/azure/cosmosdb/cosmosdb.go b/bindings/azure/cosmosdb/cosmosdb.go index 785514f74f..381f8e667a 100644 --- a/bindings/azure/cosmosdb/cosmosdb.go +++ b/bindings/azure/cosmosdb/cosmosdb.go @@ -192,9 +192,8 @@ func (c *CosmosDB) lookup(m map[string]interface{}, ks []string) (val interface{ } // GetComponentMetadata returns the metadata of the component. -func (c *CosmosDB) GetComponentMetadata() map[string]string { +func (c *CosmosDB) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := cosmosDBCredentials{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/azure/cosmosdbgremlinapi/cosmosdbgremlinapi.go b/bindings/azure/cosmosdbgremlinapi/cosmosdbgremlinapi.go index f235d1ee91..74109da384 100644 --- a/bindings/azure/cosmosdbgremlinapi/cosmosdbgremlinapi.go +++ b/bindings/azure/cosmosdbgremlinapi/cosmosdbgremlinapi.go @@ -130,9 +130,8 @@ func (c *CosmosDBGremlinAPI) Invoke(_ context.Context, req *bindings.InvokeReque } // GetComponentMetadata returns the metadata of the component. -func (c *CosmosDBGremlinAPI) GetComponentMetadata() map[string]string { +func (c *CosmosDBGremlinAPI) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := cosmosDBGremlinAPICredentials{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/azure/eventgrid/eventgrid.go b/bindings/azure/eventgrid/eventgrid.go index 4c7edb4da4..377f9f4bc2 100644 --- a/bindings/azure/eventgrid/eventgrid.go +++ b/bindings/azure/eventgrid/eventgrid.go @@ -535,9 +535,8 @@ func (a *AzureEventGrid) subscriptionNeedsUpdating(res armeventgrid.EventSubscri } // GetComponentMetadata returns the metadata of the component. -func (a *AzureEventGrid) GetComponentMetadata() map[string]string { +func (a *AzureEventGrid) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := azureEventGridMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/azure/eventgrid/metadata.yaml b/bindings/azure/eventgrid/metadata.yaml index 7c99fc44fa..dd8bbc4dda 100644 --- a/bindings/azure/eventgrid/metadata.yaml +++ b/bindings/azure/eventgrid/metadata.yaml @@ -27,7 +27,7 @@ metadata: output: false description: | The HTTPS endpoint of the webhook Event Grid sends events (formatted as - Cloud Events) to. If you’re not re-writing URLs on ingress, it should be + Cloud Events) to. If you're not re-writing URLs on ingress, it should be in the form of: `"https://[YOUR HOSTNAME]/"` If testing on your local machine, you can use something like `ngrok` to create a public endpoint. diff --git a/bindings/azure/eventhubs/eventhubs.go b/bindings/azure/eventhubs/eventhubs.go index cd48ab6bb2..ec68b4e378 100644 --- a/bindings/azure/eventhubs/eventhubs.go +++ b/bindings/azure/eventhubs/eventhubs.go @@ -96,9 +96,8 @@ func (a *AzureEventHubs) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (a *AzureEventHubs) GetComponentMetadata() map[string]string { +func (a *AzureEventHubs) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := impl.AzureEventHubsMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/azure/eventhubs/metadata.yaml b/bindings/azure/eventhubs/metadata.yaml index 77c221cfa2..f6250ce69d 100644 --- a/bindings/azure/eventhubs/metadata.yaml +++ b/bindings/azure/eventhubs/metadata.yaml @@ -89,14 +89,6 @@ builtinAuthenticationProfiles: Number of partitions for the new Event Hub namespace. Used only when entity management is enabled. metadata: - # Input and output metadata - - name: partitionId - type: string - required: false - description: | - DEPRECATED. - deprecated: true - example: "" # Input-only metadata # consumerGroup is an alias for consumerId, if both are defined consumerId takes precedence. - name: consumerId diff --git a/bindings/azure/openai/openai.go b/bindings/azure/openai/openai.go index 83d189203a..9df8009a33 100644 --- a/bindings/azure/openai/openai.go +++ b/bindings/azure/openai/openai.go @@ -318,9 +318,8 @@ func (p *AzOpenAI) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (p *AzOpenAI) GetComponentMetadata() map[string]string { +func (p *AzOpenAI) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := openAIMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/azure/servicebusqueues/servicebusqueues.go b/bindings/azure/servicebusqueues/servicebusqueues.go index 27d74f4088..3ab133fbe4 100644 --- a/bindings/azure/servicebusqueues/servicebusqueues.go +++ b/bindings/azure/servicebusqueues/servicebusqueues.go @@ -204,10 +204,9 @@ func (a *AzureServiceBusQueues) Close() (err error) { } // GetComponentMetadata returns the metadata of the component. -func (a *AzureServiceBusQueues) GetComponentMetadata() map[string]string { +func (a *AzureServiceBusQueues) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := impl.Metadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) delete(metadataInfo, "consumerID") // only applies to topics, not queues - return metadataInfo + return } diff --git a/bindings/azure/signalr/signalr.go b/bindings/azure/signalr/signalr.go index a446ffc9a1..2e4e3821fa 100644 --- a/bindings/azure/signalr/signalr.go +++ b/bindings/azure/signalr/signalr.go @@ -301,9 +301,8 @@ func (s *SignalR) getToken(ctx context.Context, url string) (string, error) { } // GetComponentMetadata returns the metadata of the component. -func (s *SignalR) GetComponentMetadata() map[string]string { +func (s *SignalR) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := SignalRMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/azure/storagequeues/storagequeues.go b/bindings/azure/storagequeues/storagequeues.go index 183b2f92cd..babfb2feab 100644 --- a/bindings/azure/storagequeues/storagequeues.go +++ b/bindings/azure/storagequeues/storagequeues.go @@ -370,9 +370,8 @@ func (a *AzureStorageQueues) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (a *AzureStorageQueues) GetComponentMetadata() map[string]string { +func (a *AzureStorageQueues) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := storageQueuesMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/cloudflare/queues/cfqueues.go b/bindings/cloudflare/queues/cfqueues.go index 1b3d04a106..30d89ccb49 100644 --- a/bindings/cloudflare/queues/cfqueues.go +++ b/bindings/cloudflare/queues/cfqueues.go @@ -136,9 +136,8 @@ func (q *CFQueues) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (q *CFQueues) GetComponentMetadata() map[string]string { +func (q *CFQueues) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := componentMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/commercetools/commercetools.go b/bindings/commercetools/commercetools.go index 55010f4e2e..7efd4053cf 100644 --- a/bindings/commercetools/commercetools.go +++ b/bindings/commercetools/commercetools.go @@ -201,9 +201,8 @@ func (ct *Binding) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (ct Binding) GetComponentMetadata() map[string]string { +func (ct Binding) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := commercetoolsMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/cron/cron.go b/bindings/cron/cron.go index 53a5754582..db892616b9 100644 --- a/bindings/cron/cron.go +++ b/bindings/cron/cron.go @@ -132,9 +132,8 @@ func (b *Binding) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (b *Binding) GetComponentMetadata() map[string]string { +func (b *Binding) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := metadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/dubbo/dubbo_output.go b/bindings/dubbo/dubbo_output.go index d57d823739..ab117589b3 100644 --- a/bindings/dubbo/dubbo_output.go +++ b/bindings/dubbo/dubbo_output.go @@ -27,6 +27,7 @@ import ( dubboImpl "dubbo.apache.org/dubbo-go/v3/protocol/dubbo/impl" "github.com/dapr/components-contrib/bindings" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" ) @@ -93,7 +94,6 @@ func (out *DubboOutputBinding) Operations() []bindings.OperationKind { } // GetComponentMetadata returns the metadata of the component. -func (out *DubboOutputBinding) GetComponentMetadata() map[string]string { - metadataInfo := map[string]string{} - return metadataInfo +func (out *DubboOutputBinding) GetComponentMetadata() metadata.MetadataMap { + return metadata.MetadataMap{} } diff --git a/bindings/gcp/bucket/bucket.go b/bindings/gcp/bucket/bucket.go index e682c170b1..941b43ff4c 100644 --- a/bindings/gcp/bucket/bucket.go +++ b/bindings/gcp/bucket/bucket.go @@ -31,7 +31,7 @@ import ( "github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/internal/utils" - contribMetadata "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" ) @@ -110,7 +110,7 @@ func (g *GCPStorage) Init(ctx context.Context, metadata bindings.Metadata) error func (g *GCPStorage) parseMetadata(meta bindings.Metadata) (*gcpMetadata, error) { m := gcpMetadata{} - err := contribMetadata.DecodeMetadata(meta.Properties, &m) + err := metadata.DecodeMetadata(meta.Properties, &m) if err != nil { return nil, err } @@ -311,9 +311,8 @@ func (g *GCPStorage) handleBackwardCompatibilityForMetadata(metadata map[string] } // GetComponentMetadata returns the metadata of the component. -func (g *GCPStorage) GetComponentMetadata() map[string]string { +func (g *GCPStorage) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := gcpMetadata{} - metadataInfo := map[string]string{} - contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) + return } diff --git a/bindings/gcp/pubsub/pubsub.go b/bindings/gcp/pubsub/pubsub.go index 7cc7483f09..dfc8a69787 100644 --- a/bindings/gcp/pubsub/pubsub.go +++ b/bindings/gcp/pubsub/pubsub.go @@ -151,9 +151,8 @@ func (g *GCPPubSub) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (g *GCPPubSub) GetComponentMetadata() map[string]string { +func (g *GCPPubSub) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := pubSubMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/graphql/graphql.go b/bindings/graphql/graphql.go index 91edb090e9..379c863e54 100644 --- a/bindings/graphql/graphql.go +++ b/bindings/graphql/graphql.go @@ -186,9 +186,8 @@ func (gql *GraphQL) runRequest(ctx context.Context, requestKey string, req *bind } // GetComponentMetadata returns the metadata of the component. -func (gql *GraphQL) GetComponentMetadata() map[string]string { +func (gql *GraphQL) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := graphQLMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/http/http.go b/bindings/http/http.go index 63fe4ee4f6..177a3af087 100644 --- a/bindings/http/http.go +++ b/bindings/http/http.go @@ -338,9 +338,8 @@ func (h *HTTPSource) Invoke(parentCtx context.Context, req *bindings.InvokeReque } // GetComponentMetadata returns the metadata of the component. -func (h *HTTPSource) GetComponentMetadata() map[string]string { +func (h *HTTPSource) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := httpMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/huawei/obs/obs.go b/bindings/huawei/obs/obs.go index 8e68e2d8d2..26cad599f3 100644 --- a/bindings/huawei/obs/obs.go +++ b/bindings/huawei/obs/obs.go @@ -322,9 +322,8 @@ func (o *HuaweiOBS) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*b } // GetComponentMetadata returns the metadata of the component. -func (o *HuaweiOBS) GetComponentMetadata() map[string]string { +func (o *HuaweiOBS) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := obsMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/influx/influx.go b/bindings/influx/influx.go index 87fad6b805..30bc43595e 100644 --- a/bindings/influx/influx.go +++ b/bindings/influx/influx.go @@ -167,9 +167,8 @@ func (i *Influx) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (i *Influx) GetComponentMetadata() map[string]string { +func (i *Influx) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := influxMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/input_binding.go b/bindings/input_binding.go index 5892bf2d72..b87f7b9d4c 100644 --- a/bindings/input_binding.go +++ b/bindings/input_binding.go @@ -19,10 +19,13 @@ import ( "io" "github.com/dapr/components-contrib/health" + "github.com/dapr/components-contrib/metadata" ) // InputBinding is the interface to define a binding that triggers on incoming events. type InputBinding interface { + metadata.ComponentWithMetadata + // Init passes connection and properties metadata to the binding implementation. Init(ctx context.Context, metadata Metadata) error // Read is a method that runs in background and triggers the callback function whenever an event arrives. @@ -30,7 +33,6 @@ type InputBinding interface { // Close is a method that closes the connection to the binding. Must be // called when the binding is no longer needed to free up resources. io.Closer - GetComponentMetadata() map[string]string } // Handler is the handler used to invoke the app handler. diff --git a/bindings/kafka/kafka.go b/bindings/kafka/kafka.go index ddcb899bb6..266c4a43b2 100644 --- a/bindings/kafka/kafka.go +++ b/bindings/kafka/kafka.go @@ -138,9 +138,8 @@ func adaptHandler(handler bindings.Handler) kafka.EventHandler { } // GetComponentMetadata returns the metadata of the component. -func (b *Binding) GetComponentMetadata() map[string]string { +func (b *Binding) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := kafka.KafkaMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType) - return metadataInfo + return } diff --git a/bindings/kitex/kitex_output.go b/bindings/kitex/kitex_output.go index 4709218df3..171a832837 100644 --- a/bindings/kitex/kitex_output.go +++ b/bindings/kitex/kitex_output.go @@ -18,6 +18,7 @@ import ( "sync" "github.com/dapr/components-contrib/bindings" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/kit/logger" ) @@ -79,7 +80,6 @@ func (out *kitexOutputBinding) Operations() []bindings.OperationKind { } // GetComponentMetadata returns the metadata of the component. -func (out *kitexOutputBinding) GetComponentMetadata() map[string]string { - metadataInfo := map[string]string{} - return metadataInfo +func (out *kitexOutputBinding) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + return } diff --git a/bindings/kubemq/kubemq.go b/bindings/kubemq/kubemq.go index dde819568a..bee647248e 100644 --- a/bindings/kubemq/kubemq.go +++ b/bindings/kubemq/kubemq.go @@ -172,9 +172,8 @@ func (k *kubeMQ) processQueueMessage(ctx context.Context, handler bindings.Handl } // GetComponentMetadata returns the metadata of the component. -func (k *kubeMQ) GetComponentMetadata() map[string]string { +func (k *kubeMQ) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := options{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/kubernetes/kubernetes.go b/bindings/kubernetes/kubernetes.go index f854fa6678..0312888db3 100644 --- a/bindings/kubernetes/kubernetes.go +++ b/bindings/kubernetes/kubernetes.go @@ -202,9 +202,8 @@ func (k *kubernetesInput) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (k *kubernetesInput) GetComponentMetadata() map[string]string { +func (k *kubernetesInput) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := kubernetesMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/localstorage/localstorage.go b/bindings/localstorage/localstorage.go index 654bc54346..79b10388ff 100644 --- a/bindings/localstorage/localstorage.go +++ b/bindings/localstorage/localstorage.go @@ -336,9 +336,8 @@ func (ls *LocalStorage) Invoke(_ context.Context, req *bindings.InvokeRequest) ( } // GetComponentMetadata returns the metadata of the component. -func (ls *LocalStorage) GetComponentMetadata() map[string]string { +func (ls *LocalStorage) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := Metadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/mqtt3/mqtt.go b/bindings/mqtt3/mqtt.go index d11ecb0b2c..bac9ae5ea6 100644 --- a/bindings/mqtt3/mqtt.go +++ b/bindings/mqtt3/mqtt.go @@ -384,9 +384,8 @@ func (m *MQTT) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (m *MQTT) GetComponentMetadata() map[string]string { +func (m *MQTT) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := mqtt3Metadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/mysql/mysql.go b/bindings/mysql/mysql.go index 2127caf0f3..5143a02096 100644 --- a/bindings/mysql/mysql.go +++ b/bindings/mysql/mysql.go @@ -370,9 +370,8 @@ func (m *Mysql) convert(columnTypes []*sql.ColumnType, values []any) map[string] } // GetComponentMetadata returns the metadata of the component. -func (m *Mysql) GetComponentMetadata() map[string]string { +func (m *Mysql) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := mysqlMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/nacos/nacos.go b/bindings/nacos/nacos.go index c0e506f5e9..7296e70f65 100644 --- a/bindings/nacos/nacos.go +++ b/bindings/nacos/nacos.go @@ -457,9 +457,8 @@ func parseServerURL(s string) (*constant.ServerConfig, error) { } // GetComponentMetadata returns the metadata of the component. -func (n *Nacos) GetComponentMetadata() map[string]string { +func (n *Nacos) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := Settings{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/output_binding.go b/bindings/output_binding.go index 39f52988c5..42c46bb9ab 100644 --- a/bindings/output_binding.go +++ b/bindings/output_binding.go @@ -18,14 +18,16 @@ import ( "fmt" "github.com/dapr/components-contrib/health" + "github.com/dapr/components-contrib/metadata" ) // OutputBinding is the interface for an output binding, allowing users to invoke remote systems with optional payloads. type OutputBinding interface { + metadata.ComponentWithMetadata + Init(ctx context.Context, metadata Metadata) error Invoke(ctx context.Context, req *InvokeRequest) (*InvokeResponse, error) Operations() []OperationKind - GetComponentMetadata() map[string]string } func PingOutBinding(ctx context.Context, outputBinding OutputBinding) error { diff --git a/bindings/postgres/postgres.go b/bindings/postgres/postgres.go index 637b88d344..3a0ab98252 100644 --- a/bindings/postgres/postgres.go +++ b/bindings/postgres/postgres.go @@ -213,9 +213,8 @@ func (p *Postgres) exec(ctx context.Context, sql string, args ...any) (result in } // GetComponentMetadata returns the metadata of the component. -func (p *Postgres) GetComponentMetadata() map[string]string { +func (p *Postgres) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := psqlMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/postmark/postmark.go b/bindings/postmark/postmark.go index 21bece8a93..7560bff28b 100644 --- a/bindings/postmark/postmark.go +++ b/bindings/postmark/postmark.go @@ -161,9 +161,8 @@ func (p *Postmark) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bi } // GetComponentMetadata returns the metadata of the component. -func (p *Postmark) GetComponentMetadata() map[string]string { +func (p *Postmark) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := postmarkMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/rabbitmq/rabbitmq.go b/bindings/rabbitmq/rabbitmq.go index b8a59457f8..e10f7422f3 100644 --- a/bindings/rabbitmq/rabbitmq.go +++ b/bindings/rabbitmq/rabbitmq.go @@ -548,9 +548,8 @@ func (r *RabbitMQ) reset() (err error) { return err } -func (r *RabbitMQ) GetComponentMetadata() map[string]string { +func (r *RabbitMQ) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := rabbitMQMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/redis/redis.go b/bindings/redis/redis.go index 0da6043f0c..5d029886d3 100644 --- a/bindings/redis/redis.go +++ b/bindings/redis/redis.go @@ -145,9 +145,8 @@ func (r *Redis) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (r *Redis) GetComponentMetadata() map[string]string { +func (r *Redis) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := rediscomponent.Settings{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/rethinkdb/statechange/statechange.go b/bindings/rethinkdb/statechange/statechange.go index 0d3188b80e..81639d473c 100644 --- a/bindings/rethinkdb/statechange/statechange.go +++ b/bindings/rethinkdb/statechange/statechange.go @@ -166,9 +166,8 @@ func metadataToConfig(cfg map[string]string, logger logger.Logger) (StateConfig, } // GetComponentMetadata returns the metadata of the component. -func (b *Binding) GetComponentMetadata() map[string]string { +func (b *Binding) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := StateConfig{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/smtp/smtp.go b/bindings/smtp/smtp.go index 668b88ddda..12ff6473af 100644 --- a/bindings/smtp/smtp.go +++ b/bindings/smtp/smtp.go @@ -231,9 +231,8 @@ func (metadata Metadata) parseAddresses(addresses string) []string { } // GetComponentMetadata returns the metadata of the component. -func (s *Mailer) GetComponentMetadata() map[string]string { +func (s *Mailer) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := Metadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/twilio/sendgrid/sendgrid.go b/bindings/twilio/sendgrid/sendgrid.go index 1bb75ffbb6..1204c977a0 100644 --- a/bindings/twilio/sendgrid/sendgrid.go +++ b/bindings/twilio/sendgrid/sendgrid.go @@ -264,11 +264,10 @@ func (sg *SendGrid) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*b } // GetComponentMetadata returns the metadata of the component. -func (sg *SendGrid) GetComponentMetadata() map[string]string { +func (sg *SendGrid) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := sendGridMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } // Function that unmarshals the Dynamic Template Data JSON String into a map[string]any. diff --git a/bindings/twilio/sms/sms.go b/bindings/twilio/sms/sms.go index b4e35c5a5d..13723c6f0a 100644 --- a/bindings/twilio/sms/sms.go +++ b/bindings/twilio/sms/sms.go @@ -135,9 +135,8 @@ func (t *SMS) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*binding } // GetComponentMetadata returns the metadata of the component. -func (t *SMS) GetComponentMetadata() map[string]string { +func (t *SMS) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := twilioMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/wasm/output.go b/bindings/wasm/output.go index 9d80d5511e..429eb8f6a0 100644 --- a/bindings/wasm/output.go +++ b/bindings/wasm/output.go @@ -168,9 +168,8 @@ func detectImports(imports []api.FunctionDefinition) importMode { } // GetComponentMetadata returns the metadata of the component. -func (out *outputBinding) GetComponentMetadata() map[string]string { +func (out *outputBinding) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := wasm.InitMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/zeebe/command/command.go b/bindings/zeebe/command/command.go index 1e7ab1b71e..a0f8a76011 100644 --- a/bindings/zeebe/command/command.go +++ b/bindings/zeebe/command/command.go @@ -131,9 +131,8 @@ func (z *ZeebeCommand) Invoke(ctx context.Context, req *bindings.InvokeRequest) } // GetComponentMetadata returns the metadata of the component. -func (z *ZeebeCommand) GetComponentMetadata() map[string]string { +func (z *ZeebeCommand) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := zeebe.ClientMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/bindings/zeebe/jobworker/jobworker.go b/bindings/zeebe/jobworker/jobworker.go index 60703a4f02..21638ca26a 100644 --- a/bindings/zeebe/jobworker/jobworker.go +++ b/bindings/zeebe/jobworker/jobworker.go @@ -256,9 +256,8 @@ func (h *jobHandler) failJob(ctx context.Context, client worker.JobClient, job e } // GetComponentMetadata returns the metadata of the component. -func (z *ZeebeJobWorker) GetComponentMetadata() map[string]string { +func (z *ZeebeJobWorker) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := jobWorkerMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) - return metadataInfo + return } diff --git a/component-metadata-schema.json b/component-metadata-schema.json index be4333ad68..b6d7020727 100644 --- a/component-metadata-schema.json +++ b/component-metadata-schema.json @@ -76,7 +76,7 @@ "properties": { "name": { "type": "string", - "description": "Name of the built-in authentication profile.\nCurrently supports:\n\n- `azuread` (Azure AD, including Managed Identity)." + "description": "Name of the built-in authentication profile." }, "metadata": { "items": { diff --git a/configuration/azure/appconfig/appconfig.go b/configuration/azure/appconfig/appconfig.go index 2a27d410ee..a18f5d367c 100644 --- a/configuration/azure/appconfig/appconfig.go +++ b/configuration/azure/appconfig/appconfig.go @@ -338,9 +338,8 @@ func (r *ConfigurationStore) Unsubscribe(ctx context.Context, req *configuration } // GetComponentMetadata returns the metadata of the component. -func (r *ConfigurationStore) GetComponentMetadata() map[string]string { +func (r *ConfigurationStore) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := metadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.ConfigurationStoreType) - return metadataInfo + return } diff --git a/configuration/postgres/postgres.go b/configuration/postgres/postgres.go index 3a072da851..f5cef79234 100644 --- a/configuration/postgres/postgres.go +++ b/configuration/postgres/postgres.go @@ -376,9 +376,8 @@ func (p *ConfigurationStore) subscribeToChannel(ctx context.Context, pgNotifyCha } // GetComponentMetadata returns the metadata of the component. -func (p *ConfigurationStore) GetComponentMetadata() map[string]string { +func (p *ConfigurationStore) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := metadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.ConfigurationStoreType) - return metadataInfo + return } diff --git a/configuration/redis/redis.go b/configuration/redis/redis.go index 2ddb62012f..e5125e5601 100644 --- a/configuration/redis/redis.go +++ b/configuration/redis/redis.go @@ -245,9 +245,8 @@ func (r *ConfigurationStore) handleSubscribedChange(ctx context.Context, req *co } // GetComponentMetadata returns the metadata of the component. -func (r *ConfigurationStore) GetComponentMetadata() map[string]string { +func (r *ConfigurationStore) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := rediscomponent.Settings{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.ConfigurationStoreType) - return metadataInfo + return } diff --git a/configuration/store.go b/configuration/store.go index 0a82b4d8a2..c0f167fe57 100644 --- a/configuration/store.go +++ b/configuration/store.go @@ -13,10 +13,16 @@ limitations under the License. package configuration -import "context" +import ( + "context" + + "github.com/dapr/components-contrib/metadata" +) // Store is an interface to perform operations on store. type Store interface { + metadata.ComponentWithMetadata + // Init configuration store. Init(ctx context.Context, metadata Metadata) error @@ -28,9 +34,6 @@ type Store interface { // Unsubscribe configuration with keys Unsubscribe(ctx context.Context, req *UnsubscribeRequest) error - - // GetComponentMetadata returns information on the component's metadata. - GetComponentMetadata() map[string]string } // UpdateHandler is the handler used to send event to daprd. diff --git a/crypto/azure/keyvault/component.go b/crypto/azure/keyvault/component.go index b009ed00a5..558775b919 100644 --- a/crypto/azure/keyvault/component.go +++ b/crypto/azure/keyvault/component.go @@ -406,11 +406,10 @@ func (keyvaultCrypto) SupportedSignatureAlgorithms() []string { return signatureAlgsList } -func (keyvaultCrypto) GetComponentMetadata() map[string]string { +func (keyvaultCrypto) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := keyvaultMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.CryptoType) - return metadataInfo + return } type keyID struct { diff --git a/crypto/jwks/component.go b/crypto/jwks/component.go index 56e22c1184..42a18d11d9 100644 --- a/crypto/jwks/component.go +++ b/crypto/jwks/component.go @@ -126,9 +126,8 @@ func (k *jwksCrypto) retrieveKeyFromSecretFn(parentCtx context.Context, kid stri return key, nil } -func (k *jwksCrypto) GetComponentMetadata() map[string]string { +func (k *jwksCrypto) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := jwksMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.CryptoType) - return metadataInfo + return } diff --git a/crypto/kubernetes/secrets/component.go b/crypto/kubernetes/secrets/component.go index c4a7436533..7acd88238d 100644 --- a/crypto/kubernetes/secrets/component.go +++ b/crypto/kubernetes/secrets/component.go @@ -28,7 +28,7 @@ import ( contribCrypto "github.com/dapr/components-contrib/crypto" kubeclient "github.com/dapr/components-contrib/internal/authentication/kubernetes" - contribMetadata "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/metadata" internals "github.com/dapr/kit/crypto" "github.com/dapr/kit/logger" ) @@ -136,9 +136,8 @@ func (k *kubeSecretsCrypto) parseKeyString(param string) (namespace string, secr return } -func (kubeSecretsCrypto) GetComponentMetadata() map[string]string { +func (kubeSecretsCrypto) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := secretsMetadata{} - metadataInfo := map[string]string{} - contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.CryptoType) - return metadataInfo + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.CryptoType) + return } diff --git a/crypto/localstorage/component.go b/crypto/localstorage/component.go index 45cc7f654c..f74bae7d03 100644 --- a/crypto/localstorage/component.go +++ b/crypto/localstorage/component.go @@ -105,9 +105,8 @@ func (l *localStorageCrypto) retrieveKey(parentCtx context.Context, key string) return jwkObj, nil } -func (localStorageCrypto) GetComponentMetadata() map[string]string { +func (localStorageCrypto) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := localStorageMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.CryptoType) - return metadataInfo + return } diff --git a/crypto/subtlecrypto.go b/crypto/subtlecrypto.go index dad9dba3c1..6f227f807d 100644 --- a/crypto/subtlecrypto.go +++ b/crypto/subtlecrypto.go @@ -17,10 +17,14 @@ import ( "context" "github.com/lestrrat-go/jwx/v2/jwk" + + "github.com/dapr/components-contrib/metadata" ) // SubtleCrypto offers an interface to perform low-level ("subtle") cryptographic operations with keys stored in a vault. type SubtleCrypto interface { + metadata.ComponentWithMetadata + SubtleCryptoAlgorithms // Init the component. @@ -161,9 +165,6 @@ type SubtleCrypto interface { valid bool, err error, ) - - // GetComponentMetadata returns information on the component's metadata. - GetComponentMetadata() map[string]string } // SubtleCryptoAlgorithms is an extension to SubtleCrypto that includes methods to return information on the supported algorithms. diff --git a/internal/component/azure/blobstorage/metadata.go b/internal/component/azure/blobstorage/metadata.go index 4a24f72f09..743ecbeebd 100644 --- a/internal/component/azure/blobstorage/metadata.go +++ b/internal/component/azure/blobstorage/metadata.go @@ -25,7 +25,7 @@ import ( type BlobStorageMetadata struct { ContainerClientOpts `json:",inline" mapstructure:",squash"` - DecodeBase64 bool `json:"decodeBase64,string" mapstructure:"decodeBase64" only:"bindings"` + DecodeBase64 bool `json:"decodeBase64,string" mapstructure:"decodeBase64" mdonly:"bindings"` PublicAccessLevel azblob.PublicAccessType } diff --git a/internal/component/azure/eventhubs/metadata.go b/internal/component/azure/eventhubs/metadata.go index 4f6152c198..2547f7fb73 100644 --- a/internal/component/azure/eventhubs/metadata.go +++ b/internal/component/azure/eventhubs/metadata.go @@ -40,9 +40,8 @@ type AzureEventHubsMetadata struct { ResourceGroupName string `json:"resourceGroupName" mapstructure:"resourceGroupName"` // Binding only - EventHub string `json:"eventHub" mapstructure:"eventHub" only:"bindings"` - ConsumerGroup string `json:"consumerGroup" mapstructure:"consumerGroup" only:"bindings"` // Alias for ConsumerID - PartitionID string `json:"partitionID" mapstructure:"partitionID" only:"bindings"` // Deprecated + EventHub string `json:"eventHub" mapstructure:"eventHub" mdonly:"bindings"` + ConsumerGroup string `json:"consumerGroup" mapstructure:"consumerGroup" mdonly:"bindings"` // Alias for ConsumerID // Internal properties namespaceName string @@ -91,16 +90,9 @@ func parseEventHubsMetadata(meta map[string]string, isBinding bool, log logger.L return nil, errors.New("the provided connection string does not contain a value for 'EntityPath' and no 'eventHub' property was passed") } } - - // Property partitionID is deprecated - if m.PartitionID != "" { - log.Info("Property partitionID is deprecated and will be ignored") - m.PartitionID = "" - } } else { // Ignored when not a binding m.EventHub = "" - m.PartitionID = "" // If connecting using a connection string, parse hubName if m.ConnectionString != "" { diff --git a/internal/component/azure/servicebus/metadata.go b/internal/component/azure/servicebus/metadata.go index 84950e9583..5f49e23e9c 100644 --- a/internal/component/azure/servicebus/metadata.go +++ b/internal/component/azure/servicebus/metadata.go @@ -49,7 +49,7 @@ type Metadata struct { NamespaceName string `mapstructure:"namespaceName"` // Only for Azure AD /** For bindings only **/ - QueueName string `mapstructure:"queueName" only:"bindings"` // Only queues + QueueName string `mapstructure:"queueName" mdonly:"bindings"` // Only queues } // Keys. diff --git a/internal/component/postgresql/postgresql.go b/internal/component/postgresql/postgresql.go index f44d8abdfa..76af6bb568 100644 --- a/internal/component/postgresql/postgresql.go +++ b/internal/component/postgresql/postgresql.go @@ -120,9 +120,8 @@ func (p *PostgreSQL) GetDBAccess() dbAccess { return p.dbaccess } -func (p *PostgreSQL) GetComponentMetadata() map[string]string { +func (p *PostgreSQL) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := postgresMetadataStruct{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/internal/component/redis/settings.go b/internal/component/redis/settings.go index f71afff8ef..172e7a183e 100644 --- a/internal/component/redis/settings.go +++ b/internal/component/redis/settings.go @@ -79,23 +79,23 @@ type Settings struct { EnableTLS bool `mapstructure:"enableTLS"` // == state only properties == - TTLInSeconds *int `mapstructure:"ttlInSeconds" only:"state"` - QueryIndexes string `mapstructure:"queryIndexes" only:"state"` + TTLInSeconds *int `mapstructure:"ttlInSeconds" mdonly:"state"` + QueryIndexes string `mapstructure:"queryIndexes" mdonly:"state"` // == pubsub only properties == // The consumer identifier - ConsumerID string `mapstructure:"consumerID" only:"pubsub"` + ConsumerID string `mapstructure:"consumerID" mdonly:"pubsub"` // The interval between checking for pending messages to redelivery (0 disables redelivery) - RedeliverInterval time.Duration `mapstructure:"-" only:"pubsub"` + RedeliverInterval time.Duration `mapstructure:"-" mdonly:"pubsub"` // The amount time a message must be pending before attempting to redeliver it (0 disables redelivery) - ProcessingTimeout time.Duration `mapstructure:"processingTimeout" only:"pubsub"` + ProcessingTimeout time.Duration `mapstructure:"processingTimeout" mdonly:"pubsub"` // The size of the message queue for processing - QueueDepth uint `mapstructure:"queueDepth" only:"pubsub"` + QueueDepth uint `mapstructure:"queueDepth" mdonly:"pubsub"` // The number of concurrent workers that are processing messages - Concurrency uint `mapstructure:"concurrency" only:"pubsub"` + Concurrency uint `mapstructure:"concurrency" mdonly:"pubsub"` - // the max len of stream - MaxLenApprox int64 `mapstructure:"maxLenApprox" only:"pubsub"` + // The max len of stream + MaxLenApprox int64 `mapstructure:"maxLenApprox" mdonly:"pubsub"` } func (s *Settings) Decode(in interface{}) error { diff --git a/lock/redis/standalone.go b/lock/redis/standalone.go index 672b78e85c..12f3305de0 100644 --- a/lock/redis/standalone.go +++ b/lock/redis/standalone.go @@ -183,9 +183,8 @@ func (r *StandaloneRedisLock) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (r *StandaloneRedisLock) GetComponentMetadata() map[string]string { +func (r *StandaloneRedisLock) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := rediscomponent.Settings{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.LockStoreType) - return metadataInfo + return } diff --git a/lock/store.go b/lock/store.go index 9617a9d11b..0fb1758e2c 100644 --- a/lock/store.go +++ b/lock/store.go @@ -13,9 +13,15 @@ limitations under the License. package lock -import "context" +import ( + "context" + + "github.com/dapr/components-contrib/metadata" +) type Store interface { + metadata.ComponentWithMetadata + // Init this component. InitLockStore(ctx context.Context, metadata Metadata) error @@ -24,7 +30,4 @@ type Store interface { // Unlock tries to release a lock. Unlock(ctx context.Context, req *UnlockRequest) (*UnlockResponse, error) - - // GetComponentMetadata returns information on the component's metadata. - GetComponentMetadata() map[string]string } diff --git a/metadata/componentmetadata.go b/metadata/componentmetadata.go new file mode 100644 index 0000000000..d7dd83e6b6 --- /dev/null +++ b/metadata/componentmetadata.go @@ -0,0 +1,23 @@ +//go:build !metadata +// +build !metadata + +/* +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 metadata + +// ComponentWithMetadata is empty when the `metadata` build tag is not present. +// The build tag is present when running the linter. +type ComponentWithMetadata interface { + // Empty +} diff --git a/metadata/componentmetadata_metadata.go b/metadata/componentmetadata_metadata.go new file mode 100644 index 0000000000..3aa007d8e6 --- /dev/null +++ b/metadata/componentmetadata_metadata.go @@ -0,0 +1,23 @@ +//go:build metadata +// +build metadata + +/* +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 metadata + +// ComponentWithMetadata includes the GetComponentMetadata method when the `metadata` build tag is present. +// The build tag is present when running the linter. +type ComponentWithMetadata interface { + GetComponentMetadata() MetadataMap +} diff --git a/metadata/utils.go b/metadata/utils.go index 49d1811a84..1c9248c9ab 100644 --- a/metadata/utils.go +++ b/metadata/utils.go @@ -140,7 +140,7 @@ func GetMetadataProperty(props map[string]string, keys ...string) (val string, o // DecodeMetadata decodes metadata into a struct // This is an extension of mitchellh/mapstructure which also supports decoding durations -func DecodeMetadata(input interface{}, result interface{}) error { +func DecodeMetadata(input any, result any) error { // avoids a common mistake of passing the metadata struct, instead of the properties map // if input is of type struct, case it to metadata.Base and access the Properties instead v := reflect.ValueOf(input) @@ -174,8 +174,8 @@ func toTruthyBoolHookFunc() mapstructure.DecodeHookFunc { return func( f reflect.Type, t reflect.Type, - data interface{}, - ) (interface{}, error) { + data any, + ) (any, error) { if f == reflect.TypeOf("") && t == reflect.TypeOf(true) { val := data.(string) return utils.IsTruthy(val), nil @@ -192,8 +192,8 @@ func toStringArrayHookFunc() mapstructure.DecodeHookFunc { return func( f reflect.Type, t reflect.Type, - data interface{}, - ) (interface{}, error) { + data any, + ) (any, error) { if f == reflect.TypeOf("") && t == reflect.TypeOf([]string{}) { val := data.(string) return strings.Split(val, ","), nil @@ -231,8 +231,8 @@ func toTimeDurationArrayHookFunc() mapstructure.DecodeHookFunc { return func( f reflect.Type, t reflect.Type, - data interface{}, - ) (interface{}, error) { + data any, + ) (any, error) { if f == reflect.TypeOf("") && t == reflect.TypeOf([]time.Duration{}) { inputArrayString := data.(string) return convert(inputArrayString) @@ -296,9 +296,22 @@ func (t ComponentType) BuiltInMetadataProperties() []string { } } +type MetadataField struct { + // Field type + Type string + // True if the field should be ignored by the metadata analyzer + Ignored bool + // True if the field is deprecated + Deprecated bool + // Aliases used for old, deprecated names + Aliases []string +} + +type MetadataMap map[string]MetadataField + // GetMetadataInfoFromStructType converts a struct to a map of field name (or struct tag) to field type. // This is used to generate metadata documentation for components. -func GetMetadataInfoFromStructType(t reflect.Type, metadataMap *map[string]string, componentType ComponentType) error { +func GetMetadataInfoFromStructType(t reflect.Type, metadataMap *MetadataMap, componentType ComponentType) error { // Return if not struct or pointer to struct. if t.Kind() == reflect.Ptr { t = t.Elem() @@ -307,6 +320,10 @@ func GetMetadataInfoFromStructType(t reflect.Type, metadataMap *map[string]strin return fmt.Errorf("not a struct: %s", t.Kind().String()) } + if *metadataMap == nil { + *metadataMap = MetadataMap{} + } + for i := 0; i < t.NumField(); i++ { currentField := t.Field(i) // fields that are not exported cannot be set via the mapstructure metadata decoding mechanism @@ -318,10 +335,11 @@ func GetMetadataInfoFromStructType(t reflect.Type, metadataMap *map[string]strin if mapStructureTag == "-" { continue } - onlyTag := currentField.Tag.Get("only") - if onlyTag != "" { + + // If there's a "mdonly" tag, that metadata option is only included for certain component types + if mdOnlyTag := currentField.Tag.Get("mdonly"); mdOnlyTag != "" { include := false - onlyTags := strings.Split(onlyTag, ",") + onlyTags := strings.Split(mdOnlyTag, ",") for _, tag := range onlyTags { if tag == string(componentType) { include = true @@ -332,6 +350,24 @@ func GetMetadataInfoFromStructType(t reflect.Type, metadataMap *map[string]strin continue } } + + mdField := MetadataField{ + Type: currentField.Type.String(), + } + + // If there's a mdignore tag and that's truthy, the field should be ignored by the metadata analyzer + mdField.Ignored = utils.IsTruthy(currentField.Tag.Get("mdignore")) + + // If there's a "mddeprecated" tag, the field may be deprecated + mdField.Deprecated = utils.IsTruthy(currentField.Tag.Get("mddeprecated")) + + // If there's a "mdaliases" tag, the field contains aliases + // The value is a comma-separated string + if mdAliasesTag := currentField.Tag.Get("mdaliases"); mdAliasesTag != "" { + mdField.Aliases = strings.Split(mdAliasesTag, ",") + } + + // Handle mapstructure tags and get the field name mapStructureTags := strings.Split(mapStructureTag, ",") numTags := len(mapStructureTags) if numTags > 1 && mapStructureTags[numTags-1] == "squash" && currentField.Anonymous { @@ -345,7 +381,9 @@ func GetMetadataInfoFromStructType(t reflect.Type, metadataMap *map[string]strin } else { fieldName = currentField.Name } - (*metadataMap)[fieldName] = currentField.Type.String() + + // Add the field + (*metadataMap)[fieldName] = mdField } return nil diff --git a/metadata/utils_test.go b/metadata/utils_test.go index a26f9a608a..33a4af23c5 100644 --- a/metadata/utils_test.go +++ b/metadata/utils_test.go @@ -245,33 +245,61 @@ func TestMetadataStructToStringMap(t *testing.T) { Mybool *bool MyRegularDuration time.Duration SomethingWithCustomName string `mapstructure:"something_with_custom_name"` - PubSubOnlyProperty string `mapstructure:"pubsub_only_property" only:"pubsub"` - BindingOnlyProperty string `mapstructure:"binding_only_property" only:"bindings"` - PubSubAndBindingProperty string `mapstructure:"pubsub_and_binding_property" only:"pubsub,bindings"` + PubSubOnlyProperty string `mapstructure:"pubsub_only_property" mdonly:"pubsub"` + BindingOnlyProperty string `mapstructure:"binding_only_property" mdonly:"bindings"` + PubSubAndBindingProperty string `mapstructure:"pubsub_and_binding_property" mdonly:"pubsub,bindings"` MyDurationArray []time.Duration NotExportedByMapStructure string `mapstructure:"-"` notExported string //nolint:structcheck,unused + DeprecatedProperty string `mapstructure:"something_deprecated" mddeprecated:"true"` + Aliased string `mapstructure:"aliased" mdaliases:"another,name"` + Ignored string `mapstructure:"ignored" mdignore:"true"` } m := testMetadata{} - metadatainfo := map[string]string{} + metadatainfo := MetadataMap{} GetMetadataInfoFromStructType(reflect.TypeOf(m), &metadatainfo, BindingType) - assert.Equal(t, "string", metadatainfo["Mystring"]) - assert.Equal(t, "metadata.Duration", metadatainfo["Myduration"]) - assert.Equal(t, "int", metadatainfo["Myinteger"]) - assert.Equal(t, "float64", metadatainfo["Myfloat64"]) - assert.Equal(t, "*bool", metadatainfo["Mybool"]) - assert.Equal(t, "time.Duration", metadatainfo["MyRegularDuration"]) - assert.Equal(t, "string", metadatainfo["something_with_custom_name"]) + _ = assert.NotEmpty(t, metadatainfo["Mystring"]) && + assert.Equal(t, "string", metadatainfo["Mystring"].Type) + _ = assert.NotEmpty(t, metadatainfo["Myduration"]) && + assert.Equal(t, "metadata.Duration", metadatainfo["Myduration"].Type) + _ = assert.NotEmpty(t, metadatainfo["Myinteger"]) && + assert.Equal(t, "int", metadatainfo["Myinteger"].Type) + _ = assert.NotEmpty(t, metadatainfo["Myfloat64"]) && + assert.Equal(t, "float64", metadatainfo["Myfloat64"].Type) + _ = assert.NotEmpty(t, metadatainfo["Mybool"]) && + assert.Equal(t, "*bool", metadatainfo["Mybool"].Type) + _ = assert.NotEmpty(t, metadatainfo["MyRegularDuration"]) && + assert.Equal(t, "time.Duration", metadatainfo["MyRegularDuration"].Type) + _ = assert.NotEmpty(t, metadatainfo["something_with_custom_name"]) && + assert.Equal(t, "string", metadatainfo["something_with_custom_name"].Type) assert.NotContains(t, metadatainfo, "NestedStruct") assert.NotContains(t, metadatainfo, "SomethingWithCustomName") - assert.Equal(t, "string", metadatainfo["nested_string_custom"]) - assert.Equal(t, "string", metadatainfo["NestedString"]) + _ = assert.NotEmpty(t, metadatainfo["nested_string_custom"]) && + assert.Equal(t, "string", metadatainfo["nested_string_custom"].Type) + _ = assert.NotEmpty(t, metadatainfo["NestedString"]) && + assert.Equal(t, "string", metadatainfo["NestedString"].Type) assert.NotContains(t, metadatainfo, "pubsub_only_property") - assert.Equal(t, "string", metadatainfo["binding_only_property"]) - assert.Equal(t, "string", metadatainfo["pubsub_and_binding_property"]) - assert.Equal(t, "[]time.Duration", metadatainfo["MyDurationArray"]) + _ = assert.NotEmpty(t, metadatainfo["binding_only_property"]) && + assert.Equal(t, "string", metadatainfo["binding_only_property"].Type) + _ = assert.NotEmpty(t, metadatainfo["pubsub_and_binding_property"]) && + assert.Equal(t, "string", metadatainfo["pubsub_and_binding_property"].Type) + _ = assert.NotEmpty(t, metadatainfo["MyDurationArray"]) && + assert.Equal(t, "[]time.Duration", metadatainfo["MyDurationArray"].Type) assert.NotContains(t, metadatainfo, "NotExportedByMapStructure") assert.NotContains(t, metadatainfo, "notExported") + _ = assert.NotEmpty(t, metadatainfo["something_deprecated"]) && + assert.Equal(t, "string", metadatainfo["something_deprecated"].Type) && + assert.True(t, metadatainfo["something_deprecated"].Deprecated) + _ = assert.NotEmpty(t, metadatainfo["aliased"]) && + assert.Equal(t, "string", metadatainfo["aliased"].Type) && + assert.False(t, metadatainfo["aliased"].Deprecated) && + assert.False(t, metadatainfo["aliased"].Ignored) && + assert.Equal(t, []string{"another", "name"}, metadatainfo["aliased"].Aliases) + _ = assert.NotEmpty(t, metadatainfo["ignored"]) && + assert.Equal(t, "string", metadatainfo["ignored"].Type) && + assert.False(t, metadatainfo["ignored"].Deprecated) && + assert.True(t, metadatainfo["ignored"].Ignored) && + assert.Empty(t, metadatainfo["ignored"].Aliases) }) } diff --git a/middleware/http/bearer/bearer_middleware.go b/middleware/http/bearer/bearer_middleware.go index 404dd2dcd0..030403716a 100644 --- a/middleware/http/bearer/bearer_middleware.go +++ b/middleware/http/bearer/bearer_middleware.go @@ -126,9 +126,8 @@ func (m *Middleware) GetHandler(ctx context.Context, metadata middleware.Metadat }, nil } -func (m *Middleware) GetComponentMetadata() map[string]string { +func (m *Middleware) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := bearerMiddlewareMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.MiddlewareType) - return metadataInfo + return } diff --git a/middleware/http/oauth2/oauth2_middleware.go b/middleware/http/oauth2/oauth2_middleware.go index 7a7cb78c72..6ad773ea8d 100644 --- a/middleware/http/oauth2/oauth2_middleware.go +++ b/middleware/http/oauth2/oauth2_middleware.go @@ -155,9 +155,8 @@ func (m *Middleware) getNativeMetadata(metadata middleware.Metadata) (*oAuth2Mid return &middlewareMetadata, nil } -func (m *Middleware) GetComponentMetadata() map[string]string { +func (m *Middleware) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { metadataStruct := oAuth2MiddlewareMetadata{} - metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) - return metadataInfo + return } diff --git a/middleware/http/oauth2clientcredentials/oauth2clientcredentials_middleware.go b/middleware/http/oauth2clientcredentials/oauth2clientcredentials_middleware.go index f71a7ae36a..a3cfff4036 100644 --- a/middleware/http/oauth2clientcredentials/oauth2clientcredentials_middleware.go +++ b/middleware/http/oauth2clientcredentials/oauth2clientcredentials_middleware.go @@ -178,9 +178,8 @@ func (m *Middleware) GetToken(ctx context.Context, conf *clientcredentials.Confi return tokenSource.Token() } -func (m *Middleware) GetComponentMetadata() map[string]string { +func (m *Middleware) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { metadataStruct := oAuth2ClientCredentialsMiddlewareMetadata{} - metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) - return metadataInfo + return } diff --git a/middleware/http/opa/middleware.go b/middleware/http/opa/middleware.go index 12175152ae..c0c664441f 100644 --- a/middleware/http/opa/middleware.go +++ b/middleware/http/opa/middleware.go @@ -260,9 +260,8 @@ func (m *Middleware) getNativeMetadata(metadata middleware.Metadata) (*middlewar return &meta, nil } -func (m *Middleware) GetComponentMetadata() map[string]string { +func (m *Middleware) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := middlewareMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.MiddlewareType) - return metadataInfo + return } diff --git a/middleware/http/ratelimit/ratelimit_middleware.go b/middleware/http/ratelimit/ratelimit_middleware.go index 1bd342b425..b3caa87821 100644 --- a/middleware/http/ratelimit/ratelimit_middleware.go +++ b/middleware/http/ratelimit/ratelimit_middleware.go @@ -101,9 +101,8 @@ func (m *Middleware) getNativeMetadata(metadata middleware.Metadata) (*rateLimit return &middlewareMetadata, nil } -func (m *Middleware) GetComponentMetadata() map[string]string { +func (m *Middleware) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := rateLimitMiddlewareMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.MiddlewareType) - return metadataInfo + return } diff --git a/middleware/http/routeralias/routeralias_middleware.go b/middleware/http/routeralias/routeralias_middleware.go index 36ed67310e..1ac1929955 100644 --- a/middleware/http/routeralias/routeralias_middleware.go +++ b/middleware/http/routeralias/routeralias_middleware.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "net/http" + "reflect" "github.com/gorilla/mux" "gopkg.in/yaml.v3" @@ -118,6 +119,10 @@ func vars(r *http.Request) map[string]string { return nil } -func (m *Middleware) GetComponentMetadata() map[string]string { - return map[string]string{} +func (m *Middleware) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { + metadataStruct := struct { + Routes string `mapstructure:"routes"` + }{} + mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) + return } diff --git a/middleware/http/routerchecker/routerchecker_middleware.go b/middleware/http/routerchecker/routerchecker_middleware.go index ed9bd5ce32..eaf01825eb 100644 --- a/middleware/http/routerchecker/routerchecker_middleware.go +++ b/middleware/http/routerchecker/routerchecker_middleware.go @@ -74,9 +74,8 @@ func (m *Middleware) getNativeMetadata(metadata middleware.Metadata) (*Metadata, return &middlewareMetadata, nil } -func (m *Middleware) GetComponentMetadata() map[string]string { +func (m *Middleware) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { metadataStruct := Metadata{} - metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) - return metadataInfo + return } diff --git a/middleware/http/sentinel/middleware.go b/middleware/http/sentinel/middleware.go index 8ea851ac33..4680824d07 100644 --- a/middleware/http/sentinel/middleware.go +++ b/middleware/http/sentinel/middleware.go @@ -157,9 +157,8 @@ func getNativeMetadata(metadata middleware.Metadata) (*middlewareMetadata, error return &md, nil } -func (m *Middleware) GetComponentMetadata() map[string]string { +func (m *Middleware) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { metadataStruct := middlewareMetadata{} - metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) - return metadataInfo + return } diff --git a/middleware/http/wasm/httpwasm.go b/middleware/http/wasm/httpwasm.go index aa024f6a55..af707c8e2d 100644 --- a/middleware/http/wasm/httpwasm.go +++ b/middleware/http/wasm/httpwasm.go @@ -121,9 +121,8 @@ func (rh *requestHandler) Close() error { return rh.mw.Close(ctx) } -func (m *middleware) GetComponentMetadata() map[string]string { +func (m *middleware) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { metadataStruct := wasm.InitMetadata{} - metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.MiddlewareType) - return metadataInfo + return } diff --git a/middleware/middleware.go b/middleware/middleware.go index 4e61da34f3..88e13660e5 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -21,5 +21,4 @@ import ( // Middleware is the interface for a middleware. type Middleware interface { GetHandler(ctx context.Context, metadata Metadata) (func(next http.Handler) http.Handler, error) - GetComponentMetadata() map[string]string } diff --git a/pubsub/aws/snssqs/snssqs.go b/pubsub/aws/snssqs/snssqs.go index 9c8fd09aad..1d71d21ae9 100644 --- a/pubsub/aws/snssqs/snssqs.go +++ b/pubsub/aws/snssqs/snssqs.go @@ -931,9 +931,8 @@ func (s *snsSqs) Features() []pubsub.Feature { } // GetComponentMetadata returns the metadata of the component. -func (s *snsSqs) GetComponentMetadata() map[string]string { +func (s *snsSqs) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := snsSqsMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/azure/eventhubs/eventhubs.go b/pubsub/azure/eventhubs/eventhubs.go index 7fca9570c7..766c904a0d 100644 --- a/pubsub/azure/eventhubs/eventhubs.go +++ b/pubsub/azure/eventhubs/eventhubs.go @@ -147,9 +147,8 @@ func (aeh *AzureEventHubs) Close() (err error) { } // GetComponentMetadata returns the metadata of the component. -func (aeh *AzureEventHubs) GetComponentMetadata() map[string]string { +func (aeh *AzureEventHubs) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := impl.AzureEventHubsMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/azure/servicebus/queues/servicebus.go b/pubsub/azure/servicebus/queues/servicebus.go index 2870efc08f..61328d3b61 100644 --- a/pubsub/azure/servicebus/queues/servicebus.go +++ b/pubsub/azure/servicebus/queues/servicebus.go @@ -227,10 +227,9 @@ func (a *azureServiceBus) Features() []pubsub.Feature { } // GetComponentMetadata returns the metadata of the component. -func (a *azureServiceBus) GetComponentMetadata() map[string]string { +func (a *azureServiceBus) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := impl.Metadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - delete(metadataInfo, "consumerID") // does not apply to queues - return metadataInfo + delete(metadataInfo, "consumerID") // only applies to topics, not queues + return } diff --git a/pubsub/azure/servicebus/topics/servicebus.go b/pubsub/azure/servicebus/topics/servicebus.go index 244c045740..97fbb6baef 100644 --- a/pubsub/azure/servicebus/topics/servicebus.go +++ b/pubsub/azure/servicebus/topics/servicebus.go @@ -317,9 +317,8 @@ func (a *azureServiceBus) connectAndReceiveWithSessions(ctx context.Context, req } // GetComponentMetadata returns the metadata of the component. -func (a *azureServiceBus) GetComponentMetadata() map[string]string { +func (a *azureServiceBus) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := impl.Metadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/gcp/pubsub/pubsub.go b/pubsub/gcp/pubsub/pubsub.go index d960371737..43db539871 100644 --- a/pubsub/gcp/pubsub/pubsub.go +++ b/pubsub/gcp/pubsub/pubsub.go @@ -402,9 +402,8 @@ func (g *GCPPubSub) Features() []pubsub.Feature { } // GetComponentMetadata returns the metadata of the component. -func (g *GCPPubSub) GetComponentMetadata() map[string]string { +func (g *GCPPubSub) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := metadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/in-memory/in-memory.go b/pubsub/in-memory/in-memory.go index 1521230453..b6b4ab2696 100644 --- a/pubsub/in-memory/in-memory.go +++ b/pubsub/in-memory/in-memory.go @@ -21,6 +21,7 @@ import ( "time" "github.com/dapr/components-contrib/internal/eventbus" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/pubsub" "github.com/dapr/kit/logger" ) @@ -112,6 +113,6 @@ func (a *bus) Subscribe(ctx context.Context, req pubsub.SubscribeRequest, handle } // GetComponentMetadata returns the metadata of the component. -func (a *bus) GetComponentMetadata() map[string]string { - return map[string]string{} +func (a *bus) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + return } diff --git a/pubsub/jetstream/jetstream.go b/pubsub/jetstream/jetstream.go index 1c5f743360..e991a157c8 100644 --- a/pubsub/jetstream/jetstream.go +++ b/pubsub/jetstream/jetstream.go @@ -299,9 +299,8 @@ func sigHandler(seedKey string, nonce []byte) ([]byte, error) { } // GetComponentMetadata returns the metadata of the component. -func (js *jetstreamPubSub) GetComponentMetadata() map[string]string { +func (js *jetstreamPubSub) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { metadataStruct := metadata{} - metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.PubSubType) - return metadataInfo + return } diff --git a/pubsub/kafka/kafka.go b/pubsub/kafka/kafka.go index c2902d55ac..c341c77ea8 100644 --- a/pubsub/kafka/kafka.go +++ b/pubsub/kafka/kafka.go @@ -177,9 +177,8 @@ func adaptBulkHandler(handler pubsub.BulkHandler) kafka.BulkEventHandler { } // GetComponentMetadata returns the metadata of the component. -func (p *PubSub) GetComponentMetadata() map[string]string { +func (p *PubSub) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := kafka.KafkaMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/kubemq/kubemq.go b/pubsub/kubemq/kubemq.go index b0760fc694..164a9f2a58 100644 --- a/pubsub/kubemq/kubemq.go +++ b/pubsub/kubemq/kubemq.go @@ -79,9 +79,8 @@ func getRandomID() string { } // GetComponentMetadata returns the metadata of the component. -func (k *kubeMQ) GetComponentMetadata() map[string]string { +func (k *kubeMQ) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := &kubemqMetadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/mqtt3/mqtt.go b/pubsub/mqtt3/mqtt.go index 1f5c9fab13..2d50fbcc89 100644 --- a/pubsub/mqtt3/mqtt.go +++ b/pubsub/mqtt3/mqtt.go @@ -495,9 +495,8 @@ func buildRegexForTopic(topicName string) string { } // GetComponentMetadata returns the metadata of the component. -func (m *mqttPubSub) GetComponentMetadata() map[string]string { +func (m *mqttPubSub) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := mqttMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/natsstreaming/natsstreaming.go b/pubsub/natsstreaming/natsstreaming.go index a45f66707e..51817ac721 100644 --- a/pubsub/natsstreaming/natsstreaming.go +++ b/pubsub/natsstreaming/natsstreaming.go @@ -363,9 +363,8 @@ func (n *natsStreamingPubSub) Features() []pubsub.Feature { } // GetComponentMetadata returns the metadata of the component. -func (n *natsStreamingPubSub) GetComponentMetadata() map[string]string { +func (n *natsStreamingPubSub) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := natsMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/pubsub.go b/pubsub/pubsub.go index c17ed652ad..fb83761a8a 100644 --- a/pubsub/pubsub.go +++ b/pubsub/pubsub.go @@ -18,16 +18,18 @@ import ( "fmt" "github.com/dapr/components-contrib/health" + "github.com/dapr/components-contrib/metadata" ) // PubSub is the interface for message buses. type PubSub interface { + metadata.ComponentWithMetadata + Init(ctx context.Context, metadata Metadata) error Features() []Feature Publish(ctx context.Context, req *PublishRequest) error Subscribe(ctx context.Context, req SubscribeRequest, handler Handler) error Close() error - GetComponentMetadata() map[string]string } // BulkPublisher is the interface that wraps the BulkPublish method. diff --git a/pubsub/pulsar/pulsar.go b/pubsub/pulsar/pulsar.go index bf49defa68..a04bac1acb 100644 --- a/pubsub/pulsar/pulsar.go +++ b/pubsub/pulsar/pulsar.go @@ -496,11 +496,10 @@ func (p *Pulsar) formatTopic(topic string) string { } // GetComponentMetadata returns the metadata of the component. -func (p *Pulsar) GetComponentMetadata() map[string]string { +func (p *Pulsar) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := pulsarMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } // isValidPEM validates the provided input has PEM formatted block. diff --git a/pubsub/rabbitmq/rabbitmq.go b/pubsub/rabbitmq/rabbitmq.go index 249d5d8b70..e332b58ade 100644 --- a/pubsub/rabbitmq/rabbitmq.go +++ b/pubsub/rabbitmq/rabbitmq.go @@ -677,9 +677,8 @@ func mustReconnect(channel rabbitMQChannelBroker, err error) bool { } // GetComponentMetadata returns the metadata of the component. -func (r *rabbitMQ) GetComponentMetadata() map[string]string { +func (r *rabbitMQ) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := rabbitmqMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/redis/redis.go b/pubsub/redis/redis.go index bd501bd531..7beca3fe69 100644 --- a/pubsub/redis/redis.go +++ b/pubsub/redis/redis.go @@ -401,9 +401,8 @@ func (r *redisStreams) Ping(ctx context.Context) error { return nil } -func (r *redisStreams) GetComponentMetadata() map[string]string { +func (r *redisStreams) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := rediscomponent.Settings{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/rocketmq/rocketmq.go b/pubsub/rocketmq/rocketmq.go index 761680d928..83c54e74c6 100644 --- a/pubsub/rocketmq/rocketmq.go +++ b/pubsub/rocketmq/rocketmq.go @@ -554,9 +554,8 @@ func (r *rocketMQ) Close() error { } // GetComponentMetadata returns the metadata of the component. -func (r *rocketMQ) GetComponentMetadata() map[string]string { +func (r *rocketMQ) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := rocketMQMetaData{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.PubSubType) - return metadataInfo + return } diff --git a/pubsub/solace/amqp/amqp.go b/pubsub/solace/amqp/amqp.go index 8e56aaaa06..11a95e1977 100644 --- a/pubsub/solace/amqp/amqp.go +++ b/pubsub/solace/amqp/amqp.go @@ -327,9 +327,8 @@ func (a *amqpPubSub) Features() []pubsub.Feature { } // GetComponentMetadata returns the metadata of the component. -func (a *amqpPubSub) GetComponentMetadata() map[string]string { +func (a *amqpPubSub) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) { metadataStruct := metadata{} - metadataInfo := map[string]string{} contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.PubSubType) - return metadataInfo + return } diff --git a/secretstores/alicloud/parameterstore/parameterstore.go b/secretstores/alicloud/parameterstore/parameterstore.go index 66ce2f85d9..df42109129 100644 --- a/secretstores/alicloud/parameterstore/parameterstore.go +++ b/secretstores/alicloud/parameterstore/parameterstore.go @@ -197,9 +197,8 @@ func (o *oosSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{} // No Feature supported. } -func (o *oosSecretStore) GetComponentMetadata() map[string]string { +func (o *oosSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := ParameterStoreMetaData{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/secretstores/aws/parameterstore/parameterstore.go b/secretstores/aws/parameterstore/parameterstore.go index e66632aab5..fef4294f32 100644 --- a/secretstores/aws/parameterstore/parameterstore.go +++ b/secretstores/aws/parameterstore/parameterstore.go @@ -172,9 +172,8 @@ func (s *ssmSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{} // No Feature supported. } -func (s *ssmSecretStore) GetComponentMetadata() map[string]string { +func (s *ssmSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := ParameterStoreMetaData{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/secretstores/aws/secretmanager/secretmanager.go b/secretstores/aws/secretmanager/secretmanager.go index 1403b2535e..d30ce79e96 100644 --- a/secretstores/aws/secretmanager/secretmanager.go +++ b/secretstores/aws/secretmanager/secretmanager.go @@ -165,9 +165,8 @@ func (s *smSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{} // No Feature supported. } -func (s *smSecretStore) GetComponentMetadata() map[string]string { +func (s *smSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := SecretManagerMetaData{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/secretstores/azure/keyvault/keyvault.go b/secretstores/azure/keyvault/keyvault.go index 80ca7c077f..3dc97d1b62 100644 --- a/secretstores/azure/keyvault/keyvault.go +++ b/secretstores/azure/keyvault/keyvault.go @@ -206,9 +206,8 @@ func (k *keyvaultSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{} // No Feature supported. } -func (k *keyvaultSecretStore) GetComponentMetadata() map[string]string { +func (k *keyvaultSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := KeyvaultMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/secretstores/gcp/secretmanager/secretmanager.go b/secretstores/gcp/secretmanager/secretmanager.go index ef6dc5a2a6..01ea2133f1 100644 --- a/secretstores/gcp/secretmanager/secretmanager.go +++ b/secretstores/gcp/secretmanager/secretmanager.go @@ -203,9 +203,8 @@ func (s *Store) Features() []secretstores.Feature { return []secretstores.Feature{} // No Feature supported. } -func (s *Store) GetComponentMetadata() map[string]string { +func (s *Store) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := GcpSecretManagerMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/secretstores/hashicorp/vault/vault.go b/secretstores/hashicorp/vault/vault.go index f9daa7a6ec..8fabf2819d 100644 --- a/secretstores/hashicorp/vault/vault.go +++ b/secretstores/hashicorp/vault/vault.go @@ -531,9 +531,8 @@ func (v *vaultSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{secretstores.FeatureMultipleKeyValuesPerSecret} } -func (v *vaultSecretStore) GetComponentMetadata() map[string]string { +func (v *vaultSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := VaultMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/secretstores/huaweicloud/csms/csms.go b/secretstores/huaweicloud/csms/csms.go index c7d73e01e3..797b48cb14 100644 --- a/secretstores/huaweicloud/csms/csms.go +++ b/secretstores/huaweicloud/csms/csms.go @@ -157,9 +157,8 @@ func (c *csmsSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{} // No Feature supported. } -func (c *csmsSecretStore) GetComponentMetadata() map[string]string { +func (c *csmsSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := CsmsSecretStoreMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/secretstores/kubernetes/kubernetes.go b/secretstores/kubernetes/kubernetes.go index 030b7a980c..3147c1ba37 100644 --- a/secretstores/kubernetes/kubernetes.go +++ b/secretstores/kubernetes/kubernetes.go @@ -18,7 +18,6 @@ import ( "context" "errors" "os" - "reflect" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -117,10 +116,7 @@ func (k *kubernetesSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{} } -func (k *kubernetesSecretStore) GetComponentMetadata() map[string]string { - type unusedMetadataStruct struct{} - metadataStruct := unusedMetadataStruct{} - metadataInfo := map[string]string{} - metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo +func (k *kubernetesSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + // No component metadata + return } diff --git a/secretstores/local/env/envstore.go b/secretstores/local/env/envstore.go index 4395f227fe..11dbf2b005 100644 --- a/secretstores/local/env/envstore.go +++ b/secretstores/local/env/envstore.go @@ -114,11 +114,10 @@ func (s *envSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{} // No Feature supported. } -func (s *envSecretStore) GetComponentMetadata() map[string]string { +func (s *envSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := Metadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } func (s *envSecretStore) isKeyAllowed(key string) bool { diff --git a/secretstores/local/file/filestore.go b/secretstores/local/file/filestore.go index 4373b075ae..a9b065a4b4 100644 --- a/secretstores/local/file/filestore.go +++ b/secretstores/local/file/filestore.go @@ -277,9 +277,8 @@ func (j *localSecretStore) Features() []secretstores.Feature { return j.features } -func (j *localSecretStore) GetComponentMetadata() map[string]string { +func (j *localSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := localSecretStoreMetaData{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/secretstores/secret_store.go b/secretstores/secret_store.go index 4257222b57..2241ba7580 100644 --- a/secretstores/secret_store.go +++ b/secretstores/secret_store.go @@ -18,10 +18,13 @@ import ( "fmt" "github.com/dapr/components-contrib/health" + "github.com/dapr/components-contrib/metadata" ) // SecretStore is the interface for a component that handles secrets management. type SecretStore interface { + metadata.ComponentWithMetadata + // Init authenticates with the actual secret store and performs other init operation Init(ctx context.Context, metadata Metadata) error // GetSecret retrieves a secret using a key and returns a map of decrypted string/string values. @@ -30,8 +33,6 @@ type SecretStore interface { BulkGetSecret(ctx context.Context, req BulkGetSecretRequest) (BulkGetSecretResponse, error) // Features lists the features supported by the secret store. Features() []Feature - // GetComponentMetadata returns the metadata options for the secret store. - GetComponentMetadata() map[string]string } func Ping(ctx context.Context, secretStore SecretStore) error { diff --git a/secretstores/tencentcloud/ssm/ssm.go b/secretstores/tencentcloud/ssm/ssm.go index 9f7834c3af..d40887661a 100644 --- a/secretstores/tencentcloud/ssm/ssm.go +++ b/secretstores/tencentcloud/ssm/ssm.go @@ -193,9 +193,8 @@ func (s *ssmSecretStore) Features() []secretstores.Feature { return []secretstores.Feature{} // No Feature supported. } -func (s *ssmSecretStore) GetComponentMetadata() map[string]string { +func (s *ssmSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := SsmMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) - return metadataInfo + return } diff --git a/state/aerospike/aerospike.go b/state/aerospike/aerospike.go index 9568a76296..0a27094376 100644 --- a/state/aerospike/aerospike.go +++ b/state/aerospike/aerospike.go @@ -271,9 +271,8 @@ func convertETag(eTag string) (uint32, error) { return uint32(i), nil } -func (aspike *Aerospike) GetComponentMetadata() map[string]string { +func (aspike *Aerospike) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := aerospikeMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/alicloud/tablestore/tablestore.go b/state/alicloud/tablestore/tablestore.go index 1ea6cf3cf9..cf31709c79 100644 --- a/state/alicloud/tablestore/tablestore.go +++ b/state/alicloud/tablestore/tablestore.go @@ -228,9 +228,8 @@ func (s *AliCloudTableStore) primaryKey(key string) *tablestore.PrimaryKey { return pk } -func (s *AliCloudTableStore) GetComponentMetadata() map[string]string { +func (s *AliCloudTableStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := tablestoreMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/aws/dynamodb/dynamodb.go b/state/aws/dynamodb/dynamodb.go index 094a801f00..ba7c3ab96b 100644 --- a/state/aws/dynamodb/dynamodb.go +++ b/state/aws/dynamodb/dynamodb.go @@ -223,11 +223,10 @@ func (d *StateStore) Delete(ctx context.Context, req *state.DeleteRequest) error return err } -func (d *StateStore) GetComponentMetadata() map[string]string { +func (d *StateStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := dynamoDBMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } func (d *StateStore) getDynamoDBMetadata(meta state.Metadata) (*dynamoDBMetadata, error) { diff --git a/state/azure/blobstorage/blobstorage.go b/state/azure/blobstorage/blobstorage.go index 65644c5c51..803d50bb50 100644 --- a/state/azure/blobstorage/blobstorage.go +++ b/state/azure/blobstorage/blobstorage.go @@ -106,11 +106,10 @@ func (r *StateStore) Ping(ctx context.Context) error { return nil } -func (r *StateStore) GetComponentMetadata() map[string]string { +func (r *StateStore) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { metadataStruct := storageinternal.BlobStorageMetadata{} - metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.StateStoreType) - return metadataInfo + return } // NewAzureBlobStorageStore instance. diff --git a/state/azure/cosmosdb/cosmosdb.go b/state/azure/cosmosdb/cosmosdb.go index c809634747..622c091b06 100644 --- a/state/azure/cosmosdb/cosmosdb.go +++ b/state/azure/cosmosdb/cosmosdb.go @@ -106,11 +106,10 @@ func NewCosmosDBStateStore(logger logger.Logger) state.Store { return s } -func (c *StateStore) GetComponentMetadata() map[string]string { +func (c *StateStore) GetComponentMetadata() (metadataInfo contribmeta.MetadataMap) { metadataStruct := metadata{} - metadataInfo := map[string]string{} contribmeta.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribmeta.StateStoreType) - return metadataInfo + return } // Init does metadata and connection parsing. diff --git a/state/azure/tablestorage/tablestorage.go b/state/azure/tablestorage/tablestorage.go index 8c89c66766..41a5e558c2 100644 --- a/state/azure/tablestorage/tablestorage.go +++ b/state/azure/tablestorage/tablestorage.go @@ -204,11 +204,10 @@ func (r *StateStore) Set(ctx context.Context, req *state.SetRequest) error { return r.writeRow(ctx, req) } -func (r *StateStore) GetComponentMetadata() map[string]string { +func (r *StateStore) GetComponentMetadata() (metadataInfo mdutils.MetadataMap) { metadataStruct := tablesMetadata{} - metadataInfo := map[string]string{} mdutils.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, mdutils.StateStoreType) - return metadataInfo + return } func NewAzureTablesStateStore(logger logger.Logger) state.Store { diff --git a/state/bulk_test.go b/state/bulk_test.go index d9e098d7ce..9c179fe985 100644 --- a/state/bulk_test.go +++ b/state/bulk_test.go @@ -22,6 +22,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/metadata" ) var errSimulated = errors.New("simulated") @@ -162,8 +164,8 @@ func (s *storeBulk) Set(ctx context.Context, req *SetRequest) error { return nil } -func (s *storeBulk) GetComponentMetadata() map[string]string { - return map[string]string{} +func (s *storeBulk) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + return } func (s *storeBulk) Features() []Feature { diff --git a/state/cassandra/cassandra.go b/state/cassandra/cassandra.go index 9a74a962a0..3ac0cda60a 100644 --- a/state/cassandra/cassandra.go +++ b/state/cassandra/cassandra.go @@ -320,11 +320,10 @@ func (c *Cassandra) createSession(consistency gocql.Consistency) (*gocql.Session return session, nil } -func (c *Cassandra) GetComponentMetadata() map[string]string { +func (c *Cassandra) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := cassandraMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } // Close the connection to Cassandra. diff --git a/state/cloudflare/workerskv/workerskv.go b/state/cloudflare/workerskv/workerskv.go index 3e54e785c8..c1a8609f6b 100644 --- a/state/cloudflare/workerskv/workerskv.go +++ b/state/cloudflare/workerskv/workerskv.go @@ -82,11 +82,10 @@ func (q *CFWorkersKV) Init(_ context.Context, metadata state.Metadata) error { return q.Base.Init(workerBindings, componentDocsURL, infoResponseValidate) } -func (q *CFWorkersKV) GetComponentMetadata() map[string]string { +func (q *CFWorkersKV) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := componentMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } // Features returns the features supported by this state store. diff --git a/state/couchbase/couchbase.go b/state/couchbase/couchbase.go index 0ea4293dc9..3da43f1567 100644 --- a/state/couchbase/couchbase.go +++ b/state/couchbase/couchbase.go @@ -266,9 +266,8 @@ func eTagToCas(eTag string) (gocb.Cas, error) { return cas, nil } -func (cbs *Couchbase) GetComponentMetadata() map[string]string { +func (cbs *Couchbase) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := couchbaseMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/etcd/etcd.go b/state/etcd/etcd.go index 30b5c93df1..4bac16a911 100644 --- a/state/etcd/etcd.go +++ b/state/etcd/etcd.go @@ -303,11 +303,10 @@ func (e *Etcd) doValidateEtag(key string, etag *string, concurrency string) erro return nil } -func (e *Etcd) GetComponentMetadata() map[string]string { +func (e *Etcd) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := etcdConfig{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } func (e *Etcd) Close() error { diff --git a/state/gcp/firestore/firestore.go b/state/gcp/firestore/firestore.go index 8697651a8c..efda8e7d9c 100644 --- a/state/gcp/firestore/firestore.go +++ b/state/gcp/firestore/firestore.go @@ -168,11 +168,10 @@ func (f *Firestore) Delete(ctx context.Context, req *state.DeleteRequest) error return nil } -func (f *Firestore) GetComponentMetadata() map[string]string { +func (f *Firestore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := firestoreMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } func getFirestoreMetadata(meta state.Metadata) (*firestoreMetadata, error) { diff --git a/state/hashicorp/consul/consul.go b/state/hashicorp/consul/consul.go index eb79055204..6c5a2c5a92 100644 --- a/state/hashicorp/consul/consul.go +++ b/state/hashicorp/consul/consul.go @@ -157,9 +157,8 @@ func (c *Consul) Delete(ctx context.Context, req *state.DeleteRequest) error { return nil } -func (c *Consul) GetComponentMetadata() map[string]string { +func (c *Consul) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := consulConfig{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/hazelcast/hazelcast.go b/state/hazelcast/hazelcast.go index 6fae9efb62..0a41f04848 100644 --- a/state/hazelcast/hazelcast.go +++ b/state/hazelcast/hazelcast.go @@ -159,9 +159,8 @@ func (store *Hazelcast) Delete(ctx context.Context, req *state.DeleteRequest) er return nil } -func (store *Hazelcast) GetComponentMetadata() map[string]string { +func (store *Hazelcast) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := hazelcastMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/in-memory/in_memory.go b/state/in-memory/in_memory.go index 1d3d0e4753..a05f2bd94e 100644 --- a/state/in-memory/in_memory.go +++ b/state/in-memory/in_memory.go @@ -26,6 +26,7 @@ import ( "github.com/google/uuid" "k8s.io/utils/clock" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/state" "github.com/dapr/components-contrib/state/utils" "github.com/dapr/kit/logger" @@ -410,9 +411,9 @@ func (store *inMemoryStore) doCleanExpiredItems() { } } -func (store *inMemoryStore) GetComponentMetadata() map[string]string { +func (store *inMemoryStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { // no metadata, hence no metadata struct to convert here - return map[string]string{} + return } type inMemStateStoreItem struct { diff --git a/state/jetstream/jetstream.go b/state/jetstream/jetstream.go index c493d82d1a..fedb1e2f68 100644 --- a/state/jetstream/jetstream.go +++ b/state/jetstream/jetstream.go @@ -170,9 +170,8 @@ func escape(key string) string { return strings.ReplaceAll(key, "||", ".") } -func (js *StateStore) GetComponentMetadata() map[string]string { +func (js *StateStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := jetstreamMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/memcached/memcached.go b/state/memcached/memcached.go index be2f427bb3..d4171e13ff 100644 --- a/state/memcached/memcached.go +++ b/state/memcached/memcached.go @@ -217,9 +217,8 @@ func (m *Memcached) Close() (err error) { return nil } -func (m *Memcached) GetComponentMetadata() map[string]string { +func (m *Memcached) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := memcachedMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/mongodb/mongodb.go b/state/mongodb/mongodb.go index 26a8847f6d..c8dac85c20 100644 --- a/state/mongodb/mongodb.go +++ b/state/mongodb/mongodb.go @@ -675,11 +675,10 @@ func getReadConcernObject(cn string) (*readconcern.ReadConcern, error) { return nil, fmt.Errorf("readConcern %s not found", cn) } -func (m *MongoDB) GetComponentMetadata() map[string]string { +func (m *MongoDB) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := mongoDBMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } // Close connection to the database. diff --git a/state/mysql/mysql.go b/state/mysql/mysql.go index 4c94a41ba8..6e5f6414b9 100644 --- a/state/mysql/mysql.go +++ b/state/mysql/mysql.go @@ -893,9 +893,8 @@ type querier interface { QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } -func (m *MySQL) GetComponentMetadata() map[string]string { +func (m *MySQL) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := mySQLMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/oci/objectstorage/objectstorage.go b/state/oci/objectstorage/objectstorage.go index 2612d086df..7ed5748eca 100644 --- a/state/oci/objectstorage/objectstorage.go +++ b/state/oci/objectstorage/objectstorage.go @@ -517,9 +517,8 @@ func (c *ociObjectStorageClient) pingBucket(ctx context.Context) error { return nil } -func (r *StateStore) GetComponentMetadata() map[string]string { +func (r *StateStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := objectStoreMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/oracledatabase/oracledatabase.go b/state/oracledatabase/oracledatabase.go index 570f36453e..32e883287b 100644 --- a/state/oracledatabase/oracledatabase.go +++ b/state/oracledatabase/oracledatabase.go @@ -98,9 +98,8 @@ func (o *OracleDatabase) getDB() *sql.DB { return o.dbaccess.(*oracleDatabaseAccess).db } -func (o *OracleDatabase) GetComponentMetadata() map[string]string { +func (o *OracleDatabase) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := oracleDatabaseMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/redis/redis.go b/state/redis/redis.go index 2435a26da8..d5e1c33ee4 100644 --- a/state/redis/redis.go +++ b/state/redis/redis.go @@ -547,10 +547,8 @@ func (r *StateStore) Close() error { return r.client.Close() } -func (r *StateStore) GetComponentMetadata() map[string]string { +func (r *StateStore) GetComponentMetadata() (metadataInfo daprmetadata.MetadataMap) { settingsStruct := rediscomponent.Settings{} - metadataInfo := map[string]string{} daprmetadata.GetMetadataInfoFromStructType(reflect.TypeOf(settingsStruct), &metadataInfo, daprmetadata.StateStoreType) - - return metadataInfo + return } diff --git a/state/redis/redis_test.go b/state/redis/redis_test.go index fe2fed70e9..599de3ae41 100644 --- a/state/redis/redis_test.go +++ b/state/redis/redis_test.go @@ -507,8 +507,6 @@ func TestGetMetadata(t *testing.T) { metadataInfo := ss.GetComponentMetadata() assert.Contains(t, metadataInfo, "redisHost") assert.Contains(t, metadataInfo, "idleCheckFrequency") - assert.Equal(t, metadataInfo["redisHost"], "string") - assert.Equal(t, metadataInfo["idleCheckFrequency"], "redis.Duration") } func setupMiniredis() (*miniredis.Miniredis, rediscomponent.RedisClient) { diff --git a/state/rethinkdb/rethinkdb.go b/state/rethinkdb/rethinkdb.go index f506920a5d..b725ea17bc 100644 --- a/state/rethinkdb/rethinkdb.go +++ b/state/rethinkdb/rethinkdb.go @@ -306,9 +306,8 @@ func metadataToConfig(cfg map[string]string, logger logger.Logger) (*stateConfig return &c, nil } -func (s *RethinkDB) GetComponentMetadata() map[string]string { +func (s *RethinkDB) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := stateConfig{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/state/sqlite/sqlite.go b/state/sqlite/sqlite.go index fcb5f28635..218c16508f 100644 --- a/state/sqlite/sqlite.go +++ b/state/sqlite/sqlite.go @@ -58,11 +58,10 @@ func (s *SQLiteStore) Init(ctx context.Context, metadata state.Metadata) error { return s.dbaccess.Init(ctx, metadata) } -func (s SQLiteStore) GetComponentMetadata() map[string]string { +func (s SQLiteStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := sqliteMetadataStruct{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } // Features returns the features available in this state store. diff --git a/state/sqlserver/sqlserver.go b/state/sqlserver/sqlserver.go index 89806f3ee9..c87cafa491 100644 --- a/state/sqlserver/sqlserver.go +++ b/state/sqlserver/sqlserver.go @@ -20,9 +20,11 @@ import ( "encoding/json" "errors" "fmt" + "reflect" "time" internalsql "github.com/dapr/components-contrib/internal/component/sql" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/state" "github.com/dapr/components-contrib/state/utils" "github.com/dapr/kit/logger" @@ -347,8 +349,10 @@ func (s *SQLServer) executeSet(ctx context.Context, db dbExecutor, req *state.Se return nil } -func (s *SQLServer) GetComponentMetadata() map[string]string { - return map[string]string{} +func (s *SQLServer) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + settingsStruct := sqlServerMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(settingsStruct), &metadataInfo, metadata.StateStoreType) + return } // Close implements io.Closer. diff --git a/state/store.go b/state/store.go index 9d23b50750..fa86851bd7 100644 --- a/state/store.go +++ b/state/store.go @@ -18,10 +18,13 @@ import ( "errors" "github.com/dapr/components-contrib/health" + "github.com/dapr/components-contrib/metadata" ) // Store is an interface to perform operations on store. type Store interface { + metadata.ComponentWithMetadata + BaseStore BulkStore } @@ -33,7 +36,6 @@ type BaseStore interface { Delete(ctx context.Context, req *DeleteRequest) error Get(ctx context.Context, req *GetRequest) (*GetResponse, error) Set(ctx context.Context, req *SetRequest) error - GetComponentMetadata() map[string]string } // TransactionalStore is an interface for initialization and support multiple transactional requests. diff --git a/state/zookeeper/zk.go b/state/zookeeper/zk.go index b4c92e2871..fe1985b941 100644 --- a/state/zookeeper/zk.go +++ b/state/zookeeper/zk.go @@ -303,9 +303,8 @@ func (s *StateStore) marshalData(v interface{}) ([]byte, error) { return jsoniter.ConfigFastest.Marshal(v) } -func (s *StateStore) GetComponentMetadata() map[string]string { +func (s *StateStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := properties{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) - return metadataInfo + return } diff --git a/workflows/temporal/temporal.go b/workflows/temporal/temporal.go index 0b528c04c0..9b28bd555c 100644 --- a/workflows/temporal/temporal.go +++ b/workflows/temporal/temporal.go @@ -172,11 +172,10 @@ func (c *TemporalWF) parseMetadata(meta workflows.Metadata) (*temporalMetadata, return &m, err } -func (c *TemporalWF) GetComponentMetadata() map[string]string { +func (c *TemporalWF) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { metadataStruct := temporalMetadata{} - metadataInfo := map[string]string{} metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.WorkflowType) - return metadataInfo + return } func lookupStatus(status enums.WorkflowExecutionStatus) string { diff --git a/workflows/workflow.go b/workflows/workflow.go index 1dee2ba398..93a4591d79 100644 --- a/workflows/workflow.go +++ b/workflows/workflow.go @@ -30,5 +30,4 @@ type Workflow interface { Purge(ctx context.Context, req *PurgeRequest) error Pause(ctx context.Context, req *PauseRequest) error Resume(ctx context.Context, req *ResumeRequest) error - GetComponentMetadata() map[string]string }