Skip to content

Provide an option to hook up DTS emulator when locally debugging DTS projects #4538

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

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type EventHubsConnectionTypeValues, type SqlDbConnectionTypeValues, type StorageConnectionTypeValues } from "../../../constants";
import { type EventHubsConnectionType, type SqlDbConnectionType, type StorageConnectionType } from "./IConnectionTypesContext";


export interface IConnectionPromptOptions {
preselectedConnectionType?: StorageConnectionTypeValues | EventHubsConnectionTypeValues | SqlDbConnectionTypeValues;
preselectedConnectionType?: StorageConnectionType | EventHubsConnectionType | SqlDbConnectionType;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type ConnectionType } from "../../../constants";

export type StorageConnectionType = ConnectionType.Azure | ConnectionType.Emulator;
export type DTSConnectionType = ConnectionType;
export type EventHubsConnectionType = ConnectionType.Azure | ConnectionType.Emulator;
export type SqlDbConnectionType = ConnectionType.Azure | ConnectionType.Custom;

export interface IConnectionTypesContext {
azureWebJobsStorageType?: StorageConnectionType;
dtsConnectionType?: DTSConnectionType;
eventHubsConnectionType?: EventHubsConnectionType;
sqlDbConnectionType?: SqlDbConnectionType;
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

import { type IActionContext } from "@microsoft/vscode-azext-utils";
import { type CodeActionValues, type ConnectionKey } from "../../../constants";
import { type IConnectionTypesContext } from "./IConnectionTypesContext";

export interface ISetConnectionSettingContext extends IActionContext {
export interface ISetConnectionSettingContext extends IActionContext, IConnectionTypesContext {
action: CodeActionValues;
projectPath: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
import { StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication } from '@microsoft/vscode-azext-azureutils';
import { AzureWizardPromptStep, type ISubscriptionActionContext, type IWizardOptions } from '@microsoft/vscode-azext-utils';
import { type MessageItem } from 'vscode';
import { ConnectionType, type EventHubsConnectionTypeValues, type SqlDbConnectionTypeValues } from '../../../../constants';
import { ConnectionType } from '../../../../constants';
import { useEmulator } from '../../../../constants-nls';
import { ext } from '../../../../extensionVariables';
import { localize } from '../../../../localize';
import { type IConnectionPromptOptions } from '../IConnectionPromptOptions';
import { type StorageConnectionType } from '../IConnectionTypesContext';
import { type IAzureWebJobsStorageWizardContext } from './IAzureWebJobsStorageWizardContext';

export class AzureWebJobsStoragePromptStep<T extends IAzureWebJobsStorageWizardContext> extends AzureWizardPromptStep<T> {
Expand All @@ -36,17 +37,16 @@ export class AzureWebJobsStoragePromptStep<T extends IAzureWebJobsStorageWizardC
context.telemetry.properties.azureWebJobsStorageType = context.azureWebJobsStorageType;
}

public async configureBeforePrompt(context: T & { eventHubsConnectionType?: EventHubsConnectionTypeValues, sqlDbConnectionType?: SqlDbConnectionTypeValues }): Promise<void> {
public async configureBeforePrompt(context: T): Promise<void> {
const matchingConnectionType: StorageConnectionType | undefined = tryFindMatchingConnectionType([context.dtsConnectionType, context.eventHubsConnectionType, context.sqlDbConnectionType]);

if (this.options?.preselectedConnectionType === ConnectionType.Azure || this.options?.preselectedConnectionType === ConnectionType.Emulator) {
context.azureWebJobsStorageType = this.options.preselectedConnectionType;
} else if (!!context.storageAccount || !!context.newStorageAccountName) {
// Only should prompt if no storage account was selected
context.azureWebJobsStorageType = ConnectionType.Azure;
} else if (context.eventHubsConnectionType) {
context.azureWebJobsStorageType = context.eventHubsConnectionType;
} else if (context.sqlDbConnectionType === ConnectionType.Azure) {
// No official support for an `Emulator` scenario yet
context.azureWebJobsStorageType = context.sqlDbConnectionType;
} else if (matchingConnectionType) {
context.azureWebJobsStorageType = matchingConnectionType;
}

// Even if we end up skipping the prompt, we should still record the flow in telemetry
Expand Down Expand Up @@ -88,3 +88,18 @@ export class AzureWebJobsStoragePromptStep<T extends IAzureWebJobsStorageWizardC
return { promptSteps };
}
}

const availableStorageConnections: Set<ConnectionType> = new Set([ConnectionType.Azure, ConnectionType.Emulator]);

function tryFindMatchingConnectionType(connections: (ConnectionType | undefined)[]): StorageConnectionType | undefined {
for (const c of connections) {
if (!c) {
continue;
}

if (availableStorageConnections.has(c)) {
return c as StorageConnectionType;
}
}
return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

import { type StorageAccount } from "@azure/arm-storage";
import { type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { type StorageConnectionTypeValues } from "../../../../constants";
import { type StorageConnectionType } from "../IConnectionTypesContext";
import { type ISetConnectionSettingContext } from "../ISetConnectionSettingContext";

export interface IAzureWebJobsStorageWizardContext extends ISetConnectionSettingContext, Partial<ISubscriptionContext> {
storageAccount?: StorageAccount;
newStorageAccountName?: string;

azureWebJobsStorageType?: StorageConnectionTypeValues;
azureWebJobsStorageType?: StorageConnectionType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, validationUtils } from '@microsoft/vscode-azext-utils';
import { ConnectionType } from '../../../../constants';
import { localize } from '../../../../localize';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSConnectionCustomPromptStep<T extends IDTSConnectionWizardContext> extends AzureWizardPromptStep<T> {
public async prompt(context: T): Promise<void> {
context.newDTSConnection = (await context.ui.showInputBox({
prompt: localize('customDTSConnectionPrompt', 'Provide a custom DTS connection string.'),
validateInput: (value: string | undefined) => this.validateInput(value)
})).trim();
}

public shouldPrompt(context: T): boolean {
return !context.newDTSConnection && context.dtsConnectionType === ConnectionType.Custom;
}

private validateInput(name: string | undefined): string | undefined {
name = name ? name.trim() : '';
if (!validationUtils.hasValidCharLength(name)) {
return validationUtils.getInvalidCharLengthMessage();
}
return undefined;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { nonNullProp } from '@microsoft/vscode-azext-utils';
import { ConnectionKey } from '../../../../constants';
import { SetConnectionSettingStepBase } from '../SetConnectionSettingStepBase';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSConnectionSetSettingStep<T extends IDTSConnectionWizardContext> extends SetConnectionSettingStepBase<T> {
public priority: number = 240;
public debugDeploySetting: ConnectionKey = ConnectionKey.DTS;

public async execute(context: T): Promise<void> {
await this.setConnectionSetting(context, nonNullProp(context, 'newDTSConnection'));
}

public shouldExecute(context: T): boolean {
return !!context.newDTSConnection;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, type AzureWizardExecuteStep, type IWizardOptions } from '@microsoft/vscode-azext-utils';
import { type MessageItem } from 'vscode';
import { ConnectionType } from '../../../../constants';
import { useEmulator } from '../../../../constants-nls';
import { localize } from '../../../../localize';
import { DTSConnectionCustomPromptStep } from './DTSConnectionCustomPromptStep';
import { DTSConnectionSetSettingStep } from './DTSConnectionSetSettingStep';
import { DTSEmulatorStartStep } from './DTSEmulatorStartStep';
import { DTSHubNameCustomPromptStep } from './DTSHubNameCustomPromptStep';
import { DTSHubNameSetSettingStep } from './DTSHubNameSetSettingStep';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSConnectionTypeListStep<T extends IDTSConnectionWizardContext> extends AzureWizardPromptStep<T> {
constructor(readonly connectionTypes: Set<ConnectionType>) {
super();
}

public async prompt(context: T): Promise<void> {
const connectAzureButton = { title: localize('connectAzureTaskScheduler', 'Connect Azure Task Scheduler'), data: ConnectionType.Azure };
const connectEmulatorButton = { title: useEmulator, data: ConnectionType.Emulator };
const connectCustomDTSButton = { title: localize('connectCustomTaskScheduler', 'Connect Custom Task Scheduler'), data: ConnectionType.Custom };

const buttons: MessageItem[] = [];
if (this.connectionTypes.has(ConnectionType.Azure)) {
buttons.push(connectAzureButton);
}
if (this.connectionTypes.has(ConnectionType.Emulator)) {
buttons.push(connectEmulatorButton);
}
if (this.connectionTypes.has(ConnectionType.Custom)) {
buttons.push(connectCustomDTSButton);
}

const message: string = localize('selectDTSConnection', 'In order to proceed, you must connect a Durable Task Scheduler for internal use by the Azure Functions runtime.');
context.dtsConnectionType = (await context.ui.showWarningMessage(message, { modal: true }, ...buttons) as {
title: string;
data: ConnectionType;
}).data;

context.telemetry.properties.dtsConnectionType = context.dtsConnectionType;
}

public shouldPrompt(context: T): boolean {
return !context.dtsConnectionType;
}

public async getSubWizard(context: T): Promise<IWizardOptions<T> | undefined> {
const promptSteps: AzureWizardPromptStep<T>[] = [];
const executeSteps: AzureWizardExecuteStep<T>[] = [];

switch (context.dtsConnectionType) {
case ConnectionType.Azure:
throw new Error('Needs implementation.');
case ConnectionType.Emulator:
executeSteps.push(new DTSEmulatorStartStep());
break;
case ConnectionType.Custom:
promptSteps.push(
new DTSConnectionCustomPromptStep(),
new DTSHubNameCustomPromptStep(),
);
break;
}

executeSteps.push(
new DTSConnectionSetSettingStep(),
new DTSHubNameSetSettingStep(),
);

return { promptSteps, executeSteps };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardExecuteStep, nonNullValue } from '@microsoft/vscode-azext-utils';
import { commands } from 'vscode';
import { ConnectionType } from '../../../../constants';
import { localize } from '../../../../localize';
import { type DurableTaskSchedulerEmulator } from '../../../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSEmulatorStartStep<T extends IDTSConnectionWizardContext> extends AzureWizardExecuteStep<T> {
public priority: number = 200;

public async execute(context: T): Promise<void> {
const emulatorId: string = nonNullValue(
await commands.executeCommand('azureFunctions.durableTaskScheduler.startEmulator'),
localize('failedToStartEmulator', 'Internal error: Failed to start DTS emulator.'),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure how important it is for me to add all these failure messages since preDebugValidate will end up swallowing these messages anyway 😆

);

const emulators: DurableTaskSchedulerEmulator[] = nonNullValue(
await commands.executeCommand('azureFunctions.durableTaskScheduler.getEmulators'),
localize('failedToGetEmulators', 'Internal error: Failed to retrieve the list of DTS emulators.'),
);

const emulator: DurableTaskSchedulerEmulator = nonNullValue(
emulators.find(e => e.id === emulatorId),
localize('couldNotFindEmulator', 'Internal error: Failed to retrieve info on the started DTS emulator.'),
);

context.newDTSConnection = `Endpoint=${emulator.schedulerEndpoint};Authentication=None`;
context.newDTSHubName = 'default';
}

public shouldExecute(context: T): boolean {
return !context.newDTSConnection && context.dtsConnectionType === ConnectionType.Emulator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, validationUtils, type IActionContext } from '@microsoft/vscode-azext-utils';
import * as path from 'path';
import { ConnectionKey, ConnectionType, localSettingsFileName } from '../../../../constants';
import { getLocalSettingsJson } from '../../../../funcConfig/local.settings';
import { localize } from '../../../../localize';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSHubNameCustomPromptStep<T extends IDTSConnectionWizardContext> extends AzureWizardPromptStep<T> {
public async prompt(context: T): Promise<void> {
context.newDTSHubName = (await context.ui.showInputBox({
prompt: localize('customDTSConnectionPrompt', 'Provide the custom DTS hub name.'),
value: await getDTSHubName(context, context.projectPath),
validateInput: (value: string) => this.validateInput(value)
})).trim();
}

public shouldPrompt(context: T): boolean {
return !context.newDTSHubName && context.dtsConnectionType === ConnectionType.Custom;
}

private validateInput(name: string): string | undefined {
name = name.trim();

if (!validationUtils.hasValidCharLength(name)) {
return validationUtils.getInvalidCharLengthMessage();
}
return undefined;
}
}

async function getDTSHubName(context: IActionContext, projectPath: string): Promise<string | undefined> {
const localSettingsJson = await getLocalSettingsJson(context, path.join(projectPath, localSettingsFileName));
return localSettingsJson.Values?.[ConnectionKey.DTSHub];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { nonNullProp } from '@microsoft/vscode-azext-utils';
import { ConnectionKey } from '../../../../constants';
import { SetConnectionSettingStepBase } from '../SetConnectionSettingStepBase';
import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext';

export class DTSHubNameSetSettingStep<T extends IDTSConnectionWizardContext> extends SetConnectionSettingStepBase<T> {
public priority: number = 241;
public debugDeploySetting: ConnectionKey = ConnectionKey.DTSHub;

public async execute(context: T): Promise<void> {
await this.setConnectionSetting(context, nonNullProp(context, 'newDTSHubName'));
}

public shouldExecute(context: T): boolean {
return !!context.newDTSHubName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type ResourceGroup } from "@azure/arm-resources";
import { type ConnectionType } from "../../../../constants";
import { type StorageConnectionType } from "../IConnectionTypesContext";
import { type ISetConnectionSettingContext } from "../ISetConnectionSettingContext";

export interface IDTSConnectionWizardContext extends ISetConnectionSettingContext {
resourceGroup?: ResourceGroup;

// Connection Types
azureWebJobsStorageType?: StorageConnectionType;
dtsConnectionType?: ConnectionType;

newDTSConnection?: string;
newDTSHubName?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class EventHubsConnectionPromptStep<T extends IEventHubsConnectionWizardC
context.eventHubsConnectionType = ConnectionType.Emulator;
}

context.telemetry.properties.eventHubConnectionType = context.eventHubsConnectionType;
context.telemetry.properties.eventHubsConnectionType = context.eventHubsConnectionType;
}

public async configureBeforePrompt(context: T): Promise<void> {
Expand All @@ -51,7 +51,7 @@ export class EventHubsConnectionPromptStep<T extends IEventHubsConnectionWizardC

// Even if we skip the prompting, we should still record the flow in telemetry
if (context.eventHubsConnectionType) {
context.telemetry.properties.eventHubConnectionType = context.eventHubsConnectionType;
context.telemetry.properties.eventHubsConnectionType = context.eventHubsConnectionType;
}
}

Expand Down
Loading