Merge branch 'master' into redis-pubsub-fix

This commit is contained in:
Yaron Schneider 2022-08-29 07:00:16 -07:00 committed by GitHub
commit edd2c9d5a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
398 changed files with 20008 additions and 4222 deletions

View File

@ -11,5 +11,5 @@ coverage:
# See https://docs.codecov.io/docs/commit-status#disabling-a-status. # See https://docs.codecov.io/docs/commit-status#disabling-a-status.
default: false default: false
comment: comment:
# Delete old comment and post new one for new coverage information. # Update old comment with new coverage information if the PR is changed. Avoids triggering multiple emails.
behavior: new behavior: once

View File

@ -216,6 +216,7 @@ KEYVAULT_NAME_VAR_NAME="AzureKeyVaultName"
RESOURCE_GROUP_NAME_VAR_NAME="AzureResourceGroupName" RESOURCE_GROUP_NAME_VAR_NAME="AzureResourceGroupName"
SERVICE_BUS_CONNECTION_STRING_VAR_NAME="AzureServiceBusConnectionString" SERVICE_BUS_CONNECTION_STRING_VAR_NAME="AzureServiceBusConnectionString"
SERVICE_BUS_NAMESPACE_VAR_NAME="AzureServiceBusNamespace"
SQL_SERVER_NAME_VAR_NAME="AzureSqlServerName" SQL_SERVER_NAME_VAR_NAME="AzureSqlServerName"
SQL_SERVER_DB_NAME_VAR_NAME="AzureSqlServerDbName" SQL_SERVER_DB_NAME_VAR_NAME="AzureSqlServerDbName"
@ -613,6 +614,9 @@ echo "Configuring Service Bus test settings ..."
SERVICE_BUS_CONNECTION_STRING="$(az servicebus namespace authorization-rule keys list --name RootManageSharedAccessKey --namespace-name "${SERVICE_BUS_NAME}" --resource-group "${RESOURCE_GROUP_NAME}" --query "primaryConnectionString" --output tsv)" SERVICE_BUS_CONNECTION_STRING="$(az servicebus namespace authorization-rule keys list --name RootManageSharedAccessKey --namespace-name "${SERVICE_BUS_NAME}" --resource-group "${RESOURCE_GROUP_NAME}" --query "primaryConnectionString" --output tsv)"
echo export ${SERVICE_BUS_CONNECTION_STRING_VAR_NAME}=\"${SERVICE_BUS_CONNECTION_STRING}\" >> "${ENV_CONFIG_FILENAME}" echo export ${SERVICE_BUS_CONNECTION_STRING_VAR_NAME}=\"${SERVICE_BUS_CONNECTION_STRING}\" >> "${ENV_CONFIG_FILENAME}"
az keyvault secret set --name "${SERVICE_BUS_CONNECTION_STRING_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${SERVICE_BUS_CONNECTION_STRING}" az keyvault secret set --name "${SERVICE_BUS_CONNECTION_STRING_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${SERVICE_BUS_CONNECTION_STRING}"
SERVICE_BUS_NAMESPACE="${SERVICE_BUS_NAME}.servicebus.windows.net"
echo export ${SERVICE_BUS_NAMESPACE_VAR_NAME}=\"${SERVICE_BUS_NAMESPACE}\" >> "${ENV_CONFIG_FILENAME}"
az keyvault secret set --name "${SERVICE_BUS_NAMESPACE_VAR_NAME}" --vault-name "${KEYVAULT_NAME}" --value "${SERVICE_BUS_NAMESPACE}"
# ---------------------------------- # ----------------------------------
# Populate SQL Server test settings # Populate SQL Server test settings
@ -726,6 +730,9 @@ az role assignment create --assignee "${CERTIFICATION_SPAUTH_SP_PRINCIPAL_ID}" -
# IOT hub used in eventhubs certification test # IOT hub used in eventhubs certification test
az role assignment create --assignee "${CERTIFICATION_SPAUTH_SP_PRINCIPAL_ID}" --role "Owner" --scope "/subscriptions/${SUB_ID}/resourceGroups/${RESOURCE_GROUP_NAME}/providers/Microsoft.Devices/IotHubs/${IOT_HUB_NAME}" az role assignment create --assignee "${CERTIFICATION_SPAUTH_SP_PRINCIPAL_ID}" --role "Owner" --scope "/subscriptions/${SUB_ID}/resourceGroups/${RESOURCE_GROUP_NAME}/providers/Microsoft.Devices/IotHubs/${IOT_HUB_NAME}"
az role assignment create --assignee "${CERTIFICATION_SPAUTH_SP_PRINCIPAL_ID}" --role "IoT Hub Data Contributor" --scope "/subscriptions/${SUB_ID}/resourceGroups/${RESOURCE_GROUP_NAME}/providers/Microsoft.Devices/IotHubs/${IOT_HUB_NAME}" az role assignment create --assignee "${CERTIFICATION_SPAUTH_SP_PRINCIPAL_ID}" --role "IoT Hub Data Contributor" --scope "/subscriptions/${SUB_ID}/resourceGroups/${RESOURCE_GROUP_NAME}/providers/Microsoft.Devices/IotHubs/${IOT_HUB_NAME}"
# Azure Service Bus
ASB_ID=$(az servicebus namespace show --resource-group "${RESOURCE_GROUP_NAME}" --name "${SERVICE_BUS_NAME}" --query "id" -otsv)
az role assignment create --assignee "${CERTIFICATION_SPAUTH_SP_PRINCIPAL_ID}" --role "Azure Service Bus Data Owner" --scope "${ASB_ID}"
# Now export the service principal information # Now export the service principal information
CERTIFICATION_TENANT_ID="$(az ad sp list --display-name "${CERTIFICATION_SPAUTH_SP_NAME}" --query "[].appOwnerTenantId" --output tsv)" CERTIFICATION_TENANT_ID="$(az ad sp list --display-name "${CERTIFICATION_SPAUTH_SP_NAME}" --query "[].appOwnerTenantId" --output tsv)"

View File

@ -0,0 +1,7 @@
version: '2'
services:
memcached:
image: docker.io/memcached:1.6
ports:
- '11211:11211'

View File

@ -0,0 +1,9 @@
version: '2'
services:
rethinkdb:
image: rethinkdb:2.4
ports:
- 8081:8080
- 28015:28015
- 29015:29015

View File

@ -25,24 +25,11 @@ on:
- release-* - release-*
jobs: jobs:
pre_job:
name: Skip Duplicate Actions
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v3.4.0
with:
cancel_others: 'true'
paths_ignore: '["**.md", ".codecov.yaml", ".github/workflows/dapr-automerge.yml"]'
# Based on whether this is a PR or a scheduled run, we will run a different # Based on whether this is a PR or a scheduled run, we will run a different
# subset of the certification tests. This allows all the tests not requiring # subset of the certification tests. This allows all the tests not requiring
# secrets to be executed on pull requests. # secrets to be executed on pull requests.
generate-matrix: generate-matrix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'repository_dispatch'
steps: steps:
- name: Install yq - name: Install yq
run: | run: |
@ -59,6 +46,9 @@ jobs:
- state.postgresql - state.postgresql
- bindings.alicloud.dubbo - bindings.alicloud.dubbo
- bindings.kafka - bindings.kafka
- secretstores.local.env
- secretstores.local.file
- bindings.rabbitmq
EOF EOF
) )
echo "::set-output name=pr-components::$PR_COMPONENTS" echo "::set-output name=pr-components::$PR_COMPONENTS"
@ -101,12 +91,16 @@ jobs:
required-secrets: AzureEventHubsBindingsConnectionString,AzureBlobStorageAccount,AzureBlobStorageAccessKey,AzureEventHubsBindingsHub,AzureEventHubsBindingsNamespace,AzureEventHubsBindingsConsumerGroup,AzureCertificationServicePrincipalClientId,AzureCertificationTenantId,AzureCertificationServicePrincipalClientSecret,AzureResourceGroupName,AzureCertificationSubscriptionId,AzureEventHubsBindingsContainer,AzureIotHubEventHubConnectionString,AzureIotHubName,AzureIotHubBindingsConsumerGroup required-secrets: AzureEventHubsBindingsConnectionString,AzureBlobStorageAccount,AzureBlobStorageAccessKey,AzureEventHubsBindingsHub,AzureEventHubsBindingsNamespace,AzureEventHubsBindingsConsumerGroup,AzureCertificationServicePrincipalClientId,AzureCertificationTenantId,AzureCertificationServicePrincipalClientSecret,AzureResourceGroupName,AzureCertificationSubscriptionId,AzureEventHubsBindingsContainer,AzureIotHubEventHubConnectionString,AzureIotHubName,AzureIotHubBindingsConsumerGroup
- component: pubsub.azure.eventhubs - component: pubsub.azure.eventhubs
required-secrets: AzureEventHubsPubsubTopicActiveConnectionString,AzureEventHubsPubsubNamespace,AzureEventHubsPubsubNamespaceConnectionString,AzureBlobStorageAccount,AzureBlobStorageAccessKey,AzureEventHubsPubsubContainer,AzureIotHubName,AzureIotHubEventHubConnectionString,AzureCertificationTenantId,AzureCertificationServicePrincipalClientId,AzureCertificationServicePrincipalClientSecret,AzureResourceGroupName,AzureCertificationSubscriptionId required-secrets: AzureEventHubsPubsubTopicActiveConnectionString,AzureEventHubsPubsubNamespace,AzureEventHubsPubsubNamespaceConnectionString,AzureBlobStorageAccount,AzureBlobStorageAccessKey,AzureEventHubsPubsubContainer,AzureIotHubName,AzureIotHubEventHubConnectionString,AzureCertificationTenantId,AzureCertificationServicePrincipalClientId,AzureCertificationServicePrincipalClientSecret,AzureResourceGroupName,AzureCertificationSubscriptionId
- component: pubsub.azure.servicebus
required-secrets: AzureServiceBusConnectionString,AzureServiceBusNamespace, AzureCertificationTenantId,AzureCertificationServicePrincipalClientId,AzureCertificationServicePrincipalClientSecret
- component: bindings.azure.blobstorage - component: bindings.azure.blobstorage
required-secrets: AzureBlobStorageAccount,AzureBlobStorageAccessKey,AzureBlobStorageContainer,AzureCertificationTenantId,AzureCertificationServicePrincipalClientId,AzureCertificationServicePrincipalClientSecret required-secrets: AzureBlobStorageAccount,AzureBlobStorageAccessKey,AzureBlobStorageContainer,AzureCertificationTenantId,AzureCertificationServicePrincipalClientId,AzureCertificationServicePrincipalClientSecret
- component: bindings.azure.storagequeues - component: bindings.azure.storagequeues
required-secrets: AzureBlobStorageAccount, AzureBlobStorageAccessKey required-secrets: AzureBlobStorageAccount, AzureBlobStorageAccessKey
- component: state.azure.tablestorage - component: state.azure.tablestorage
required-secrets: AzureBlobStorageAccount, AzureBlobStorageAccessKey, AzureCertificationTenantId, AzureCertificationServicePrincipalClientId, AzureCertificationServicePrincipalClientSecret required-secrets: AzureBlobStorageAccount, AzureBlobStorageAccessKey, AzureCertificationTenantId, AzureCertificationServicePrincipalClientId, AzureCertificationServicePrincipalClientSecret
- component: state.azure.blobstorage
required-secrets: AzureBlobStorageContainer,AzureBlobStorageAccount, AzureBlobStorageAccessKey, AzureCertificationTenantId, AzureCertificationServicePrincipalClientId, AzureCertificationServicePrincipalClientSecret
EOF EOF
) )
echo "::set-output name=cloud-components::$CRON_COMPONENTS" echo "::set-output name=cloud-components::$CRON_COMPONENTS"
@ -156,11 +150,13 @@ jobs:
export TEST_OUTPUT_FILE_PREFIX=$GITHUB_WORKSPACE/test_report export TEST_OUTPUT_FILE_PREFIX=$GITHUB_WORKSPACE/test_report
echo "TEST_OUTPUT_FILE_PREFIX=$TEST_OUTPUT_FILE_PREFIX" >> $GITHUB_ENV echo "TEST_OUTPUT_FILE_PREFIX=$TEST_OUTPUT_FILE_PREFIX" >> $GITHUB_ENV
- name: Configure certification test path - name: Configure certification test and source path
run: | run: |
TEST_COMPONENT=$(echo ${{ matrix.component }} | sed -E 's/\./\//g') TEST_COMPONENT=$(echo ${{ matrix.component }} | sed -E 's/\./\//g')
export TEST_PATH="${PROJECT_PATH}/tests/certification/${TEST_COMPONENT}" export TEST_PATH="${PROJECT_PATH}/tests/certification/${TEST_COMPONENT}"
echo "TEST_PATH=$TEST_PATH" >> $GITHUB_ENV echo "TEST_PATH=$TEST_PATH" >> $GITHUB_ENV
export SOURCE_PATH="github.com/dapr/components-contrib/${TEST_COMPONENT}"
echo "SOURCE_PATH=$SOURCE_PATH" >> $GITHUB_ENV
- uses: Azure/login@v1 - uses: Azure/login@v1
with: with:
@ -194,18 +190,19 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: '^1.18' go-version: '^1.19'
- name: Download Go dependencies - name: Download Go dependencies
working-directory: ${{ env.TEST_PATH }} working-directory: ${{ env.TEST_PATH }}
run: | run: |
go mod download go mod download
go install gotest.tools/gotestsum@latest go install gotest.tools/gotestsum@latest
go install github.com/axw/gocov/gocov@v1.1.0
- name: Check that go mod tidy is up-to-date - name: Check that go mod tidy is up-to-date
working-directory: ${{ env.TEST_PATH }} working-directory: ${{ env.TEST_PATH }}
run: | run: |
go mod tidy -compat=1.18 go mod tidy -compat=1.19
git diff --exit-code ./go.mod git diff --exit-code ./go.mod
git diff --exit-code ./go.sum git diff --exit-code ./go.sum
@ -218,8 +215,7 @@ jobs:
set +e set +e
gotestsum --jsonfile ${{ env.TEST_OUTPUT_FILE_PREFIX }}_certification.json \ gotestsum --jsonfile ${{ env.TEST_OUTPUT_FILE_PREFIX }}_certification.json \
--junitfile ${{ env.TEST_OUTPUT_FILE_PREFIX }}_certification.xml --format standard-quiet -- \ --junitfile ${{ env.TEST_OUTPUT_FILE_PREFIX }}_certification.xml --format standard-quiet -- \
-count=1 -timeout=15m -coverprofile=cover.out -covermode=set -coverpkg=${{ env.SOURCE_PATH }}
status=$? status=$?
echo "Completed certification tests for ${{ matrix.component }} ... " echo "Completed certification tests for ${{ matrix.component }} ... "
if test $status -ne 0; then if test $status -ne 0; then
@ -228,6 +224,12 @@ jobs:
fi fi
set -e set -e
COVERAGE_REPORT=$(gocov convert cover.out | gocov report)
COVERAGE_LINE=$(echo $COVERAGE_REPORT | grep -oP '(?<=Total Coverage:).*') # example: "80.00% (40/50)"
COVERAGE_PERCENTAGE=$(echo $COVERAGE_LINE | grep -oP '([0-9\.]*)' | head -n 1) # example "80.00"
echo "COVERAGE_LINE=$COVERAGE_LINE" >> $GITHUB_ENV
echo "COMPONENT_PERCENTAGE=$COVERAGE_PERCENTAGE" >> $GITHUB_ENV
# Fail the step if we found no test to run # Fail the step if we found no test to run
if grep -q "\[no test files\]" ${{ env.TEST_OUTPUT_FILE_PREFIX }}_certification.json ; then if grep -q "\[no test files\]" ${{ env.TEST_OUTPUT_FILE_PREFIX }}_certification.json ; then
echo "::error:: No certification test file was found for component ${{ matrix.component }}" echo "::error:: No certification test file was found for component ${{ matrix.component }}"
@ -245,6 +247,27 @@ jobs:
exit 1 exit 1
fi fi
- name: Prepare Cert Coverage Info
run: |
mkdir -p tmp/cov_files
SOURCE_PATH_LINEAR=$(echo ${{ env.SOURCE_PATH }} |sed 's#/#\.#g') # converts slashes to dots in this string, so that it doesn't consider them sub-folders
echo "${{ env.COVERAGE_LINE }}" >> tmp/cov_files/$SOURCE_PATH_LINEAR.txt
- name: Upload Cert Coverage Artifact
uses: actions/upload-artifact@v3
with:
name: certtest_cov
path: tmp/cov_files
retention-days: 1
- name: Component Coverage Discord Notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_WEBHOOK_URL }}
uses: Ilshidur/action-discord@0c4b27844ba47cb1c7bee539c8eead5284ce9fa9
continue-on-error: true
with:
args: 'Cert Test Coverage for {{ SOURCE_PATH }} is {{ COVERAGE_LINE }}'
# Upload logs for test analytics to consume # Upload logs for test analytics to consume
- name: Upload test results - name: Upload test results
if: always() if: always()
@ -252,3 +275,41 @@ jobs:
with: with:
name: ${{ matrix.component }}_certification_test name: ${{ matrix.component }}_certification_test
path: ${{ env.TEST_OUTPUT_FILE_PREFIX }}_certification.* path: ${{ env.TEST_OUTPUT_FILE_PREFIX }}_certification.*
post_job:
name: Notify Total coverage
runs-on: ubuntu-latest
needs: certification
if: always()
steps:
- name: Download Cert Coverage Artifact
uses: actions/download-artifact@v3
continue-on-error: true
id: download
with:
name: certtest_cov
path: tmp/cov_files
- name: Calculate total coverage
run: |
ls "${{steps.download.outputs.download-path}}" | while read f; do
while read LINE;
do
ratio=$(echo $LINE | cut -d "(" -f2 | cut -d ")" -f1)
tempNumerator=$(echo $ratio | cut -d'/' -f1)
tempDenominator=$(echo $ratio | cut -d'/' -f2)
export numerator=$(($numerator+$tempNumerator))
export denominator=$(($denominator+$tempDenominator))
totalPer=$(awk "BEGIN { print (($numerator / $denominator) * 100) }")
echo "totalPer=$totalPer" >> $GITHUB_ENV
done < ${{steps.download.outputs.download-path}}/$f
done
continue-on-error: true
- name: Final Coverage Discord Notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_WEBHOOK_URL }}
uses: Ilshidur/action-discord@0c4b27844ba47cb1c7bee539c8eead5284ce9fa9
continue-on-error: true
with:
args: 'Total Coverage for Certification Tests is {{ totalPer }}%'

View File

@ -29,11 +29,11 @@ jobs:
name: Build ${{ matrix.target_os }}_${{ matrix.target_arch }} binaries name: Build ${{ matrix.target_os }}_${{ matrix.target_arch }} binaries
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
GOVER: 1.18 GOVER: "1.19"
GOOS: ${{ matrix.target_os }} GOOS: ${{ matrix.target_os }}
GOARCH: ${{ matrix.target_arch }} GOARCH: ${{ matrix.target_arch }}
GOPROXY: https://proxy.golang.org GOPROXY: https://proxy.golang.org
GOLANGCI_LINT_VER: v1.45.2 GOLANGCI_LINT_VER: "v1.48.0"
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
@ -51,12 +51,6 @@ jobs:
- os: macOS-latest - os: macOS-latest
target_arch: arm target_arch: arm
steps: steps:
- name: Check if need skip
id: skip_check
uses: fkirc/skip-duplicate-actions@v3.4.0
with:
cancel_others: 'true'
paths_ignore: '["**.md", ".codecov.yaml", ".github/workflows/dapr-automerge.yml"]'
- name: Set up Go ${{ env.GOVER }} - name: Set up Go ${{ env.GOVER }}
if: ${{ steps.skip_check.outputs.should_skip != 'true' }} if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
uses: actions/setup-go@v2 uses: actions/setup-go@v2
@ -65,19 +59,52 @@ jobs:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
if: ${{ steps.skip_check.outputs.should_skip != 'true' }} if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Cache Go modules (Linux)
if: matrix.target_os == 'linux'
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ matrix.target_os }}-${{ matrix.target_arch }}-go-${{ env.GOVER }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ matrix.target_os }}-${{ matrix.target_arch }}-go-${{ env.GOVER }}-
- name: Cache Go modules (Windows)
if: matrix.target_os == 'windows'
uses: actions/cache@v3
with:
path: |
~\AppData\Local\go-build
~\go\pkg\mod
key: ${{ matrix.target_os }}-${{ matrix.target_arch }}-go-${{ env.GOVER }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ matrix.target_os }}-${{ matrix.target_arch }}-go-${{ env.GOVER }}-
- name: Cache Go modules (macOS)
if: matrix.target_os == 'darwin'
uses: actions/cache@v3
with:
path: |
~/Library/Caches/go-build
~/go/pkg/mod
key: ${{ matrix.target_os }}-${{ matrix.target_arch }}-go-${{ env.GOVER }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ matrix.target_os }}-${{ matrix.target_arch }}-go-${{ env.GOVER }}-
- name: Run golangci-lint - name: Run golangci-lint
if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux' && steps.skip_check.outputs.should_skip != 'true' if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux' && steps.skip_check.outputs.should_skip != 'true'
uses: golangci/golangci-lint-action@v3.1.0 uses: golangci/golangci-lint-action@v3.2.0
with: with:
version: ${{ env.GOLANGCI_LINT_VER }} version: ${{ env.GOLANGCI_LINT_VER }}
skip-cache: true
args: --timeout 15m
- name: Run go mod tidy check diff - name: Run go mod tidy check diff
if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux' && steps.skip_check.outputs.should_skip != 'true' if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux' && steps.skip_check.outputs.should_skip != 'true'
run: make modtidy-all check-diff run: make modtidy-all check-diff
- name: Run make test - name: Run make test
env: env:
COVERAGE_OPTS: "-coverprofile=coverage.txt -covermode=atomic" COVERAGE_OPTS: "-coverprofile=coverage.txt -covermode=atomic"
IPFS_TEST: "1"
if: matrix.target_arch != 'arm' && steps.skip_check.outputs.should_skip != 'true' if: matrix.target_arch != 'arm' && steps.skip_check.outputs.should_skip != 'true'
run: make test run: make test
- name: Codecov - name: Codecov
if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux' if: matrix.target_arch == 'amd64' && matrix.target_os == 'linux'
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3

View File

@ -25,24 +25,11 @@ on:
- release-* - release-*
jobs: jobs:
pre_job:
name: Skip Duplicate Actions
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v3.4.0
with:
cancel_others: 'true'
paths_ignore: '["**.md", ".codecov.yaml", ".github/workflows/dapr-automerge.yml"]'
# Based on whether this is a PR or a scheduled run, we will run a different # Based on whether this is a PR or a scheduled run, we will run a different
# subset of the conformance tests. This allows all the tests not requiring # subset of the conformance tests. This allows all the tests not requiring
# secrets to be executed on pull requests. # secrets to be executed on pull requests.
generate-matrix: generate-matrix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'repository_dispatch'
steps: steps:
- name: Install yq - name: Install yq
run: | run: |
@ -59,6 +46,7 @@ jobs:
- bindings.mqtt-mosquitto - bindings.mqtt-mosquitto
- bindings.mqtt-vernemq - bindings.mqtt-vernemq
- bindings.redis - bindings.redis
- bindings.rabbitmq
- pubsub.aws.snssqs - pubsub.aws.snssqs
- pubsub.hazelcast - pubsub.hazelcast
- pubsub.in-memory - pubsub.in-memory
@ -74,12 +62,14 @@ jobs:
- secretstores.localenv - secretstores.localenv
- secretstores.localfile - secretstores.localfile
- state.cassandra - state.cassandra
- state.memcached
- state.mongodb - state.mongodb
- state.mysql - state.mysql
- state.postgresql - state.postgresql
- state.redis - state.redis
- state.sqlserver - state.sqlserver
- state.cockroachdb - state.cockroachdb
- state.rethinkdb
EOF EOF
) )
echo "::set-output name=pr-components::$PR_COMPONENTS" echo "::set-output name=pr-components::$PR_COMPONENTS"
@ -134,6 +124,8 @@ jobs:
required-certs: AzureKeyVaultSecretStoreCert required-certs: AzureKeyVaultSecretStoreCert
- component: secretstores.azure.keyvault.serviceprincipal - component: secretstores.azure.keyvault.serviceprincipal
required-secrets: AzureKeyVaultName,AzureKeyVaultSecretStoreTenantId,AzureKeyVaultSecretStoreServicePrincipalClientId,AzureKeyVaultSecretStoreServicePrincipalClientSecret required-secrets: AzureKeyVaultName,AzureKeyVaultSecretStoreTenantId,AzureKeyVaultSecretStoreServicePrincipalClientId,AzureKeyVaultSecretStoreServicePrincipalClientSecret
- component: bindings.azure.cosmosdb
required-secrets: AzureCosmosDBMasterKey,AzureCosmosDBUrl,AzureCosmosDB,AzureCosmosDBCollection
EOF EOF
) )
echo "::set-output name=cron-components::$CRON_COMPONENTS" echo "::set-output name=cron-components::$CRON_COMPONENTS"
@ -247,6 +239,10 @@ jobs:
- name: Start kafka - name: Start kafka
run: docker-compose -f ./.github/infrastructure/docker-compose-kafka.yml -p kafka up -d run: docker-compose -f ./.github/infrastructure/docker-compose-kafka.yml -p kafka up -d
if: contains(matrix.component, 'kafka') if: contains(matrix.component, 'kafka')
- name: Start memcached
run: docker-compose -f ./.github/infrastructure/docker-compose-memcached.yml -p memcached up -d
if: contains(matrix.component, 'memcached')
- name: Start natsstreaming - name: Start natsstreaming
run: docker-compose -f ./.github/infrastructure/docker-compose-natsstreaming.yml -p natsstreaming up -d run: docker-compose -f ./.github/infrastructure/docker-compose-natsstreaming.yml -p natsstreaming up -d
@ -311,6 +307,11 @@ jobs:
docker-compose -f ./.github/infrastructure/docker-compose-cockroachdb.yml -p cockroachdb up -d docker-compose -f ./.github/infrastructure/docker-compose-cockroachdb.yml -p cockroachdb up -d
if: contains(matrix.component, 'cockroachdb') if: contains(matrix.component, 'cockroachdb')
- name: Start rethinkdb
run: |
docker-compose -f ./.github/infrastructure/docker-compose-rethinkdb.yml -p rethinkdb up -d
if: contains(matrix.component, 'rethinkdb')
- name: Setup KinD test data - name: Setup KinD test data
if: contains(matrix.component, 'kubernetes') if: contains(matrix.component, 'kubernetes')
run: | run: |
@ -320,7 +321,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: '^1.18' go-version: '^1.19'
- name: Download Go dependencies - name: Download Go dependencies
run: | run: |

