Skip to content

Commit

Permalink
Add basic support for Durable Task Scheduler resources (#4361)
Browse files Browse the repository at this point in the history
* Scaffold BDP.

Signed-off-by: Phillip Hoff <[email protected]>

* Refactor type hierarchy.

Signed-off-by: Phillip Hoff <[email protected]>

* Sketch retrieval of task hubs.

Signed-off-by: Phillip Hoff <[email protected]>

* Update task hub icon.

Signed-off-by: Phillip Hoff <[email protected]>

* Enable "open in portal" command for task hubs.

Signed-off-by: Phillip Hoff <[email protected]>

* Scaffold "open in dashboard" command.

Signed-off-by: Phillip Hoff <[email protected]>

* Sketch "open in dashboard" implementation.

Signed-off-by: Phillip Hoff <[email protected]>

* Move DTS management to separate client type.

Signed-off-by: Phillip Hoff <[email protected]>

* Support viewing task hub properties.

Signed-off-by: Phillip Hoff <[email protected]>

* Split apart types.

Signed-off-by: Phillip Hoff <[email protected]>

* Add file headers.

Signed-off-by: Phillip Hoff <[email protected]>

* Consolidate client logic and add localizable strings.

Signed-off-by: Phillip Hoff <[email protected]>

---------

Signed-off-by: Phillip Hoff <[email protected]>
  • Loading branch information
philliphoff authored Jan 27, 2025
1 parent 51fb959 commit 02aeea2
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 4 deletions.
18 changes: 16 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"branches": [
{
"type": "FunctionApp"
},
{
"type": "DurableTaskScheduler"
}
]
},
Expand All @@ -69,10 +72,12 @@
],
"activation": {
"onFetch": [
"microsoft.web/sites"
"microsoft.web/sites",
"microsoft.durabletask/schedulers"
],
"onResolve": [
"microsoft.web/sites"
"microsoft.web/sites",
"microsoft.durabletask/schedulers"
]
}
},
Expand Down Expand Up @@ -369,6 +374,11 @@
"title": "%azureFunctions.eventGrid.sendMockRequest%",
"category": "Azure Functions",
"icon": "$(notebook-execute)"
},
{
"command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard",
"title": "%azureFunctions.durableTaskScheduler.openTaskHubDashboard%",
"category": "Azure Functions"
}
],
"submenus": [
Expand Down Expand Up @@ -662,6 +672,10 @@
"command": "azureResourceGroups.refresh",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.*folder/",
"group": "1@1"
},
{
"command": "azureFunctions.durableTaskScheduler.openTaskHubDashboard",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /azFunc.dts.taskHub/"
}
],
"explorer/context": [
Expand Down
4 changes: 3 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,7 @@
"azureFunctions.walkthrough.functionsStart.initialize.title": "Initialize an existing project",
"azureFunctions.walkthrough.functionsStart.scenarios.description": "Learn how you can use Azure Functions to build event-driven systems.\n\nIf you're just getting started with Azure Functions, you can [learn about the anatomy of an Azure Functions application](https://aka.ms/functions-getstarted-devguide).",
"azureFunctions.walkthrough.functionsStart.scenarios.title": "Explore common scenarios",
"azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions"
"azureFunctions.walkthrough.functionsStart.title": "Get Started with Azure Functions",

"azureFunctions.durableTaskScheduler.openTaskHubDashboard": "Open in Dashboard"
}
30 changes: 30 additions & 0 deletions resources/durableTaskScheduler/DurableTaskScheduler.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/commands/durableTaskScheduler/openTaskHubDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { openUrl, type IActionContext } from "@microsoft/vscode-azext-utils";
import { type DurableTaskHubResourceModel } from "../../tree/durableTaskScheduler/DurableTaskHubResourceModel";
import { localize } from '../../localize';

export async function openTaskHubDashboard(_: IActionContext, taskHub: DurableTaskHubResourceModel | undefined): Promise<void> {
if (!taskHub) {
throw new Error(localize('noTaskHubSelectedErrorMessage', 'No task hub was selected.'));
}

await openUrl(taskHub?.dashboardUrl.toString(/* skipEncoding: */ true));
}
3 changes: 3 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { stopFunctionApp } from './stopFunctionApp';
import { swapSlot } from './swapSlot';
import { disableFunction, enableFunction } from './updateDisabledState';
import { viewProperties } from './viewProperties';
import { openTaskHubDashboard } from './durableTaskScheduler/openTaskHubDashboard';

