Create the storage container if it doesn't exist (to preserve compatibility with old SDK's behavior)

Also fixes to tests

Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
ItalyPaleAle 2023-01-20 23:09:48 +00:00
parent c76526c805
commit a0251482c2
3 changed files with 144 additions and 78 deletions

View File

@ -26,6 +26,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs/checkpoints"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"golang.org/x/exp/maps"
azauth "github.com/dapr/components-contrib/internal/authentication/azure"
@ -486,7 +488,7 @@ func (aeh *AzureEventHubs) getProducerClientForTopic(ctx context.Context, topic
// Creates a processor for a given topic.
func (aeh *AzureEventHubs) getProcessorForTopic(ctx context.Context, topic string) (*azeventhubs.Processor, error) {
// Get the checkpoint store
checkpointStore, err := aeh.getCheckpointStore()
checkpointStore, err := aeh.getCheckpointStore(ctx)
if err != nil {
return nil, fmt.Errorf("unable to connect to the checkpoint store: %w", err)
}
@ -546,7 +548,7 @@ func (aeh *AzureEventHubs) getProcessorForTopic(ctx context.Context, topic strin
}
// Returns the checkpoint store from the object. If it doesn't exist, it lazily initializes it.
func (aeh *AzureEventHubs) getCheckpointStore() (azeventhubs.CheckpointStore, error) {
func (aeh *AzureEventHubs) getCheckpointStore(ctx context.Context) (azeventhubs.CheckpointStore, error) {
// Check if we have the checkpoint store
aeh.checkpointStoreLock.RLock()
if aeh.checkpointStoreCache != nil {
@ -565,7 +567,7 @@ func (aeh *AzureEventHubs) getCheckpointStore() (azeventhubs.CheckpointStore, er
// Init the checkpoint store and store it in the object
var err error
aeh.checkpointStoreCache, err = aeh.createCheckpointStore()
aeh.checkpointStoreCache, err = aeh.createCheckpointStore(ctx)
if err != nil {
return nil, fmt.Errorf("unable to connect to the checkpoint store: %w", err)
}
@ -574,7 +576,7 @@ func (aeh *AzureEventHubs) getCheckpointStore() (azeventhubs.CheckpointStore, er
}
// Initializes a new checkpoint store
func (aeh *AzureEventHubs) createCheckpointStore() (checkpointStore azeventhubs.CheckpointStore, err error) {
func (aeh *AzureEventHubs) createCheckpointStore(ctx context.Context) (checkpointStore azeventhubs.CheckpointStore, err error) {
if aeh.metadata.StorageAccountName == "" {
return nil, errors.New("property storageAccountName is required to subscribe to an Event Hub topic")
}
@ -582,6 +584,13 @@ func (aeh *AzureEventHubs) createCheckpointStore() (checkpointStore azeventhubs.
return nil, errors.New("property storageContainerName is required to subscribe to an Event Hub topic")
}
// Ensure the container exists
err = aeh.ensureStorageContainer(ctx)
if err != nil {
return nil, err
}
// Create the checkpoint store
checkpointStoreOpts := &checkpoints.BlobStoreOptions{
ClientOptions: policy.ClientOptions{
Telemetry: policy.TelemetryOptions{
@ -620,6 +629,89 @@ func (aeh *AzureEventHubs) createCheckpointStore() (checkpointStore azeventhubs.
return checkpointStore, nil
}
// Ensures that the container exists in the Azure Storage Account.
// This is done to preserve backwards-compatibility with Dapr 1.9, as the old checkpoint SDK created them automatically.
func (aeh *AzureEventHubs) ensureStorageContainer(parentCtx context.Context) error {
// Get a client to Azure Blob Storage
client, err := aeh.createStorageClient()
if err != nil {
return err
}
// Create the container
// This will return an error if it already exists
ctx, cancel := context.WithTimeout(parentCtx, resourceCreationTimeout)
defer cancel()
_, err = client.CreateContainer(ctx, aeh.metadata.StorageContainerName, &container.CreateOptions{
// Default is private
Access: nil,
})
if err != nil {
// Check if it's an Azure Storage error
resErr := &azcore.ResponseError{}
// If the container already exists, return no error
if errors.As(err, &resErr) && (resErr.ErrorCode == "ContainerAlreadyExists" || resErr.ErrorCode == "ResourceAlreadyExists") {
return nil
}
return fmt.Errorf("failed to create Azure Storage container %s: %w", aeh.metadata.StorageContainerName, err)
}
return nil
}
// Creates a client to access Azure Blob Storage
func (aeh *AzureEventHubs) createStorageClient() (*azblob.Client, error) {
options := azblob.ClientOptions{
ClientOptions: azcore.ClientOptions{
Telemetry: policy.TelemetryOptions{
ApplicationID: "dapr-" + logger.DaprVersion,
},
},
}
var (
err error
client *azblob.Client
)
// Use the global URL for Azure Storage
accountURL := fmt.Sprintf("https://%s.blob.%s", aeh.metadata.StorageAccountName, "core.windows.net")
if aeh.metadata.StorageConnectionString != "" {
// Authenticate with a connection string
client, err = azblob.NewClientFromConnectionString(aeh.metadata.StorageConnectionString, &options)
if err != nil {
return nil, fmt.Errorf("error creating Azure Storage client from connection string: %w", err)
}
} else if aeh.metadata.StorageAccountKey != "" {
// Authenticate with a shared key
credential, newSharedKeyErr := azblob.NewSharedKeyCredential(aeh.metadata.StorageAccountName, aeh.metadata.StorageAccountKey)
if newSharedKeyErr != nil {
return nil, fmt.Errorf("invalid Azure Storage shared key credentials with error: %w", newSharedKeyErr)
}
client, err = azblob.NewClientWithSharedKeyCredential(accountURL, credential, &options)
if err != nil {
return nil, fmt.Errorf("error creating Azure Storage client from shared key credentials: %w", err)
}
} else {
// Use Azure AD
settings, err := azauth.NewEnvironmentSettings("storage", aeh.metadata.properties)
if err != nil {
return nil, err
}
credential, tokenErr := settings.GetTokenCredential()
if tokenErr != nil {
return nil, fmt.Errorf("invalid Azure Storage token credentials with error: %w", tokenErr)
}
client, err = azblob.NewClient(accountURL, credential, &options)
if err != nil {
return nil, fmt.Errorf("error creating Azure Storage client from token credentials: %w", err)
}
}
return client, nil
}
// Returns a connection string with the Event Hub name (entity path) set if not present.
func (aeh *AzureEventHubs) constructConnectionStringFromTopic(topic string) (string, error) {
if aeh.metadata.hubName != "" {

View File

@ -17,16 +17,13 @@ import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"github.com/cenkalti/backoff/v4"
azauth "github.com/dapr/components-contrib/internal/authentication/azure"
"github.com/dapr/kit/logger"
"github.com/dapr/kit/retry"
)
@ -37,7 +34,7 @@ import (
// TODO(@ItalyPaleAle): Remove this for Dapr 1.13
func (aeh *AzureEventHubs) ensureNoTrack1Subscribers(parentCtx context.Context, topic string) error {
// Get a client to Azure Blob Storage
client, err := aeh.createContainerStorageClient()
client, err := aeh.createStorageClient()
if err != nil {
return err
}
@ -55,7 +52,7 @@ func (aeh *AzureEventHubs) ensureNoTrack1Subscribers(parentCtx context.Context,
backOffConfig.MaxRetries = -1
b := backOffConfig.NewBackOffWithContext(parentCtx)
err = backoff.Retry(func() error {
pager := client.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{
pager := client.NewListBlobsFlatPager(aeh.metadata.StorageContainerName, &container.ListBlobsFlatOptions{
Prefix: &prefix,
})
for pager.More() {
@ -64,6 +61,12 @@ func (aeh *AzureEventHubs) ensureNoTrack1Subscribers(parentCtx context.Context,
cancel()
if innerErr != nil {
// Treat these errors as permanent
resErr := &azcore.ResponseError{}
if !errors.As(err, &resErr) || resErr.StatusCode != http.StatusNotFound {
// A "not-found" error means that the storage container doesn't exist, so let's not handle it here
// Just return no error
return nil
}
return backoff.Permanent(fmt.Errorf("failed to list blobs: %w", innerErr))
}
for _, blob := range resp.Segment.BlobItems {
@ -87,54 +90,3 @@ func (aeh *AzureEventHubs) ensureNoTrack1Subscribers(parentCtx context.Context,
}
return err
}
func (aeh *AzureEventHubs) createContainerStorageClient() (*container.Client, error) {
options := container.ClientOptions{
ClientOptions: azcore.ClientOptions{
Telemetry: policy.TelemetryOptions{
ApplicationID: "dapr-" + logger.DaprVersion,
},
},
}
var (
err error
client *container.Client
)
// Use the global URL for Azure Storage
containerURL := fmt.Sprintf("https://%s.blob.%s/%s", aeh.metadata.StorageAccountName, "core.windows.net", aeh.metadata.StorageContainerName)
if aeh.metadata.StorageConnectionString != "" {
// Authenticate with a connection string
client, err = container.NewClientFromConnectionString(aeh.metadata.StorageConnectionString, aeh.metadata.StorageContainerName, &options)
if err != nil {
return nil, fmt.Errorf("error creating Azure Storage client from connection string: %w", err)
}
} else if aeh.metadata.StorageAccountKey != "" {
// Authenticate with a shared key
credential, newSharedKeyErr := azblob.NewSharedKeyCredential(aeh.metadata.StorageAccountName, aeh.metadata.StorageAccountKey)
if newSharedKeyErr != nil {
return nil, fmt.Errorf("invalid Azure Storage shared key credentials with error: %w", newSharedKeyErr)
}
client, err = container.NewClientWithSharedKeyCredential(containerURL, credential, &options)
if err != nil {
return nil, fmt.Errorf("error creating Azure Storage client from shared key credentials: %w", err)
}
} else {
// Use Azure AD
settings, err := azauth.NewEnvironmentSettings("storage", aeh.metadata.properties)
if err != nil {
return nil, err
}
credential, tokenErr := settings.GetTokenCredential()
if tokenErr != nil {
return nil, fmt.Errorf("invalid Azure Storage token credentials with error: %w", tokenErr)
}
client, err = container.NewClient(containerURL, credential, &options)
if err != nil {
return nil, fmt.Errorf("error creating Azure Storage client from token credentials: %w", err)
}
}
return client, nil
}

View File

@ -27,8 +27,8 @@ import (
"go.uber.org/multierr"
// Pub-Sub.
pubsub_evethubs "github.com/dapr/components-contrib/pubsub/azure/eventhubs"
"github.com/dapr/components-contrib/pubsub"
pubsub_eventhubs "github.com/dapr/components-contrib/pubsub/azure/eventhubs"
secretstore_env "github.com/dapr/components-contrib/secretstores/local/env"
pubsub_loader "github.com/dapr/dapr/pkg/components/pubsub"
secretstores_loader "github.com/dapr/dapr/pkg/components/secretstores"
@ -131,7 +131,7 @@ func TestEventhubs(t *testing.T) {
client := sidecar.GetClient(ctx, sidecarName)
// publish messages
ctx.Logf("Publishing messages. sidecarName: %s, topicName: %s", sidecarName, topicName)
logs := fmt.Sprintf("Published messages. sidecarName: %s, topicName: %s", sidecarName, topicName)
var publishOptions dapr.PublishEventOption
@ -139,8 +139,8 @@ func TestEventhubs(t *testing.T) {
publishOptions = dapr.PublishEventWithMetadata(metadata)
}
for _, message := range messages {
ctx.Logf("Publishing: %q", message)
for i, message := range messages {
logs += fmt.Sprintf("\nMessage %d: %s", i, message)
var err error
if publishOptions != nil {
@ -149,8 +149,11 @@ func TestEventhubs(t *testing.T) {
err = client.PublishEvent(ctx, pubsubName, topicName, message)
}
require.NoError(ctx, err, "error publishing message")
require.NoErrorf(ctx, err, "error publishing message %s", message)
}
ctx.Log(logs)
return nil
}
}
@ -159,7 +162,7 @@ func TestEventhubs(t *testing.T) {
return func(ctx flow.Context) error {
// assert for messages
for _, m := range messageWatchers {
m.Assert(ctx, 25*timeout)
m.Assert(ctx, 10*timeout)
}
return nil
@ -181,13 +184,19 @@ func TestEventhubs(t *testing.T) {
messageWatchers.ExpectStrings(messages...)
output, err := exec.Command("/bin/sh", "send-iot-device-events.sh", topicToBeCreated).Output()
assert.Nil(t, err, "Error in send-iot-device-events.sh.:\n%s", string(output))
assert.NoErrorf(t, err, "Error in send-iot-device-events.sh.:\n%s", string(output))
return nil
}
}
// Topic name for a IOT device is same as IOTHubName
iotHubName := os.Getenv(iotHubNameEnvKey)
// Here so we can comment out tests as needed
_ = publishMessageAsDevice
_ = iotHubName
_ = consumerGroup4
_ = consumerGroup5
flow.New(t, "eventhubs certification").
// Test : single publisher, multiple subscriber with their own consumerID
@ -201,7 +210,8 @@ func TestEventhubs(t *testing.T) {
embedded.WithAppProtocol(runtime.HTTPProtocol, appPort),
embedded.WithDaprGRPCPort(runtime.DefaultDaprAPIGRPCPort),
embedded.WithDaprHTTPPort(runtime.DefaultDaprHTTPPort),
componentRuntimeOptions(),
embedded.WithProfilePort(runtime.DefaultProfilePort),
componentRuntimeOptions(1),
)).
// Run subscriberApplication app2
@ -215,8 +225,9 @@ func TestEventhubs(t *testing.T) {
embedded.WithDaprGRPCPort(runtime.DefaultDaprAPIGRPCPort+portOffset),
embedded.WithDaprHTTPPort(runtime.DefaultDaprHTTPPort+portOffset),
embedded.WithProfilePort(runtime.DefaultProfilePort+portOffset),
componentRuntimeOptions(),
componentRuntimeOptions(2),
)).
Step("wait", flow.Sleep(10*time.Second)).
Step("publish messages to topic1", publishMessages(nil, sidecarName1, topicActiveName, consumerGroup1, consumerGroup2)).
Step("publish messages to unUsedTopic", publishMessages(nil, sidecarName1, topicPassiveName)).
Step("verify if app1 has recevied messages published to topic1", assertMessages(10*time.Second, consumerGroup1)).
@ -235,13 +246,15 @@ func TestEventhubs(t *testing.T) {
embedded.WithDaprGRPCPort(runtime.DefaultDaprAPIGRPCPort+portOffset*2),
embedded.WithDaprHTTPPort(runtime.DefaultDaprHTTPPort+portOffset*2),
embedded.WithProfilePort(runtime.DefaultProfilePort+portOffset*2),
componentRuntimeOptions(),
componentRuntimeOptions(3),
)).
Step("wait", flow.Sleep(10*time.Second)).
// publish message in topic1 from two publisher apps, however there are two subscriber apps (app2,app3) with same consumerID
Step("publish messages to topic1", publishMessages(metadata, sidecarName1, topicActiveName, consumerGroup2)).
Step("publish messages to topic1", publishMessages(metadata1, sidecarName2, topicActiveName, consumerGroup2)).
Step("publish messages to topic1 from app1", publishMessages(metadata, sidecarName1, topicActiveName, consumerGroup2)).
Step("publish messages to topic1 from app2", publishMessages(metadata1, sidecarName2, topicActiveName, consumerGroup2)).
Step("verify if app2, app3 together have recevied messages published to topic1", assertMessages(10*time.Second, consumerGroup2)).
// Test : Entitymanagement , Test partition key, in order processing with single publisher/subscriber
// Run subscriberApplication app4
Step(app.Run(appID4, fmt.Sprintf(":%d", appPort+portOffset*3),
@ -254,7 +267,7 @@ func TestEventhubs(t *testing.T) {
embedded.WithDaprGRPCPort(runtime.DefaultDaprAPIGRPCPort+portOffset*3),
embedded.WithDaprHTTPPort(runtime.DefaultDaprHTTPPort+portOffset*3),
embedded.WithProfilePort(runtime.DefaultProfilePort+portOffset*3),
componentRuntimeOptions(),
componentRuntimeOptions(4),
)).
Step(fmt.Sprintf("publish messages to topicToBeCreated: %s", topicToBeCreated), publishMessages(metadata, sidecarName4, topicToBeCreated, consumerGroup4)).
Step("verify if app4 has recevied messages published to newly created topic", assertMessages(10*time.Second, consumerGroup4)).
@ -270,22 +283,31 @@ func TestEventhubs(t *testing.T) {
embedded.WithDaprGRPCPort(runtime.DefaultDaprAPIGRPCPort+portOffset*4),
embedded.WithDaprHTTPPort(runtime.DefaultDaprHTTPPort+portOffset*4),
embedded.WithProfilePort(runtime.DefaultProfilePort+portOffset*4),
componentRuntimeOptions(),
componentRuntimeOptions(5),
)).
Step("add expected IOT messages (simulate add message to iot)", publishMessageAsDevice(consumerGroup5)).
Step("verify if app5 has recevied messages published to iot topic", assertMessages(40*time.Second, consumerGroup5)).
Step("wait", flow.Sleep(5*time.Second)).
// cleanup azure assets created as part of tests
Step("delete eventhub created as part of the eventhub management test", deleteEventhub).
Run()
}
func componentRuntimeOptions() []runtime.Option {
func componentRuntimeOptions(instance int) []runtime.Option {
log := logger.NewLogger("dapr.components")
log.SetOutputLevel(logger.DebugLevel)
pubsubRegistry := pubsub_loader.NewRegistry()
pubsubRegistry.Logger = log
pubsubRegistry.RegisterComponent(pubsub_evethubs.NewAzureEventHubs, "azure.eventhubs")
pubsubRegistry.RegisterComponent(func(l logger.Logger) pubsub.PubSub {
l = l.WithFields(map[string]any{
"component": "pubsub.azure.eventhubs",
"instance": instance,
})
l.Infof("Instantiated log for instance %d", instance)
return pubsub_eventhubs.NewAzureEventHubs(l)
}, "azure.eventhubs")
secretstoreRegistry := secretstores_loader.NewRegistry()
secretstoreRegistry.Logger = log