View File

@ -45,7 +45,12 @@ jobs:
"shubham1172", "shubham1172",
"skyao", "skyao",
"msfussell", "msfussell",
"Taction" "Taction",
"RyanLettieri",
"DeepanshuA",
"yash-nisar",
"addjuarez",
"tmacam",
]; ];
const payload = context.payload; const payload = context.payload;
const issue = context.issue; const issue = context.issue;

View File

@ -4,7 +4,7 @@ run:
concurrency: 4 concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m # timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 5m deadline: 15m
# exit code when at least one issue was found, default is 1 # exit code when at least one issue was found, default is 1
issues-exit-code: 1 issues-exit-code: 1
@ -244,6 +244,7 @@ linters:
- nestif - nestif
- nlreturn - nlreturn
- exhaustive - exhaustive
- exhaustruct
- noctx - noctx
- gci - gci
- golint - golint
@ -259,7 +260,6 @@ linters:
- godot - godot
- cyclop - cyclop
- varnamelen - varnamelen
- gosec
- errorlint - errorlint
- forcetypeassert - forcetypeassert
- ifshort - ifshort
@ -271,3 +271,8 @@ linters:
- wastedassign - wastedassign
- containedctx - containedctx
- gosimple - gosimple
- nonamedreturns
- asasalint
- rowserrcheck
- sqlclosecheck
- structcheck

View File

@ -22,40 +22,76 @@ export GOSUMDB ?= sum.golang.org
GIT_COMMIT = $(shell git rev-list -1 HEAD) GIT_COMMIT = $(shell git rev-list -1 HEAD)
GIT_VERSION = $(shell git describe --always --abbrev=7 --dirty) GIT_VERSION = $(shell git describe --always --abbrev=7 --dirty)
# By default, disable CGO_ENABLED. See the details on https://golang.org/cmd/cgo # By default, disable CGO_ENABLED. See the details on https://golang.org/cmd/cgo
CGO ?= 0 CGO ?= 0
LOCAL_ARCH := $(shell uname -m) LOCAL_ARCH := $(shell uname -m)
ifeq ($(LOCAL_ARCH),x86_64) ifeq ($(LOCAL_ARCH),x86_64)
TARGET_ARCH_LOCAL=amd64 TARGET_ARCH_LOCAL=amd64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8) else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
TARGET_ARCH_LOCAL=arm64 TARGET_ARCH_LOCAL=arm64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv) else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv)
TARGET_ARCH_LOCAL=arm TARGET_ARCH_LOCAL=arm
else else
TARGET_ARCH_LOCAL=amd64 TARGET_ARCH_LOCAL=amd64
endif endif
export GOARCH ?= $(TARGET_ARCH_LOCAL) export GOARCH ?= $(TARGET_ARCH_LOCAL)
LOCAL_OS := $(shell uname) LOCAL_OS := $(shell uname)
ifeq ($(LOCAL_OS),Linux) ifeq ($(LOCAL_OS),Linux)
TARGET_OS_LOCAL = linux TARGET_OS_LOCAL = linux
else ifeq ($(LOCAL_OS),Darwin) else ifeq ($(LOCAL_OS),Darwin)
TARGET_OS_LOCAL = darwin TARGET_OS_LOCAL = darwin
else else
TARGET_OS_LOCAL ?= windows TARGET_OS_LOCAL ?= windows
endif endif
export GOOS ?= $(TARGET_OS_LOCAL) export GOOS ?= $(TARGET_OS_LOCAL)
ifeq ($(GOOS),windows) ifeq ($(GOOS),windows)
BINARY_EXT_LOCAL:=.exe FINDBIN := where
GOLANGCI_LINT:=golangci-lint.exe BINARY_EXT_LOCAL:=.exe
# Workaround for https://github.com/golang/go/issues/40795 GOLANGCI_LINT:=golangci-lint.exe
BUILDMODE:=-buildmode=exe # Workaround for https://github.com/golang/go/issues/40795
BUILDMODE:=-buildmode=exe
else else
BINARY_EXT_LOCAL:= FINDBIN := which
GOLANGCI_LINT:=golangci-lint BINARY_EXT_LOCAL:=
GOLANGCI_LINT:=golangci-lint
endif endif
# Get linter versions
LINTER_BINARY := $(shell $(FINDBIN) $(GOLANGCI_LINT))
export GH_LINT_VERSION := $(shell grep 'GOLANGCI_LINT_VER:' .github/workflows/components-contrib.yml | xargs | cut -d" " -f2)
ifeq (,$(LINTER_BINARY))
INSTALLED_LINT_VERSION := "v0.0.0"
else
INSTALLED_LINT_VERSION=v$(shell $(LINTER_BINARY) version | grep -Eo '([0-9]+\.)+[0-9]+' - || "")
endif
################################################################################
# Linter targets #
################################################################################
.PHONY: verify-linter-installed
verify-linter-installed:
@if [ -z $(LINTER_BINARY) ]; then \
echo "[!] golangci-lint not installed"; \
echo "[!] You can install it from https://golangci-lint.run/usage/install/"; \
echo "[!] or by running"; \
echo "[!] curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $(GH_LINT_VERSION)"; \
exit 1; \
fi;
.PHONY: verify-linter-version
verify-linter-version:
@if [ "$(GH_LINT_VERSION)" != "$(INSTALLED_LINT_VERSION)" ]; then \
echo "[!] Your locally installed version of golangci-lint is different from the pipeline"; \
echo "[!] This will likely cause linting issues for you locally"; \
echo "[!] Yours: $(INSTALLED_LINT_VERSION)"; \
echo "[!] Theirs: $(GH_LINT_VERSION)"; \
echo "[!] Upgrade: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $(GH_LINT_VERSION)"; \
sleep 3; \
fi;
################################################################################ ################################################################################
# Target: test # # Target: test #
################################################################################ ################################################################################
@ -67,7 +103,7 @@ test:
# Target: lint # # Target: lint #
################################################################################ ################################################################################
.PHONY: lint .PHONY: lint
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 # Due to https://github.com/golangci/golangci-lint/issues/580, we need to add --fix for windows
$(GOLANGCI_LINT) run --timeout=20m $(GOLANGCI_LINT) run --timeout=20m
@ -79,7 +115,7 @@ MODFILES := $(shell find . -name go.mod)
define modtidy-target define modtidy-target
.PHONY: modtidy-$(1) .PHONY: modtidy-$(1)
modtidy-$(1): modtidy-$(1):
cd $(shell dirname $(1)); go mod tidy -compat=1.18; cd - cd $(shell dirname $(1)); go mod tidy -compat=1.19; cd -
endef endef
# Generate modtidy target action for each go.mod file # Generate modtidy target action for each go.mod file

View File

