diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseService.ts b/extensions/azurecore/src/azureResource/providers/database/databaseService.ts index 49bc3767ab..2dac744ab3 100644 --- a/extensions/azurecore/src/azureResource/providers/database/databaseService.ts +++ b/extensions/azurecore/src/azureResource/providers/database/databaseService.ts @@ -5,7 +5,8 @@ import { ServiceClientCredentials } from '@azure/ms-rest-js'; import { IAzureResourceService } from '../../interfaces'; -import { serversQuery, DbServerGraphData } from '../databaseServer/databaseServerService'; +import { DbServerGraphData, SynapseWorkspaceGraphData } from '../databaseServer/databaseServerService'; +import { synapseWorkspacesQuery, sqlServersQuery } from '../databaseServer/serverQueryStrings'; import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { queryGraphResources, GraphData } from '../resourceTreeDataProviderBase'; import { AzureAccount, azureResource } from 'azurecore'; @@ -18,19 +19,45 @@ export class AzureResourceDatabaseService implements IAzureResourceService(resourceClient, subscriptions, serversQuery); + // Query servers, synapse workspaces, and databases in parallel (start all promises before waiting on the 1st) + let servers: DbServerGraphData[]; + let synapseWorkspaces: SynapseWorkspaceGraphData[]; + let combined: (DbServerGraphData | SynapseWorkspaceGraphData)[] = []; + /** + * We need to get the list of servers minus the Synapse Workspaces, + * then we need to make another query to get them. + * + * This is done because the first query provides invalid endpoints for Synapse Workspaces + * While the second one provides them as one of its properties. + * + * They have to be processed in different ways as their structure differs + * in terms of properties. (See databaseServer/databaseServerService.ts for more info) + * + * Queries must be made separately due to union not being recognized by resourceGraph resource calls + */ + let synapseQueryPromise = queryGraphResources(resourceClient, subscriptions, synapseWorkspacesQuery); + let serverQueryPromise = queryGraphResources(resourceClient, subscriptions, sqlServersQuery); let dbQueryPromise = queryGraphResources(resourceClient, subscriptions, `where type == "${azureResource.AzureResourceType.sqlDatabase}"`); - let servers: DbServerGraphData[] = await serverQueryPromise as DbServerGraphData[]; + servers = await serverQueryPromise as DbServerGraphData[]; + synapseWorkspaces = await synapseQueryPromise as SynapseWorkspaceGraphData[]; let dbByGraph: DatabaseGraphData[] = await dbQueryPromise as DatabaseGraphData[]; + combined = combined.concat(servers).concat(synapseWorkspaces); // Group servers by resource group, then merge DB results with servers so we // can get the login name and server fully qualified name to use for connections - let rgMap = new Map(); - servers.forEach(s => { - let serversForRg = rgMap.get(s.resourceGroup) || []; - serversForRg.push(s); - rgMap.set(s.resourceGroup, serversForRg); + let rgMap = new Map(); + combined.forEach(s => { + if ((s as SynapseWorkspaceGraphData).properties.connectivityEndpoints) { + // If the resource is a Synapse Workspace, we need to use the managedResourceGroupName + // (any SQL pools inside will use this instead of the regular resource group associated with the workspace itself). + let serversForRg = rgMap.get((s as SynapseWorkspaceGraphData).properties.managedResourceGroupName) || []; + serversForRg.push(s as SynapseWorkspaceGraphData); + rgMap.set((s as SynapseWorkspaceGraphData).properties.managedResourceGroupName, serversForRg); + } else { + let serversForRg = rgMap.get(s.resourceGroup) || []; + serversForRg.push(s); + rgMap.set(s.resourceGroup, serversForRg); + } }); // Match database ID. When calling exec [0] is full match, [1] is resource group name, [2] is server name @@ -42,14 +69,15 @@ export class AzureResourceDatabaseService implements IAzureResourceService s.name === serverName); + let server = combined.find(s => s.name === serverName); if (server) { databases.push({ name: db.name, id: db.id, serverName: server.name, - serverFullName: server.properties.fullyQualifiedDomainName, - loginName: server.properties.administratorLogin, + // Determine if server object is for Synapse Workspace or not and get the needed property from the correct place. + serverFullName: (server as SynapseWorkspaceGraphData).properties.connectivityEndpoints?.sql ?? (server as DbServerGraphData).properties.fullyQualifiedDomainName, + loginName: (server as SynapseWorkspaceGraphData).properties.sqlAdministratorLogin ?? (server as DbServerGraphData).properties.administratorLogin, subscription: { id: db.subscriptionId, name: (subscriptions.find(sub => sub.id === db.subscriptionId))?.name diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts index c9a93011a6..40553851eb 100644 --- a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts @@ -3,9 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; -import { azureResource } from 'azurecore'; +import { ServiceClientCredentials } from '@azure/ms-rest-js'; +import { ResourceGraphClient } from '@azure/arm-resourcegraph'; +import { GraphData, queryGraphResources } from '../resourceTreeDataProviderBase'; +import { azureResource, AzureAccount } from 'azurecore'; +import { sqlServersQuery, synapseWorkspacesQuery } from './serverQueryStrings'; +import { IAzureResourceService } from '../../interfaces'; export interface DbServerGraphData extends GraphData { properties: { @@ -14,20 +17,73 @@ export interface DbServerGraphData extends GraphData { }; } -export const serversQuery = `where type == "${azureResource.AzureResourceType.sqlServer}"`; +/** + * Properties returned by the Synapse query are different from the server ones and have to be treated differently. + */ +export interface SynapseWorkspaceGraphData extends GraphData { + properties: { + /** + * SQL connectivity endpoint and other endpoints are found here, instead of fullyQualifiedDomainName. + */ + connectivityEndpoints: { sql: string }; + /** + * managedResourceGroupName is the resource group used by any SQL pools inside the workspace + * which is different from the resource group of the workspace itself. + */ + managedResourceGroupName: string; + /** + * administratorLogin is called sqlAdministratorLogin here. + */ + sqlAdministratorLogin: string; + }; +} -export class AzureResourceDatabaseServerService extends ResourceServiceBase { +export class AzureResourceDatabaseServerService implements IAzureResourceService { protected get query(): string { - return serversQuery; + return sqlServersQuery; } - protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceDatabaseServer { + public async getResources(subscriptions: azureResource.AzureResourceSubscription[], credential: ServiceClientCredentials, account: AzureAccount): Promise { + const convertedResources: azureResource.AzureResourceDatabaseServer[] = []; + const resourceClient = new ResourceGraphClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint }); + /** + * We need to get the list of servers minus the Synapse Workspaces, + * then we need to make another query to get them. + * + * This is done because the first query provides invalid endpoints for Synapse Workspaces + * While the second one provides them as one of its properties. + * + * They have to be processed in different ways by convertResource as their structure differs + * in terms of properties. (See above) + * + * Queries must be made separately due to union not being recognized by resourceGraph resource calls. + */ + let combinedGraphResources: (DbServerGraphData | SynapseWorkspaceGraphData)[] = []; + let serverGraphResources: DbServerGraphData[] = await queryGraphResources(resourceClient, subscriptions, this.query); + let synapseGraphResources: SynapseWorkspaceGraphData[] = await queryGraphResources(resourceClient, subscriptions, synapseWorkspacesQuery); + combinedGraphResources = combinedGraphResources.concat(serverGraphResources).concat(synapseGraphResources); + const ids = new Set(); + combinedGraphResources.forEach((res) => { + if (!ids.has(res.id)) { + ids.add(res.id); + res.subscriptionName = subscriptions.find(sub => sub.id === res.subscriptionId).name; + const converted = this.convertResource(res); + convertedResources.push(converted); + } + }); + + return convertedResources; + } + + protected convertResource(resource: DbServerGraphData | SynapseWorkspaceGraphData): azureResource.AzureResourceDatabaseServer { + return { id: resource.id, name: resource.name, - fullName: resource.properties.fullyQualifiedDomainName, - loginName: resource.properties.administratorLogin, + // Determine if resource object is for Synapse Workspace or not and get the needed property from the correct place. + fullName: (resource as SynapseWorkspaceGraphData).properties.connectivityEndpoints?.sql ?? (resource as DbServerGraphData).properties.fullyQualifiedDomainName, + loginName: (resource as SynapseWorkspaceGraphData).properties.sqlAdministratorLogin ?? (resource as DbServerGraphData).properties.administratorLogin, defaultDatabaseName: 'master', subscription: { id: resource.subscriptionId, diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/serverQueryStrings.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/serverQueryStrings.ts new file mode 100644 index 0000000000..00eb64bf6e --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/serverQueryStrings.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { azureResource } from 'azurecore'; +/** + * Get list of Synapse Workspaces with information such as SQL connection endpoints. + */ +export const synapseWorkspacesQuery = `where type == "microsoft.synapse/workspaces"`; + + +/** + * Lists all Sql Servers except for Synapse Pool Servers + * (they have different properties and need to be handled separately, + * see databaseServerService.ts for more details) + */ +export const sqlServersQuery = `where type == "${azureResource.AzureResourceType.sqlServer}" and kind != "v12.0,analytics"`;