export function registerCommands(): void {
commands.registerCommand('azureFunctions.agent.getCommands', getCommands);
Expand Down Expand Up @@ -154,4 +155,6 @@ export function registerCommands(): void {
ext.eventGridProvider = new EventGridCodeLensProvider();
ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider));
registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest);

registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard);
}
8 changes: 7 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { registerAppServiceExtensionVariables } from '@microsoft/vscode-azext-azureappservice';
import { registerAzureUtilsExtensionVariables, type AzureAccountTreeItemBase } from '@microsoft/vscode-azext-azureutils';
import { callWithTelemetryAndErrorHandling, createApiProvider, createAzExtOutputChannel, createExperimentationService, registerErrorHandler, registerEvent, registerReportIssueCommand, registerUIExtensionVariables, type IActionContext, type apiUtils } from '@microsoft/vscode-azext-utils';
import { AzExtResourceType } from '@microsoft/vscode-azureresources-api';
import { AzExtResourceType, getAzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api';
import * as vscode from 'vscode';
import { FunctionAppResolver } from './FunctionAppResolver';
import { FunctionsLocalResourceProvider } from './LocalResourceProvider';
Expand Down Expand Up @@ -38,6 +38,8 @@ import { verifyVSCodeConfigOnActivate } from './vsCodeConfig/verifyVSCodeConfigO
import { type AzureFunctionsExtensionApi } from './vscode-azurefunctions.api';
import { listLocalFunctions } from './workspace/listLocalFunctions';
import { listLocalProjects } from './workspace/listLocalProjects';
import { DurableTaskSchedulerDataBranchProvider } from './tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider';
import { HttpDurableTaskSchedulerClient } from './tree/durableTaskScheduler/DurableTaskSchedulerClient';

export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise<apiUtils.AzureExtensionApiProvider> {
ext.context = context;
Expand Down Expand Up @@ -104,6 +106,10 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta
ext.azureAccountTreeItem = ext.rgApi.appResourceTree._rootTreeItem as AzureAccountTreeItemBase;
ext.rgApi.registerApplicationResourceResolver(AzExtResourceType.FunctionApp, new FunctionAppResolver());
ext.rgApi.registerWorkspaceResourceProvider('func', new FunctionsLocalResourceProvider());

const azureResourcesApi = await getAzureResourcesExtensionApi(context, '2.0.0');

azureResourcesApi.resources.registerAzureResourceBranchDataProvider('DurableTaskScheduler' as AzExtResourceType, new DurableTaskSchedulerDataBranchProvider(new HttpDurableTaskSchedulerClient()));
});

