Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Service Bus] Update constructors to add overloads and use Azure Identity #5436

Merged
merged 25 commits into from
Jan 15, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions sdk/servicebus/service-bus/.scripts/buildBrowserTestResources.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ EnvVarKeys = {
TOPIC_FILTER_NAME: "TOPIC_FILTER_NAME_BROWSER",
TOPIC_FILTER_SUBSCRIPTION_NAME: "TOPIC_FILTER_SUBSCRIPTION_NAME_BROWSER",
TOPIC_FILTER_DEFAULT_SUBSCRIPTION_NAME: "TOPIC_FILTER_DEFAULT_SUBSCRIPTION_NAME_BROWSER",
AAD_CLIENT_ID: "AAD_CLIENT_ID",
AAD_CLIENT_SECRET: "AAD_CLIENT_SECRET",
AAD_TENANT_ID: "AAD_TENANT_ID",
AZURE_CLIENT_ID: "AZURE_CLIENT_ID",
AZURE_CLIENT_SECRET: "AZURE_CLIENT_SECRET",
AZURE_TENANT_ID: "AZURE_TENANT_ID",
RESOURCE_GROUP: "RESOURCE_GROUP",
AZURE_SUBSCRIPTION_ID: "AZURE_SUBSCRIPTION_ID",
CLEAN_NAMESPACE: "CLEAN_NAMESPACE"
Expand All @@ -35,9 +35,9 @@ EnvVarKeys = {
const mandatoryEnvVars = [EnvVarKeys.SERVICEBUS_CONNECTION_STRING];

const aadRelatedEnvVars = [
EnvVarKeys.AAD_CLIENT_ID,
EnvVarKeys.AAD_CLIENT_SECRET,
EnvVarKeys.AAD_TENANT_ID,
EnvVarKeys.AZURE_CLIENT_ID,
EnvVarKeys.AZURE_CLIENT_SECRET,
EnvVarKeys.AZURE_TENANT_ID,
EnvVarKeys.AZURE_SUBSCRIPTION_ID,
EnvVarKeys.RESOURCE_GROUP
];
Expand Down Expand Up @@ -155,9 +155,9 @@ async function buildResources() {
async function recreateQueue(queueName, parameters) {
await msRestNodeAuth
.loginWithServicePrincipalSecret(
env[EnvVarKeys.AAD_CLIENT_ID],
env[EnvVarKeys.AAD_CLIENT_SECRET],
env[EnvVarKeys.AAD_TENANT_ID]
env[EnvVarKeys.AZURE_CLIENT_ID],
env[EnvVarKeys.AZURE_CLIENT_SECRET],
env[EnvVarKeys.AZURE_TENANT_ID]
)
.then(async (creds) => {
const client = await new ServiceBusManagementClient(
Expand Down Expand Up @@ -187,9 +187,9 @@ async function buildResources() {
async function recreateTopic(topicName, parameters) {
await msRestNodeAuth
.loginWithServicePrincipalSecret(
env[EnvVarKeys.AAD_CLIENT_ID],
env[EnvVarKeys.AAD_CLIENT_SECRET],
env[EnvVarKeys.AAD_TENANT_ID]
env[EnvVarKeys.AZURE_CLIENT_ID],
env[EnvVarKeys.AZURE_CLIENT_SECRET],
env[EnvVarKeys.AZURE_TENANT_ID]
)
.then(async (creds) => {
const client = await new ServiceBusManagementClient(
Expand Down Expand Up @@ -219,9 +219,9 @@ async function buildResources() {
async function recreateSubscription(topicName, subscriptionName, parameters) {
await msRestNodeAuth
.loginWithServicePrincipalSecret(
env[EnvVarKeys.AAD_CLIENT_ID],
env[EnvVarKeys.AAD_CLIENT_SECRET],
env[EnvVarKeys.AAD_TENANT_ID]
env[EnvVarKeys.AZURE_CLIENT_ID],
env[EnvVarKeys.AZURE_CLIENT_SECRET],
env[EnvVarKeys.AZURE_TENANT_ID]
)
.then(async (creds) => {
const client = await new ServiceBusManagementClient(
Expand Down Expand Up @@ -309,9 +309,9 @@ function getEnvVars() {
[EnvVarKeys.TOPIC_FILTER_DEFAULT_SUBSCRIPTION_NAME]:
process.env[EnvVarKeys.TOPIC_FILTER_DEFAULT_SUBSCRIPTION_NAME] ||
"topic-filter-default-subscription-browser",
[EnvVarKeys.AAD_CLIENT_ID]: process.env[EnvVarKeys.AAD_CLIENT_ID],
[EnvVarKeys.AAD_CLIENT_SECRET]: process.env[EnvVarKeys.AAD_CLIENT_SECRET],
[EnvVarKeys.AAD_TENANT_ID]: process.env[EnvVarKeys.AAD_TENANT_ID],
[EnvVarKeys.AZURE_CLIENT_ID]: process.env[EnvVarKeys.AZURE_CLIENT_ID],
[EnvVarKeys.AZURE_CLIENT_SECRET]: process.env[EnvVarKeys.AZURE_CLIENT_SECRET],
[EnvVarKeys.AZURE_TENANT_ID]: process.env[EnvVarKeys.AZURE_TENANT_ID],
[EnvVarKeys.RESOURCE_GROUP]: process.env[EnvVarKeys.RESOURCE_GROUP],
[EnvVarKeys.AZURE_SUBSCRIPTION_ID]: process.env[EnvVarKeys.AZURE_SUBSCRIPTION_ID],
[EnvVarKeys.CLEAN_NAMESPACE]: process.env[EnvVarKeys.CLEAN_NAMESPACE] || false
Expand Down
6 changes: 3 additions & 3 deletions sdk/servicebus/service-bus/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ module.exports = function(config) {
"TOPIC_FILTER_NAME_BROWSER",
"TOPIC_FILTER_SUBSCRIPTION_NAME_BROWSER",
"TOPIC_FILTER_DEFAULT_SUBSCRIPTION_NAME_BROWSER",
"AAD_CLIENT_ID",
"AAD_CLIENT_SECRET",
"AAD_TENANT_ID",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"AZURE_TENANT_ID",
"RESOURCE_GROUP",
"AZURE_SUBSCRIPTION_ID",
"CLEAN_NAMESPACE"
Expand Down
3 changes: 2 additions & 1 deletion sdk/servicebus/service-bus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@
]
},
"dependencies": {
"@azure/core-amqp": "1.0.0-preview.3",
Copy link
Contributor

@ramya-rao-a ramya-rao-a Jan 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to use latest of core-amqp and fixing MessagingError as a consequence is being done as part of #6861
Any reason to include these changes in this PR?

Copy link
Member Author

@ramya0820 ramya0820 Jan 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason, ideally that should have been merged already - branch will get updated once that's done

EDIT: I think this was necessary for build to pass based on version used in event hubs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to use the allowedAlternativeVersions section in the common-versions.json file to get rush to allow you to use different versions of @azure/core-amqp in Service Bus vs Event Hubs.

This PR is for the Azure identity work and it would help if the changes here are restricted to using the @azure/identity library in the constructors.

If #6861 gets merged before this PR, we can then

  • rebase your branch from feature/service-bus-track-2
  • revert the change to common-versions.json file

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked over the diffs - the duplicate changes related to core-amqp are very few and can be ignored - it's this one line change updating version, and the type casting ones in files messageReceiver.ts, messageSender.ts and batchingReceiver.ts. (You can ignore the files altogether if they already appear in #6861)

All other 24-25 files are relevant and can be reviewed. The build succeeds as well.

"@azure/core-amqp": "^1.0.0",
"@azure/core-http": "^1.0.0",
"@azure/ms-rest-nodeauth": "^0.9.2",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@azure/ms-rest-nodeauth should no longer be needed now that we are using @azure/identity

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still being used in the tests - moved it to dev dependency

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the removal of the aadutils file, I believe @azure/ms-rest-nodeauth is not used in tests either.

"@azure/identity": "^1.0.0",
"@opentelemetry/types": "^0.2.0",
"@types/is-buffer": "^2.0.0",
"@types/long": "^4.0.0",
Expand Down
3 changes: 2 additions & 1 deletion sdk/servicebus/service-bus/review/service-bus.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ export class Sender {

// @public
export class ServiceBusClient {
constructor(connectionString: string, options?: ServiceBusClientOptions);
constructor(host: string, credential: TokenCredential, options?: ServiceBusClientOptions);
close(): Promise<any>;
static createFromConnectionString(connectionString: string, options?: ServiceBusClientOptions): ServiceBusClient;
createQueueClient(queueName: string): QueueClient;
createSubscriptionClient(topicName: string, subscriptionName: string): SubscriptionClient;
createTopicClient(topicName: string): TopicClient;
Expand Down
4 changes: 2 additions & 2 deletions sdk/servicebus/service-bus/src/core/batchingReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class BatchingReceiver extends MessageReceiver {
const sessionError = context.session && context.session.error;
let error = new MessagingError("An error occurred while receiving messages.");
if (sessionError) {
error = translate(sessionError);
error = translate(sessionError) as MessagingError;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This casting is not required. Same for a few lines below.

log.error(
"[%s] 'session_close' event occurred for Receiver '%s' received an error:\n%O",
this._context.namespace.connectionId,
Expand Down Expand Up @@ -263,7 +263,7 @@ export class BatchingReceiver extends MessageReceiver {
const receiverError = context.receiver && context.receiver.error;
let error = new MessagingError("An error occurred while receiving messages.");
if (receiverError) {
error = translate(receiverError);
error = translate(receiverError) as MessagingError;
log.error(
"[%s] Receiver '%s' received an error:\n%O",
this._context.namespace.connectionId,
Expand Down
6 changes: 3 additions & 3 deletions sdk/servicebus/service-bus/src/core/messageReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ export class MessageReceiver extends LinkEntity {
const receiver = this._receiver || context.receiver!;
const receiverError = context.receiver && context.receiver.error;
if (receiverError) {
const sbError = translate(receiverError);
const sbError = translate(receiverError) as MessagingError;
log.error(
"[%s] An error occurred for Receiver '%s': %O.",
connectionId,
Expand Down Expand Up @@ -587,7 +587,7 @@ export class MessageReceiver extends LinkEntity {
const receiver = this._receiver || context.receiver!;
const sessionError = context.session && context.session.error;
if (sessionError) {
const sbError = translate(sessionError);
const sbError = translate(sessionError) as MessagingError;
log.error(
"[%s] An error occurred on the session for Receiver '%s': %O.",
connectionId,
Expand Down Expand Up @@ -875,7 +875,7 @@ export class MessageReceiver extends LinkEntity {
// We should attempt to reopen only when the receiver(sdk) did not initiate the close
let shouldReopen = false;
if (receiverError && !wasCloseInitiated) {
const translatedError = translate(receiverError);
const translatedError = translate(receiverError) as MessagingError;
if (translatedError.retryable) {
shouldReopen = true;
log.error(
Expand Down
5 changes: 3 additions & 2 deletions sdk/servicebus/service-bus/src/core/messageSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
RetryConfig,
RetryOperationType,
Constants,
delay
delay,
MessagingError
} from "@azure/core-amqp";
import {
SendableMessageInfo,
Expand Down Expand Up @@ -496,7 +497,7 @@ export class MessageSender extends LinkEntity {
// We should attempt to reopen only when the sender(sdk) did not initiate the close
let shouldReopen = false;
if (senderError && !wasCloseInitiated) {
const translatedError = translate(senderError);
const translatedError = translate(senderError) as MessagingError;
if (translatedError.retryable) {
shouldReopen = true;
log.error(
Expand Down
112 changes: 75 additions & 37 deletions sdk/servicebus/service-bus/src/serviceBusClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
ConnectionConfig,
DataTransformer,
TokenCredential,
SharedKeyCredential
SharedKeyCredential,
isTokenCredential
} from "@azure/core-amqp";
import { SubscriptionClient } from "./subscriptionClient";

Expand Down Expand Up @@ -56,23 +57,87 @@ export class ServiceBusClient {
*/
private _context: ConnectionContext;

/**
* Creates a ServiceBusClient for the Service Bus Namespace represented in the given connection
* string.
* @param connectionString - Connection string of the form
* 'Endpoint=sb://my-servicebus-namespace.servicebus.windows.net/;SharedAccessKeyName=my-SA-name;SharedAccessKey=my-SA-key'
* @param options Options to control ways to interact with the
* Service Bus Namespace.
* @returns ServiceBusClient
*/
constructor(connectionString: string, options?: ServiceBusClientOptions);

/**
* Instantiates a ServiceBusClient to interact with a Service Bus Namespace.
*
* @constructor
* @param {ConnectionConfig} config - The connection configuration needed to connect to the
* Service Bus Namespace.
* @param {TokenCredential} [tokenCredential] - SharedKeyCredential object or your
* credential that implements the TokenCredential interface.
* @param {ServiceBusClientOptions} - Options to control ways to interact with the Service Bus
* @param host - The host name for the Service Bus namespace. This is likely to be similar to
* <yournamespace>.servicebus.windows.net
* @param credential - credential that implements the TokenCredential interface.
* @param options - Options to control ways to interact with the Service Bus
* Namespace.
*/
private constructor(
config: ConnectionConfig,
credential: SharedKeyCredential | TokenCredential,
constructor(host: string, credential: TokenCredential, options?: ServiceBusClientOptions);
constructor(
hostOrConnectionString: string,
credentialOrServiceBusClientOptions?: TokenCredential | ServiceBusClientOptions,
options?: ServiceBusClientOptions
) {
if (!options) options = {};
let config;
let credential;
let connectionString;

if (!options) {
options = {};
}

if (
hostOrConnectionString == undefined ||
hostOrConnectionString.toString == undefined ||
hostOrConnectionString.toString().trim() == ""
) {
throw new Error(
"Input parameter of host or connection string must be defined and coercible to string."
);
}
hostOrConnectionString = hostOrConnectionString.toString();

let isConnectionStringConstructorInvoked = true;
if (hostOrConnectionString.indexOf("=") < 0) {
isConnectionStringConstructorInvoked = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above checks are not needed. ConnectionConfig.create() ensures that String() is called on the given connection string. If the connection string thus stringified doesnt meet the requirements of a connection string, then appropriate errors are thrown from ConnectionConfig.create().

I would suggest the below for handling the overloads

if (!isTokenCredential(credentialOrServiceBusClientOptions)) {
    // assume that first overload is used & create the ConnectionConfig
} else {
    // assume that second overload is used & create the ConnectionConfig
}
// create the connection context

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for converting to String is because if it's a hostname then we use a .endsWith() check in here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should still be able to use hostOrConnectionString.endsWith(), TS should not complain as the type is already set to string in the constructor signature.

If you want to avoid the case of runtime error for when a bad type is passed, then you can use hostOrConnectionString = String(hostOrConnectionString). Since then endsWith() check is needed only in the case of TokenCredential, you can move this inside the if/else block as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why we want to convert it to type String and not use .toString()? If i remember correctly, I think @bterlson had once discussed, advised about not doing so and using string where possible.
And yes, the conversion was to help with both compile and run time scenarios.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention here is to simplify the logic to handle the constructor overloads.
Currently, we are adding multiple lines of code to ensure that hostOrConnectionString is indeed a string and that it has = in it. We can avoid the need for this and the future tax of maintaining them as well.

For example, when the constructor overload is using connection string, we shouldnt need to do any checks on hostOrConnectionString as that work is offloaded to the downstream ConnectionConfig.create().

When the constructor overload for credential is being used, us using String() will ensure that if user had provided an object with .toString() defined, then it gets called and used correctly. If the passed object does not have .toString() on it, then the connection to the service would fail eventually with the service providing an error message hinting that the provided host name is not valid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a trade off with quality of expected user experience here possibly - for instance, if a user is passing in the hostname correctly and receives a connection string malformed error, wouldn't that be confusing / inappropriate?

Copy link
Member Author

@ramya0820 ramya0820 Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For above, let's say the inputs are passed along with a malformed / incorrect / unsupported type of credentials.
Current changes address these cases.
Offloading the check to service or core-amqp, which expect a connection string, would make it not possible to do so. (So I'd keep the current changes unless we want or are okay that user receives the connection string related error, in these scenarios - I had added tests reflecting the behavior current changes emulate, can take a look at those. Will need to update those as well, based on what we decide to do.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use a different signature for the credential based constructor - should likely help with both simplification/maintenance and user ex concerns.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just catching up here and I might have missed some things as changes have been made.
1st, I remarked on this in the API file but I do think we want to keep connectionString and host as the first parameter in their respective constructors to be consistent with our other SDKs/guidelines.

I think the original comment was about the checks being done on hostOrConnectionString.

if (
      hostOrConnectionString == undefined ||
      hostOrConnectionString.toString == undefined ||
      hostOrConnectionString.toString().trim() == ""
    ) {
      throw new Error(
        "Input parameter of host or connection string must be defined and coercible to string."
      );
    }

I think @ramya-rao-a 's had 2 points:

  1. We can ensure hostOrConnectionString is a string by wrapping it in String: String(hostOrConnectionString)
    Note that this is different from new String(hostOrConnectionString), which is not what you want (and what I think @bterlson would warn to stay away from).
  2. There are other places where hostOrConnectionString is validated so this check isn't needed.

The EventHubClient constructor shows both of these aspects off (only linking to one line here but look at the rest of the function): https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/eventhub/event-hubs/src/impl/eventHubClient.ts#L339

Event Hubs is a bit more complicated since it has 3 overloads instead of 2, so service bus would be even simpler.

I think the Event Hubs constructor would throw helpful errors for the cases that were being checked before, but if there's something being missed let me know!


if (isConnectionStringConstructorInvoked) {
// connectionString and options based constructor was invoked
connectionString = hostOrConnectionString;
config = ConnectionConfig.create(connectionString);

options = credentialOrServiceBusClientOptions as ServiceBusClientOptions;
config.webSocket = options && options.webSocket;
config.webSocketEndpointPath = "$servicebus/websocket";
config.webSocketConstructorOptions = options && options.webSocketConstructorOptions;

// Since connectionstring was passed, create a SharedKeyCredential
credential = new SharedKeyCredential(config.sharedAccessKeyName, config.sharedAccessKey);

ConnectionConfig.validate(config);
} else {
// host, credential and options based constructor was invoked
if (!isTokenCredential(credentialOrServiceBusClientOptions)) {
throw new Error(
"'credentials' is a required parameter and must be an implementation of TokenCredential when using host based constructor overload."
);
}
credential = credentialOrServiceBusClientOptions as TokenCredential;

if (!hostOrConnectionString.endsWith("/")) {
hostOrConnectionString += "/";
}
connectionString = `Endpoint=sb://${hostOrConnectionString};SharedAccessKeyName=defaultKeyName;SharedAccessKey=defaultKeyValue;`;
config = ConnectionConfig.create(connectionString);
}

this.name = config.endpoint;
this._context = ConnectionContext.create(config, credential, options);
}
Expand Down Expand Up @@ -153,33 +218,6 @@ export class ServiceBusClient {
}
}

/**
* Creates a ServiceBusClient for the Service Bus Namespace represented in the given connection
* string.
* @param {string} connectionString - Connection string of the form
* 'Endpoint=sb://my-servicebus-namespace.servicebus.windows.net/;SharedAccessKeyName=my-SA-name;SharedAccessKey=my-SA-key'
* @param {ServiceBusClientOptions} [options] Options to control ways to interact with the
* Service Bus Namespace.
* @returns {ServiceBusClient}
*/
static createFromConnectionString(
connectionString: string,
options?: ServiceBusClientOptions
): ServiceBusClient {
const config = ConnectionConfig.create(connectionString);

config.webSocket = options && options.webSocket;
config.webSocketEndpointPath = "$servicebus/websocket";
config.webSocketConstructorOptions = options && options.webSocketConstructorOptions;

// Since connectionstring was passed, create a SharedKeyCredential
const credential = new SharedKeyCredential(config.sharedAccessKeyName, config.sharedAccessKey);

ConnectionConfig.validate(config);

return new ServiceBusClient(config, credential, options);
}

// /**
// * Creates a ServiceBusClient for the Service Bus Namespace represented by the given host using
// * the given TokenProvider.
ramya-rao-a marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function RunTest(
maxConcurrentCalls: number,
messages: number
): Promise<void> {
const ns = ServiceBusClient.createFromConnectionString(connectionString);
const ns = new ServiceBusClient(connectionString);

const client = ns.createQueueClient(entityPath);
const receiver = client.createReceiver(ReceiveMode.receiveAndDelete);
Expand Down
2 changes: 1 addition & 1 deletion sdk/servicebus/service-bus/test/perf/service-bus/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function RunTest(
maxInflight: number,
messages: number
): Promise<void> {
const ns = ServiceBusClient.createFromConnectionString(connectionString);
const ns = new ServiceBusClient(connectionString);

const client = ns.createQueueClient(entityPath);
const sender = client.createSender();
Expand Down
Loading