@ -53,12 +53,12 @@ type outgoingWebhook struct {
handler bindings.Handler handler bindings.Handler
} }
var webhooks = struct { //nolint: gochecknoglobals var webhooks = struct { //nolint:gochecknoglobals
sync.RWMutex sync.RWMutex
m map[string]*outgoingWebhook m map[string]*outgoingWebhook
}{m: make(map[string]*outgoingWebhook)} }{m: make(map[string]*outgoingWebhook)}
func NewDingTalkWebhook(l logger.Logger) *DingTalkWebhook { func NewDingTalkWebhook(l logger.Logger) bindings.InputOutputBinding {
// See guidance on proper HTTP client settings here: // See guidance on proper HTTP client settings here:
// https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779 // https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
dialer := &net.Dialer{ //nolint:exhaustivestruct dialer := &net.Dialer{ //nolint:exhaustivestruct
@ -162,7 +162,7 @@ func (t *DingTalkWebhook) sendMessage(ctx context.Context, req *bindings.InvokeR
ctx, cancel := context.WithTimeout(ctx, defaultHTTPClientTimeout) ctx, cancel := context.WithTimeout(ctx, defaultHTTPClientTimeout)
defer cancel() defer cancel()
httpReq, err := http.NewRequestWithContext(ctx, "POST", postURL, bytes.NewReader(msg)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, postURL, bytes.NewReader(msg))
if err != nil { if err != nil {
return fmt.Errorf("dingtalk webhook error: new request failed. %w", err) return fmt.Errorf("dingtalk webhook error: new request failed. %w", err)
} }

View File

@ -15,7 +15,7 @@ package webhook
import ( import (
"context" "context"
"io/ioutil" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"sync/atomic" "sync/atomic"
@ -26,6 +26,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -43,17 +44,17 @@ func TestPublishMsg(t *testing.T) { //nolint:paralleltest
t.Errorf("Expected request to '/test', got '%s'", r.URL.EscapedPath()) t.Errorf("Expected request to '/test', got '%s'", r.URL.EscapedPath())
} }
body, err := ioutil.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, msg, string(body)) assert.Equal(t, msg, string(body))
})) }))
defer ts.Close() defer ts.Close()
m := bindings.Metadata{Name: "test", Properties: map[string]string{ m := bindings.Metadata{Base: metadata.Base{Name: "test", Properties: map[string]string{
"url": ts.URL + "/test", "url": ts.URL + "/test",
"secret": "", "secret": "",
"id": "x", "id": "x",
}} }}}
d := NewDingTalkWebhook(logger.NewLogger("test")) d := NewDingTalkWebhook(logger.NewLogger("test"))
err := d.Init(m) err := d.Init(m)
@ -67,14 +68,14 @@ func TestPublishMsg(t *testing.T) { //nolint:paralleltest
func TestBindingReadAndInvoke(t *testing.T) { //nolint:paralleltest func TestBindingReadAndInvoke(t *testing.T) { //nolint:paralleltest
msg := "{\"type\": \"text\",\"text\": {\"content\": \"hello\"}}" msg := "{\"type\": \"text\",\"text\": {\"content\": \"hello\"}}"
m := bindings.Metadata{ m := bindings.Metadata{Base: metadata.Base{
Name: "test", Name: "test",
Properties: map[string]string{ Properties: map[string]string{
"url": "/test", "url": "/test",
"secret": "", "secret": "",
"id": "x", "id": "x",
}, },
} }}
d := NewDingTalkWebhook(logger.NewLogger("test")) d := NewDingTalkWebhook(logger.NewLogger("test"))
err := d.Init(m) err := d.Init(m)

View File

@ -25,12 +25,12 @@ import (
) )
const ( const (
metadataRpcGroup = "group" metadataRPCGroup = "group"
metadataRpcVersion = "version" metadataRPCVersion = "version"
metadataRpcInterface = "interfaceName" metadataRPCInterface = "interfaceName"
metadataRpcMethodName = "methodName" metadataRPCMethodName = "methodName"
metadataRpcProviderHostname = "providerHostname" metadataRPCProviderHostname = "providerHostname"
metadataRpcProviderPort = "providerPort" metadataRPCProviderPort = "providerPort"
) )
type dubboContext struct { type dubboContext struct {
@ -47,12 +47,12 @@ type dubboContext struct {
func newDubboContext(metadata map[string]string) *dubboContext { func newDubboContext(metadata map[string]string) *dubboContext {
dubboMetadata := &dubboContext{} dubboMetadata := &dubboContext{}
dubboMetadata.group = metadata[metadataRpcGroup] dubboMetadata.group = metadata[metadataRPCGroup]
dubboMetadata.interfaceName = metadata[metadataRpcInterface] dubboMetadata.interfaceName = metadata[metadataRPCInterface]
dubboMetadata.version = metadata[metadataRpcVersion] dubboMetadata.version = metadata[metadataRPCVersion]
dubboMetadata.method = metadata[metadataRpcMethodName] dubboMetadata.method = metadata[metadataRPCMethodName]
dubboMetadata.hostname = metadata[metadataRpcProviderHostname] dubboMetadata.hostname = metadata[metadataRPCProviderHostname]
dubboMetadata.port = metadata[metadataRpcProviderPort] dubboMetadata.port = metadata[metadataRPCProviderPort]
dubboMetadata.inited = false dubboMetadata.inited = false
return dubboMetadata return dubboMetadata
} }

View File

@ -37,7 +37,7 @@ type DubboOutputBinding struct {
var dubboBinding *DubboOutputBinding var dubboBinding *DubboOutputBinding
func NewDubboOutput(logger logger.Logger) *DubboOutputBinding { func NewDubboOutput(logger logger.Logger) bindings.OutputBinding {
if dubboBinding == nil { if dubboBinding == nil {
dubboBinding = &DubboOutputBinding{ dubboBinding = &DubboOutputBinding{
ctxCache: make(map[string]*dubboContext), ctxCache: make(map[string]*dubboContext),

View File

@ -78,10 +78,10 @@ func TestInvoke(t *testing.T) {
// 3. invoke dapr dubbo output binding, get rsp bytes // 3. invoke dapr dubbo output binding, get rsp bytes
rsp, err := output.Invoke(context.Background(), &bindings.InvokeRequest{ rsp, err := output.Invoke(context.Background(), &bindings.InvokeRequest{
Metadata: map[string]string{ Metadata: map[string]string{
metadataRpcProviderPort: dubboPort, metadataRPCProviderPort: dubboPort,
metadataRpcProviderHostname: localhostIP, metadataRPCProviderHostname: localhostIP,
metadataRpcMethodName: methodName, metadataRPCMethodName: methodName,
metadataRpcInterface: providerInterfaceName, metadataRPCInterface: providerInterfaceName,
}, },
Data: reqData, Data: reqData,
Operation: bindings.GetOperation, Operation: bindings.GetOperation,

View File

@ -11,6 +11,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
//nolint:nosnakecase
package dubbo package dubbo
import ( import (

View File

@ -54,12 +54,12 @@ type Nacos struct {
watches []configParam watches []configParam
servers []constant.ServerConfig servers []constant.ServerConfig
logger logger.Logger logger logger.Logger
configClient config_client.IConfigClient configClient config_client.IConfigClient //nolint:nosnakecase
readHandler func(ctx context.Context, response *bindings.ReadResponse) ([]byte, error) readHandler func(ctx context.Context, response *bindings.ReadResponse) ([]byte, error)
} }
// NewNacos returns a new Nacos instance. // NewNacos returns a new Nacos instance.
func NewNacos(logger logger.Logger) *Nacos { func NewNacos(logger logger.Logger) bindings.OutputBinding {
return &Nacos{ return &Nacos{
logger: logger, logger: logger,
watchesLock: sync.Mutex{}, watchesLock: sync.Mutex{},

View File

@ -25,15 +25,16 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
func TestInputBindingRead(t *testing.T) { //nolint:paralleltest func TestInputBindingRead(t *testing.T) { //nolint:paralleltest
m := bindings.Metadata{Name: "test", Properties: nil} m := bindings.Metadata{Base: metadata.Base{Name: "test", Properties: nil}}
var err error var err error
m.Properties, err = getNacosLocalCacheMetadata() m.Properties, err = getNacosLocalCacheMetadata()
require.NoError(t, err) require.NoError(t, err)
n := NewNacos(logger.NewLogger("test")) n := NewNacos(logger.NewLogger("test")).(*Nacos)
err = n.Init(m) err = n.Init(m)
require.NoError(t, err) require.NoError(t, err)
var count int32 var count int32
@ -68,7 +69,7 @@ func getNacosLocalCacheMetadata() (map[string]string, error) {
} }
cfgFile := path.Join(tmpDir, fmt.Sprintf("%s@@%s@@", dataID, group)) cfgFile := path.Join(tmpDir, fmt.Sprintf("%s@@%s@@", dataID, group))
file, err := os.OpenFile(cfgFile, os.O_RDWR|os.O_CREATE, os.ModePerm) file, err := os.OpenFile(cfgFile, os.O_RDWR|os.O_CREATE, os.ModePerm) //nolint:nosnakecase
if err != nil || file == nil { if err != nil || file == nil {
return nil, fmt.Errorf("open %s failed. %w", cfgFile, err) return nil, fmt.Errorf("open %s failed. %w", cfgFile, err)
} }

View File

@ -40,7 +40,7 @@ type ossMetadata struct {
} }
// NewAliCloudOSS returns a new instance. // NewAliCloudOSS returns a new instance.
func NewAliCloudOSS(logger logger.Logger) *AliCloudOSS { func NewAliCloudOSS(logger logger.Logger) bindings.OutputBinding {
return &AliCloudOSS{logger: logger} return &AliCloudOSS{logger: logger}
} }

View File

@ -90,11 +90,12 @@ func (a *AliCloudRocketMQ) Read(ctx context.Context, handler bindings.Handler) e
if topicStr == "" { if topicStr == "" {
continue continue
} }
mqType, mqExpression, topic, err := parseTopic(topicStr)
if err != nil { var mqType, mqExpression, topic string
if mqType, mqExpression, topic, err = parseTopic(topicStr); err != nil {
return err return err
} }
if err := consumer.Subscribe( if err = consumer.Subscribe(
topic, topic,
mqc.MessageSelector{ mqc.MessageSelector{
Type: mqc.ExpressionType(mqType), Type: mqc.ExpressionType(mqType),
@ -106,7 +107,7 @@ func (a *AliCloudRocketMQ) Read(ctx context.Context, handler bindings.Handler) e
} }
} }
if err := consumer.Start(); err != nil { if err = consumer.Start(); err != nil {
return fmt.Errorf("binding-rocketmq: consumer start failed. %w", err) return fmt.Errorf("binding-rocketmq: consumer start failed. %w", err)
} }
@ -121,7 +122,7 @@ func (a *AliCloudRocketMQ) Read(ctx context.Context, handler bindings.Handler) e
innerErr := consumer.Shutdown() innerErr := consumer.Shutdown()
if innerErr != nil && !errors.Is(innerErr, context.Canceled) { if innerErr != nil && !errors.Is(innerErr, context.Canceled) {
a.logger.Warnf("binding-rocketmq: error while shutting down consumer: %v") a.logger.Warnf("binding-rocketmq: error while shutting down consumer: %v", innerErr)
} }
}() }()

View File

@ -0,0 +1,127 @@
package sls
import (
"context"
"encoding/json"
"fmt"
"time"
sls "github.com/aliyun/aliyun-log-go-sdk"
"github.com/aliyun/aliyun-log-go-sdk/producer"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/kit/config"
"github.com/dapr/kit/logger"
)
type AliCloudSlsLogstorage struct {
logger logger.Logger
producer *producer.Producer
metadata SlsLogstorageMetadata
}
type SlsLogstorageMetadata struct {
Endpoint string `json:"endpoint"`
AccessKeyID string `json:"accessKeyID"`
AccessKeySecret string `json:"accessKeySecret"`
}
type Callback struct {
s *AliCloudSlsLogstorage
}
// parse metadata field
func (s *AliCloudSlsLogstorage) Init(metadata bindings.Metadata) error {
m, err := s.parseMeta(metadata)
if err != nil {
return err
}
s.metadata = *m
producerConfig := producer.GetDefaultProducerConfig()
// the config properties in the component yaml file
producerConfig.Endpoint = m.Endpoint
producerConfig.AccessKeyID = m.AccessKeyID
producerConfig.AccessKeySecret = m.AccessKeySecret
s.producer = producer.InitProducer(producerConfig)
s.producer.Start()
return nil
}
func NewAliCloudSlsLogstorage(logger logger.Logger) bindings.OutputBinding {
logger.Debug("initialized Sls log storage binding component")
s := &AliCloudSlsLogstorage{
logger: logger,
}
return s
}
func (s *AliCloudSlsLogstorage) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
// verify the metadata property
if logProject := req.Metadata["project"]; logProject == "" {
return nil, fmt.Errorf("SLS binding error: project property not supplied")
}
if logstore := req.Metadata["logstore"]; logstore == "" {
return nil, fmt.Errorf("SLS binding error: logstore property not supplied")
}
if topic := req.Metadata["topic"]; topic == "" {
return nil, fmt.Errorf("SLS binding error: topic property not supplied")
}
if source := req.Metadata["source"]; source == "" {
return nil, fmt.Errorf("SLS binding error: source property not supplied")
}
log, err := s.parseLog(req)
if err != nil {
s.logger.Info(err)
return nil, err
}
s.logger.Debug(log)
callBack := &Callback{}
err = s.producer.SendLogWithCallBack(req.Metadata["project"], req.Metadata["logstore"], req.Metadata["topic"], req.Metadata["source"], log, callBack)
if err != nil {
s.logger.Info(err)
return nil, err
}
return nil, nil
}
// parse the log content
func (s *AliCloudSlsLogstorage) parseLog(req *bindings.InvokeRequest) (*sls.Log, error) {
var logInfo map[string]string
err := json.Unmarshal(req.Data, &logInfo)
if err != nil {
return nil, err
}
return producer.GenerateLog(uint32(time.Now().Unix()), logInfo), nil
}
func (s *AliCloudSlsLogstorage) parseMeta(metadata bindings.Metadata) (*SlsLogstorageMetadata, error) {
var m SlsLogstorageMetadata
err := config.Decode(metadata.Properties, &m)
if err != nil {
return nil, err
}
return &m, nil
}
func (s *AliCloudSlsLogstorage) Operations() []bindings.OperationKind {
return []bindings.OperationKind{bindings.CreateOperation}
}
func (callback *Callback) Success(result *producer.Result) {
}
func (callback *Callback) Fail(result *producer.Result) {
msg := "unknown reason"
if result.GetErrorMessage() != "" {
msg = result.GetErrorMessage()
}
if result.GetErrorCode() != "" {
callback.s.logger.Debug("Failed error code:", result.GetErrorCode())
}
callback.s.logger.Info("Log storage failed:", msg)
}

View File

@ -0,0 +1,59 @@
package sls
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings"
)
/**
* test the metadata in the yaml file
*/
func TestSlsLogstorageMetadata(t *testing.T) {
m := bindings.Metadata{}
m.Properties = map[string]string{
"Endpoint": "ENDPOINT",
"AccessKeyID": "ACCESSKEYID",
"AccessKeySecret": "ACCESSKEYSECRET",
}
aliCloudSlsLogstorage := AliCloudSlsLogstorage{}
meta, err := aliCloudSlsLogstorage.parseMeta(m)
assert.Nil(t, err)
assert.Equal(t, "ENDPOINT", meta.Endpoint)
assert.Equal(t, "ACCESSKEYID", meta.AccessKeyID)
assert.Equal(t, "ACCESSKEYSECRET", meta.AccessKeySecret)
}
/*
* test the log content
*/
func TestParseLog(t *testing.T) {
aliCloudSlsLogstorage := AliCloudSlsLogstorage{}
d, _ := json.Marshal(map[string]string{
"log1": "LOG1",
"log2": "LOG2",
})
log := bindings.InvokeRequest{
Data: d,
Metadata: map[string]string{
"project": "PROJECT",
"logstore": "LOGSTORE",
"topic": "TOPIC",
"source": "SOURCE",
},
}
parseLog, _ := aliCloudSlsLogstorage.parseLog(&log)
for _, v := range parseLog.Contents {
switch *v.Key {
case "log1":
assert.Equal(t, "LOG1", *v.Value)
case "log2":
assert.Equal(t, "LOG2", *v.Value)
}
}
}

View File

@ -51,7 +51,7 @@ type AliCloudTableStore struct {
metadata tablestoreMetadata metadata tablestoreMetadata
} }
func NewAliCloudTableStore(log logger.Logger) *AliCloudTableStore { func NewAliCloudTableStore(log logger.Logger) bindings.OutputBinding {
return &AliCloudTableStore{ return &AliCloudTableStore{
logger: log, logger: log,
client: nil, client: nil,
@ -262,11 +262,11 @@ func (s *AliCloudTableStore) create(req *bindings.InvokeRequest, resp *bindings.
TableName: s.getTableName(req.Metadata), TableName: s.getTableName(req.Metadata),
PrimaryKey: &tablestore.PrimaryKey{PrimaryKeys: pks}, PrimaryKey: &tablestore.PrimaryKey{PrimaryKeys: pks},
Columns: columns, Columns: columns,
ReturnType: tablestore.ReturnType_RT_NONE, ReturnType: tablestore.ReturnType_RT_NONE, //nolint:nosnakecase
TransactionId: nil, TransactionId: nil,
} }
change.SetCondition(tablestore.RowExistenceExpectation_IGNORE) change.SetCondition(tablestore.RowExistenceExpectation_IGNORE) //nolint:nosnakecase
putRequest := &tablestore.PutRowRequest{ putRequest := &tablestore.PutRowRequest{
PutRowChange: &change, PutRowChange: &change,
@ -301,7 +301,7 @@ func (s *AliCloudTableStore) delete(req *bindings.InvokeRequest, resp *bindings.
TableName: s.getTableName(req.Metadata), TableName: s.getTableName(req.Metadata),
PrimaryKey: &tablestore.PrimaryKey{PrimaryKeys: pks}, PrimaryKey: &tablestore.PrimaryKey{PrimaryKeys: pks},
} }
change.SetCondition(tablestore.RowExistenceExpectation_IGNORE) change.SetCondition(tablestore.RowExistenceExpectation_IGNORE) //nolint:nosnakecase
deleteReq := &tablestore.DeleteRowRequest{DeleteRowChange: change} deleteReq := &tablestore.DeleteRowRequest{DeleteRowChange: change}
_, err = s.client.DeleteRow(deleteReq) _, err = s.client.DeleteRow(deleteReq)

View File

@ -22,6 +22,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -47,9 +48,9 @@ func TestDataEncodeAndDecode(t *testing.T) {
aliCloudTableStore := NewAliCloudTableStore(logger.NewLogger("test")) aliCloudTableStore := NewAliCloudTableStore(logger.NewLogger("test"))
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: getTestProperties(), Properties: getTestProperties(),
} }}
aliCloudTableStore.Init(metadata) aliCloudTableStore.Init(metadata)
// test create // test create

View File

@ -65,7 +65,7 @@ type APNS struct {
} }
// NewAPNS will create a new APNS output binding. // NewAPNS will create a new APNS output binding.
func NewAPNS(logger logger.Logger) *APNS { func NewAPNS(logger logger.Logger) bindings.OutputBinding {
return &APNS{ return &APNS{
logger: logger, logger: logger,
client: &http.Client{}, client: &http.Client{},

View File

@ -16,7 +16,7 @@ package apns
import ( import (
"bytes" "bytes"
"context" "context"
"io/ioutil" "io"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -41,136 +42,136 @@ func TestInit(t *testing.T) {
testLogger := logger.NewLogger("test") testLogger := logger.NewLogger("test")
t.Run("uses the development service", func(t *testing.T) { t.Run("uses the development service", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
developmentKey: "true", developmentKey: "true",
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, developmentPrefix, binding.urlPrefix) assert.Equal(t, developmentPrefix, binding.urlPrefix)
}) })
t.Run("uses the production service", func(t *testing.T) { t.Run("uses the production service", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
developmentKey: "false", developmentKey: "false",
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, productionPrefix, binding.urlPrefix) assert.Equal(t, productionPrefix, binding.urlPrefix)
}) })
t.Run("defaults to the production service", func(t *testing.T) { t.Run("defaults to the production service", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, productionPrefix, binding.urlPrefix) assert.Equal(t, productionPrefix, binding.urlPrefix)
}) })
t.Run("invalid development value", func(t *testing.T) { t.Run("invalid development value", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
developmentKey: "True", developmentKey: "True",
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Error(t, err, "invalid value for development parameter: True") assert.Error(t, err, "invalid value for development parameter: True")
}) })
t.Run("the key ID is required", func(t *testing.T) { t.Run("the key ID is required", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Error(t, err, "the key-id parameter is required") assert.Error(t, err, "the key-id parameter is required")
}) })
t.Run("valid key ID", func(t *testing.T) { t.Run("valid key ID", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, testKeyID, binding.authorizationBuilder.keyID) assert.Equal(t, testKeyID, binding.authorizationBuilder.keyID)
}) })
t.Run("the team ID is required", func(t *testing.T) { t.Run("the team ID is required", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
keyIDKey: testKeyID, keyIDKey: testKeyID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Error(t, err, "the team-id parameter is required") assert.Error(t, err, "the team-id parameter is required")
}) })
t.Run("valid team ID", func(t *testing.T) { t.Run("valid team ID", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, testTeamID, binding.authorizationBuilder.teamID) assert.Equal(t, testTeamID, binding.authorizationBuilder.teamID)
}) })
t.Run("the private key is required", func(t *testing.T) { t.Run("the private key is required", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Error(t, err, "the private-key parameter is required") assert.Error(t, err, "the private-key parameter is required")
}) })
t.Run("valid private key", func(t *testing.T) { t.Run("valid private key", func(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
binding := NewAPNS(testLogger) binding := NewAPNS(testLogger).(*APNS)
err := binding.Init(metadata) err := binding.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, binding.authorizationBuilder.privateKey) assert.NotNil(t, binding.authorizationBuilder.privateKey)
@ -179,7 +180,7 @@ func TestInit(t *testing.T) {
func TestOperations(t *testing.T) { func TestOperations(t *testing.T) {
testLogger := logger.NewLogger("test") testLogger := logger.NewLogger("test")
testBinding := NewAPNS(testLogger) testBinding := NewAPNS(testLogger).(*APNS)
operations := testBinding.Operations() operations := testBinding.Operations()
assert.Equal(t, 1, len(operations)) assert.Equal(t, 1, len(operations))
assert.Equal(t, bindings.CreateOperation, operations[0]) assert.Equal(t, bindings.CreateOperation, operations[0])
@ -316,7 +317,7 @@ func TestInvoke(t *testing.T) {
return &http.Response{ return &http.Response{
StatusCode: http.StatusBadRequest, StatusCode: http.StatusBadRequest,
Body: ioutil.NopCloser(strings.NewReader(body)), Body: io.NopCloser(strings.NewReader(body)),
} }
}) })
_, err := testBinding.Invoke(context.TODO(), successRequest) _, err := testBinding.Invoke(context.TODO(), successRequest)
@ -325,15 +326,15 @@ func TestInvoke(t *testing.T) {
} }
func makeTestBinding(t *testing.T, log logger.Logger) *APNS { func makeTestBinding(t *testing.T, log logger.Logger) *APNS {
testBinding := NewAPNS(log) testBinding := NewAPNS(log).(*APNS)
bindingMetadata := bindings.Metadata{ bindingMetadata := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
developmentKey: "true", developmentKey: "true",
keyIDKey: testKeyID, keyIDKey: testKeyID,
teamIDKey: testTeamID, teamIDKey: testTeamID,
privateKeyKey: testPrivateKey, privateKeyKey: testPrivateKey,
}, },
} }}
err := testBinding.Init(bindingMetadata) err := testBinding.Init(bindingMetadata)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -65,7 +65,8 @@ func (a *authorizationBuilder) generateAuthorizationHeader() (string, error) {
a.logger.Debug("Authorization token expired; generating new token") a.logger.Debug("Authorization token expired; generating new token")
now := time.Now() now := time.Now()
claims := jwt.StandardClaims{ // TODO: Use jwt.RegisteredClaims instead of jwt.StandardClaims.
claims := jwt.StandardClaims{ //nolint:staticcheck
IssuedAt: time.Now().Unix(), IssuedAt: time.Now().Unix(),
Issuer: a.teamID, Issuer: a.teamID,
} }

View File

@ -15,7 +15,7 @@ limitations under the License.
// send push notifications to Apple devices and Mac computers using Apple's // send push notifications to Apple devices and Mac computers using Apple's
// Push Notification Service (APNS). // Push Notification Service (APNS).
// //
// Configuring the Binding // # Configuring the Binding
// //
// To use the APNS output binding, you will need to create the binding // To use the APNS output binding, you will need to create the binding
// configuration and add it to your components directory. The binding // configuration and add it to your components directory. The binding
@ -31,37 +31,37 @@ limitations under the License.
// //
// A sample configuration file for the APNS binding is shown below: // A sample configuration file for the APNS binding is shown below:
// //
// apiVersion: dapr.io/v1alpha1 // apiVersion: dapr.io/v1alpha1
// kind: Component // kind: Component
// metadata: // metadata:
// name: apns // name: apns
// namespace: default // namespace: default
// spec: // spec:
// type: bindings.apns // type: bindings.apns
// metadata: // metadata:
// - name: development // - name: development
// value: false // value: false
// - name: key-id // - name: key-id
// value: PUT-KEY-ID-HERE // value: PUT-KEY-ID-HERE
// - name: team-id // - name: team-id
// value: PUT-APPLE-TEAM-ID-HERE // value: PUT-APPLE-TEAM-ID-HERE
// - name: private-key // - name: private-key
// secretKeyRef: // secretKeyRef:
// name: apns-secrets // name: apns-secrets
// key: private-key // key: private-key
// //
// If using Kubernetes, a sample secret configuration may look like this: // If using Kubernetes, a sample secret configuration may look like this:
// //
// apiVersion: v1 // apiVersion: v1
// kind: Secret // kind: Secret
// metadata: // metadata:
// name: apns-secrets // name: apns-secrets
// namespace: default // namespace: default
// stringData: // stringData:
// private-key: | // private-key: |
// -----BEGIN PRIVATE KEY----- // -----BEGIN PRIVATE KEY-----
// KEY-DATA-GOES-HERE // KEY-DATA-GOES-HERE
// -----END PRIVATE KEY----- // -----END PRIVATE KEY-----
// //
// The development parameter can be either "true" or "false". The development // The development parameter can be either "true" or "false". The development
// parameter controls which APNS service is used. If development is set to // parameter controls which APNS service is used. If development is set to
@ -70,7 +70,7 @@ limitations under the License.
// be used to send push notifications. If not specified, the production service // be used to send push notifications. If not specified, the production service
// will be chosen by default. // will be chosen by default.
// //
// Push Notification Format // # Push Notification Format
// //
// The APNS binding is a pass-through wrapper over the Apple Push Notification // The APNS binding is a pass-through wrapper over the Apple Push Notification
// Service. The APNS binding will send the request directly to the APNS service // Service. The APNS binding will send the request directly to the APNS service
@ -81,14 +81,14 @@ limitations under the License.
// Requests sent to the APNS binding should be a JSON object. A simple push // Requests sent to the APNS binding should be a JSON object. A simple push
// notification appears below: // notification appears below:
// //
// { // {
// "aps": { // "aps": {
// "alert": { // "alert": {
// "title": "New Updates!", // "title": "New Updates!",
// "body": "New updates are now available for your review." // "body": "New updates are now available for your review."
// } // }
// } // }
// } // }
// //
// The aps child object contains the push notification details that are used // The aps child object contains the push notification details that are used
// by the Apple Push Notification Service and target devices to route and show // by the Apple Push Notification Service and target devices to route and show
@ -124,27 +124,27 @@ limitations under the License.
// notifications from a chat room may have the same identifier causing them // notifications from a chat room may have the same identifier causing them
// to show up together in the device's notifications list. // to show up together in the device's notifications list.
// //
// Sending a Push Notification Using the APNS Binding // # Sending a Push Notification Using the APNS Binding
// //
// A simple request to the APNS binding looks like this: // A simple request to the APNS binding looks like this:
// //
// { // {
// "data": { // "data": {
// "aps": { // "aps": {
// "alert": { // "alert": {
// "title": "New Updates!", // "title": "New Updates!",
// "body": "New updates are available for your review." // "body": "New updates are available for your review."
// } // }
// } // }
// }, // },
// "metadata": { // "metadata": {
// "device-token": "PUT-DEVICE-TOKEN-HERE", // "device-token": "PUT-DEVICE-TOKEN-HERE",
// "apns-push-type": "alert", // "apns-push-type": "alert",
// "apns-priority": "10", // "apns-priority": "10",
// "apns-topic": "com.example.helloworld" // "apns-topic": "com.example.helloworld"
// }, // },
// "operation": "create" // "operation": "create"
// } // }
// //
// The device-token metadata field is required and should contain the token // The device-token metadata field is required and should contain the token
// for the device that will receive the push notification. Only one device // for the device that will receive the push notification. Only one device
@ -158,9 +158,9 @@ limitations under the License.
// the apns-id metadata value, then the Apple Push Notification Serivce will // the apns-id metadata value, then the Apple Push Notification Serivce will
// generate a unique ID and will return it. // generate a unique ID and will return it.
// //
// { // {
// "messageID": "12345678-1234-1234-1234-1234567890AB" // "messageID": "12345678-1234-1234-1234-1234567890AB"
// } // }
// //
// If the push notification could not be sent due to an authentication error // If the push notification could not be sent due to an authentication error
// or payload error, the error code returned by Apple will be returned. For // or payload error, the error code returned by Apple will be returned. For

View File

@ -23,7 +23,7 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
aws_auth "github.com/dapr/components-contrib/internal/authentication/aws" awsAuth "github.com/dapr/components-contrib/internal/authentication/aws"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -44,7 +44,7 @@ type dynamoDBMetadata struct {
} }
// NewDynamoDB returns a new DynamoDB instance. // NewDynamoDB returns a new DynamoDB instance.
func NewDynamoDB(logger logger.Logger) *DynamoDB { func NewDynamoDB(logger logger.Logger) bindings.OutputBinding {
return &DynamoDB{logger: logger} return &DynamoDB{logger: logger}
} }
@ -104,7 +104,7 @@ func (d *DynamoDB) getDynamoDBMetadata(spec bindings.Metadata) (*dynamoDBMetadat
} }
func (d *DynamoDB) getClient(metadata *dynamoDBMetadata) (*dynamodb.DynamoDB, error) { func (d *DynamoDB) getClient(metadata *dynamoDBMetadata) (*dynamodb.DynamoDB, error) {
sess, err := aws_auth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint) sess, err := awsAuth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -30,7 +30,7 @@ import (
"github.com/vmware/vmware-go-kcl/clientlibrary/worker" "github.com/vmware/vmware-go-kcl/clientlibrary/worker"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
aws_auth "github.com/dapr/components-contrib/internal/authentication/aws" awsAuth "github.com/dapr/components-contrib/internal/authentication/aws"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -82,7 +82,7 @@ type recordProcessor struct {
} }
// NewAWSKinesis returns a new AWS Kinesis instance. // NewAWSKinesis returns a new AWS Kinesis instance.
func NewAWSKinesis(logger logger.Logger) *AWSKinesis { func NewAWSKinesis(logger logger.Logger) bindings.InputOutputBinding {
return &AWSKinesis{logger: logger} return &AWSKinesis{logger: logger}
} }
@ -313,7 +313,7 @@ func (a *AWSKinesis) waitUntilConsumerExists(ctx aws.Context, input *kinesis.Des
} }
func (a *AWSKinesis) getClient(metadata *kinesisMetadata) (*kinesis.Kinesis, error) { func (a *AWSKinesis) getClient(metadata *kinesisMetadata) (*kinesis.Kinesis, error) {
sess, err := aws_auth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint) sess, err := awsAuth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -32,7 +32,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
aws_auth "github.com/dapr/components-contrib/internal/authentication/aws" awsAuth "github.com/dapr/components-contrib/internal/authentication/aws"
"github.com/dapr/components-contrib/internal/utils" "github.com/dapr/components-contrib/internal/utils"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -84,7 +84,7 @@ type listPayload struct {
} }
// NewAWSS3 returns a new AWSS3 instance. // NewAWSS3 returns a new AWSS3 instance.
func NewAWSS3(logger logger.Logger) *AWSS3 { func NewAWSS3(logger logger.Logger) bindings.OutputBinding {
return &AWSS3{logger: logger} return &AWSS3{logger: logger}
} }
@ -313,7 +313,7 @@ func (s *AWSS3) parseMetadata(metadata bindings.Metadata) (*s3Metadata, error) {
} }
func (s *AWSS3) getSession(metadata *s3Metadata) (*session.Session, error) { func (s *AWSS3) getSession(metadata *s3Metadata) (*session.Session, error) {
sess, err := aws_auth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint) sess, err := awsAuth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -143,7 +143,7 @@ func TestMergeWithRequestMetadata(t *testing.T) {
} }
func TestGetOption(t *testing.T) { func TestGetOption(t *testing.T) {
s3 := NewAWSS3(logger.NewLogger("s3")) s3 := NewAWSS3(logger.NewLogger("s3")).(*AWSS3)
s3.metadata = &s3Metadata{} s3.metadata = &s3Metadata{}
t.Run("return error if key is missing", func(t *testing.T) { t.Run("return error if key is missing", func(t *testing.T) {
@ -154,7 +154,7 @@ func TestGetOption(t *testing.T) {
} }
func TestDeleteOption(t *testing.T) { func TestDeleteOption(t *testing.T) {
s3 := NewAWSS3(logger.NewLogger("s3")) s3 := NewAWSS3(logger.NewLogger("s3")).(*AWSS3)
s3.metadata = &s3Metadata{} s3.metadata = &s3Metadata{}
t.Run("return error if key is missing", func(t *testing.T) { t.Run("return error if key is missing", func(t *testing.T) {

View File

@ -23,7 +23,7 @@ import (
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
aws_auth "github.com/dapr/components-contrib/internal/authentication/aws" awsAuth "github.com/dapr/components-contrib/internal/authentication/aws"
"github.com/aws/aws-sdk-go/service/ses" "github.com/aws/aws-sdk-go/service/ses"
@ -56,7 +56,7 @@ type sesMetadata struct {
} }
// NewAWSSES creates a new AWSSES binding instance. // NewAWSSES creates a new AWSSES binding instance.
func NewAWSSES(logger logger.Logger) *AWSSES { func NewAWSSES(logger logger.Logger) bindings.OutputBinding {
return &AWSSES{logger: logger} return &AWSSES{logger: logger}
} }
@ -192,7 +192,7 @@ func (metadata sesMetadata) mergeWithRequestMetadata(req *bindings.InvokeRequest
} }
func (a *AWSSES) getClient(metadata *sesMetadata) (*ses.SES, error) { func (a *AWSSES) getClient(metadata *sesMetadata) (*ses.SES, error) {
sess, err := aws_auth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, "") sess, err := awsAuth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, "")
if err != nil { if err != nil {
return nil, fmt.Errorf("SES binding error: error creating AWS session %w", err) return nil, fmt.Errorf("SES binding error: error creating AWS session %w", err)
} }

View File

@ -21,7 +21,7 @@ import (
"github.com/aws/aws-sdk-go/service/sns" "github.com/aws/aws-sdk-go/service/sns"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
aws_auth "github.com/dapr/components-contrib/internal/authentication/aws" awsAuth "github.com/dapr/components-contrib/internal/authentication/aws"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -48,7 +48,7 @@ type dataPayload struct {
} }
// NewAWSSNS creates a new AWSSNS binding instance. // NewAWSSNS creates a new AWSSNS binding instance.
func NewAWSSNS(logger logger.Logger) *AWSSNS { func NewAWSSNS(logger logger.Logger) bindings.OutputBinding {
return &AWSSNS{logger: logger} return &AWSSNS{logger: logger}
} }
@ -84,7 +84,7 @@ func (a *AWSSNS) parseMetadata(metadata bindings.Metadata) (*snsMetadata, error)
} }
func (a *AWSSNS) getClient(metadata *snsMetadata) (*sns.SNS, error) { func (a *AWSSNS) getClient(metadata *snsMetadata) (*sns.SNS, error) {
sess, err := aws_auth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint) sess, err := awsAuth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -22,7 +22,7 @@ import (
"github.com/aws/aws-sdk-go/service/sqs" "github.com/aws/aws-sdk-go/service/sqs"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
aws_auth "github.com/dapr/components-contrib/internal/authentication/aws" awsAuth "github.com/dapr/components-contrib/internal/authentication/aws"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -44,7 +44,7 @@ type sqsMetadata struct {
} }
// NewAWSSQS returns a new AWS SQS instance. // NewAWSSQS returns a new AWS SQS instance.
func NewAWSSQS(logger logger.Logger) *AWSSQS { func NewAWSSQS(logger logger.Logger) bindings.InputOutputBinding {
return &AWSSQS{logger: logger} return &AWSSQS{logger: logger}
} }
@ -149,7 +149,7 @@ func (a *AWSSQS) parseSQSMetadata(metadata bindings.Metadata) (*sqsMetadata, err
} }
func (a *AWSSQS) getClient(metadata *sqsMetadata) (*sqs.SQS, error) { func (a *AWSSQS) getClient(metadata *sqsMetadata) (*sqs.SQS, error) {
sess, err := aws_auth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint) sess, err := awsAuth.GetClient(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken, metadata.Region, metadata.Endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -105,7 +105,7 @@ type listPayload struct {
} }
// NewAzureBlobStorage returns a new Azure Blob Storage instance. // NewAzureBlobStorage returns a new Azure Blob Storage instance.
func NewAzureBlobStorage(logger logger.Logger) *AzureBlobStorage { func NewAzureBlobStorage(logger logger.Logger) bindings.OutputBinding {
return &AzureBlobStorage{logger: logger} return &AzureBlobStorage{logger: logger}
} }

View File

@ -26,7 +26,7 @@ import (
func TestParseMetadata(t *testing.T) { func TestParseMetadata(t *testing.T) {
m := bindings.Metadata{} m := bindings.Metadata{}
blobStorage := NewAzureBlobStorage(logger.NewLogger("test")) blobStorage := NewAzureBlobStorage(logger.NewLogger("test")).(*AzureBlobStorage)
t.Run("parse all metadata", func(t *testing.T) { t.Run("parse all metadata", func(t *testing.T) {
m.Properties = map[string]string{ m.Properties = map[string]string{
@ -83,7 +83,7 @@ func TestParseMetadata(t *testing.T) {
} }
func TestGetOption(t *testing.T) { func TestGetOption(t *testing.T) {
blobStorage := NewAzureBlobStorage(logger.NewLogger("test")) blobStorage := NewAzureBlobStorage(logger.NewLogger("test")).(*AzureBlobStorage)
t.Run("return error if blobName is missing", func(t *testing.T) { t.Run("return error if blobName is missing", func(t *testing.T) {
r := bindings.InvokeRequest{} r := bindings.InvokeRequest{}
@ -95,7 +95,7 @@ func TestGetOption(t *testing.T) {
} }
func TestDeleteOption(t *testing.T) { func TestDeleteOption(t *testing.T) {
blobStorage := NewAzureBlobStorage(logger.NewLogger("test")) blobStorage := NewAzureBlobStorage(logger.NewLogger("test")).(*AzureBlobStorage)
t.Run("return error if blobName is missing", func(t *testing.T) { t.Run("return error if blobName is missing", func(t *testing.T) {
r := bindings.InvokeRequest{} r := bindings.InvokeRequest{}

View File

@ -20,8 +20,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/a8m/documentdb" "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
backoff "github.com/cenkalti/backoff/v4"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/internal/authentication/azure" "github.com/dapr/components-contrib/internal/authentication/azure"
@ -30,8 +29,7 @@ import (
// CosmosDB allows performing state operations on collections. // CosmosDB allows performing state operations on collections.
type CosmosDB struct { type CosmosDB struct {
client *documentdb.DocumentDB client *azcosmos.ContainerClient
collection string
partitionKey string partitionKey string
logger logger.Logger logger logger.Logger
@ -45,10 +43,11 @@ type cosmosDBCredentials struct {
PartitionKey string `json:"partitionKey"` PartitionKey string `json:"partitionKey"`
} }
const statusTooManyRequests = "429" // RFC 6585, 4 // Value used for timeout durations
const timeoutValue = 30
// NewCosmosDB returns a new CosmosDB instance. // NewCosmosDB returns a new CosmosDB instance.
func NewCosmosDB(logger logger.Logger) *CosmosDB { func NewCosmosDB(logger logger.Logger) bindings.OutputBinding {
return &CosmosDB{logger: logger} return &CosmosDB{logger: logger}
} }
@ -62,57 +61,43 @@ func (c *CosmosDB) Init(metadata bindings.Metadata) error {
c.partitionKey = m.PartitionKey c.partitionKey = m.PartitionKey
// Create the client; first, try authenticating with a master key, if present // Create the client; first, try authenticating with a master key, if present
var config *documentdb.Config var client *azcosmos.Client
if m.MasterKey != "" { if m.MasterKey != "" {
config = documentdb.NewConfig(&documentdb.Key{ cred, keyErr := azcosmos.NewKeyCredential(m.MasterKey)
Key: m.MasterKey, if keyErr != nil {
}) return keyErr
}
client, err = azcosmos.NewClientWithKey(m.URL, cred, nil)
if err != nil {
return err
}
} else { } else {
// Fallback to using Azure AD // Fallback to using Azure AD
env, errB := azure.NewEnvironmentSettings("cosmosdb", metadata.Properties) env, errEnv := azure.NewEnvironmentSettings("cosmosdb", metadata.Properties)
if errB != nil { if errEnv != nil {
return errB return errEnv
} }
spt, errB := env.GetServicePrincipalToken() token, errToken := env.GetTokenCredential()
if errB != nil { if errToken != nil {
return errB return errToken
} }
config = documentdb.NewConfigWithServicePrincipal(spt) client, err = azcosmos.NewClient(m.URL, token, nil)
}
// disable the identification hydrator (which autogenerates IDs if missing from the request)
// so we aren't forced to use a struct by the upstream SDK
// this allows us to provide the most flexibility in the request document sent to this binding
config.IdentificationHydrator = nil
config.WithAppIdentifier("dapr-" + logger.DaprVersion)
c.client = documentdb.New(m.URL, config)
// Retries initializing the client if a TooManyRequests error is encountered
err = retryOperation(func() (err error) {
collLink := fmt.Sprintf("dbs/%s/colls/%s/", m.Database, m.Collection)
coll, err := c.client.ReadCollection(collLink)
if err != nil { if err != nil {
if isTooManyRequestsError(err) { return err
return err
}
return backoff.Permanent(err)
} else if coll == nil || coll.Self == "" {
return backoff.Permanent(
fmt.Errorf("collection %s in database %s for CosmosDB state store not found. This must be created before Dapr uses it", m.Collection, m.Database),
)
} }
}
c.collection = coll.Self // Create a container client
dbContainer, err := client.NewContainer(m.Database, m.Collection)
return nil
}, func(err error, d time.Duration) {
c.logger.Warnf("CosmosDB binding initialization failed: %v; retrying in %s", err, d)
}, 5*time.Minute)
if err != nil { if err != nil {
return err return err
} }
return nil c.client = dbContainer
ctx, cancel := context.WithTimeout(context.Background(), timeoutValue*time.Second)
_, err = c.client.Read(ctx, nil)
cancel()
return err
} }
func (c *CosmosDB) parseMetadata(metadata bindings.Metadata) (*cosmosDBCredentials, error) { func (c *CosmosDB) parseMetadata(metadata bindings.Metadata) (*cosmosDBCredentials, error) {
@ -135,7 +120,7 @@ func (c *CosmosDB) Operations() []bindings.OperationKind {
return []bindings.OperationKind{bindings.CreateOperation} return []bindings.OperationKind{bindings.CreateOperation}
} }
func (c *CosmosDB) Invoke(_ context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { func (c *CosmosDB) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
switch req.Operation { switch req.Operation {
case bindings.CreateOperation: case bindings.CreateOperation:
var obj interface{} var obj interface{}
@ -144,41 +129,34 @@ func (c *CosmosDB) Invoke(_ context.Context, req *bindings.InvokeRequest) (*bind
return nil, err return nil, err
} }
val, err := c.getPartitionKeyValue(c.partitionKey, obj) pkString, err := c.getPartitionKeyValue(c.partitionKey, obj)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pk := azcosmos.NewPartitionKeyString(pkString)
err = retryOperation(func() error { _, err = c.client.CreateItem(ctx, pk, req.Data, nil)
_, innerErr := c.client.CreateDocument(c.collection, obj, documentdb.PartitionKey(val))
if innerErr != nil {
if isTooManyRequestsError(innerErr) {
return innerErr
}
return backoff.Permanent(innerErr)
}
return nil
}, func(err error, d time.Duration) {
c.logger.Warnf("CosmosDB binding Invoke request failed: %v; retrying in %s", err, d)
}, 20*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, nil return nil, nil
default: default:
return nil, fmt.Errorf("operation kind %s not supported", req.Operation) return nil, fmt.Errorf("operation kind %s not supported", req.Operation)
} }
} }
func (c *CosmosDB) getPartitionKeyValue(key string, obj interface{}) (interface{}, error) { func (c *CosmosDB) getPartitionKeyValue(key string, obj interface{}) (string, error) {
val, err := c.lookup(obj.(map[string]interface{}), strings.Split(key, ".")) valI, err := c.lookup(obj.(map[string]interface{}), strings.Split(key, "."))
if err != nil { if err != nil {
return nil, fmt.Errorf("missing partitionKey field %s from request body - %w", c.partitionKey, err) return "", fmt.Errorf("missing partitionKey field %s from request body - %w", c.partitionKey, err)
}
val, ok := valI.(string)
if !ok {
return "", fmt.Errorf("partition key is not a string")
} }
if val == "" { if val == "" {
return nil, fmt.Errorf("partitionKey field %s from request body is empty", c.partitionKey) return "", fmt.Errorf("partitionKey field %s from request body is empty", c.partitionKey)
} }
return val, nil return val, nil
@ -209,24 +187,3 @@ func (c *CosmosDB) lookup(m map[string]interface{}, ks []string) (val interface{
return c.lookup(m, ks[1:]) return c.lookup(m, ks[1:])
} }
func retryOperation(operation backoff.Operation, notify backoff.Notify, maxElapsedTime time.Duration) error {
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = 2 * time.Second
bo.MaxElapsedTime = maxElapsedTime
return backoff.RetryNotify(operation, bo, notify)
}
func isTooManyRequestsError(err error) bool {
if err == nil {
return false
}
if requestError, ok := err.(*documentdb.RequestError); ok {
if requestError.Code == statusTooManyRequests {
return true
}
}
return false
}

View File

@ -54,7 +54,7 @@ type cosmosDBGremlinAPICredentials struct {
} }
// NewCosmosDBGremlinAPI returns a new CosmosDBGremlinAPI instance. // NewCosmosDBGremlinAPI returns a new CosmosDBGremlinAPI instance.
func NewCosmosDBGremlinAPI(logger logger.Logger) *CosmosDBGremlinAPI { func NewCosmosDBGremlinAPI(logger logger.Logger) bindings.OutputBinding {
return &CosmosDBGremlinAPI{logger: logger} return &CosmosDBGremlinAPI{logger: logger}
} }

View File

@ -18,7 +18,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"time" "time"
"github.com/Azure/azure-sdk-for-go/services/eventgrid/mgmt/2021-12-01/eventgrid" "github.com/Azure/azure-sdk-for-go/services/eventgrid/mgmt/2021-12-01/eventgrid"
@ -58,7 +58,7 @@ type azureEventGridMetadata struct {
} }
// NewAzureEventGrid returns a new Azure Event Grid instance. // NewAzureEventGrid returns a new Azure Event Grid instance.
func NewAzureEventGrid(logger logger.Logger) *AzureEventGrid { func NewAzureEventGrid(logger logger.Logger) bindings.InputOutputBinding {
return &AzureEventGrid{logger: logger} return &AzureEventGrid{logger: logger}
} }
@ -279,7 +279,7 @@ func (a *AzureEventGrid) createSubscription(ctx context.Context) error {
res := result.FutureAPI.Response() res := result.FutureAPI.Response()
if res.StatusCode != fasthttp.StatusCreated { if res.StatusCode != fasthttp.StatusCreated {
bodyBytes, err := ioutil.ReadAll(res.Body) bodyBytes, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
a.logger.Debugf("Failed reading error body when creating or updating Event Grid subscription: %v", err) a.logger.Debugf("Failed reading error body when creating or updating Event Grid subscription: %v", err)

View File

@ -18,6 +18,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/Azure/azure-amqp-common-go/v3/aad" "github.com/Azure/azure-amqp-common-go/v3/aad"
@ -145,7 +146,7 @@ func (m azureEventHubsMetadata) partitioned() bool {
} }
// NewAzureEventHubs returns a new Azure Event hubs instance. // NewAzureEventHubs returns a new Azure Event hubs instance.
func NewAzureEventHubs(logger logger.Logger) *AzureEventHubs { func NewAzureEventHubs(logger logger.Logger) bindings.InputOutputBinding {
return &AzureEventHubs{logger: logger} return &AzureEventHubs{logger: logger}
} }
@ -154,6 +155,28 @@ func validate(connectionString string) error {
return err return err
} }
func (a *AzureEventHubs) getStoragePrefixString() (string, error) {
hubName, err := a.validateAndGetHubName()
if err != nil {
return "", err
}
// empty string in the end of slice to have a suffix "-".
return strings.Join([]string{"dapr", hubName, a.metadata.consumerGroup, ""}, "-"), nil
}
func (a *AzureEventHubs) validateAndGetHubName() (string, error) {
hubName := a.metadata.eventHubName
if hubName == "" {
parsed, err := conn.ParsedConnectionFromStr(a.metadata.connectionString)
if err != nil {
return "", err
}
hubName = parsed.HubName
}
return hubName, nil
}
// Init performs metadata init. // Init performs metadata init.
func (a *AzureEventHubs) Init(metadata bindings.Metadata) error { func (a *AzureEventHubs) Init(metadata bindings.Metadata) error {
m, err := parseMetadata(metadata) m, err := parseMetadata(metadata)
@ -360,7 +383,13 @@ func (a *AzureEventHubs) RegisterPartitionedEventProcessor(ctx context.Context,
// RegisterEventProcessor - receive eventhub messages by eventprocessor // RegisterEventProcessor - receive eventhub messages by eventprocessor
// host by balancing partitions. // host by balancing partitions.
func (a *AzureEventHubs) RegisterEventProcessor(ctx context.Context, handler bindings.Handler) error { func (a *AzureEventHubs) RegisterEventProcessor(ctx context.Context, handler bindings.Handler) error {
leaserCheckpointer, err := storage.NewStorageLeaserCheckpointer(a.storageCredential, a.metadata.storageAccountName, a.metadata.storageContainerName, *a.azureEnvironment) storagePrefix, err := a.getStoragePrefixString()
if err != nil {
return err
}
leaserPrefixOpt := storage.WithPrefixInBlobPath(storagePrefix)
leaserCheckpointer, err := storage.NewStorageLeaserCheckpointer(a.storageCredential, a.metadata.storageAccountName, a.metadata.storageContainerName, *a.azureEnvironment, leaserPrefixOpt)
if err != nil { if err != nil {
return err return err
} }

View File

@ -17,15 +17,51 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger"
) )
var testLogger = logger.NewLogger("test")
func TestGetStoragePrefixString(t *testing.T) {
props := map[string]string{"storageAccountName": "fake", "storageAccountKey": "fake", "consumerGroup": "default", "storageContainerName": "test", "eventHub": "hubName", "eventHubNamespace": "fake"}
metadata := bindings.Metadata{Base: metadata.Base{Properties: props}}
m, err := parseMetadata(metadata)
require.NoError(t, err)
aeh := &AzureEventHubs{logger: testLogger, metadata: m}
actual, _ := aeh.getStoragePrefixString()
assert.Equal(t, "dapr-hubName-default-", actual)
}
func TestGetStoragePrefixStringWithHubNameFromConnectionString(t *testing.T) {
connectionString := "Endpoint=sb://fake.servicebus.windows.net/;SharedAccessKeyName=fakeKey;SharedAccessKey=key;EntityPath=hubName"
props := map[string]string{"storageAccountName": "fake", "storageAccountKey": "fake", "consumerGroup": "default", "storageContainerName": "test", "connectionString": connectionString}
metadata := bindings.Metadata{Base: metadata.Base{Properties: props}}
m, err := parseMetadata(metadata)
require.NoError(t, err)
aeh := &AzureEventHubs{logger: testLogger, metadata: m}
actual, _ := aeh.getStoragePrefixString()
assert.Equal(t, "dapr-hubName-default-", actual)
}
func TestParseMetadata(t *testing.T) { func TestParseMetadata(t *testing.T) {
t.Run("test valid configuration", func(t *testing.T) { t.Run("test valid configuration", func(t *testing.T) {
props := map[string]string{connectionString: "fake", consumerGroup: "mygroup", storageAccountName: "account", storageAccountKey: "key", storageContainerName: "container"} props := map[string]string{connectionString: "fake", consumerGroup: "mygroup", storageAccountName: "account", storageAccountKey: "key", storageContainerName: "container"}
bindingsMetadata := bindings.Metadata{Properties: props} bindingsMetadata := bindings.Metadata{Base: metadata.Base{Properties: props}}
m, err := parseMetadata(bindingsMetadata) m, err := parseMetadata(bindingsMetadata)
@ -77,7 +113,7 @@ func TestParseMetadata(t *testing.T) {
for _, c := range invalidConfigTestCases { for _, c := range invalidConfigTestCases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
bindingsMetadata := bindings.Metadata{Properties: c.config} bindingsMetadata := bindings.Metadata{Base: metadata.Base{Properties: c.config}}
_, err := parseMetadata(bindingsMetadata) _, err := parseMetadata(bindingsMetadata)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, err.Error(), c.errMsg) assert.Equal(t, err.Error(), c.errMsg)

View File

@ -10,7 +10,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
contrib_metadata "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/internal/utils"
contribMetadata "github.com/dapr/components-contrib/metadata"
) )
type serviceBusQueuesMetadata struct { type serviceBusQueuesMetadata struct {
@ -25,6 +26,7 @@ type serviceBusQueuesMetadata struct {
LockRenewalInSec int `json:"lockRenewalInSec"` LockRenewalInSec int `json:"lockRenewalInSec"`
MaxConcurrentHandlers int `json:"maxConcurrentHandlers"` MaxConcurrentHandlers int `json:"maxConcurrentHandlers"`
ttl time.Duration ttl time.Duration
DisableEntityManagement bool `json:"disableEntityManagement"`
} }
const ( const (
@ -39,6 +41,7 @@ const (
maxActiveMessages = "maxActiveMessages" maxActiveMessages = "maxActiveMessages"
lockRenewalInSec = "lockRenewalInSec" lockRenewalInSec = "lockRenewalInSec"
maxConcurrentHandlers = "maxConcurrentHandlers" maxConcurrentHandlers = "maxConcurrentHandlers"
disableEntityManagement = "disableEntityManagement"
// Default time to live for queues, which is 14 days. The same way Azure Portal does. // Default time to live for queues, which is 14 days. The same way Azure Portal does.
defaultMessageTimeToLive = time.Hour * 24 * 14 defaultMessageTimeToLive = time.Hour * 24 * 14
@ -64,6 +67,9 @@ const (
// Default rate of retriable errors per second // Default rate of retriable errors per second
defaultMaxRetriableErrorsPerSec = 10 defaultMaxRetriableErrorsPerSec = 10
// By default entity management is enabled
defaultDisableEntityManagement = false
errorMessagePrefix = "azure service bus error:" errorMessagePrefix = "azure service bus error:"
) )
@ -82,7 +88,7 @@ func (a *AzureServiceBusQueues) parseMetadata(metadata bindings.Metadata) (*serv
return nil, errors.New("connectionString and namespaceName are mutually exclusive") return nil, errors.New("connectionString and namespaceName are mutually exclusive")
} }
ttl, ok, err := contrib_metadata.TryGetTTL(metadata.Properties) ttl, ok, err := contribMetadata.TryGetTTL(metadata.Properties)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -162,5 +168,10 @@ func (a *AzureServiceBusQueues) parseMetadata(metadata bindings.Metadata) (*serv
m.MaxRetriableErrorsPerSec = to.Ptr(mRetriableErrorsPerSec) m.MaxRetriableErrorsPerSec = to.Ptr(mRetriableErrorsPerSec)
} }
m.DisableEntityManagement = defaultDisableEntityManagement
if val, ok := metadata.Properties[disableEntityManagement]; ok && val != "" {
m.DisableEntityManagement = utils.IsTruthy(val)
}
return &m, nil return &m, nil
} }

View File

@ -103,7 +103,7 @@ func TestParseMetadata(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
m := bindings.Metadata{} m := bindings.Metadata{}
m.Properties = tt.properties m.Properties = tt.properties
a := NewAzureServiceBusQueues(logger.NewLogger("test")) a := NewAzureServiceBusQueues(logger.NewLogger("test")).(*AzureServiceBusQueues)
meta, err := a.parseMetadata(m) meta, err := a.parseMetadata(m)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, tt.expectedConnectionString, meta.ConnectionString) assert.Equal(t, tt.expectedConnectionString, meta.ConnectionString)
@ -137,7 +137,7 @@ func TestParseMetadataWithInvalidTTL(t *testing.T) {
m := bindings.Metadata{} m := bindings.Metadata{}
m.Properties = tt.properties m.Properties = tt.properties
a := NewAzureServiceBusQueues(logger.NewLogger("test")) a := NewAzureServiceBusQueues(logger.NewLogger("test")).(*AzureServiceBusQueues)
_, err := a.parseMetadata(m) _, err := a.parseMetadata(m)
assert.NotNil(t, err) assert.NotNil(t, err)
}) })
@ -183,7 +183,7 @@ func TestParseMetadataConnectionStringAndNamespaceNameExclusivity(t *testing.T)
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
m := bindings.Metadata{} m := bindings.Metadata{}
m.Properties = tt.properties m.Properties = tt.properties
a := NewAzureServiceBusQueues(logger.NewLogger("test")) a := NewAzureServiceBusQueues(logger.NewLogger("test")).(*AzureServiceBusQueues)
meta, err := a.parseMetadata(m) meta, err := a.parseMetadata(m)
if tt.expectedErr { if tt.expectedErr {
assert.NotNil(t, err) assert.NotNil(t, err)

View File

@ -28,7 +28,7 @@ import (
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
azauth "github.com/dapr/components-contrib/internal/authentication/azure" azauth "github.com/dapr/components-contrib/internal/authentication/azure"
impl "github.com/dapr/components-contrib/internal/component/azure/servicebus" impl "github.com/dapr/components-contrib/internal/component/azure/servicebus"
contrib_metadata "github.com/dapr/components-contrib/metadata" contribMetadata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -52,7 +52,7 @@ type AzureServiceBusQueues struct {
} }
// NewAzureServiceBusQueues returns a new AzureServiceBusQueues instance. // NewAzureServiceBusQueues returns a new AzureServiceBusQueues instance.
func NewAzureServiceBusQueues(logger logger.Logger) *AzureServiceBusQueues { func NewAzureServiceBusQueues(logger logger.Logger) bindings.InputOutputBinding {
return &AzureServiceBusQueues{ return &AzureServiceBusQueues{
senderLock: sync.RWMutex{}, senderLock: sync.RWMutex{},
logger: logger, logger: logger,
@ -76,9 +76,11 @@ func (a *AzureServiceBusQueues) Init(metadata bindings.Metadata) (err error) {
return err return err
} }
a.adminClient, err = sbadmin.NewClientFromConnectionString(a.metadata.ConnectionString, nil) if !a.metadata.DisableEntityManagement {
if err != nil { a.adminClient, err = sbadmin.NewClientFromConnectionString(a.metadata.ConnectionString, nil)
return err if err != nil {
return err
}
} }
} else { } else {
settings, innerErr := azauth.NewEnvironmentSettings(azauth.AzureServiceBusResourceName, metadata.Properties) settings, innerErr := azauth.NewEnvironmentSettings(azauth.AzureServiceBusResourceName, metadata.Properties)
@ -98,37 +100,40 @@ func (a *AzureServiceBusQueues) Init(metadata bindings.Metadata) (err error) {
return innerErr return innerErr
} }
a.adminClient, innerErr = sbadmin.NewClient(a.metadata.NamespaceName, token, nil) if !a.metadata.DisableEntityManagement {
if innerErr != nil { a.adminClient, innerErr = sbadmin.NewClient(a.metadata.NamespaceName, token, nil)
return innerErr if innerErr != nil {
return innerErr
}
} }
} }
ctx, cancel := context.WithTimeout(context.Background(), a.timeout) if a.adminClient != nil {
defer cancel()
getQueueRes, err := a.adminClient.GetQueue(ctx, a.metadata.QueueName, nil)
if err != nil {
return err
}
if getQueueRes == nil {
// Need to create the queue
ttlDur := contrib_metadata.Duration{
Duration: a.metadata.ttl,
}
ctx, cancel := context.WithTimeout(context.Background(), a.timeout) ctx, cancel := context.WithTimeout(context.Background(), a.timeout)
defer cancel() defer cancel()
_, err = a.adminClient.CreateQueue(ctx, a.metadata.QueueName, &sbadmin.CreateQueueOptions{ getQueueRes, err := a.adminClient.GetQueue(ctx, a.metadata.QueueName, nil)
Properties: &sbadmin.QueueProperties{
DefaultMessageTimeToLive: to.Ptr(ttlDur.ToISOString()),
},
})
if err != nil { if err != nil {
return err return err
} }
if getQueueRes == nil {
// Need to create the queue
ttlDur := contribMetadata.Duration{
Duration: a.metadata.ttl,
}
ctx, cancel := context.WithTimeout(context.Background(), a.timeout)
defer cancel()
_, err = a.adminClient.CreateQueue(ctx, a.metadata.QueueName, &sbadmin.CreateQueueOptions{
Properties: &sbadmin.QueueProperties{
DefaultMessageTimeToLive: to.Ptr(ttlDur.ToISOString()),
},
})
if err != nil {
return err
}
}
a.ctx, a.cancel = context.WithCancel(context.Background())
} }
a.ctx, a.cancel = context.WithCancel(context.Background())
return nil return nil
} }
@ -155,7 +160,8 @@ func (a *AzureServiceBusQueues) Invoke(ctx context.Context, req *bindings.Invoke
} }
msg := &servicebus.Message{ msg := &servicebus.Message{
Body: req.Data, Body: req.Data,
ApplicationProperties: make(map[string]interface{}),
} }
if val, ok := req.Metadata[id]; ok && val != "" { if val, ok := req.Metadata[id]; ok && val != "" {
msg.MessageID = &val msg.MessageID = &val
@ -163,7 +169,17 @@ func (a *AzureServiceBusQueues) Invoke(ctx context.Context, req *bindings.Invoke
if val, ok := req.Metadata[correlationID]; ok && val != "" { if val, ok := req.Metadata[correlationID]; ok && val != "" {
msg.CorrelationID = &val msg.CorrelationID = &val
} }
ttl, ok, err := contrib_metadata.TryGetTTL(req.Metadata)
// Include incoming metadata in the message to be used when it is read.
for k, v := range req.Metadata {
// Don't include the values that are saved in MessageID or CorrelationID.
if k == id || k == correlationID {
continue
}
msg.ApplicationProperties[k] = v
}
ttl, ok, err := contribMetadata.TryGetTTL(req.Metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -262,6 +278,13 @@ func (a *AzureServiceBusQueues) getHandlerFunc(handler bindings.Handler) impl.Ha
metadata[label] = *msg.Subject metadata[label] = *msg.Subject
} }
// Passthrough any custom metadata to the handler.
for key, val := range msg.ApplicationProperties {
if stringVal, ok := val.(string); ok {
metadata[key] = stringVal
}
}
_, err := handler(a.ctx, &bindings.ReadResponse{ _, err := handler(a.ctx, &bindings.ReadResponse{
Data: msg.Body, Data: msg.Body,
Metadata: metadata, Metadata: metadata,

View File

@ -17,7 +17,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -58,7 +58,7 @@ func init() {
} }
// NewSignalR creates a new output binding for Azure SignalR. // NewSignalR creates a new output binding for Azure SignalR.
func NewSignalR(logger logger.Logger) *SignalR { func NewSignalR(logger logger.Logger) bindings.OutputBinding {
return &SignalR{ return &SignalR{
logger: logger, logger: logger,
httpClient: httpClient, httpClient: httpClient,
@ -197,7 +197,7 @@ func (s *SignalR) resolveAPIURL(req *bindings.InvokeRequest) (string, error) {
} }
func (s *SignalR) sendMessageToSignalR(ctx context.Context, url string, token string, data []byte) error { func (s *SignalR) sendMessageToSignalR(ctx context.Context, url string, token string, data []byte) error {
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(data)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(data))
if err != nil { if err != nil {
return err return err
} }
@ -213,7 +213,7 @@ func (s *SignalR) sendMessageToSignalR(ctx context.Context, url string, token st
defer resp.Body.Close() defer resp.Body.Close()
// Read the body regardless to drain it and ensure the connection can be reused // Read the body regardless to drain it and ensure the connection can be reused
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }
@ -263,7 +263,8 @@ func (s *SignalR) getToken(ctx context.Context, url string) (token string, err e
} }
token = at.Token token = at.Token
} else { } else {
claims := &jwt.StandardClaims{ // TODO: Use jwt.RegisteredClaims instead
claims := &jwt.StandardClaims{ //nolint:staticcheck
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), ExpiresAt: time.Now().Add(15 * time.Minute).Unix(),
Audience: url, Audience: url,
} }

View File

@ -17,7 +17,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -186,7 +186,7 @@ func TestConfigurationValid(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := NewSignalR(logger.NewLogger("test")) s := NewSignalR(logger.NewLogger("test")).(*SignalR)
err := s.parseMetadata(tt.properties) err := s.parseMetadata(tt.properties)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, tt.expectedEndpoint, s.endpoint) assert.Equal(t, tt.expectedEndpoint, s.endpoint)
@ -256,7 +256,7 @@ func TestInvalidConfigurations(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := NewSignalR(logger.NewLogger("test")) s := NewSignalR(logger.NewLogger("test")).(*SignalR)
err := s.parseMetadata(tt.properties) err := s.parseMetadata(tt.properties)
assert.NotNil(t, err) assert.NotNil(t, err)
}) })
@ -284,10 +284,10 @@ func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
func TestWriteShouldFail(t *testing.T) { func TestWriteShouldFail(t *testing.T) {
httpTransport := &mockTransport{ httpTransport := &mockTransport{
response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(""))}, response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(""))},
} }
s := NewSignalR(logger.NewLogger("test")) s := NewSignalR(logger.NewLogger("test")).(*SignalR)
s.endpoint = "https://fake.service.signalr.net" s.endpoint = "https://fake.service.signalr.net"
s.accessKey = "G7+nIt9n48+iYSltPRf1v8kE+MupFfEt/9NSNTKOdzA=" s.accessKey = "G7+nIt9n48+iYSltPRf1v8kE+MupFfEt/9NSNTKOdzA="
s.httpClient = &http.Client{ s.httpClient = &http.Client{
@ -335,10 +335,10 @@ func TestWriteShouldFail(t *testing.T) {
func TestWriteShouldSucceed(t *testing.T) { func TestWriteShouldSucceed(t *testing.T) {
httpTransport := &mockTransport{ httpTransport := &mockTransport{
response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(""))}, response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(""))},
} }
s := NewSignalR(logger.NewLogger("test")) s := NewSignalR(logger.NewLogger("test")).(*SignalR)
s.endpoint = "https://fake.service.signalr.net" s.endpoint = "https://fake.service.signalr.net"
s.accessKey = "fakekey" s.accessKey = "fakekey"
s.httpClient = &http.Client{ s.httpClient = &http.Client{

View File

@ -26,8 +26,7 @@ import (
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
azauth "github.com/dapr/components-contrib/internal/authentication/azure" azauth "github.com/dapr/components-contrib/internal/authentication/azure"
"github.com/dapr/components-contrib/internal/utils" "github.com/dapr/components-contrib/internal/utils"
contrib_metadata "github.com/dapr/components-contrib/metadata" contribMetadata "github.com/dapr/components-contrib/metadata"
mdutils "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -179,7 +178,7 @@ type storageQueuesMetadata struct {
} }
// NewAzureStorageQueues returns a new AzureStorageQueues instance. // NewAzureStorageQueues returns a new AzureStorageQueues instance.
func NewAzureStorageQueues(logger logger.Logger) *AzureStorageQueues { func NewAzureStorageQueues(logger logger.Logger) bindings.InputOutputBinding {
return &AzureStorageQueues{helper: NewAzureQueueHelper(logger), logger: logger} return &AzureStorageQueues{helper: NewAzureQueueHelper(logger), logger: logger}
} }
@ -197,25 +196,25 @@ func parseMetadata(metadata bindings.Metadata) (*storageQueuesMetadata, error) {
var m storageQueuesMetadata var m storageQueuesMetadata
// AccountKey is parsed in azauth // AccountKey is parsed in azauth
if val, ok := mdutils.GetMetadataProperty(metadata.Properties, azauth.StorageAccountNameKeys...); ok && val != "" { if val, ok := contribMetadata.GetMetadataProperty(metadata.Properties, azauth.StorageAccountNameKeys...); ok && val != "" {
m.AccountName = val m.AccountName = val
} else { } else {
return nil, fmt.Errorf("missing or empty %s field from metadata", azauth.StorageAccountNameKeys[0]) return nil, fmt.Errorf("missing or empty %s field from metadata", azauth.StorageAccountNameKeys[0])
} }
if val, ok := mdutils.GetMetadataProperty(metadata.Properties, azauth.StorageQueueNameKeys...); ok && val != "" { if val, ok := contribMetadata.GetMetadataProperty(metadata.Properties, azauth.StorageQueueNameKeys...); ok && val != "" {
m.QueueName = val m.QueueName = val
} else { } else {
return nil, fmt.Errorf("missing or empty %s field from metadata", azauth.StorageQueueNameKeys[0]) return nil, fmt.Errorf("missing or empty %s field from metadata", azauth.StorageQueueNameKeys[0])
} }
if val, ok := mdutils.GetMetadataProperty(metadata.Properties, azauth.StorageEndpointKeys...); ok && val != "" { if val, ok := contribMetadata.GetMetadataProperty(metadata.Properties, azauth.StorageEndpointKeys...); ok && val != "" {
m.QueueEndpoint = val m.QueueEndpoint = val
} }
m.DecodeBase64 = utils.IsTruthy(metadata.Properties["decodeBase64"]) m.DecodeBase64 = utils.IsTruthy(metadata.Properties["decodeBase64"])
ttl, ok, err := contrib_metadata.TryGetTTL(metadata.Properties) ttl, ok, err := contribMetadata.TryGetTTL(metadata.Properties)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -232,7 +231,7 @@ func (a *AzureStorageQueues) Operations() []bindings.OperationKind {
func (a *AzureStorageQueues) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { func (a *AzureStorageQueues) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
ttlToUse := a.metadata.ttl ttlToUse := a.metadata.ttl
ttl, ok, err := contrib_metadata.TryGetTTL(req.Metadata) ttl, ok, err := contribMetadata.TryGetTTL(req.Metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -287,7 +287,7 @@ func TestReadQueueNoMessage(t *testing.T) {
} }
func TestParseMetadata(t *testing.T) { func TestParseMetadata(t *testing.T) {
var oneSecondDuration time.Duration = time.Second oneSecondDuration := time.Second
testCases := []struct { testCases := []struct {
name string name string
@ -295,7 +295,7 @@ func TestParseMetadata(t *testing.T) {
// Account key is parsed in azauth // Account key is parsed in azauth
// expectedAccountKey string // expectedAccountKey string
expectedQueueName string expectedQueueName string
expectedQueueEndpointUrl string expectedQueueEndpointURL string
expectedTTL *time.Duration expectedTTL *time.Duration
}{ }{
{ {
@ -303,21 +303,21 @@ func TestParseMetadata(t *testing.T) {
properties: map[string]string{"storageAccessKey": "myKey", "queue": "queue1", "storageAccount": "devstoreaccount1"}, properties: map[string]string{"storageAccessKey": "myKey", "queue": "queue1", "storageAccount": "devstoreaccount1"},
// expectedAccountKey: "myKey", // expectedAccountKey: "myKey",
expectedQueueName: "queue1", expectedQueueName: "queue1",
expectedQueueEndpointUrl: "", expectedQueueEndpointURL: "",
}, },
{ {
name: "Accout, key, and endpoint", name: "Accout, key, and endpoint",
properties: map[string]string{"accountKey": "myKey", "queueName": "queue1", "storageAccount": "someAccount", "queueEndpointUrl": "https://foo.example.com:10001"}, properties: map[string]string{"accountKey": "myKey", "queueName": "queue1", "storageAccount": "someAccount", "queueEndpointUrl": "https://foo.example.com:10001"},
// expectedAccountKey: "myKey", // expectedAccountKey: "myKey",
expectedQueueName: "queue1", expectedQueueName: "queue1",
expectedQueueEndpointUrl: "https://foo.example.com:10001", expectedQueueEndpointURL: "https://foo.example.com:10001",
}, },
{ {
name: "Empty TTL", name: "Empty TTL",
properties: map[string]string{"storageAccessKey": "myKey", "queue": "queue1", "storageAccount": "devstoreaccount1", metadata.TTLMetadataKey: ""}, properties: map[string]string{"storageAccessKey": "myKey", "queue": "queue1", "storageAccount": "devstoreaccount1", metadata.TTLMetadataKey: ""},
// expectedAccountKey: "myKey", // expectedAccountKey: "myKey",
expectedQueueName: "queue1", expectedQueueName: "queue1",
expectedQueueEndpointUrl: "", expectedQueueEndpointURL: "",
}, },
{ {
name: "With TTL", name: "With TTL",
@ -325,7 +325,7 @@ func TestParseMetadata(t *testing.T) {
// expectedAccountKey: "myKey", // expectedAccountKey: "myKey",
expectedQueueName: "queue1", expectedQueueName: "queue1",
expectedTTL: &oneSecondDuration, expectedTTL: &oneSecondDuration,
expectedQueueEndpointUrl: "", expectedQueueEndpointURL: "",
}, },
} }
@ -340,7 +340,7 @@ func TestParseMetadata(t *testing.T) {
// assert.Equal(t, tt.expectedAccountKey, meta.AccountKey) // assert.Equal(t, tt.expectedAccountKey, meta.AccountKey)
assert.Equal(t, tt.expectedQueueName, meta.QueueName) assert.Equal(t, tt.expectedQueueName, meta.QueueName)
assert.Equal(t, tt.expectedTTL, meta.ttl) assert.Equal(t, tt.expectedTTL, meta.ttl)
assert.Equal(t, tt.expectedQueueEndpointUrl, meta.QueueEndpoint) assert.Equal(t, tt.expectedQueueEndpointURL, meta.QueueEndpoint)
}) })
} }
} }

20
bindings/bindings.go Normal file
View File

@ -0,0 +1,20 @@
/*
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 bindings
// InputOutputBinding is the interface for bindings that implement both input and output bindings.
type InputOutputBinding interface {
InputBinding
OutputBinding
}

View File

@ -43,7 +43,7 @@ type commercetoolsMetadata struct {
scopes string scopes string
} }
func NewCommercetools(logger logger.Logger) *Binding { func NewCommercetools(logger logger.Logger) bindings.OutputBinding {
return &Binding{logger: logger} return &Binding{logger: logger}
} }

View File

@ -36,7 +36,7 @@ type Binding struct {
} }
// NewCron returns a new Cron event input binding. // NewCron returns a new Cron event input binding.
func NewCron(logger logger.Logger) *Binding { func NewCron(logger logger.Logger) bindings.InputOutputBinding {
return &Binding{ return &Binding{
logger: logger, logger: logger,
parser: cron.NewParser( parser: cron.NewParser(
@ -47,8 +47,9 @@ func NewCron(logger logger.Logger) *Binding {
// Init initializes the Cron binding // Init initializes the Cron binding
// Examples from https://godoc.org/github.com/robfig/cron: // Examples from https://godoc.org/github.com/robfig/cron:
// "15 * * * * *" - Every 15 sec //
// "0 30 * * * *" - Every 30 min // "15 * * * * *" - Every 15 sec
// "0 30 * * * *" - Every 30 min
func (b *Binding) Init(metadata bindings.Metadata) error { func (b *Binding) Init(metadata bindings.Metadata) error {
b.name = metadata.Name b.name = metadata.Name
s, f := metadata.Properties["schedule"] s, f := metadata.Properties["schedule"]
@ -85,12 +86,12 @@ func (b *Binding) Read(ctx context.Context, handler bindings.Handler) error {
b.logger.Debugf("name: %s, next run: %v", b.name, time.Until(c.Entry(id).Next)) b.logger.Debugf("name: %s, next run: %v", b.name, time.Until(c.Entry(id).Next))
go func() { go func() {
// Wait for a context to be canceled or a message on the stopCh // Wait for a context to be canceled
select { select {
case <-b.runningCtx.Done(): case <-b.runningCtx.Done():
// Do nothing // Do nothing
case <-ctx.Done(): case <-ctx.Done():
b.runningCancel() b.resetContext()
} }
b.logger.Debugf("name: %s, stopping schedule: %s", b.name, b.schedule) b.logger.Debugf("name: %s, stopping schedule: %s", b.name, b.schedule)
c.Stop() c.Stop()

View File

@ -40,7 +40,7 @@ func getNewCron() *Binding {
l.SetOutputLevel(logger.DebugLevel) l.SetOutputLevel(logger.DebugLevel)
} }
return NewCron(l) return NewCron(l).(*Binding)
} }
// go test -v -timeout 15s -count=1 ./bindings/cron/. // go test -v -timeout 15s -count=1 ./bindings/cron/.

View File

@ -20,7 +20,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/url" "net/url"
"strconv" "strconv"
@ -79,7 +78,7 @@ type createResponse struct {
} }
// NewGCPStorage returns a new GCP storage instance. // NewGCPStorage returns a new GCP storage instance.
func NewGCPStorage(logger logger.Logger) *GCPStorage { func NewGCPStorage(logger logger.Logger) bindings.OutputBinding {
return &GCPStorage{logger: logger} return &GCPStorage{logger: logger}
} }
@ -214,9 +213,9 @@ func (g *GCPStorage) get(ctx context.Context, req *bindings.InvokeRequest) (*bin
} }
defer rc.Close() defer rc.Close()
data, err := ioutil.ReadAll(rc) data, err := io.ReadAll(rc)
if err != nil { if err != nil {
return nil, fmt.Errorf("gcp bucketgcp bucket binding error: ioutil.ReadAll: %v", err) return nil, fmt.Errorf("gcp bucketgcp bucket binding error: io.ReadAll: %v", err)
} }
if metadata.EncodeBase64 { if metadata.EncodeBase64 {

View File

@ -54,7 +54,7 @@ type pubSubMetadata struct {
} }
// NewGCPPubSub returns a new GCPPubSub instance. // NewGCPPubSub returns a new GCPPubSub instance.
func NewGCPPubSub(logger logger.Logger) *GCPPubSub { func NewGCPPubSub(logger logger.Logger) bindings.InputOutputBinding {
return &GCPPubSub{logger: logger} return &GCPPubSub{logger: logger}
} }

View File

@ -53,7 +53,7 @@ type GraphQL struct {
} }
// NewGraphQL returns a new GraphQL binding instance. // NewGraphQL returns a new GraphQL binding instance.
func NewGraphQL(logger logger.Logger) *GraphQL { func NewGraphQL(logger logger.Logger) bindings.OutputBinding {
return &GraphQL{logger: logger} return &GraphQL{logger: logger}
} }
@ -112,7 +112,7 @@ func (gql *GraphQL) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*b
var graphqlResponse interface{} var graphqlResponse interface{}
switch req.Operation { // nolint: exhaustive switch req.Operation { //nolint:exhaustive
case QueryOperation: case QueryOperation:
if err := gql.runRequest(ctx, commandQuery, req, &graphqlResponse); err != nil { if err := gql.runRequest(ctx, commandQuery, req, &graphqlResponse); err != nil {
return nil, err return nil, err

View File

@ -18,7 +18,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -33,6 +32,7 @@ import (
) )
// HTTPSource is a binding for an http url endpoint invocation // HTTPSource is a binding for an http url endpoint invocation
//
//revive:disable-next-line //revive:disable-next-line
type HTTPSource struct { type HTTPSource struct {
metadata httpMetadata metadata httpMetadata
@ -46,7 +46,7 @@ type httpMetadata struct {
} }
// NewHTTP returns a new HTTPSource. // NewHTTP returns a new HTTPSource.
func NewHTTP(logger logger.Logger) *HTTPSource { func NewHTTP(logger logger.Logger) bindings.OutputBinding {
return &HTTPSource{logger: logger} return &HTTPSource{logger: logger}
} }
@ -148,7 +148,7 @@ func (h *HTTPSource) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*
// Read the response body. For empty responses (e.g. 204 No Content) // Read the response body. For empty responses (e.g. 204 No Content)
// `b` will be an empty slice. // `b` will be an empty slice.
b, err := ioutil.ReadAll(resp.Body) b, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -15,7 +15,7 @@ package http_test
import ( import (
"context" "context"
"io/ioutil" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@ -25,12 +25,13 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
binding_http "github.com/dapr/components-contrib/bindings/http" bindingHttp "github.com/dapr/components-contrib/bindings/http"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
func TestOperations(t *testing.T) { func TestOperations(t *testing.T) {
opers := (*binding_http.HTTPSource)(nil).Operations() opers := (*bindingHttp.HTTPSource)(nil).Operations()
assert.Equal(t, []bindings.OperationKind{ assert.Equal(t, []bindings.OperationKind{
bindings.CreateOperation, bindings.CreateOperation,
"get", "get",
@ -53,7 +54,7 @@ func TestInit(t *testing.T) {
input := req.Method input := req.Method
if req.Body != nil { if req.Body != nil {
defer req.Body.Close() defer req.Body.Close()
b, _ := ioutil.ReadAll(req.Body) b, _ := io.ReadAll(req.Body)
if len(b) > 0 { if len(b) > 0 {
input = string(b) input = string(b)
} }
@ -64,19 +65,19 @@ func TestInit(t *testing.T) {
} }
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
if input == "internal server error" { if input == "internal server error" {
w.WriteHeader(500) w.WriteHeader(http.StatusInternalServerError)
} }
w.Write([]byte(strings.ToUpper(input))) w.Write([]byte(strings.ToUpper(input)))
}), }),
) )
defer s.Close() defer s.Close()
m := bindings.Metadata{ m := bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{ Properties: map[string]string{
"url": s.URL, "url": s.URL,
}, },
} }}
hs := binding_http.NewHTTP(logger.NewLogger("test")) hs := bindingHttp.NewHTTP(logger.NewLogger("test"))
err := hs.Init(m) err := hs.Init(m)
require.NoError(t, err) require.NoError(t, err)

View File

@ -18,7 +18,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io"
"strconv" "strconv"
"github.com/google/uuid" "github.com/google/uuid"
@ -70,7 +70,7 @@ type listPayload struct {
} }
// NewHuaweiOBS returns a new Huawei OBS instance. // NewHuaweiOBS returns a new Huawei OBS instance.
func NewHuaweiOBS(logger logger.Logger) *HuaweiOBS { func NewHuaweiOBS(logger logger.Logger) bindings.OutputBinding {
return &HuaweiOBS{logger: logger} return &HuaweiOBS{logger: logger}
} }
@ -228,7 +228,7 @@ func (o *HuaweiOBS) get(ctx context.Context, req *bindings.InvokeRequest) (*bind
} }
}() }()
data, err := ioutil.ReadAll(out.Body) data, err := io.ReadAll(out.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("obs binding error. error reading obs object content: %w", err) return nil, fmt.Errorf("obs binding error. error reading obs object content: %w", err)
} }

View File

@ -18,7 +18,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"strings" "strings"
"testing" "testing"
"testing/iotest" "testing/iotest"
@ -61,7 +61,7 @@ func (m *MockHuaweiOBSService) ListObjects(ctx context.Context, input *obs.ListO
} }
func TestParseMetadata(t *testing.T) { func TestParseMetadata(t *testing.T) {
obs := NewHuaweiOBS(logger.NewLogger("test")) obs := NewHuaweiOBS(logger.NewLogger("test")).(*HuaweiOBS)
t.Run("Has correct metadata", func(t *testing.T) { t.Run("Has correct metadata", func(t *testing.T) {
m := bindings.Metadata{} m := bindings.Metadata{}
@ -374,7 +374,7 @@ func TestGetOperation(t *testing.T) {
}, },
Metadata: map[string]string{}, Metadata: map[string]string{},
}, },
Body: ioutil.NopCloser(strings.NewReader("Hello Dapr")), Body: io.NopCloser(strings.NewReader("Hello Dapr")),
}, nil }, nil
}, },
}, },
@ -447,7 +447,7 @@ func TestGetOperation(t *testing.T) {
}, },
Metadata: map[string]string{}, Metadata: map[string]string{},
}, },
Body: ioutil.NopCloser(iotest.ErrReader(errors.New("unexpected data reading error"))), Body: io.NopCloser(iotest.ErrReader(errors.New("unexpected data reading error"))),
}, nil }, nil
}, },
}, },
@ -667,7 +667,7 @@ func TestInvoke(t *testing.T) {
}, },
Metadata: map[string]string{}, Metadata: map[string]string{},
}, },
Body: ioutil.NopCloser(strings.NewReader("Hello Dapr")), Body: io.NopCloser(strings.NewReader("Hello Dapr")),
}, nil }, nil
}, },
}, },

View File

@ -59,7 +59,7 @@ type influxMetadata struct {
} }
// NewInflux returns a new kafka binding instance. // NewInflux returns a new kafka binding instance.
func NewInflux(logger logger.Logger) *Influx { func NewInflux(logger logger.Logger) bindings.OutputBinding {
return &Influx{logger: logger} return &Influx{logger: logger}
} }

View File

@ -22,6 +22,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -46,13 +47,13 @@ func TestOperations(t *testing.T) {
} }
func TestInflux_Init(t *testing.T) { func TestInflux_Init(t *testing.T) {
influx := NewInflux(logger.NewLogger("test")) influx := NewInflux(logger.NewLogger("test")).(*Influx)
assert.Nil(t, influx.queryAPI) assert.Nil(t, influx.queryAPI)
assert.Nil(t, influx.writeAPI) assert.Nil(t, influx.writeAPI)
assert.Nil(t, influx.metadata) assert.Nil(t, influx.metadata)
assert.Nil(t, influx.client) assert.Nil(t, influx.client)
m := bindings.Metadata{Properties: map[string]string{"Url": "a", "Token": "a", "Org": "a", "Bucket": "a"}} m := bindings.Metadata{Base: metadata.Base{Properties: map[string]string{"Url": "a", "Token": "a", "Org": "a", "Bucket": "a"}}}
err := influx.Init(m) err := influx.Init(m)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -36,6 +36,6 @@ func PingInpBinding(inputBinding InputBinding) error {
if inputBindingWithPing, ok := inputBinding.(health.Pinger); ok { if inputBindingWithPing, ok := inputBinding.(health.Pinger); ok {
return inputBindingWithPing.Ping() return inputBindingWithPing.Ping()
} else { } else {
return fmt.Errorf("Ping is not implemented by this input binding") return fmt.Errorf("ping is not implemented by this input binding")
} }
} }

254
bindings/ipfs/ipfs.go Normal file
View File

@ -0,0 +1,254 @@
/*
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 ipfs
import (
"context"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
ipfsHttpclient "github.com/ipfs/go-ipfs-http-client"
ipfsIcore "github.com/ipfs/interface-go-ipfs-core"
ipfsConfig "github.com/ipfs/kubo/config"
ipfsCore "github.com/ipfs/kubo/core"
ipfsCoreapi "github.com/ipfs/kubo/core/coreapi"
ipfsLibp2p "github.com/ipfs/kubo/core/node/libp2p"
ipfsLoader "github.com/ipfs/kubo/plugin/loader"
ipfsRepo "github.com/ipfs/kubo/repo"
ipfsFsrepo "github.com/ipfs/kubo/repo/fsrepo"
"github.com/multiformats/go-multiaddr"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/kit/logger"
)
const swarmKeyFile = "swarm.key"
var (
loadPluginsOnce sync.Once
httpClient *http.Client
)
func init() {
httpClient = &http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
}).Dial,
},
}
}
// IPFSBinding is a binding for interacting with an IPFS network.
type IPFSBinding struct {
metadata ipfsMetadata
ipfsAPI ipfsIcore.CoreAPI
ipfsNode *ipfsCore.IpfsNode
ipfsRepo ipfsRepo.Repo
ctx context.Context
cancel context.CancelFunc
logger logger.Logger
}
// NewIPFSBinding returns a new IPFSBinding.
func NewIPFSBinding(logger logger.Logger) bindings.OutputBinding {
return &IPFSBinding{
logger: logger,
}
}
// Init the binding.
func (b *IPFSBinding) Init(metadata bindings.Metadata) (err error) {
b.ctx, b.cancel = context.WithCancel(context.Background())
err = b.metadata.FromMap(metadata.Properties)
if err != nil {
return err
}
if b.metadata.ExternalAPI == "" {
var onceErr error
loadPluginsOnce.Do(func() {
onceErr = setupPlugins("")
})
if onceErr != nil {
return onceErr
}
err = b.createNode()
if err != nil {
return fmt.Errorf("failed to start IPFS node: %v", err)
}
} else {
if b.metadata.ExternalAPI[0] == '/' {
var maddr multiaddr.Multiaddr
maddr, err = multiaddr.NewMultiaddr(b.metadata.ExternalAPI)
if err != nil {
return fmt.Errorf("failed to parse external API multiaddr: %v", err)
}
b.ipfsAPI, err = ipfsHttpclient.NewApiWithClient(maddr, httpClient)
} else {
b.ipfsAPI, err = ipfsHttpclient.NewURLApiWithClient(b.metadata.ExternalAPI, httpClient)
}
if err != nil {
return fmt.Errorf("failed to initialize external IPFS API: %v", err)
}
b.logger.Infof("Using IPFS APIs at %s", b.metadata.ExternalAPI)
}
return nil
}
func (b *IPFSBinding) Close() (err error) {
if b.cancel != nil {
b.cancel()
b.cancel = nil
}
if b.ipfsNode != nil {
err = b.ipfsNode.Close()
if err != nil {
b.logger.Errorf("Error while closing IPFS node: %v", err)
}
b.ipfsNode = nil
}
if b.ipfsRepo != nil {
err = b.ipfsRepo.Close()
if err != nil {
b.logger.Errorf("Error while closing IPFS repo: %v", err)
}
b.ipfsRepo = nil
}
return nil
}
func (b *IPFSBinding) createNode() (err error) {
// Init the repo if needed
if !ipfsFsrepo.IsInitialized(b.metadata.RepoPath) {
var cfg *ipfsConfig.Config
cfg, err = b.metadata.IPFSConfig()
if err != nil {
return err
}
err = ipfsFsrepo.Init(b.metadata.RepoPath, cfg)
if err != nil {
return err
}
if b.metadata.SwarmKey != "" {
skPath := filepath.Join(b.metadata.RepoPath, swarmKeyFile)
err = os.WriteFile(skPath, []byte(b.metadata.SwarmKey), 0o600)
if err != nil {
return fmt.Errorf("error writing swarm key to file '%s': %v", skPath, err)
}
}
b.logger.Infof("Initialized a new IPFS repo at path %s", b.metadata.RepoPath)
}
// Open the repo
repo, err := ipfsFsrepo.Open(b.metadata.RepoPath)
if err != nil {
return err
}
b.logger.Infof("Opened IPFS repo at path %s", b.metadata.RepoPath)
// Create the node
nodeOptions := &ipfsCore.BuildCfg{
Online: true,
Repo: repo,
}
r := strings.ToLower(b.metadata.Routing)
switch r {
case "", "dht":
nodeOptions.Routing = ipfsLibp2p.DHTOption
case "dhtclient":
nodeOptions.Routing = ipfsLibp2p.DHTClientOption
case "dhtserver":
nodeOptions.Routing = ipfsLibp2p.DHTServerOption
case "none":
nodeOptions.Routing = ipfsLibp2p.NilRouterOption
default:
return fmt.Errorf("invalid value for metadata property 'routing'")
}
b.ipfsNode, err = ipfsCore.NewNode(b.ctx, nodeOptions)
if err != nil {
return err
}
b.logger.Infof("Started IPFS node %s", b.ipfsNode.Identity)
// Init API
b.ipfsAPI, err = ipfsCoreapi.NewCoreAPI(b.ipfsNode)
if err != nil {
return err
}
return nil
}
// Operations returns the supported operations for this binding.
func (b *IPFSBinding) Operations() []bindings.OperationKind {
return []bindings.OperationKind{
bindings.GetOperation,
bindings.CreateOperation, // alias for "add"
bindings.ListOperation, // alias for "ls"
bindings.DeleteOperation, // alias for "pin-rm"
"add",
"ls",
"pin-add",
"pin-rm",
"pin-ls",
}
}
// Invoke performs an HTTP request to the configured HTTP endpoint.
func (b *IPFSBinding) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
switch req.Operation {
case bindings.GetOperation:
return b.getOperation(ctx, req)
case "add", bindings.CreateOperation:
return b.addOperation(ctx, req)
case "ls", bindings.ListOperation:
return b.lsOperation(ctx, req)
case "pin-add":
return b.pinAddOperation(ctx, req)
case "pin-ls":
return b.pinLsOperation(ctx, req)
case "pin-rm", bindings.DeleteOperation:
return b.pinRmOperation(ctx, req)
}
return &bindings.InvokeResponse{
Data: nil,
Metadata: nil,
}, nil
}
func setupPlugins(externalPluginsPath string) error {
plugins, err := ipfsLoader.NewPluginLoader("")
if err != nil {
return fmt.Errorf("error loading plugins: %s", err)
}
if err := plugins.Initialize(); err != nil {
return fmt.Errorf("error initializing plugins: %s", err)
}
if err := plugins.Inject(); err != nil {
return fmt.Errorf("error initializing plugins: %s", err)
}
return nil
}

425
bindings/ipfs/ipfs_test.go Normal file
View File

@ -0,0 +1,425 @@
/*
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 ipfs
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"log"
"os"
"reflect"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/internal/utils"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger"
)
func TestMain(m *testing.M) {
if !utils.IsTruthy(os.Getenv("IPFS_TEST")) {
log.Println("IPFS_TEST env var is not set to a truthy value; skipping tests")
os.Exit(0)
}
os.Exit(m.Run())
}
func TestSingleNodeGlobalNetwork(t *testing.T) {
var b *IPFSBinding
repoPath := t.TempDir()
// CIDS contained in the QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG folder
folderCids := []string{
"QmZTR5bcpQD7cFgTorqxZDYaew1Wqgfbd2ud9QqGPAkK2V",
"QmYCvbfNbCwFR45HiNP45rwJgvatpiW38D961L5qAhUM5Y",
"QmY5heUM5qgRubMDD1og9fhCPA6QdkMp3QCwd4s7gJsyE7",
"QmdncfsVm2h5Kqq9hPmU7oAVX2zTSVP3L869tgTbPYnsha",
"QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB",
"QmTumTjvcYCAvRRwQ8sDRxh8ezmrcr88YFU7iYNroGGTBZ",
}
sort.Strings(folderCids)
t.Run("init node", func(t *testing.T) {
b = NewIPFSBinding(logger.NewLogger("tests")).(*IPFSBinding)
err := b.Init(bindings.Metadata{Base: metadata.Base{
Properties: map[string]string{
"repoPath": repoPath,
"routing": "dhtclient",
},
}})
require.NoError(t, err)
})
t.Run("get operation", func(t *testing.T) {
t.Run("empty path", func(t *testing.T) {
_, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "get",
})
if assert.Error(t, err) {
assert.Equal(t, err.Error(), "metadata property 'path' is empty")
}
})
t.Run("retrieve document by CID", func(t *testing.T) {
data := getDocument(t, b, "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB")
compareHash(t, data, "a48161fca5edd15f4649bb928c10769216fccdf317265fc75d747c1e6892f53c")
})
t.Run("retrieve document by IPLD", func(t *testing.T) {
// Same document, but different addressing method
data := getDocument(t, b, "/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/readme")
compareHash(t, data, "a48161fca5edd15f4649bb928c10769216fccdf317265fc75d747c1e6892f53c")
})
t.Run("cannot retrieve folder", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := b.Invoke(ctx, &bindings.InvokeRequest{
Operation: "get",
Metadata: map[string]string{
"path": "/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
},
})
if assert.Error(t, err) {
assert.Equal(t, err.Error(), "path does not represent a file")
}
})
// Retrieve files also to speed up pinning later
t.Run("retrieve files", func(t *testing.T) {
for _, e := range folderCids {
getDocument(t, b, e)
}
getDocument(t, b, "QmdytmR4wULMd3SLo6ePF4s3WcRHWcpnJZ7bHhoj3QB13v")
})
})
t.Run("ls operation", func(t *testing.T) {
t.Run("empty path", func(t *testing.T) {
_, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "ls",
})
if assert.Error(t, err) {
assert.Equal(t, err.Error(), "metadata property 'path' is empty")
}
})
testLsOperationResponse := func(t *testing.T, list lsOperationResponse) {
cids := make([]string, len(list))
for i, e := range list {
cids[i] = e.Cid
}
sort.Strings(cids)
assert.True(t, reflect.DeepEqual(cids, folderCids), "received='%v' expected='%v'", cids, folderCids)
}
t.Run("list by CID", func(t *testing.T) {
list := listPath(t, b, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG")
require.NotEmpty(t, list)
testLsOperationResponse(t, list)
})
t.Run("list by IPLD", func(t *testing.T) {
list := listPath(t, b, "/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG")
require.NotEmpty(t, list)
testLsOperationResponse(t, list)
})
})
t.Run("pin operations", func(t *testing.T) {
t.Run("pin-ls: nothing is pinned on a new node", func(t *testing.T) {
list := listPins(t, b)
assert.Empty(t, list)
})
t.Run("pin-add: pin file by CID", func(t *testing.T) {
res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "pin-add",
Metadata: map[string]string{
"path": "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB",
},
})
require.NoError(t, err)
require.NotNil(t, res)
require.Empty(t, res.Data)
})
t.Run("pin-ls: list added pin", func(t *testing.T) {
list := listPins(t, b)
assert.Len(t, list, 1)
assert.Equal(t, "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB", list[0].Cid)
})
t.Run("pin-add: pin file by IPLD", func(t *testing.T) {
res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "pin-add",
Metadata: map[string]string{
"path": "/ipfs/QmdytmR4wULMd3SLo6ePF4s3WcRHWcpnJZ7bHhoj3QB13v",
},
})
require.NoError(t, err)
require.NotNil(t, res)
require.Empty(t, res.Data)
})
t.Run("pin-add: recursively pin folder", func(t *testing.T) {
res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "pin-add",
Metadata: map[string]string{
"path": "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
},
})
require.NoError(t, err)
require.NotNil(t, res)
require.Empty(t, res.Data)
})
// Add the folder to the list of expected items
expect := make([]string, len(folderCids)+2)
expect[0] = "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"
expect[1] = "QmdytmR4wULMd3SLo6ePF4s3WcRHWcpnJZ7bHhoj3QB13v"
copy(expect[2:], folderCids)
sort.Strings(expect)
t.Run("pin-ls: list added folder", func(t *testing.T) {
cids := listPinnedCids(t, b)
assert.True(t, reflect.DeepEqual(cids, expect), "received='%v' expected='%v'", cids, expect)
})
t.Run("pin-rm: remove file", func(t *testing.T) {
res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "pin-rm",
Metadata: map[string]string{
"path": "QmdytmR4wULMd3SLo6ePF4s3WcRHWcpnJZ7bHhoj3QB13v",
},
})
require.NoError(t, err)
require.NotNil(t, res)
require.Empty(t, res.Data)
})
// Remove the un-pinned file
i := 0
for _, e := range expect {
if e != "QmdytmR4wULMd3SLo6ePF4s3WcRHWcpnJZ7bHhoj3QB13v" {
expect[i] = e
i++
}
}
expect = expect[:i]
t.Run("pin-ls: updated after removed file", func(t *testing.T) {
cids := listPinnedCids(t, b)
assert.True(t, reflect.DeepEqual(cids, expect), "received='%v' expected='%v'", cids, expect)
})
t.Run("pin-rm: recursively remove folder", func(t *testing.T) {
res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "pin-rm",
Metadata: map[string]string{
"path": "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
},
})
require.NoError(t, err)
require.NotNil(t, res)
require.Empty(t, res.Data)
})
t.Run("pin-rm: remove explicitly-pinned file", func(t *testing.T) {
res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "pin-rm",
Metadata: map[string]string{
"path": "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB",
},
})
require.NoError(t, err)
require.NotNil(t, res)
require.Empty(t, res.Data)
})
t.Run("pin-ls: updated list is empty", func(t *testing.T) {
list := listPins(t, b)
assert.Empty(t, list)
})
})
t.Run("create operation", func(t *testing.T) {
expectPins := []string{}
t.Run("add with default options", func(t *testing.T) {
path := addDocument(t, b,
[]byte("Quel ramo del lago di Como, che volge a mezzogiorno"),
nil,
)
assert.Equal(t, "/ipfs/QmRW7jvkePyaAFvtapaqZ9kNkziUrmkhi4ue5oNpXS2qUx", path)
expectPins = append(expectPins, "QmRW7jvkePyaAFvtapaqZ9kNkziUrmkhi4ue5oNpXS2qUx")
})
t.Run("add with CID v1", func(t *testing.T) {
path := addDocument(t, b,
[]byte("Quel ramo del lago di Como, che volge a mezzogiorno"),
map[string]string{"cidVersion": "1"},
)
assert.Equal(t, "/ipfs/bafkreidhuwuwgycmsbj4sesi3pm6vpxpbm6byt3twex7sc2nadaxksnqeq", path)
expectPins = append(expectPins, "bafkreidhuwuwgycmsbj4sesi3pm6vpxpbm6byt3twex7sc2nadaxksnqeq")
})
t.Run("added files are pinned", func(t *testing.T) {
cids := listPinnedCids(t, b)
assert.True(t, reflect.DeepEqual(cids, expectPins), "received='%v' expected='%v'", cids, expectPins)
})
t.Run("add without pinning", func(t *testing.T) {
path := addDocument(t, b,
[]byte("😁🐶"),
map[string]string{"pin": "false"},
)
assert.Equal(t, "/ipfs/QmWsLpV1UUD26qHaEJqXfHazSRRZVX82M51EQ87UT7ryiR", path)
})
t.Run("pinned documents haven't changed", func(t *testing.T) {
cids := listPinnedCids(t, b)
assert.True(t, reflect.DeepEqual(cids, expectPins), "received='%v' expected='%v'", cids, expectPins)
})
t.Run("add inline", func(t *testing.T) {
path := addDocument(t, b,
[]byte("😁🐶"),
map[string]string{"inline": "true"},
)
assert.Equal(t, "/ipfs/bafyaaeakbyeaeeqi6cpzrapqt6ilmgai", path)
})
})
if b != nil {
b.Close()
}
}
func getDocument(t *testing.T, b *IPFSBinding, path string) []byte {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
res, err := b.Invoke(ctx, &bindings.InvokeRequest{
Operation: "get",
Metadata: map[string]string{
"path": path,
},
})
require.NoError(t, err)
require.NotNil(t, res)
require.NotEmpty(t, res.Data)
return res.Data
}
func listPath(t *testing.T, b *IPFSBinding, path string) lsOperationResponse {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
res, err := b.Invoke(ctx, &bindings.InvokeRequest{
Operation: "ls",
Metadata: map[string]string{
"path": path,
},
})
require.NoError(t, err)
require.NotNil(t, res)
require.NotEmpty(t, res.Data)
list := lsOperationResponse{}
err = json.Unmarshal(res.Data, &list)
require.NoError(t, err)
return list
}
func listPins(t *testing.T, b *IPFSBinding) pinLsOperationResponse {
res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "pin-ls",
})
require.NoError(t, err)
require.NotNil(t, res)
require.NotEmpty(t, res.Data)
list := pinLsOperationResponse{}
err = json.Unmarshal(res.Data, &list)
require.NoError(t, err)
return list
}
func listPinnedCids(t *testing.T, b *IPFSBinding) []string {
list := listPins(t, b)
require.NotEmpty(t, list)
cids := make([]string, len(list))
for i, e := range list {
cids[i] = e.Cid
}
sort.Strings(cids)
return cids
}
func addDocument(t *testing.T, b *IPFSBinding, data []byte, metadata map[string]string) string {
res, err := b.Invoke(context.Background(), &bindings.InvokeRequest{
Operation: "create",
Data: data,
Metadata: metadata,
})
require.NoError(t, err)
require.NotNil(t, res)
require.NotEmpty(t, res.Data)
o := addOperationResponse{}
err = json.Unmarshal(res.Data, &o)
require.NoError(t, err)
require.NotEmpty(t, o.Path)
return o.Path
}
func compareHash(t *testing.T, data []byte, expect string) {
require.NotEmpty(t, data)
h := sha256.Sum256(data)
digest := hex.EncodeToString(h[:])
assert.Equal(t, expect, digest)
}

130
bindings/ipfs/metadata.go Normal file
View File

@ -0,0 +1,130 @@
/*
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 ipfs
import (
"fmt"
"io"
"strings"
ipfsOptions "github.com/ipfs/interface-go-ipfs-core/options"
ipfsConfig "github.com/ipfs/kubo/config"
ipfsFsrepo "github.com/ipfs/kubo/repo/fsrepo"
"github.com/libp2p/go-libp2p-core/peer"
"github.com/dapr/kit/config"
)
type ipfsMetadata struct {
// Path where to store the IPFS repository
// It will be initialized automatically if needed
// Defaults to the "best known path" set by IPFS
RepoPath string `mapstructure:"repoPath"`
// If set, uses an external IPFS daemon, connecting to its APIs
// Can be a HTTP(S) address or a multi-address
// If set, a local node will not be initialized
ExternalAPI string `mapstructure:"externalAPI"`
// The options below can only be set when a new local repo is being initialized
// List of bootstrap nodes, as a comma-separated string
// If empty, defaults to the official bootstrap nodes provided by the IPFS project
// Users should not modify this unless they're using a private cluster
BootstrapNodes string `mapstructure:"bootstrapNodes"`
// Swarm key to use for connecting to private IPFS networks
// If empty, the node will connect to the default, public IPFS network
// Generate with https://github.com/Kubuxu/go-ipfs-swarm-key-gen
// When using a swarm key, users should also configure the bootstrap nodes
SwarmKey string `mapstructure:"swarmKey"`
// Routing mode: "dht" (default) or "dhtclient"
Routing string `mapstructure:"routing"`
// Max local storage used
// Default: the default value used by go-ipfs (currently, "10GB")
StorageMax string `mapstructure:"storageMax"`
// Watermark for running garbage collection, 0-100 (as a percentage)
// Default: the default value used by go-ipfs (currently, 90)
StorageGCWatermark int64 `mapstructure:"storageGCWatermark"`
// Interval for running garbage collection
// Default: the default value used by go-ipfs (currently, "1h")
StorageGCPeriod string `mapstructure:"storageGCPeriod"`
}
// FromMap initializes the metadata object from a map.
func (m *ipfsMetadata) FromMap(mp map[string]string) (err error) {
if len(mp) > 0 {
err = config.Decode(mp, m)
if err != nil {
return err
}
}
if m.RepoPath == "" {
m.RepoPath, err = ipfsFsrepo.BestKnownPath()
if err != nil {
return fmt.Errorf("error determining the best known repo path: %v", err)
}
}
return nil
}
// IPFSConfig returns the configuration object for using with the go-ipfs library.
// This is executed only when initializing a new repository.
func (m *ipfsMetadata) IPFSConfig() (*ipfsConfig.Config, error) {
identity, err := ipfsConfig.CreateIdentity(io.Discard, []ipfsOptions.KeyGenerateOption{
ipfsOptions.Key.Type(ipfsOptions.Ed25519Key),
})
if err != nil {
return nil, err
}
cfg, err := ipfsConfig.InitWithIdentity(identity)
if err != nil {
return nil, err
}
if m.BootstrapNodes != "" {
var peers []peer.AddrInfo
peers, err = ipfsConfig.ParseBootstrapPeers(
strings.Split(m.BootstrapNodes, ","),
)
if err != nil {
return nil, fmt.Errorf("invalid value for metadata property 'bootstrapNodes': %v", err)
}
cfg.SetBootstrapPeers(peers)
}
r := strings.ToLower(m.Routing)
switch r {
case "dht", "dhtclient", "dhtserver", "none":
cfg.Routing.Type = ipfsConfig.NewOptionalString(r)
case "":
cfg.Routing.Type = ipfsConfig.NewOptionalString("dht")
default:
return nil, fmt.Errorf("invalid value for metadata property 'routing'")
}
if m.StorageMax != "" {
cfg.Datastore.StorageMax = m.StorageMax
}
if m.StorageGCWatermark != 0 {
cfg.Datastore.StorageGCWatermark = m.StorageGCWatermark
}
if m.StorageGCPeriod != "" {
cfg.Datastore.GCPeriod = m.StorageGCPeriod
}
return cfg, nil
}

View File

@ -0,0 +1,113 @@
/*
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 ipfs
import (
"context"
"encoding/json"
"errors"
"fmt"
ipfsFiles "github.com/ipfs/go-ipfs-files"
ipfsOptions "github.com/ipfs/interface-go-ipfs-core/options"
"github.com/multiformats/go-multihash"
"github.com/mitchellh/mapstructure"
"github.com/dapr/components-contrib/bindings"
)
// Handler for the "add" operation, which adds a new file
func (b *IPFSBinding) addOperation(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
if len(req.Data) == 0 {
return nil, errors.New("data is empty")
}
reqMetadata := &addRequestMetadata{}
err := reqMetadata.FromMap(req.Metadata)
if err != nil {
return nil, err
}
opts, err := reqMetadata.UnixfsAddOptions()
if err != nil {
return nil, err
}
f := ipfsFiles.NewBytesFile(req.Data)
resolved, err := b.ipfsAPI.Unixfs().Add(ctx, f, opts...)
if err != nil {
return nil, err
}
res := addOperationResponse{
Path: resolved.String(),
}
enc, err := json.Marshal(res)
if err != nil {
return nil, err
}
return &bindings.InvokeResponse{
Data: enc,
Metadata: nil,
}, nil
}
type addOperationResponse struct {
Path string `json:"path"`
}
type addRequestMetadata struct {
CidVersion *int `mapstructure:"cidVersion"`
Pin *bool `mapstructure:"pin"`
Hash *string `mapstructure:"hash"`
Inline *bool `mapstructure:"inline"`
InlineLimit *int `mapstructure:"inlineLimit"`
}
func (m *addRequestMetadata) FromMap(mp map[string]string) (err error) {
if len(mp) > 0 {
err = mapstructure.WeakDecode(mp, m)
if err != nil {
return err
}
}
return nil
}
func (m *addRequestMetadata) UnixfsAddOptions() ([]ipfsOptions.UnixfsAddOption, error) {
opts := []ipfsOptions.UnixfsAddOption{}
if m.CidVersion != nil {
opts = append(opts, ipfsOptions.Unixfs.CidVersion(*m.CidVersion))
}
if m.Pin != nil {
opts = append(opts, ipfsOptions.Unixfs.Pin(*m.Pin))
} else {
opts = append(opts, ipfsOptions.Unixfs.Pin(true))
}
if m.Hash != nil {
hash, ok := multihash.Names[*m.Hash]
if !ok {
return nil, fmt.Errorf("invalid hash %s", *m.Hash)
}
opts = append(opts, ipfsOptions.Unixfs.Hash(hash))
}
if m.Inline != nil {
opts = append(opts, ipfsOptions.Unixfs.Inline(*m.Inline))
}
if m.InlineLimit != nil {
opts = append(opts, ipfsOptions.Unixfs.InlineLimit(*m.InlineLimit))
}
return opts, nil
}

View File

@ -0,0 +1,81 @@
/*
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 ipfs
import (
"context"
"errors"
"fmt"
"io"
ipfsFiles "github.com/ipfs/go-ipfs-files"
ipfsPath "github.com/ipfs/interface-go-ipfs-core/path"
"github.com/mitchellh/mapstructure"
"github.com/dapr/components-contrib/bindings"
)
// Handler for the "get" operation, which retrieves a document
func (b *IPFSBinding) getOperation(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
reqMetadata := &getRequestMetadata{}
err := reqMetadata.FromMap(req.Metadata)
if err != nil {
return nil, err
}
if reqMetadata.Path == "" {
return nil, errors.New("metadata property 'path' is empty")
}
p := ipfsPath.New(reqMetadata.Path)
err = p.IsValid()
if err != nil {
return nil, fmt.Errorf("invalid value for metadata property 'path': %v", err)
}
res, err := b.ipfsAPI.Unixfs().Get(ctx, p)
if err != nil {
return nil, err
}
defer res.Close()
f, ok := res.(ipfsFiles.File)
if !ok {
return nil, errors.New("path does not represent a file")
}
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return &bindings.InvokeResponse{
Data: data,
Metadata: nil,
}, nil
}
type getRequestMetadata struct {
Path string `mapstructure:"path"`
}
func (m *getRequestMetadata) FromMap(mp map[string]string) (err error) {
if len(mp) > 0 {
err = mapstructure.WeakDecode(mp, m)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,92 @@
/*
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 ipfs
import (
"context"
"encoding/json"
"errors"
"fmt"
ipfsPath "github.com/ipfs/interface-go-ipfs-core/path"
"github.com/mitchellh/mapstructure"
"github.com/dapr/components-contrib/bindings"
)
// Handler for the "ls" operation, which retrieves a document
func (b *IPFSBinding) lsOperation(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
reqMetadata := &lsRequestMetadata{}
err := reqMetadata.FromMap(req.Metadata)
if err != nil {
return nil, err
}
if reqMetadata.Path == "" {
return nil, errors.New("metadata property 'path' is empty")
}
p := ipfsPath.New(reqMetadata.Path)
err = p.IsValid()
if err != nil {
return nil, fmt.Errorf("invalid value for metadata property 'path': %v", err)
}
ls, err := b.ipfsAPI.Unixfs().Ls(ctx, p)
if err != nil {
return nil, err
}
res := lsOperationResponse{}
for e := range ls {
if e.Err != nil {
return nil, e.Err
}
res = append(res, lsOperationResponseItem{
Name: e.Name,
Size: e.Size,
Type: e.Type.String(),
Cid: e.Cid.String(),
})
}
j, _ := json.Marshal(res)
return &bindings.InvokeResponse{
Data: j,
Metadata: nil,
}, nil
}
type lsOperationResponseItem struct {
Name string `json:"name,omitempty"`
Size uint64 `json:"size,omitempty"`
Cid string `json:"cid,omitempty"`
Type string `json:"type,omitempty"`
}
type lsOperationResponse []lsOperationResponseItem
type lsRequestMetadata struct {
Path string `mapstructure:"path"`
}
func (m *lsRequestMetadata) FromMap(mp map[string]string) (err error) {
if len(mp) > 0 {
err = mapstructure.WeakDecode(mp, m)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,84 @@
/*
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 ipfs
import (
"context"
"errors"
"fmt"
ipfsOptions "github.com/ipfs/interface-go-ipfs-core/options"
ipfsPath "github.com/ipfs/interface-go-ipfs-core/path"
"github.com/mitchellh/mapstructure"
"github.com/dapr/components-contrib/bindings"
)
// Handler for the "pin-add" operation, which adds a new pin
func (b *IPFSBinding) pinAddOperation(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
reqMetadata := &pinAddRequestMetadata{}
err := reqMetadata.FromMap(req.Metadata)
if err != nil {
return nil, err
}
if reqMetadata.Path == "" {
return nil, errors.New("metadata property 'path' is empty")
}
p := ipfsPath.New(reqMetadata.Path)
err = p.IsValid()
if err != nil {
return nil, fmt.Errorf("invalid value for metadata property 'path': %v", err)
}
opts, err := reqMetadata.PinAddOptions()
if err != nil {
return nil, err
}
err = b.ipfsAPI.Pin().Add(ctx, p, opts...)
if err != nil {
return nil, err
}
return &bindings.InvokeResponse{
Data: nil,
Metadata: nil,
}, nil
}
type pinAddRequestMetadata struct {
Path string `mapstructure:"path"`
Recursive *bool `mapstructure:"recursive"`
}
func (m *pinAddRequestMetadata) FromMap(mp map[string]string) (err error) {
if len(mp) > 0 {
err = mapstructure.WeakDecode(mp, m)
if err != nil {
return err
}
}
return nil
}
func (m *pinAddRequestMetadata) PinAddOptions() ([]ipfsOptions.PinAddOption, error) {
opts := []ipfsOptions.PinAddOption{}
if m.Recursive != nil {
opts = append(opts, ipfsOptions.Pin.Recursive(*m.Recursive))
} else {
opts = append(opts, ipfsOptions.Pin.Recursive(true))
}
return opts, nil
}

View File

@ -0,0 +1,105 @@
/*
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 ipfs
import (
"context"
"encoding/json"
"fmt"
"strings"
ipfsOptions "github.com/ipfs/interface-go-ipfs-core/options"
"github.com/mitchellh/mapstructure"
"github.com/dapr/components-contrib/bindings"
)
// Handler for the "pin-ls" operation, which removes a pin
func (b *IPFSBinding) pinLsOperation(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
reqMetadata := &pinLsRequestMetadata{}
err := reqMetadata.FromMap(req.Metadata)
if err != nil {
return nil, err
}
opts, err := reqMetadata.PinLsOptions()
if err != nil {
return nil, err
}
ls, err := b.ipfsAPI.Pin().Ls(ctx, opts...)
if err != nil {
return nil, err
}
res := pinLsOperationResponse{}
for e := range ls {
err = e.Err()
if err != nil {
return nil, err
}
res = append(res, pinLsOperationResponseItem{
Type: e.Type(),
Cid: e.Path().Cid().String(),
})
}
j, _ := json.Marshal(res)
return &bindings.InvokeResponse{
Data: j,
Metadata: nil,
}, nil
}
type pinLsOperationResponseItem struct {
Cid string `json:"cid,omitempty"`
Type string `json:"type,omitempty"`
}
type pinLsOperationResponse []pinLsOperationResponseItem
type pinLsRequestMetadata struct {
Type *string `mapstructure:"type"`
}
func (m *pinLsRequestMetadata) FromMap(mp map[string]string) (err error) {
if len(mp) > 0 {
err = mapstructure.WeakDecode(mp, m)
if err != nil {
return err
}
}
return nil
}
func (m *pinLsRequestMetadata) PinLsOptions() ([]ipfsOptions.PinLsOption, error) {
opts := []ipfsOptions.PinLsOption{}
if m.Type != nil {
switch strings.ToLower(*m.Type) {
case "direct":
opts = append(opts, ipfsOptions.Pin.Ls.Direct())
case "recursive":
opts = append(opts, ipfsOptions.Pin.Ls.Recursive())
case "indirect":
opts = append(opts, ipfsOptions.Pin.Ls.Indirect())
case "all":
opts = append(opts, ipfsOptions.Pin.Ls.All())
default:
return nil, fmt.Errorf("invalid value for metadata property 'type'")
}
} else {
opts = append(opts, ipfsOptions.Pin.Ls.All())
}
return opts, nil
}

View File

@ -0,0 +1,84 @@
/*
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 ipfs
import (
"context"
"errors"
"fmt"
ipfsOptions "github.com/ipfs/interface-go-ipfs-core/options"
ipfsPath "github.com/ipfs/interface-go-ipfs-core/path"
"github.com/mitchellh/mapstructure"
"github.com/dapr/components-contrib/bindings"
)
// Handler for the "pin-rm" operation, which removes a pin
func (b *IPFSBinding) pinRmOperation(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
reqMetadata := &pinRmRequestMetadata{}
err := reqMetadata.FromMap(req.Metadata)
if err != nil {
return nil, err
}
if reqMetadata.Path == "" {
return nil, errors.New("metadata property 'path' is empty")
}
p := ipfsPath.New(reqMetadata.Path)
err = p.IsValid()
if err != nil {
return nil, fmt.Errorf("invalid value for metadata property 'path': %v", err)
}
opts, err := reqMetadata.PinRmOptions()
if err != nil {
return nil, err
}
err = b.ipfsAPI.Pin().Rm(ctx, p, opts...)
if err != nil {
return nil, err
}
return &bindings.InvokeResponse{
Data: nil,
Metadata: nil,
}, nil
}
type pinRmRequestMetadata struct {
Path string `mapstructure:"path"`
Recursive *bool `mapstructure:"recursive"`
}
func (m *pinRmRequestMetadata) FromMap(mp map[string]string) (err error) {
if len(mp) > 0 {
err = mapstructure.WeakDecode(mp, m)
if err != nil {
return err
}
}
return nil
}
func (m *pinRmRequestMetadata) PinRmOptions() ([]ipfsOptions.PinRmOption, error) {
opts := []ipfsOptions.PinRmOption{}
if m.Recursive != nil {
opts = append(opts, ipfsOptions.Pin.RmRecursive(*m.Recursive))
} else {
opts = append(opts, ipfsOptions.Pin.RmRecursive(true))
}
return opts, nil
}

View File

@ -38,7 +38,7 @@ type Binding struct {
} }
// NewKafka returns a new kafka binding instance. // NewKafka returns a new kafka binding instance.
func NewKafka(logger logger.Logger) *Binding { func NewKafka(logger logger.Logger) bindings.InputOutputBinding {
k := kafka.NewKafka(logger) k := kafka.NewKafka(logger)
// in kafka binding component, disable consumer retry by default // in kafka binding component, disable consumer retry by default
k.DefaultConsumeRetryEnabled = false k.DefaultConsumeRetryEnabled = false

View File

@ -31,10 +31,10 @@ import (
) )
type kubernetesInput struct { type kubernetesInput struct {
kubeClient kubernetes.Interface kubeClient kubernetes.Interface
namespace string namespace string
resyncPeriodInSec time.Duration resyncPeriod time.Duration
logger logger.Logger logger logger.Logger
} }
type EventResponse struct { type EventResponse struct {
@ -68,9 +68,9 @@ func (k *kubernetesInput) parseMetadata(metadata bindings.Metadata) error {
intval, err := strconv.ParseInt(val, 10, 64) intval, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
k.logger.Warnf("invalid resyncPeriodInSec %s; %v; defaulting to 10s", val, err) k.logger.Warnf("invalid resyncPeriodInSec %s; %v; defaulting to 10s", val, err)
k.resyncPeriodInSec = time.Second * 10 k.resyncPeriod = time.Second * 10
} else { } else {
k.resyncPeriodInSec = time.Second * time.Duration(intval) k.resyncPeriod = time.Second * time.Duration(intval)
} }
} }
@ -84,11 +84,11 @@ func (k *kubernetesInput) Read(ctx context.Context, handler bindings.Handler) er
k.namespace, k.namespace,
fields.Everything(), fields.Everything(),
) )
var resultChan chan EventResponse = make(chan EventResponse) resultChan := make(chan EventResponse)
_, controller := cache.NewInformer( _, controller := cache.NewInformer(
watchlist, watchlist,
&v1.Event{}, &v1.Event{},
k.resyncPeriodInSec, k.resyncPeriod,
cache.ResourceEventHandlerFuncs{ cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { AddFunc: func(obj interface{}) {
if obj != nil { if obj != nil {

View File

@ -34,7 +34,7 @@ func TestParseMetadata(t *testing.T) {
i.parseMetadata(m) i.parseMetadata(m)
assert.Equal(t, nsName, i.namespace, "The namespaces should be the same.") assert.Equal(t, nsName, i.namespace, "The namespaces should be the same.")
assert.Equal(t, resyncPeriod, i.resyncPeriodInSec, "The resyncPeriod should be the same.") assert.Equal(t, resyncPeriod, i.resyncPeriod, "The resyncPeriod should be the same.")
}) })
t.Run("parse metadata no namespace", func(t *testing.T) { t.Run("parse metadata no namespace", func(t *testing.T) {
m := bindings.Metadata{} m := bindings.Metadata{}
@ -55,6 +55,6 @@ func TestParseMetadata(t *testing.T) {
assert.Nil(t, err, "Expected err to be nil.") assert.Nil(t, err, "Expected err to be nil.")
assert.Equal(t, nsName, i.namespace, "The namespaces should be the same.") assert.Equal(t, nsName, i.namespace, "The namespaces should be the same.")
assert.Equal(t, time.Second*10, i.resyncPeriodInSec, "The resyncPeriod should be the same.") assert.Equal(t, time.Second*10, i.resyncPeriod, "The resyncPeriod should be the same.")
}) })
} }

View File

@ -19,7 +19,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -52,7 +52,7 @@ type createResponse struct {
} }
// NewLocalStorage returns a new LocalStorage instance. // NewLocalStorage returns a new LocalStorage instance.
func NewLocalStorage(logger logger.Logger) *LocalStorage { func NewLocalStorage(logger logger.Logger) bindings.OutputBinding {
return &LocalStorage{logger: logger} return &LocalStorage{logger: logger}
} }
@ -153,7 +153,7 @@ func (ls *LocalStorage) get(filename string, req *bindings.InvokeRequest) (*bind
return nil, err return nil, err
} }
b, err := ioutil.ReadAll(f) b, err := io.ReadAll(f)
if err != nil { if err != nil {
ls.logger.Debugf("%s", err) ls.logger.Debugf("%s", err)

View File

@ -25,7 +25,7 @@ import (
func TestParseMetadata(t *testing.T) { func TestParseMetadata(t *testing.T) {
m := bindings.Metadata{} m := bindings.Metadata{}
m.Properties = map[string]string{"rootPath": "/files"} m.Properties = map[string]string{"rootPath": "/files"}
localStorage := NewLocalStorage(logger.NewLogger("test")) localStorage := NewLocalStorage(logger.NewLogger("test")).(*LocalStorage)
meta, err := localStorage.parseMetadata(m) meta, err := localStorage.parseMetadata(m)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/files", meta.RootPath) assert.Equal(t, "/files", meta.RootPath)

View File

@ -13,8 +13,9 @@ limitations under the License.
package bindings package bindings
import "github.com/dapr/components-contrib/metadata"
// Metadata represents a set of binding specific properties. // Metadata represents a set of binding specific properties.
type Metadata struct { type Metadata struct {
Name string metadata.Base `json:",inline"`
Properties map[string]string `json:"properties"`
} }

View File

@ -69,7 +69,7 @@ type MQTT struct {
} }
// NewMQTT returns a new MQTT instance. // NewMQTT returns a new MQTT instance.
func NewMQTT(logger logger.Logger) *MQTT { func NewMQTT(logger logger.Logger) bindings.InputOutputBinding {
return &MQTT{logger: logger} return &MQTT{logger: logger}
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
mdata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -21,7 +22,7 @@ const (
// listener 1883 // listener 1883
// allow_anonymous true // allow_anonymous true
// And run: // And run:
// nolint:misspell //nolint:misspell
// docker run -d -v mosquitto.conf:/mosquitto/config/mosquitto.conf --name test-mqtt -p 1883:1883 eclipse-mosquitto:2 // docker run -d -v mosquitto.conf:/mosquitto/config/mosquitto.conf --name test-mqtt -p 1883:1883 eclipse-mosquitto:2
// In that case the connection string will be: tcp://127.0.0.1:1883 // In that case the connection string will be: tcp://127.0.0.1:1883
testMQTTConnectionStringEnvKey = "DAPR_TEST_MQTT_URL" testMQTTConnectionStringEnvKey = "DAPR_TEST_MQTT_URL"
@ -47,7 +48,7 @@ func TestInvokeWithTopic(t *testing.T) {
const msgCustomized = "hello from customized" const msgCustomized = "hello from customized"
dataCustomized := []byte(msgCustomized) dataCustomized := []byte(msgCustomized)
metadata := bindings.Metadata{ metadata := bindings.Metadata{Base: mdata.Base{
Name: "testQueue", Name: "testQueue",
Properties: map[string]string{ Properties: map[string]string{
"consumerID": uuid.NewString(), "consumerID": uuid.NewString(),
@ -58,11 +59,11 @@ func TestInvokeWithTopic(t *testing.T) {
"cleanSession": "true", "cleanSession": "true",
"backOffMaxRetries": "0", "backOffMaxRetries": "0",
}, },
} }}
logger := logger.NewLogger("test") logger := logger.NewLogger("test")
r := NewMQTT(logger) r := NewMQTT(logger).(*MQTT)
err := r.Init(metadata) err := r.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
mdata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -41,9 +42,9 @@ func TestParseMetadata(t *testing.T) {
t.Run("metadata is correct", func(t *testing.T) { t.Run("metadata is correct", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{ fakeMetaData := bindings.Metadata{Base: mdata.Base{
Properties: fakeProperties, Properties: fakeProperties,
} }}
m, err := parseMQTTMetaData(fakeMetaData) m, err := parseMQTTMetaData(fakeMetaData)
@ -57,7 +58,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("missing topic", func(t *testing.T) { t.Run("missing topic", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties["topic"] = "" fakeMetaData.Properties["topic"] = ""
_, err := parseMQTTMetaData(fakeMetaData) _, err := parseMQTTMetaData(fakeMetaData)
@ -67,7 +68,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("missing consumerID", func(t *testing.T) { t.Run("missing consumerID", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties["consumerID"] = "" fakeMetaData.Properties["consumerID"] = ""
_, err := parseMQTTMetaData(fakeMetaData) _, err := parseMQTTMetaData(fakeMetaData)
@ -78,7 +79,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("url is not given", func(t *testing.T) { t.Run("url is not given", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttURL] = "" fakeMetaData.Properties[mqttURL] = ""
m, err := parseMQTTMetaData(fakeMetaData) m, err := parseMQTTMetaData(fakeMetaData)
@ -91,7 +92,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("qos and retain is not given", func(t *testing.T) { t.Run("qos and retain is not given", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttQOS] = "" fakeMetaData.Properties[mqttQOS] = ""
fakeMetaData.Properties[mqttRetain] = "" fakeMetaData.Properties[mqttRetain] = ""
@ -107,7 +108,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("invalid clean session field", func(t *testing.T) { t.Run("invalid clean session field", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttCleanSession] = "randomString" fakeMetaData.Properties[mqttCleanSession] = "randomString"
m, err := parseMQTTMetaData(fakeMetaData) m, err := parseMQTTMetaData(fakeMetaData)
@ -120,7 +121,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("invalid ca certificate", func(t *testing.T) { t.Run("invalid ca certificate", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttCACert] = "randomNonPEMBlockCA" fakeMetaData.Properties[mqttCACert] = "randomNonPEMBlockCA"
_, err := parseMQTTMetaData(fakeMetaData) _, err := parseMQTTMetaData(fakeMetaData)
@ -130,7 +131,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("valid ca certificate", func(t *testing.T) { t.Run("valid ca certificate", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttCACert] = "-----BEGIN CERTIFICATE-----\nMIICyDCCAbACCQDb8BtgvbqW5jANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJJ\nTjEXMBUGA1UEAwwOZGFwck1xdHRUZXN0Q0EwHhcNMjAwODEyMDY1MzU4WhcNMjUw\nODEyMDY1MzU4WjAmMQswCQYDVQQGEwJJTjEXMBUGA1UEAwwOZGFwck1xdHRUZXN0\nQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEXte1GBxFJaygsEnK\nHV2AxazZW6Vppv+i50AuURHcaGo0i8G5CTfHzSKrYtTFfBskUspl+2N8GPV5c8Eb\ng+PP6YFn1wiHVz+wRSk3BD35DcGOT2o4XsJw5tiAzJkbpAOYCYl7KAM+BtOf41uC\nd6TdqmawhRGtv1ND2WtyJOT6A3KcUfjhL4TFEhWoljPJVay4TQoJcZMAImD/Xcxw\n6urv6wmUJby3/RJ3I46ZNH3zxEw5vSq1TuzuXxQmfPJG0ZPKJtQZ2nkZ3PNZe4bd\nNUa83YgQap7nBhYdYMMsQyLES2qy3mPcemBVoBWRGODel4PMEcsQiOhAyloAF2d3\nhd+LAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK13X5JYBy78vHYoP0Oq9fe5XBbL\nuRM8YLnet9b/bXTGG4SnCCOGqWz99swYK7SVyR5l2h8SAoLzeNV61PtaZ6fHrbar\noxSL7BoRXOhMH6LQATadyvwlJ71uqlagqya7soaPK09TtfzeebLT0QkRCWT9b9lQ\nDBvBVCaFidynJL1ts21m5yUdIY4JSu4sGZGb4FRGFdBv/hD3wH8LAkOppsSv3C/Q\nkfkDDSQzYbdMoBuXmafvi3He7Rv+e6Tj9or1rrWdx0MIKlZPzz4DOe5Rh112uRB9\n7xPHJt16c+Ya3DKpchwwdNcki0vFchlpV96HK8sMCoY9kBzPhkEQLdiBGv4=\n-----END CERTIFICATE-----\n" fakeMetaData.Properties[mqttCACert] = "-----BEGIN CERTIFICATE-----\nMIICyDCCAbACCQDb8BtgvbqW5jANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJJ\nTjEXMBUGA1UEAwwOZGFwck1xdHRUZXN0Q0EwHhcNMjAwODEyMDY1MzU4WhcNMjUw\nODEyMDY1MzU4WjAmMQswCQYDVQQGEwJJTjEXMBUGA1UEAwwOZGFwck1xdHRUZXN0\nQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEXte1GBxFJaygsEnK\nHV2AxazZW6Vppv+i50AuURHcaGo0i8G5CTfHzSKrYtTFfBskUspl+2N8GPV5c8Eb\ng+PP6YFn1wiHVz+wRSk3BD35DcGOT2o4XsJw5tiAzJkbpAOYCYl7KAM+BtOf41uC\nd6TdqmawhRGtv1ND2WtyJOT6A3KcUfjhL4TFEhWoljPJVay4TQoJcZMAImD/Xcxw\n6urv6wmUJby3/RJ3I46ZNH3zxEw5vSq1TuzuXxQmfPJG0ZPKJtQZ2nkZ3PNZe4bd\nNUa83YgQap7nBhYdYMMsQyLES2qy3mPcemBVoBWRGODel4PMEcsQiOhAyloAF2d3\nhd+LAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK13X5JYBy78vHYoP0Oq9fe5XBbL\nuRM8YLnet9b/bXTGG4SnCCOGqWz99swYK7SVyR5l2h8SAoLzeNV61PtaZ6fHrbar\noxSL7BoRXOhMH6LQATadyvwlJ71uqlagqya7soaPK09TtfzeebLT0QkRCWT9b9lQ\nDBvBVCaFidynJL1ts21m5yUdIY4JSu4sGZGb4FRGFdBv/hD3wH8LAkOppsSv3C/Q\nkfkDDSQzYbdMoBuXmafvi3He7Rv+e6Tj9or1rrWdx0MIKlZPzz4DOe5Rh112uRB9\n7xPHJt16c+Ya3DKpchwwdNcki0vFchlpV96HK8sMCoY9kBzPhkEQLdiBGv4=\n-----END CERTIFICATE-----\n"
m, err := parseMQTTMetaData(fakeMetaData) m, err := parseMQTTMetaData(fakeMetaData)
@ -146,7 +147,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("invalid client certificate", func(t *testing.T) { t.Run("invalid client certificate", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttClientCert] = "randomNonPEMBlockClientCert" fakeMetaData.Properties[mqttClientCert] = "randomNonPEMBlockClientCert"
_, err := parseMQTTMetaData(fakeMetaData) _, err := parseMQTTMetaData(fakeMetaData)
@ -156,7 +157,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("valid client certificate", func(t *testing.T) { t.Run("valid client certificate", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttClientCert] = "-----BEGIN CERTIFICATE-----\nMIICzDCCAbQCCQDBKDMS3SHsDzANBgkqhkiG9w0BAQUFADAmMQswCQYDVQQGEwJJ\nTjEXMBUGA1UEAwwOZGFwck1xdHRUZXN0Q0EwHhcNMjAwODEyMDY1NTE1WhcNMjEw\nODA3MDY1NTE1WjAqMQswCQYDVQQGEwJJTjEbMBkGA1UEAwwSZGFwck1xdHRUZXN0\nQ2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IDfsGI2pb4W\nt3CjckrKuNeTrgmla3sXxSI5wfDgLGd/XkNu++M6yi9ABaBiYChpxbylqIeAn/HT\n3r/nhcb+bldMtEkU9tODHy/QDhvN2UGFjRsMfzO9p1oMpTnRdJCHYinE+oqVced5\nHI+UEofAU+1eiIXqJGKrdfn4gvaHst4QfVPvui8WzJq9TMkEhEME+5hs3VKyKZr2\nqjIxzr7nLVod3DBf482VjxRI06Ip3fPvNuMWwzj2G+Rj8PMcBjoKeCLQL9uQh7f1\nTWHuACqNIrmFEUQWdGETnRjHWIvw0NEL40+Ur2b5+7/hoqnTzReJ3XUe1jM3l44f\nl0rOf4hu2QIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAT9yoIeX0LTsvx7/b+8V3a\nkP+j8u97QCc8n5xnMpivcMEk5cfqXX5Llv2EUJ9kBsynrJwT7ujhTJXSA/zb2UdC\nKH8PaSrgIlLwQNZMDofbz6+zPbjStkgne/ZQkTDIxY73sGpJL8LsQVO9p2KjOpdj\nSf9KuJhLzcHolh7ry3ZrkOg+QlMSvseeDRAxNhpkJrGQ6piXoUiEeKKNa0rWTMHx\nIP1Hqj+hh7jgqoQR48NL2jNng7I64HqTl6Mv2fiNfINiw+5xmXTB0QYkGU5NvPBO\naKcCRcGlU7ND89BogQPZsl/P04tAuQqpQWffzT4sEEOyWSVGda4N2Ys3GSQGBv8e\n-----END CERTIFICATE-----\n" fakeMetaData.Properties[mqttClientCert] = "-----BEGIN CERTIFICATE-----\nMIICzDCCAbQCCQDBKDMS3SHsDzANBgkqhkiG9w0BAQUFADAmMQswCQYDVQQGEwJJ\nTjEXMBUGA1UEAwwOZGFwck1xdHRUZXN0Q0EwHhcNMjAwODEyMDY1NTE1WhcNMjEw\nODA3MDY1NTE1WjAqMQswCQYDVQQGEwJJTjEbMBkGA1UEAwwSZGFwck1xdHRUZXN0\nQ2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IDfsGI2pb4W\nt3CjckrKuNeTrgmla3sXxSI5wfDgLGd/XkNu++M6yi9ABaBiYChpxbylqIeAn/HT\n3r/nhcb+bldMtEkU9tODHy/QDhvN2UGFjRsMfzO9p1oMpTnRdJCHYinE+oqVced5\nHI+UEofAU+1eiIXqJGKrdfn4gvaHst4QfVPvui8WzJq9TMkEhEME+5hs3VKyKZr2\nqjIxzr7nLVod3DBf482VjxRI06Ip3fPvNuMWwzj2G+Rj8PMcBjoKeCLQL9uQh7f1\nTWHuACqNIrmFEUQWdGETnRjHWIvw0NEL40+Ur2b5+7/hoqnTzReJ3XUe1jM3l44f\nl0rOf4hu2QIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAT9yoIeX0LTsvx7/b+8V3a\nkP+j8u97QCc8n5xnMpivcMEk5cfqXX5Llv2EUJ9kBsynrJwT7ujhTJXSA/zb2UdC\nKH8PaSrgIlLwQNZMDofbz6+zPbjStkgne/ZQkTDIxY73sGpJL8LsQVO9p2KjOpdj\nSf9KuJhLzcHolh7ry3ZrkOg+QlMSvseeDRAxNhpkJrGQ6piXoUiEeKKNa0rWTMHx\nIP1Hqj+hh7jgqoQR48NL2jNng7I64HqTl6Mv2fiNfINiw+5xmXTB0QYkGU5NvPBO\naKcCRcGlU7ND89BogQPZsl/P04tAuQqpQWffzT4sEEOyWSVGda4N2Ys3GSQGBv8e\n-----END CERTIFICATE-----\n"
m, err := parseMQTTMetaData(fakeMetaData) m, err := parseMQTTMetaData(fakeMetaData)
@ -172,7 +173,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("invalid client certificate key", func(t *testing.T) { t.Run("invalid client certificate key", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttClientKey] = "randomNonPEMBlockClientKey" fakeMetaData.Properties[mqttClientKey] = "randomNonPEMBlockClientKey"
_, err := parseMQTTMetaData(fakeMetaData) _, err := parseMQTTMetaData(fakeMetaData)
@ -182,7 +183,7 @@ func TestParseMetadata(t *testing.T) {
t.Run("valid client certificate key", func(t *testing.T) { t.Run("valid client certificate key", func(t *testing.T) {
fakeProperties := getFakeProperties() fakeProperties := getFakeProperties()
fakeMetaData := bindings.Metadata{Name: "binging-test", Properties: fakeProperties} fakeMetaData := bindings.Metadata{Base: mdata.Base{Name: "binging-test", Properties: fakeProperties}}
fakeMetaData.Properties[mqttClientKey] = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA5IDfsGI2pb4Wt3CjckrKuNeTrgmla3sXxSI5wfDgLGd/XkNu\n++M6yi9ABaBiYChpxbylqIeAn/HT3r/nhcb+bldMtEkU9tODHy/QDhvN2UGFjRsM\nfzO9p1oMpTnRdJCHYinE+oqVced5HI+UEofAU+1eiIXqJGKrdfn4gvaHst4QfVPv\nui8WzJq9TMkEhEME+5hs3VKyKZr2qjIxzr7nLVod3DBf482VjxRI06Ip3fPvNuMW\nwzj2G+Rj8PMcBjoKeCLQL9uQh7f1TWHuACqNIrmFEUQWdGETnRjHWIvw0NEL40+U\nr2b5+7/hoqnTzReJ3XUe1jM3l44fl0rOf4hu2QIDAQABAoIBAQCVMINb4TP20P55\n9IPyqlxjhPT563hijXK+lhMJyiBDPavOOs7qjLikq2bshYPVbm1o2jt6pkXXqAeB\n5t/d20fheQQurYyPfxecNBZuL78duwbcUy28m2aXLlcVRYO4zGhoMgdW4UajoNLV\nT/UIiDONWGyhTHXMHdP+6h9UOmvs3o4b225AuLrw9n6QO5I1Se8lcfOTIqR1fy4O\nGsUWEQPdW0X3Dhgpx7kDIuBTAQzbjD31PCR1U8h2wsCeEe6hPCrsMbo/D019weol\ndi40tbWR1/oNz0+vro2d9YDPJkXN0gmpT51Z4YJoexZBdyzO5z4DMSdn5yczzt6p\nQq8LsXAFAoGBAPYXRbC4OxhtuC+xr8KRkaCCMjtjUWFbFWf6OFgUS9b5uPz9xvdY\nXo7wBP1zp2dS8yFsdIYH5Six4Z5iOuDR4sVixzjabhwedL6bmS1zV5qcCWeASKX1\nURgSkfMmC4Tg3LBgZ9YxySFcVRjikxljkS3eK7Mp7Xmj5afe7qV73TJfAoGBAO20\nTtw2RGe02xnydZmmwf+NpQHOA9S0JsehZA6NRbtPEN/C8bPJIq4VABC5zcH+tfYf\nzndbDlGhuk+qpPA590rG5RSOUjYnQFq7njdSfFyok9dXSZQTjJwFnG2oy0LmgjCe\nROYnbCzD+a+gBKV4xlo2M80OLakQ3zOwPT0xNRnHAoGATLEj/tbrU8mdxP9TDwfe\nom7wyKFDE1wXZ7gLJyfsGqrog69y+lKH5XPXmkUYvpKTQq9SARMkz3HgJkPmpXnD\nelA2Vfl8pza2m1BShF+VxZErPR41hcLV6vKemXAZ1udc33qr4YzSaZskygSSYy8s\nZ2b9p3BBmc8CGzbWmKvpW3ECgYEAn7sFLxdMWj/+5221Nr4HKPn+wrq0ek9gq884\n1Ep8bETSOvrdvolPQ5mbBKJGsLC/h5eR/0Rx18sMzpIF6eOZ2GbU8z474mX36cCf\nrd9A8Gbbid3+9IE6gHGIz2uYwujw3UjNVbdyCpbahvjJhoQlDePUZVu8tRpAUpSA\nYklZvGsCgYBuIlOFTNGMVUnwfzrcS9a/31LSvWTZa8w2QFjsRPMYFezo2l4yWs4D\nPEpeuoJm+Gp6F6ayjoeyOw9mvMBH5hAZr4WjbiU6UodzEHREAsLAzCzcRyIpnDE6\nPW1c3j60r8AHVufkWTA+8B9WoLC5MqcYTV3beMGnNGGqS2PeBom63Q==\n-----END RSA PRIVATE KEY-----\n" fakeMetaData.Properties[mqttClientKey] = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA5IDfsGI2pb4Wt3CjckrKuNeTrgmla3sXxSI5wfDgLGd/XkNu\n++M6yi9ABaBiYChpxbylqIeAn/HT3r/nhcb+bldMtEkU9tODHy/QDhvN2UGFjRsM\nfzO9p1oMpTnRdJCHYinE+oqVced5HI+UEofAU+1eiIXqJGKrdfn4gvaHst4QfVPv\nui8WzJq9TMkEhEME+5hs3VKyKZr2qjIxzr7nLVod3DBf482VjxRI06Ip3fPvNuMW\nwzj2G+Rj8PMcBjoKeCLQL9uQh7f1TWHuACqNIrmFEUQWdGETnRjHWIvw0NEL40+U\nr2b5+7/hoqnTzReJ3XUe1jM3l44fl0rOf4hu2QIDAQABAoIBAQCVMINb4TP20P55\n9IPyqlxjhPT563hijXK+lhMJyiBDPavOOs7qjLikq2bshYPVbm1o2jt6pkXXqAeB\n5t/d20fheQQurYyPfxecNBZuL78duwbcUy28m2aXLlcVRYO4zGhoMgdW4UajoNLV\nT/UIiDONWGyhTHXMHdP+6h9UOmvs3o4b225AuLrw9n6QO5I1Se8lcfOTIqR1fy4O\nGsUWEQPdW0X3Dhgpx7kDIuBTAQzbjD31PCR1U8h2wsCeEe6hPCrsMbo/D019weol\ndi40tbWR1/oNz0+vro2d9YDPJkXN0gmpT51Z4YJoexZBdyzO5z4DMSdn5yczzt6p\nQq8LsXAFAoGBAPYXRbC4OxhtuC+xr8KRkaCCMjtjUWFbFWf6OFgUS9b5uPz9xvdY\nXo7wBP1zp2dS8yFsdIYH5Six4Z5iOuDR4sVixzjabhwedL6bmS1zV5qcCWeASKX1\nURgSkfMmC4Tg3LBgZ9YxySFcVRjikxljkS3eK7Mp7Xmj5afe7qV73TJfAoGBAO20\nTtw2RGe02xnydZmmwf+NpQHOA9S0JsehZA6NRbtPEN/C8bPJIq4VABC5zcH+tfYf\nzndbDlGhuk+qpPA590rG5RSOUjYnQFq7njdSfFyok9dXSZQTjJwFnG2oy0LmgjCe\nROYnbCzD+a+gBKV4xlo2M80OLakQ3zOwPT0xNRnHAoGATLEj/tbrU8mdxP9TDwfe\nom7wyKFDE1wXZ7gLJyfsGqrog69y+lKH5XPXmkUYvpKTQq9SARMkz3HgJkPmpXnD\nelA2Vfl8pza2m1BShF+VxZErPR41hcLV6vKemXAZ1udc33qr4YzSaZskygSSYy8s\nZ2b9p3BBmc8CGzbWmKvpW3ECgYEAn7sFLxdMWj/+5221Nr4HKPn+wrq0ek9gq884\n1Ep8bETSOvrdvolPQ5mbBKJGsLC/h5eR/0Rx18sMzpIF6eOZ2GbU8z474mX36cCf\nrd9A8Gbbid3+9IE6gHGIz2uYwujw3UjNVbdyCpbahvjJhoQlDePUZVu8tRpAUpSA\nYklZvGsCgYBuIlOFTNGMVUnwfzrcS9a/31LSvWTZa8w2QFjsRPMYFezo2l4yWs4D\nPEpeuoJm+Gp6F6ayjoeyOw9mvMBH5hAZr4WjbiU6UodzEHREAsLAzCzcRyIpnDE6\nPW1c3j60r8AHVufkWTA+8B9WoLC5MqcYTV3beMGnNGGqS2PeBom63Q==\n-----END RSA PRIVATE KEY-----\n"
m, err := parseMQTTMetaData(fakeMetaData) m, err := parseMQTTMetaData(fakeMetaData)
@ -198,7 +199,7 @@ func TestParseMetadata(t *testing.T) {
topic := "/topic/where/the/data/is/from" topic := "/topic/where/the/data/is/from"
logger := logger.NewLogger("test") logger := logger.NewLogger("test")
m := NewMQTT(logger) m := NewMQTT(logger).(*MQTT)
m.ctx, m.cancel = context.WithCancel(context.Background()) m.ctx, m.cancel = context.WithCancel(context.Background())
m.handleMessage(context.Background(), func(ctx context.Context, r *bindings.ReadResponse) ([]byte, error) { m.handleMessage(context.Background(), func(ctx context.Context, r *bindings.ReadResponse) ([]byte, error) {

View File

@ -21,7 +21,7 @@ import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"reflect" "reflect"
"strconv" "strconv"
"time" "time"
@ -76,7 +76,7 @@ type Mysql struct {
} }
// NewMysql returns a new MySQL output binding. // NewMysql returns a new MySQL output binding.
func NewMysql(logger logger.Logger) *Mysql { func NewMysql(logger logger.Logger) bindings.OutputBinding {
return &Mysql{logger: logger} return &Mysql{logger: logger}
} }
@ -155,7 +155,7 @@ func (m *Mysql) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindi
}, },
} }
switch req.Operation { // nolint: exhaustive switch req.Operation { //nolint:exhaustive
case execOperation: case execOperation:
r, err := m.exec(ctx, s) r, err := m.exec(ctx, s)
if err != nil { if err != nil {
@ -263,7 +263,7 @@ func initDB(url, pemPath string) (*sql.DB, error) {
if pemPath != "" { if pemPath != "" {
rootCertPool := x509.NewCertPool() rootCertPool := x509.NewCertPool()
pem, err := ioutil.ReadFile(pemPath) pem, err := os.ReadFile(pemPath)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "Error reading PEM file from %s", pemPath) return nil, errors.Wrapf(err, "Error reading PEM file from %s", pemPath)
} }

View File

@ -24,6 +24,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -72,8 +73,8 @@ func TestMysqlIntegration(t *testing.T) {
t.SkipNow() t.SkipNow()
} }
b := NewMysql(logger.NewLogger("test")) b := NewMysql(logger.NewLogger("test")).(*Mysql)
m := bindings.Metadata{Properties: map[string]string{connectionURLKey: url}} m := bindings.Metadata{Base: metadata.Base{Properties: map[string]string{connectionURLKey: url}}}
if err := b.Init(m); err != nil { if err := b.Init(m); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -180,7 +180,7 @@ func mockDatabase(t *testing.T) (*Mysql, sqlmock.Sqlmock, error) {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
} }
m := NewMysql(logger.NewLogger("test")) m := NewMysql(logger.NewLogger("test")).(*Mysql)
m.db = db m.db = db
return m, mock, err return m, mock, err

View File

@ -32,6 +32,6 @@ func PingOutBinding(outputBinding OutputBinding) error {
if outputBindingWithPing, ok := outputBinding.(health.Pinger); ok { if outputBindingWithPing, ok := outputBinding.(health.Pinger); ok {
return outputBindingWithPing.Ping() return outputBindingWithPing.Ping()
} else { } else {
return fmt.Errorf("Ping is not implemented by this output binding") return fmt.Errorf("ping is not implemented by this output binding")
} }
} }

View File

@ -43,7 +43,7 @@ type Postgres struct {
} }
// NewPostgres returns a new PostgreSQL output binding. // NewPostgres returns a new PostgreSQL output binding.
func NewPostgres(logger logger.Logger) *Postgres { func NewPostgres(logger logger.Logger) bindings.OutputBinding {
return &Postgres{logger: logger} return &Postgres{logger: logger}
} }
@ -107,7 +107,7 @@ func (p *Postgres) Invoke(ctx context.Context, req *bindings.InvokeRequest) (res
}, },
} }
switch req.Operation { // nolint: exhaustive switch req.Operation { //nolint:exhaustive
case execOperation: case execOperation:
r, err := p.exec(ctx, sql) r, err := p.exec(ctx, sql)
if err != nil { if err != nil {
@ -154,13 +154,13 @@ func (p *Postgres) query(ctx context.Context, sql string) (result []byte, err er
return nil, errors.Wrapf(err, "error executing %s", sql) return nil, errors.Wrapf(err, "error executing %s", sql)
} }
rs := make([]interface{}, 0) rs := make([]any, 0)
for rows.Next() { for rows.Next() {
val, rowErr := rows.Values() val, rowErr := rows.Values()
if rowErr != nil { if rowErr != nil {
return nil, errors.Wrapf(rowErr, "error parsing result: %v", rows.Err()) return nil, errors.Wrapf(rowErr, "error parsing result: %v", rows.Err())
} }
rs = append(rs, val) rs = append(rs, val) //nolint:asasalint
} }
if result, err = json.Marshal(rs); err != nil { if result, err = json.Marshal(rs); err != nil {

View File

@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -61,8 +62,8 @@ func TestPostgresIntegration(t *testing.T) {
} }
// live DB test // live DB test
b := NewPostgres(logger.NewLogger("test")) b := NewPostgres(logger.NewLogger("test")).(*Postgres)
m := bindings.Metadata{Properties: map[string]string{connectionURLKey: url}} m := bindings.Metadata{Base: metadata.Base{Properties: map[string]string{connectionURLKey: url}}}
if err := b.Init(m); err != nil { if err := b.Init(m); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -43,7 +43,7 @@ type postmarkMetadata struct {
} }
// NewPostmark returns a new Postmark bindings instance. // NewPostmark returns a new Postmark bindings instance.
func NewPostmark(logger logger.Logger) *Postmark { func NewPostmark(logger logger.Logger) bindings.OutputBinding {
return &Postmark{logger: logger} return &Postmark{logger: logger}
} }

View File

@ -25,7 +25,7 @@ import (
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/internal/utils" "github.com/dapr/components-contrib/internal/utils"
contrib_metadata "github.com/dapr/components-contrib/metadata" contribMetadata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -65,7 +65,7 @@ type rabbitMQMetadata struct {
} }
// NewRabbitMQ returns a new rabbitmq instance. // NewRabbitMQ returns a new rabbitmq instance.
func NewRabbitMQ(logger logger.Logger) *RabbitMQ { func NewRabbitMQ(logger logger.Logger) bindings.InputOutputBinding {
return &RabbitMQ{logger: logger} return &RabbitMQ{logger: logger}
} }
@ -110,13 +110,13 @@ func (r *RabbitMQ) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bi
Body: req.Data, Body: req.Data,
} }
contentType, ok := contrib_metadata.TryGetContentType(req.Metadata) contentType, ok := contribMetadata.TryGetContentType(req.Metadata)
if ok { if ok {
pub.ContentType = contentType pub.ContentType = contentType
} }
ttl, ok, err := contrib_metadata.TryGetTTL(req.Metadata) ttl, ok, err := contribMetadata.TryGetTTL(req.Metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -128,7 +128,7 @@ func (r *RabbitMQ) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bi
pub.Expiration = strconv.FormatInt(ttl.Milliseconds(), 10) pub.Expiration = strconv.FormatInt(ttl.Milliseconds(), 10)
} }
priority, ok, err := contrib_metadata.TryGetPriority(req.Metadata) priority, ok, err := contribMetadata.TryGetPriority(req.Metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -196,7 +196,7 @@ func (r *RabbitMQ) parseMetadata(metadata bindings.Metadata) error {
m.MaxPriority = &maxPriority m.MaxPriority = &maxPriority
} }
ttl, ok, err := contrib_metadata.TryGetTTL(metadata.Properties) ttl, ok, err := contribMetadata.TryGetTTL(metadata.Properties)
if err != nil { if err != nil {
return err return err
} }

View File

@ -30,7 +30,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
contrib_metadata "github.com/dapr/components-contrib/metadata" contribMetadata "github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -72,17 +72,17 @@ func TestQueuesWithTTL(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{
Name: "testQueue", Name: "testQueue",
Properties: map[string]string{ Properties: map[string]string{
"queueName": queueName, "queueName": queueName,
"host": rabbitmqHost, "host": rabbitmqHost,
"deleteWhenUnused": strconv.FormatBool(exclusive), "deleteWhenUnused": strconv.FormatBool(exclusive),
"durable": strconv.FormatBool(durable), "durable": strconv.FormatBool(durable),
contrib_metadata.TTLMetadataKey: strconv.FormatInt(ttlInSeconds, 10), contribMetadata.TTLMetadataKey: strconv.FormatInt(ttlInSeconds, 10),
}, },
} }
logger := logger.NewLogger("test") logger := logger.NewLogger("test")
r := NewRabbitMQ(logger) r := NewRabbitMQ(logger).(*RabbitMQ)
err := r.Init(metadata) err := r.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
@ -139,7 +139,7 @@ func TestPublishingWithTTL(t *testing.T) {
logger := logger.NewLogger("test") logger := logger.NewLogger("test")
rabbitMQBinding1 := NewRabbitMQ(logger) rabbitMQBinding1 := NewRabbitMQ(logger).(*RabbitMQ)
err := rabbitMQBinding1.Init(metadata) err := rabbitMQBinding1.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
@ -156,7 +156,7 @@ func TestPublishingWithTTL(t *testing.T) {
writeRequest := bindings.InvokeRequest{ writeRequest := bindings.InvokeRequest{
Data: []byte(tooLateMsgContent), Data: []byte(tooLateMsgContent),
Metadata: map[string]string{ Metadata: map[string]string{
contrib_metadata.TTLMetadataKey: strconv.Itoa(ttlInSeconds), contribMetadata.TTLMetadataKey: strconv.Itoa(ttlInSeconds),
}, },
} }
@ -170,7 +170,7 @@ func TestPublishingWithTTL(t *testing.T) {
assert.False(t, ok) assert.False(t, ok)
// Getting before it is expired, should return it // Getting before it is expired, should return it
rabbitMQBinding2 := NewRabbitMQ(logger) rabbitMQBinding2 := NewRabbitMQ(logger).(*RabbitMQ)
err = rabbitMQBinding2.Init(metadata) err = rabbitMQBinding2.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
@ -178,7 +178,7 @@ func TestPublishingWithTTL(t *testing.T) {
writeRequest = bindings.InvokeRequest{ writeRequest = bindings.InvokeRequest{
Data: []byte(testMsgContent), Data: []byte(testMsgContent),
Metadata: map[string]string{ Metadata: map[string]string{
contrib_metadata.TTLMetadataKey: strconv.Itoa(ttlInSeconds * 1000), contribMetadata.TTLMetadataKey: strconv.Itoa(ttlInSeconds * 1000),
}, },
} }
_, err = rabbitMQBinding2.Invoke(context.Backgound(), &writeRequest) _, err = rabbitMQBinding2.Invoke(context.Backgound(), &writeRequest)
@ -204,18 +204,18 @@ func TestExclusiveQueue(t *testing.T) {
metadata := bindings.Metadata{ metadata := bindings.Metadata{
Name: "testQueue", Name: "testQueue",
Properties: map[string]string{ Properties: map[string]string{
"queueName": queueName, "queueName": queueName,
"host": rabbitmqHost, "host": rabbitmqHost,
"deleteWhenUnused": strconv.FormatBool(exclusive), "deleteWhenUnused": strconv.FormatBool(exclusive),
"durable": strconv.FormatBool(durable), "durable": strconv.FormatBool(durable),
"exclusive": strconv.FormatBool(exclusive), "exclusive": strconv.FormatBool(exclusive),
contrib_metadata.TTLMetadataKey: strconv.FormatInt(ttlInSeconds, 10), contribMetadata.TTLMetadataKey: strconv.FormatInt(ttlInSeconds, 10),
}, },
} }
logger := logger.NewLogger("test") logger := logger.NewLogger("test")
r := NewRabbitMQ(logger) r := NewRabbitMQ(logger).(*RabbitMQ)
err := r.Init(metadata) err := r.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
@ -267,7 +267,7 @@ func TestPublishWithPriority(t *testing.T) {
logger := logger.NewLogger("test") logger := logger.NewLogger("test")
r := NewRabbitMQ(logger) r := NewRabbitMQ(logger).(*RabbitMQ)
err := r.Init(metadata) err := r.Init(metadata)
assert.Nil(t, err) assert.Nil(t, err)
@ -283,7 +283,7 @@ func TestPublishWithPriority(t *testing.T) {
const middlePriorityMsgContent = "middle" const middlePriorityMsgContent = "middle"
_, err = r.Invoke(context.Backgound(), &bindings.InvokeRequest{ _, err = r.Invoke(context.Backgound(), &bindings.InvokeRequest{
Metadata: map[string]string{ Metadata: map[string]string{
contrib_metadata.PriorityMetadataKey: "5", contribMetadata.PriorityMetadataKey: "5",
}, },
Data: []byte(middlePriorityMsgContent), Data: []byte(middlePriorityMsgContent),
}) })
@ -292,7 +292,7 @@ func TestPublishWithPriority(t *testing.T) {
const lowPriorityMsgContent = "low" const lowPriorityMsgContent = "low"
_, err = r.Invoke(context.Backgound(), &bindings.InvokeRequest{ _, err = r.Invoke(context.Backgound(), &bindings.InvokeRequest{
Metadata: map[string]string{ Metadata: map[string]string{
contrib_metadata.PriorityMetadataKey: "1", contribMetadata.PriorityMetadataKey: "1",
}, },
Data: []byte(lowPriorityMsgContent), Data: []byte(lowPriorityMsgContent),
}) })
@ -301,7 +301,7 @@ func TestPublishWithPriority(t *testing.T) {
const highPriorityMsgContent = "high" const highPriorityMsgContent = "high"
_, err = r.Invoke(context.Backgound(), &bindings.InvokeRequest{ _, err = r.Invoke(context.Backgound(), &bindings.InvokeRequest{
Metadata: map[string]string{ Metadata: map[string]string{
contrib_metadata.PriorityMetadataKey: "10", contribMetadata.PriorityMetadataKey: "10",
}, },
Data: []byte(highPriorityMsgContent), Data: []byte(highPriorityMsgContent),
}) })

View File

@ -36,7 +36,7 @@ type Redis struct {
} }
// NewRedis returns a new redis bindings instance. // NewRedis returns a new redis bindings instance.
func NewRedis(logger logger.Logger) *Redis { func NewRedis(logger logger.Logger) bindings.OutputBinding {
return &Redis{logger: logger} return &Redis{logger: logger}
} }

View File

@ -43,7 +43,7 @@ type StateConfig struct {
} }
// NewRethinkDBStateChangeBinding returns a new RethinkDB actor event input binding. // NewRethinkDBStateChangeBinding returns a new RethinkDB actor event input binding.
func NewRethinkDBStateChangeBinding(logger logger.Logger) *Binding { func NewRethinkDBStateChangeBinding(logger logger.Logger) bindings.InputBinding {
return &Binding{ return &Binding{
logger: logger, logger: logger,
} }

View File

@ -22,6 +22,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
@ -41,7 +42,7 @@ func getNewRethinkActorBinding() *Binding {
l.SetOutputLevel(logger.DebugLevel) l.SetOutputLevel(logger.DebugLevel)
} }
return NewRethinkDBStateChangeBinding(l) return NewRethinkDBStateChangeBinding(l).(*Binding)
} }
/* /*
@ -63,10 +64,10 @@ func TestBinding(t *testing.T) {
testDuration = d testDuration = d
} }
m := bindings.Metadata{ m := bindings.Metadata{Base: metadata.Base{
Name: "test", Name: "test",
Properties: getTestMetadata(), Properties: getTestMetadata(),
} }}
assert.NotNil(t, m.Properties) assert.NotNil(t, m.Properties)
b := getNewRethinkActorBinding() b := getNewRethinkActorBinding()

View File

@ -56,7 +56,7 @@ type Metadata struct {
} }
// NewSMTP returns a new smtp binding instance. // NewSMTP returns a new smtp binding instance.
func NewSMTP(logger logger.Logger) *Mailer { func NewSMTP(logger logger.Logger) bindings.OutputBinding {
return &Mailer{logger: logger} return &Mailer{logger: logger}
} }

View File

@ -56,7 +56,7 @@ type sendGridRestError struct {
} }
// NewSendGrid returns a new SendGrid bindings instance. // NewSendGrid returns a new SendGrid bindings instance.
func NewSendGrid(logger logger.Logger) *SendGrid { func NewSendGrid(logger logger.Logger) bindings.OutputBinding {
return &SendGrid{logger: logger} return &SendGrid{logger: logger}
} }

View File

@ -49,7 +49,7 @@ type twilioMetadata struct {
timeout time.Duration timeout time.Duration
} }
func NewSMS(logger logger.Logger) *SMS { func NewSMS(logger logger.Logger) bindings.OutputBinding {
return &SMS{ return &SMS{
logger: logger, logger: logger,
httpClient: &http.Client{ httpClient: &http.Client{
@ -112,7 +112,7 @@ func (t *SMS) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*binding
vDr := *strings.NewReader(v.Encode()) vDr := *strings.NewReader(v.Encode())
twilioURL := fmt.Sprintf("%s%s/Messages.json", twilioURLBase, t.metadata.accountSid) twilioURL := fmt.Sprintf("%s%s/Messages.json", twilioURLBase, t.metadata.accountSid)
httpReq, err := http.NewRequestWithContext(ctx, "POST", twilioURL, &vDr) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, twilioURL, &vDr)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -16,7 +16,7 @@ package sms
import ( import (
"context" "context"
"errors" "errors"
"io/ioutil" "io"
"net/http" "net/http"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -68,14 +68,14 @@ func TestParseDuration(t *testing.T) {
func TestWriteShouldSucceed(t *testing.T) { func TestWriteShouldSucceed(t *testing.T) {
httpTransport := &mockTransport{ httpTransport := &mockTransport{
response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(""))}, response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(""))},
} }
m := bindings.Metadata{} m := bindings.Metadata{}
m.Properties = map[string]string{ m.Properties = map[string]string{
"toNumber": "toNumber", "fromNumber": "fromNumber", "toNumber": "toNumber", "fromNumber": "fromNumber",
"accountSid": "accountSid", "authToken": "authToken", "accountSid": "accountSid", "authToken": "authToken",
} }
tw := NewSMS(logger.NewLogger("test")) tw := NewSMS(logger.NewLogger("test")).(*SMS)
tw.httpClient = &http.Client{ tw.httpClient = &http.Client{
Transport: httpTransport, Transport: httpTransport,
} }
@ -105,14 +105,14 @@ func TestWriteShouldSucceed(t *testing.T) {
func TestWriteShouldFail(t *testing.T) { func TestWriteShouldFail(t *testing.T) {
httpTransport := &mockTransport{ httpTransport := &mockTransport{
response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(""))}, response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(""))},
} }
m := bindings.Metadata{} m := bindings.Metadata{}
m.Properties = map[string]string{ m.Properties = map[string]string{
"fromNumber": "fromNumber", "fromNumber": "fromNumber",
"accountSid": "accountSid", "authToken": "authToken", "accountSid": "accountSid", "authToken": "authToken",
} }
tw := NewSMS(logger.NewLogger("test")) tw := NewSMS(logger.NewLogger("test")).(*SMS)
tw.httpClient = &http.Client{ tw.httpClient = &http.Client{
Transport: httpTransport, Transport: httpTransport,
} }

View File

@ -36,7 +36,7 @@ type Binding struct {
} }
// NewTwitter returns a new Twitter event input binding. // NewTwitter returns a new Twitter event input binding.
func NewTwitter(logger logger.Logger) *Binding { func NewTwitter(logger logger.Logger) bindings.InputOutputBinding {
return &Binding{logger: logger} return &Binding{logger: logger}
} }

View File

@ -58,7 +58,7 @@ func getRuntimeMetadata() map[string]string {
// go test -v -count=1 ./bindings/twitter/. // go test -v -count=1 ./bindings/twitter/.
func TestInit(t *testing.T) { func TestInit(t *testing.T) {
m := getTestMetadata() m := getTestMetadata()
tw := NewTwitter(logger.NewLogger("test")) tw := NewTwitter(logger.NewLogger("test")).(*Binding)
err := tw.Init(m) err := tw.Init(m)
assert.Nilf(t, err, "error initializing valid metadata properties") assert.Nilf(t, err, "error initializing valid metadata properties")
} }
@ -66,7 +66,7 @@ func TestInit(t *testing.T) {
// TestReadError excutes the Read method and fails before the Twitter API call // TestReadError excutes the Read method and fails before the Twitter API call
// go test -v -count=1 -run TestReadError ./bindings/twitter/. // go test -v -count=1 -run TestReadError ./bindings/twitter/.
func TestReadError(t *testing.T) { func TestReadError(t *testing.T) {
tw := NewTwitter(logger.NewLogger("test")) tw := NewTwitter(logger.NewLogger("test")).(*Binding)
m := getTestMetadata() m := getTestMetadata()
err := tw.Init(m) err := tw.Init(m)
assert.Nilf(t, err, "error initializing valid metadata properties") assert.Nilf(t, err, "error initializing valid metadata properties")
@ -90,7 +90,7 @@ func TestRead(t *testing.T) {
m.Properties = getRuntimeMetadata() m.Properties = getRuntimeMetadata()
// add query // add query
m.Properties["query"] = "microsoft" m.Properties["query"] = "microsoft"
tw := NewTwitter(logger.NewLogger("test")) tw := NewTwitter(logger.NewLogger("test")).(*Binding)
tw.logger.SetOutputLevel(logger.DebugLevel) tw.logger.SetOutputLevel(logger.DebugLevel)
err := tw.Init(m) err := tw.Init(m)
assert.Nilf(t, err, "error initializing read") assert.Nilf(t, err, "error initializing read")
@ -126,7 +126,7 @@ func TestInvoke(t *testing.T) {
} }
m := bindings.Metadata{} m := bindings.Metadata{}
m.Properties = getRuntimeMetadata() m.Properties = getRuntimeMetadata()
tw := NewTwitter(logger.NewLogger("test")) tw := NewTwitter(logger.NewLogger("test")).(*Binding)
tw.logger.SetOutputLevel(logger.DebugLevel) tw.logger.SetOutputLevel(logger.DebugLevel)
err := tw.Init(m) err := tw.Init(m)
assert.Nilf(t, err, "error initializing Invoke") assert.Nilf(t, err, "error initializing Invoke")

View File

@ -20,16 +20,17 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger" "github.com/dapr/kit/logger"
) )
func TestParseMetadata(t *testing.T) { func TestParseMetadata(t *testing.T) {
m := bindings.Metadata{Properties: map[string]string{ m := bindings.Metadata{Base: metadata.Base{Properties: map[string]string{
"gatewayAddr": "172.0.0.1:1234", "gatewayAddr": "172.0.0.1:1234",
"gatewayKeepAlive": "5s", "gatewayKeepAlive": "5s",
"caCertificatePath": "/cert/path", "caCertificatePath": "/cert/path",
"usePlaintextConnection": "true", "usePlaintextConnection": "true",
}} }}}
client := ClientFactoryImpl{logger: logger.NewLogger("test")} client := ClientFactoryImpl{logger: logger.NewLogger("test")}
meta, err := client.parseMetadata(m) meta, err := client.parseMetadata(m)
assert.NoError(t, err) assert.NoError(t, err)
@ -49,7 +50,7 @@ func TestGatewayAddrMetadataIsMandatory(t *testing.T) {
} }
func TestParseMetadataDefaultValues(t *testing.T) { func TestParseMetadataDefaultValues(t *testing.T) {
m := bindings.Metadata{Properties: map[string]string{"gatewayAddr": "172.0.0.1:1234"}} m := bindings.Metadata{Base: metadata.Base{Properties: map[string]string{"gatewayAddr": "172.0.0.1:1234"}}}
client := ClientFactoryImpl{logger: logger.NewLogger("test")} client := ClientFactoryImpl{logger: logger.NewLogger("test")}
meta, err := client.parseMetadata(m) meta, err := client.parseMetadata(m)
assert.NoError(t, err) assert.NoError(t, err)

Some files were not shown because too many files have changed in this diff Show More