return createApiProvider([<AzureFunctionsExtensionApi>{
Expand Down
65 changes: 65 additions & 0 deletions src/tree/durableTaskScheduler/DurableTaskHubResourceModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type AzureResource, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api";
import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel";
import { type DurableTaskHubResource, type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient";
import { type ProviderResult, TreeItem, Uri } from "vscode";
import { treeUtils } from "../../utils/treeUtils";
import { localize } from '../../localize';

export class DurableTaskHubResourceModel implements DurableTaskSchedulerModel {
constructor(
private readonly schedulerResource: AzureResource,
private readonly resource: DurableTaskHubResource,
private readonly schedulerClient: DurableTaskSchedulerClient) {
}

public get azureResourceId() { return this.resource.id; }

get dashboardUrl(): Uri { return Uri.parse(this.resource.properties.dashboardUrl); }

get id(): string { return this.resource.id; }

get portalUrl(): Uri {
const url: string = `${this.schedulerResource.subscription.environment.portalUrl}/#@${this.schedulerResource.subscription.tenantId}/resource${this.id}`;

return Uri.parse(url);
}

get viewProperties(): ViewPropertiesModel {
return {
label: this.resource.name,
getData: async () => {
if (!this.schedulerResource.resourceGroup) {
throw new Error(localize('noResourceGroupErrorMessage', 'Azure resource does not have a valid resource group name.'));
}

const json = await this.schedulerClient.getSchedulerTaskHub(
this.schedulerResource.subscription,
this.schedulerResource.resourceGroup,
this.schedulerResource.name,
this.resource.name);

return json;
}
};
}

getChildren(): ProviderResult<DurableTaskSchedulerModel[]>
{
return [];
}

getTreeItem(): TreeItem | Thenable<TreeItem>
{
const treeItem = new TreeItem(this.resource.name)

treeItem.iconPath = treeUtils.getIconPath('durableTaskScheduler/DurableTaskScheduler');
treeItem.contextValue = 'azFunc.dts.taskHub';

return treeItem;
}
}
69 changes: 69 additions & 0 deletions src/tree/durableTaskScheduler/DurableTaskSchedulerClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type AzureAuthentication, type AzureSubscription } from "@microsoft/vscode-azureresources-api";
import { localize } from '../../localize';

export interface DurableTaskHubResource {
readonly id: string;
readonly name: string;
readonly properties: {
readonly dashboardUrl: string;
};
}

export interface DurableTaskSchedulerClient {
getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise<DurableTaskHubResource>;
getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise<DurableTaskHubResource[]>;
}

export class HttpDurableTaskSchedulerClient implements DurableTaskSchedulerClient {
async getSchedulerTaskHub(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string, taskHubName: string): Promise<DurableTaskHubResource> {
const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs/${taskHubName}`;

const taskHub = await this.getAsJson<DurableTaskHubResource>(taskHubsUrl, subscription.authentication);

return taskHub;
}

async getSchedulerTaskHubs(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string): Promise<DurableTaskHubResource[]> {
const taskHubsUrl = `${HttpDurableTaskSchedulerClient.getBaseUrl(subscription, resourceGroupName, schedulerName)}/taskHubs`;

const response = await this.getAsJson<{ value: DurableTaskHubResource[] }>(taskHubsUrl, subscription.authentication);

return response.value;
}

private static getBaseUrl(subscription: AzureSubscription, resourceGroupName: string, schedulerName: string) {
const provider = 'Microsoft.DurableTask';

return `${subscription.environment.resourceManagerEndpointUrl}/subscriptions/${subscription.subscriptionId}/resourceGroups/${resourceGroupName}/providers/${provider}/schedulers/${schedulerName}`;
}

private async getAsJson<T>(url: string, authentication: AzureAuthentication): Promise<T> {
const apiVersion = '2024-10-01-preview';
const versionedUrl = `${url}?api-version=${apiVersion}`;

const authSession = await authentication.getSession();

if (!authSession) {
throw new Error(localize('noAuthenticationSessionErrorMessage', 'Unable to obtain an authentication session.'));
}

const accessToken = authSession.accessToken;

const request = new Request(versionedUrl);

request.headers.append('Authorization', `Bearer ${accessToken}`);

const response = await fetch(request);

if (!response.ok) {
throw new Error(localize('failureInvokingArmErrorMessage', 'Azure management API returned an unsuccessful response.'));
}

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

import { type AzureResource, type AzureResourceBranchDataProvider } from "@microsoft/vscode-azureresources-api";
import { type ProviderResult, type TreeItem } from "vscode";
import { type DurableTaskSchedulerClient } from "./DurableTaskSchedulerClient";
import { type DurableTaskSchedulerModel } from "./DurableTaskSchedulerModel";
import { DurableTaskSchedulerResourceModel } from "./DurableTaskSchedulerResourceModel";

export class DurableTaskSchedulerDataBranchProvider implements AzureResourceBranchDataProvider<DurableTaskSchedulerModel> {
constructor(private readonly schedulerClient: DurableTaskSchedulerClient) {
}

getChildren(element: DurableTaskSchedulerModel): ProviderResult<DurableTaskSchedulerModel[]> {
return element.getChildren();
}

getResourceItem(element: AzureResource): DurableTaskSchedulerResourceModel | Thenable<DurableTaskSchedulerResourceModel> {
return new DurableTaskSchedulerResourceModel(element, this.schedulerClient);
}

getTreeItem(element: DurableTaskSchedulerModel): TreeItem | Thenable<TreeItem> {
return element.getTreeItem();
}
}
13 changes: 13 additions & 0 deletions src/tree/durableTaskScheduler/DurableTaskSchedulerModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type AzureResourceModel } from "@microsoft/vscode-azureresources-api";
import { type ProviderResult, type TreeItem } from "vscode";

export interface DurableTaskSchedulerModel extends AzureResourceModel {
getChildren(): ProviderResult<DurableTaskSchedulerModel[]>;

getTreeItem(): TreeItem | Thenable<TreeItem>;
}
Loading

0 comments on commit 02aeea2

Please sign in to comment.