diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index 0f00160ca6..c9d9b036e4 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -288,22 +288,22 @@ }, { "command": "azure.resource.azureview.refresh", - "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/", + "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer|synapseSqlPoolContainer|synapseWorkspaceContainer)$/", "group": "inline" }, { "command": "azure.resource.azureview.refresh", - "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/", + "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer|synapseSqlPoolContainer|synapseWorkspaceContainer)$/", "group": "azurecore" }, { "command": "azure.resource.connectsqlserver", - "when": "viewItem == azure.resource.itemType.databaseServer || viewItem == azure.resource.itemType.database || viewItem == azure.resource.itemType.sqlInstance", + "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:database|databaseServer|synapseSqlPool|synapseWorkspace|sqlInstance)$/", "group": "inline" }, { "command": "azure.resource.connectsqlserver", - "when": "viewItem == azure.resource.itemType.databaseServer || viewItem == azure.resource.itemType.database || viewItem == azure.resource.itemType.sqlInstance", + "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:database|databaseServer|synapseSqlPool|synapseWorkspace|sqlInstance)$/", "group": "azurecore" }, { diff --git a/extensions/azurecore/src/azureDataGridProvider.ts b/extensions/azurecore/src/azureDataGridProvider.ts index 1e99bbeeed..18d229a796 100644 --- a/extensions/azurecore/src/azureDataGridProvider.ts +++ b/extensions/azurecore/src/azureDataGridProvider.ts @@ -16,6 +16,8 @@ import * as utils from './utils'; const typesClause = [ azureResource.AzureResourceType.sqlDatabase, azureResource.AzureResourceType.sqlServer, + azureResource.AzureResourceType.sqlSynapseWorkspace, + azureResource.AzureResourceType.sqlSynapseSqlPool, azureResource.AzureResourceType.sqlManagedInstance, azureResource.AzureResourceType.postgresServer, azureResource.AzureResourceType.azureArcService, diff --git a/extensions/azurecore/src/azureResource/constants.ts b/extensions/azurecore/src/azureResource/constants.ts index 505ef25642..c1b6f29560 100644 --- a/extensions/azurecore/src/azureResource/constants.ts +++ b/extensions/azurecore/src/azureResource/constants.ts @@ -10,6 +10,10 @@ export enum AzureResourceItemType { database = 'azure.resource.itemType.database', databaseServerContainer = 'azure.resource.itemType.databaseServerContainer', databaseServer = 'azure.resource.itemType.databaseServer', + synapseSqlPoolContainer = 'azure.resource.itemType.synapseSqlPoolContainer', + synapseSqlPool = 'azure.resource.itemType.synapseSqlPool', + synapseWorkspaceContainer = 'azure.resource.itemType.synapseWorkspaceContainer', + synapseWorkspace = 'azure.resource.itemType.synapseWorkspace', azureDataExplorerContainer = 'azure.resource.itemType.azureDataExplorerContainer', azureDataExplorer = 'azure.resource.itemType.azureDataExplorer', sqlInstance = 'azure.resource.itemType.sqlInstance', diff --git a/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorService.ts b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorService.ts index 4a56048780..dc121b35e7 100644 --- a/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorService.ts +++ b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { azureResource } from 'azurecore'; +import { logAnalyticsQuery } from '../queryStringConstants'; import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; export interface AzureMonitorGraphData extends GraphData { @@ -15,12 +16,10 @@ export interface AzureMonitorGraphData extends GraphData { }; } -const instanceQuery = `where type == "${azureResource.AzureResourceType.logAnalytics}"`; - export class AzureMonitorResourceService extends ResourceServiceBase { protected get query(): string { - return instanceQuery; + return logAnalyticsQuery; } protected convertResource(resource: AzureMonitorGraphData): azureResource.AzureResourceDatabaseServer { diff --git a/extensions/azurecore/src/azureResource/providers/cosmosdb/mongo/cosmosDbMongoService.ts b/extensions/azurecore/src/azureResource/providers/cosmosdb/mongo/cosmosDbMongoService.ts index f4fcf1ba3e..7cd1d1587a 100644 --- a/extensions/azurecore/src/azureResource/providers/cosmosdb/mongo/cosmosDbMongoService.ts +++ b/extensions/azurecore/src/azureResource/providers/cosmosdb/mongo/cosmosDbMongoService.ts @@ -6,6 +6,7 @@ import { ResourceServiceBase, GraphData } from '../../resourceTreeDataProviderBase'; import { azureResource } from 'azurecore'; +import { cosmosMongoDbQuery } from '../../queryStringConstants'; interface DbServerGraphData extends GraphData { @@ -15,12 +16,10 @@ interface DbServerGraphData extends GraphData { }; } -const serversQuery = `where type == "${azureResource.AzureResourceType.cosmosDbAccount}" and kind == "MongoDB"`; - export class CosmosDbMongoService extends ResourceServiceBase { protected get query(): string { - return serversQuery; + return cosmosMongoDbQuery; } protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceDatabaseServer { diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts b/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts index bb19ac58b2..aead7045cb 100644 --- a/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { ExtensionContext } from 'vscode'; import { azureResource } from 'azurecore'; diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseService.ts b/extensions/azurecore/src/azureResource/providers/database/databaseService.ts index a33dbc0be6..173ca59439 100644 --- a/extensions/azurecore/src/azureResource/providers/database/databaseService.ts +++ b/extensions/azurecore/src/azureResource/providers/database/databaseService.ts @@ -5,8 +5,8 @@ import { ServiceClientCredentials } from '@azure/ms-rest-js'; import { IAzureResourceService } from '../../interfaces'; -import { DbServerGraphData, SynapseWorkspaceGraphData } from '../databaseServer/databaseServerService'; -import { synapseWorkspacesQuery, sqlServersQuery } from '../databaseServer/serverQueryStrings'; +import { DbServerGraphData } from '../databaseServer/databaseServerService'; +import { sqlServerQuery, sqlDatabaseQuery } from '../queryStringConstants'; import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { queryGraphResources, GraphData } from '../resourceTreeDataProviderBase'; import { AzureAccount, azureResource } from 'azurecore'; @@ -19,69 +19,43 @@ export class AzureResourceDatabaseService implements IAzureResourceService(resourceClient, subscriptions, synapseWorkspacesQuery); - let serverQueryPromise = queryGraphResources(resourceClient, subscriptions, sqlServersQuery); - let dbQueryPromise = queryGraphResources(resourceClient, subscriptions, `where type == "${azureResource.AzureResourceType.sqlDatabase}"`); - servers = await serverQueryPromise as DbServerGraphData[]; - synapseWorkspaces = await synapseQueryPromise as SynapseWorkspaceGraphData[]; + // Query servers and databases in parallel (start all promises before waiting on the 1st) + let serverQueryPromise = queryGraphResources(resourceClient, subscriptions, sqlServerQuery); + let dbQueryPromise = queryGraphResources(resourceClient, subscriptions, sqlDatabaseQuery); + let servers: DbServerGraphData[] = await serverQueryPromise as DbServerGraphData[]; 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(); - 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); - } + let rgMap = new Map(); + servers.forEach(s => { + 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 const svrIdRegExp = new RegExp(`\/subscriptions\/.+\/resourceGroups\/(.+)\/providers\/Microsoft\.Sql\/servers\/(.+)\/databases\/.+`); + const synapseDBRegExp = new RegExp(`\/subscriptions\/.+\/resourceGroups\/(.+)\/providers\/Microsoft\.Synapse\/workspaces\/(.+)\/databases\/.+`); dbByGraph.forEach(db => { // Filter master DBs, and for all others find their server to get login info let serversForRg = rgMap.get(db.resourceGroup); - if (serversForRg && !db.kind.endsWith('system') && svrIdRegExp.test(db.id)) { - const founds = svrIdRegExp.exec(db.id); + if (serversForRg && !db.kind.endsWith('system') && (svrIdRegExp.test(db.id) || synapseDBRegExp.test(db.id))) { + const founds = svrIdRegExp.exec(db.id) ?? synapseDBRegExp.exec(db.id); if (!founds) { console.warn(`Could not parse server name from ID ${db.id}`); return; } const serverName = founds[2]; - let server = combined.find(s => s.name === serverName); + let server = servers.find(s => s.name === serverName); if (server) { databases.push({ name: db.name, id: db.id, serverName: server.name, - // 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, + serverFullName: server.properties.fullyQualifiedDomainName, + loginName: server.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 c83017a5d9..c76248cc5e 100644 --- a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts @@ -6,8 +6,8 @@ import { ServiceClientCredentials } from '@azure/ms-rest-js'; import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { GraphData, queryGraphResources } from '../resourceTreeDataProviderBase'; +import { sqlServerQuery } from '../queryStringConstants'; import { azureResource, AzureAccount } from 'azurecore'; -import { sqlServersQuery, synapseWorkspacesQuery } from './serverQueryStrings'; import { IAzureResourceService } from '../../interfaces'; export interface DbServerGraphData extends GraphData { @@ -17,54 +17,14 @@ export interface DbServerGraphData extends GraphData { }; } -/** - * 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 implements IAzureResourceService { - protected get query(): string { - return sqlServersQuery; - } - 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); + let serverGraphResources: DbServerGraphData[] = await queryGraphResources(resourceClient, subscriptions, sqlServerQuery); const ids = new Set(); - combinedGraphResources.forEach((res) => { + serverGraphResources.forEach((res) => { if (!ids.has(res.id)) { ids.add(res.id); res.subscriptionName = subscriptions.find(sub => sub.id === res.subscriptionId)?.name; @@ -76,14 +36,14 @@ export class AzureResourceDatabaseServerService implements IAzureResourceService return convertedResources; } - protected convertResource(resource: DbServerGraphData | SynapseWorkspaceGraphData): azureResource.AzureResourceDatabaseServer { + protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceDatabaseServer { return { id: resource.id, name: resource.name, // 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, + fullName: resource.properties.fullyQualifiedDomainName, + loginName: resource.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 deleted file mode 100644 index 00eb64bf6e..0000000000 --- a/extensions/azurecore/src/azureResource/providers/databaseServer/serverQueryStrings.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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"`; diff --git a/extensions/azurecore/src/azureResource/providers/kusto/kustoService.ts b/extensions/azurecore/src/azureResource/providers/kusto/kustoService.ts index b62f029bc7..ad49e1077e 100644 --- a/extensions/azurecore/src/azureResource/providers/kusto/kustoService.ts +++ b/extensions/azurecore/src/azureResource/providers/kusto/kustoService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { azureResource } from 'azurecore'; +import { kustoClusterQuery } from '../queryStringConstants'; import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; export interface KustoGraphData extends GraphData { @@ -14,12 +15,10 @@ export interface KustoGraphData extends GraphData { }; } -const instanceQuery = `where type == "${azureResource.AzureResourceType.kustoClusters}"`; - export class KustoResourceService extends ResourceServiceBase { protected get query(): string { - return instanceQuery; + return kustoClusterQuery; } protected convertResource(resource: KustoGraphData): azureResource.AzureResourceDatabaseServer { diff --git a/extensions/azurecore/src/azureResource/providers/mysqlFlexibleServer/mysqlFlexibleServerService.ts b/extensions/azurecore/src/azureResource/providers/mysqlFlexibleServer/mysqlFlexibleServerService.ts index c78e72625f..3078432015 100644 --- a/extensions/azurecore/src/azureResource/providers/mysqlFlexibleServer/mysqlFlexibleServerService.ts +++ b/extensions/azurecore/src/azureResource/providers/mysqlFlexibleServer/mysqlFlexibleServerService.ts @@ -6,6 +6,7 @@ import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; import { azureResource } from 'azurecore'; +import { mysqlFlexibleServerQuery } from '../queryStringConstants'; interface DbServerGraphData extends GraphData { @@ -15,12 +16,10 @@ interface DbServerGraphData extends GraphData { }; } -const serversQuery = `where type == "${azureResource.AzureResourceType.mysqlFlexibleServer}"`; - export class MysqlFlexibleServerService extends ResourceServiceBase { protected get query(): string { - return serversQuery; + return mysqlFlexibleServerQuery; } protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceDatabaseServer { diff --git a/extensions/azurecore/src/azureResource/providers/postgresArcServer/postgresServerService.ts b/extensions/azurecore/src/azureResource/providers/postgresArcServer/postgresServerService.ts index f1154b9567..5ae36dc8f8 100644 --- a/extensions/azurecore/src/azureResource/providers/postgresArcServer/postgresServerService.ts +++ b/extensions/azurecore/src/azureResource/providers/postgresArcServer/postgresServerService.ts @@ -5,6 +5,7 @@ import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; import { azureResource } from 'azurecore'; +import { postgresArcServerQuery } from '../queryStringConstants'; export interface PostgresArcServerGraphData extends GraphData { properties: { @@ -12,12 +13,10 @@ export interface PostgresArcServerGraphData extends GraphData { }; } -export const serversQuery = `where type == "${azureResource.AzureResourceType.azureArcPostgresServer}"`; - export class PostgresServerArcService extends ResourceServiceBase { protected get query(): string { - return serversQuery; + return postgresArcServerQuery; } protected convertResource(resource: PostgresArcServerGraphData): azureResource.AzureResourceDatabaseServer { diff --git a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerService.ts b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerService.ts index b89d525ef9..31537c3ee1 100644 --- a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerService.ts +++ b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerService.ts @@ -6,7 +6,7 @@ import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; import { azureResource } from 'azurecore'; - +import { postgresServerQuery } from '../queryStringConstants'; interface DbServerGraphData extends GraphData { properties: { @@ -15,12 +15,10 @@ interface DbServerGraphData extends GraphData { }; } -const serversQuery = `where type == "${azureResource.AzureResourceType.postgresServer}"`; - export class PostgresServerService extends ResourceServiceBase { protected get query(): string { - return serversQuery; + return postgresServerQuery; } protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceDatabaseServer { diff --git a/extensions/azurecore/src/azureResource/providers/queryStringConstants.ts b/extensions/azurecore/src/azureResource/providers/queryStringConstants.ts new file mode 100644 index 0000000000..ccfb083d29 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/queryStringConstants.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +/** + * Lists all SQL Databases and Synapse SQL Databases + */ +export const sqlDatabaseQuery = `where type == "${azureResource.AzureResourceType.sqlDatabase}" or type == "${azureResource.AzureResourceType.sqlSynapseSqlDatabase}"`; + +/** + * Lists all Synapse Workspaces with information such as SQL connection endpoints. + */ +export const synapseWorkspacesQuery = `where type == "${azureResource.AzureResourceType.sqlSynapseWorkspace}"`; + +/** + * Lists all Synapse Dedicated SQL Pools + */ +export const synapseSqlPoolsQuery = `where type == "${azureResource.AzureResourceType.sqlSynapseSqlPool}"`; + +/** + * Lists all Sql Servers excluding Synapse Pool Servers + * (they have different properties and need to be handled separately) + */ +export const sqlServerQuery = `where type == "${azureResource.AzureResourceType.sqlServer}" and kind != "v12.0,analytics"`; + +/** + * Lists all Azure Arc SQL Managed Instances + */ +export const sqlInstanceArcQuery = `where type == "${azureResource.AzureResourceType.azureArcSqlManagedInstance}"`; + +/** + * Lists all Azure SQL Managed Instances + */ +export const sqlInstanceQuery = `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`; + +/** + * Lists all resource containers and resource groups + */ +export const resourceGroupQuery = `ResourceContainers | where type=="${azureResource.AzureResourceType.resourceGroup}"`; + +/** + * Lists all postgreSQL servers + */ +export const postgresServerQuery = `where type == "${azureResource.AzureResourceType.postgresServer}"`; + +/** + * Lists all Azure Arc PostgreSQL servers + */ +export const postgresArcServerQuery = `where type == "${azureResource.AzureResourceType.azureArcPostgresServer}"`; + +/** + * Lists all MySQL Flexible servers + */ +export const mysqlFlexibleServerQuery = `where type == "${azureResource.AzureResourceType.mysqlFlexibleServer}"`; + +/** + * Lists all Kusto Clusters + */ +export const kustoClusterQuery = `where type == "${azureResource.AzureResourceType.kustoClusters}"`; + +/** + * Lists all Cosmos DB for MongoDB accounts + */ +export const cosmosMongoDbQuery = `where type == "${azureResource.AzureResourceType.cosmosDbAccount}" and kind == "MongoDB"`; + +/** + * Lists all Log Analytics workspaces + */ +export const logAnalyticsQuery = `where type == "${azureResource.AzureResourceType.logAnalytics}"`; diff --git a/extensions/azurecore/src/azureResource/providers/resourceGroup/resourceGroupService.ts b/extensions/azurecore/src/azureResource/providers/resourceGroup/resourceGroupService.ts index 47270eed6d..34cadd72c8 100644 --- a/extensions/azurecore/src/azureResource/providers/resourceGroup/resourceGroupService.ts +++ b/extensions/azurecore/src/azureResource/providers/resourceGroup/resourceGroupService.ts @@ -6,11 +6,12 @@ import { DbServerGraphData } from '../databaseServer/databaseServerService'; import { azureResource } from 'azurecore'; import { ResourceServiceBase } from '../resourceTreeDataProviderBase'; +import { resourceGroupQuery } from '../queryStringConstants'; export class AzureResourceGroupService extends ResourceServiceBase { protected get query(): string { - return `ResourceContainers | where type=="${azureResource.AzureResourceType.resourceGroup}"`; + return resourceGroupQuery; } protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceResourceGroup { diff --git a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceService.ts b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceService.ts index 771af361a4..a2f523f4e3 100644 --- a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceService.ts +++ b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { azureResource } from 'azurecore'; +import { sqlInstanceQuery } from '../queryStringConstants'; import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; interface SqlInstanceGraphData extends GraphData { @@ -13,12 +14,10 @@ interface SqlInstanceGraphData extends GraphData { }; } -const instanceQuery = `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`; - export class SqlInstanceResourceService extends ResourceServiceBase { protected get query(): string { - return instanceQuery; + return sqlInstanceQuery; } protected convertResource(resource: SqlInstanceGraphData): azureResource.AzureResourceDatabaseServer { diff --git a/extensions/azurecore/src/azureResource/providers/sqlinstanceArc/sqlInstanceArcService.ts b/extensions/azurecore/src/azureResource/providers/sqlinstanceArc/sqlInstanceArcService.ts index 733e94d17f..e57c587877 100644 --- a/extensions/azurecore/src/azureResource/providers/sqlinstanceArc/sqlInstanceArcService.ts +++ b/extensions/azurecore/src/azureResource/providers/sqlinstanceArc/sqlInstanceArcService.ts @@ -5,6 +5,7 @@ import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; import { azureResource } from 'azurecore'; +import { sqlInstanceArcQuery } from '../queryStringConstants'; export interface SqlInstanceArcGraphData extends GraphData { properties: { @@ -13,11 +14,10 @@ export interface SqlInstanceArcGraphData extends GraphData { }; } -const instanceQuery = `where type == "${azureResource.AzureResourceType.azureArcSqlManagedInstance}"`; export class SqlInstanceArcResourceService extends ResourceServiceBase { protected get query(): string { - return instanceQuery; + return sqlInstanceArcQuery; } protected convertResource(resource: SqlInstanceArcGraphData): azureResource.AzureResourceDatabaseServer { diff --git a/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolProvider.ts b/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolProvider.ts new file mode 100644 index 0000000000..d7665565be --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolProvider.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { ExtensionContext } from 'vscode'; + +import { azureResource } from 'azurecore'; +import { AzureResourceSynapseSqlPoolTreeDataProvider as AzureResourceSynapseSqlPoolTreeDataProvider } from './synapseSqlPoolTreeDataProvider'; +import { IAzureResourceService } from '../../interfaces'; + +export class AzureResourceSynapseSqlPoolProvider implements azureResource.IAzureResourceProvider { + public constructor( + private _synapseSqlPoolService: IAzureResourceService, + private _extensionContext: ExtensionContext + ) { + } + + public getTreeDataProvider(): azureResource.IAzureResourceTreeDataProvider { + return new AzureResourceSynapseSqlPoolTreeDataProvider(this._synapseSqlPoolService, this._extensionContext); + } + + public get providerId(): string { + return 'azure.resource.providers.synapseSqlPool'; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolService.ts b/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolService.ts new file mode 100644 index 0000000000..bb87812400 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolService.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceClientCredentials } from '@azure/ms-rest-js'; +import { IAzureResourceService } from '../../interfaces'; +import { SynapseWorkspaceGraphData } from '../synapseWorkspace/synapseWorkspaceService'; +import { synapseWorkspacesQuery, synapseSqlPoolsQuery } from '../queryStringConstants'; +import { ResourceGraphClient } from '@azure/arm-resourcegraph'; +import { queryGraphResources, GraphData } from '../resourceTreeDataProviderBase'; +import { AzureAccount, azureResource } from 'azurecore'; + +interface SynapseGraphData extends GraphData { + kind: string; +} +export class AzureResourceSynapseService implements IAzureResourceService { + public async getResources(subscriptions: azureResource.AzureResourceSubscription[], credential: ServiceClientCredentials, account: AzureAccount): Promise { + const databases: azureResource.AzureResourceDatabase[] = []; + const resourceClient = new ResourceGraphClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint }); + + // Query synapse servers, and databases in parallel (start all promises before waiting on the 1st) + let synapseQueryPromise = queryGraphResources(resourceClient, subscriptions, synapseSqlPoolsQuery); + let synapseWorkspaceQueryPromise = queryGraphResources(resourceClient, subscriptions, synapseWorkspacesQuery); + let synapse = await synapseQueryPromise as SynapseGraphData[]; + let synapseWorkspaceByGraph: SynapseWorkspaceGraphData[] = await synapseWorkspaceQueryPromise as SynapseWorkspaceGraphData[]; + + // 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(); + synapseWorkspaceByGraph.forEach(s => { + // As 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.properties.managedResourceGroupName) || []; + serversForRg.push(s); + rgMap.set(s.properties.managedResourceGroupName, serversForRg); + }); + + // Match database ID. When calling exec [0] is full match, [1] is resource group name, [2] is server name + const svrIdRegExp = new RegExp(`\/subscriptions\/.+\/resourceGroups\/(.+)\/providers\/Microsoft\.Synapse\/workspaces\/(.+)\/sqlPools\/.+`); + + synapse.forEach(db => { + // Filter master DBs, and for all others find their server to get login info + if (!db.kind.endsWith('system') && svrIdRegExp.test(db.id)) { + const founds = svrIdRegExp.exec(db.id); + if (!founds) { + console.warn(`Could not parse server name from ID ${db.id}`); + return; + } + const serverName = founds[2]; + let server = synapseWorkspaceByGraph.find(s => s.name === serverName); + if (server) { + databases.push({ + name: db.name, + id: db.id, + serverName: server.name, + serverFullName: server.properties.connectivityEndpoints?.sql, + loginName: server.properties.sqlAdministratorLogin, + subscription: { + id: db.subscriptionId, + name: (subscriptions.find(sub => sub.id === db.subscriptionId))?.name || '' + }, + tenant: db.tenantId, + resourceGroup: db.resourceGroup + }); + } + } + }); + + return databases; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolTreeDataProvider.ts new file mode 100644 index 0000000000..bebef79597 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolTreeDataProvider.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TreeItem, ExtensionNodeType } from 'azdata'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceItemType, mssqlProvider } from '../../constants'; +import { generateGuid } from '../../utils'; +import { IAzureResourceService } from '../../interfaces'; +import { ResourceTreeDataProviderBase } from '../resourceTreeDataProviderBase'; +import { AzureAccount, azureResource } from 'azurecore'; + +export class AzureResourceSynapseSqlPoolTreeDataProvider extends ResourceTreeDataProviderBase { + + private static readonly containerId = 'azure.resource.providers.synapseSqlPool.treeDataProvider.synapseSqlPoolContainer'; + private static readonly containerLabel = localize('azure.resource.providers.synapseSqlPool.treeDataProvider.synapseSqlPoolContainerLabel', "Dedicated SQL Pools"); + + public constructor( + synapseSqlPoolService: IAzureResourceService, + private _extensionContext: vscode.ExtensionContext + ) { + super(synapseSqlPoolService); + } + + protected getTreeItemForResource(synapse: azureResource.AzureResourceDatabase, account: AzureAccount): TreeItem { + return { + id: `synapseWorkspace_${synapse.serverFullName}.synapseSqlPool_${synapse.name}`, + label: this.browseConnectionMode ? `${synapse.serverName}/${synapse.name} (${AzureResourceSynapseSqlPoolTreeDataProvider.containerLabel}, ${synapse.subscription.name})` : `${synapse.name} (${synapse.serverName})`, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/sql_database_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/sql_database.svg') + }, + collapsibleState: this.browseConnectionMode ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed, + contextValue: AzureResourceItemType.synapseSqlPool, + payload: { + id: generateGuid(), + connectionName: undefined, + serverName: synapse.serverFullName, + databaseName: synapse.name, + userName: synapse.loginName, + password: '', + authenticationType: '', + savePassword: true, + groupFullName: '', + groupId: '', + providerName: mssqlProvider, + saveProfile: false, + options: {}, + azureAccount: account.key.accountId, + azureResourceId: synapse.id, + azureTenantId: synapse.tenant, + azurePortalEndpoint: account.properties.providerSettings.settings.portalEndpoint + }, + childProvider: mssqlProvider, + type: ExtensionNodeType.Database + }; + } + + public async getRootChildren(): Promise { + return [{ + id: AzureResourceSynapseSqlPoolTreeDataProvider.containerId, + label: AzureResourceSynapseSqlPoolTreeDataProvider.containerLabel, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/folder.svg') + }, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextValue: AzureResourceItemType.synapseSqlPoolContainer + }]; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceProvider.ts b/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceProvider.ts new file mode 100644 index 0000000000..d44c8da4c2 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceProvider.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionContext } from 'vscode'; + +import { azureResource } from 'azurecore'; +import { AzureResourceSynapseWorkspaceTreeDataProvider } from './synapseWorkspaceTreeDataProvider'; +import { IAzureResourceService } from '../../interfaces'; + +export class AzureResourceSynapseWorkspaceProvider implements azureResource.IAzureResourceProvider { + public constructor( + private _synapseWorkspaceService: IAzureResourceService, + private _extensionContext: ExtensionContext + ) { + } + + public getTreeDataProvider(): azureResource.IAzureResourceTreeDataProvider { + return new AzureResourceSynapseWorkspaceTreeDataProvider(this._synapseWorkspaceService, this._extensionContext); + } + + public get providerId(): string { + return 'azure.resource.providers.synapseWorkspace'; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceService.ts b/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceService.ts new file mode 100644 index 0000000000..6ee11f0336 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceService.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceClientCredentials } from '@azure/ms-rest-js'; +import { ResourceGraphClient } from '@azure/arm-resourcegraph'; +import { GraphData, queryGraphResources } from '../resourceTreeDataProviderBase'; +import { azureResource, AzureAccount } from 'azurecore'; +import { IAzureResourceService } from '../../interfaces'; +import { synapseWorkspacesQuery } from '../queryStringConstants'; + +/** + * 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 AzureResourceSynapseWorkspaceService implements IAzureResourceService { + + protected get query(): string { + return synapseWorkspacesQuery; + } + + 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 }); + let serverGraphResources: SynapseWorkspaceGraphData[] = await queryGraphResources(resourceClient, subscriptions, this.query); + const ids = new Set(); + serverGraphResources.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: SynapseWorkspaceGraphData): azureResource.AzureResourceDatabaseServer { + + return { + id: resource.id, + name: resource.name, + fullName: resource.properties.connectivityEndpoints?.sql, + loginName: resource.properties.sqlAdministratorLogin, + defaultDatabaseName: 'master', + subscription: { + id: resource.subscriptionId, + name: resource.subscriptionName || '' + }, + tenant: resource.tenantId, + resourceGroup: resource.resourceGroup + }; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceTreeDataProvider.ts new file mode 100644 index 0000000000..0556194ca7 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceTreeDataProvider.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionNodeType, TreeItem } from 'azdata'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceItemType, mssqlProvider } from '../../constants'; +import { generateGuid } from '../../utils'; +import { IAzureResourceService } from '../../interfaces'; +import { ResourceTreeDataProviderBase } from '../resourceTreeDataProviderBase'; +import { AzureAccount, azureResource } from 'azurecore'; + +export class AzureResourceSynapseWorkspaceTreeDataProvider extends ResourceTreeDataProviderBase { + private static readonly containerId = 'azure.resource.providers.synapseWorkspace.treeDataProvider.synapseWorkspaceContainer'; + private static readonly containerLabel = localize('azure.resource.providers.synapseWorkspace.treeDataProvider.synapseWorkspaceContainerLabel', "Azure Synapse Analytics"); + + public constructor( + synapseWorkspaceService: IAzureResourceService, + private _extensionContext: vscode.ExtensionContext + ) { + super(synapseWorkspaceService); + } + + protected getTreeItemForResource(synapseWorkspace: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem { + return { + id: `synapseWorkspace_${synapseWorkspace.id ?? synapseWorkspace.name}`, + label: this.browseConnectionMode ? `${synapseWorkspace.name} (${AzureResourceSynapseWorkspaceTreeDataProvider.containerLabel}, ${synapseWorkspace.subscription.name})` : synapseWorkspace.name, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg') + }, + collapsibleState: this.browseConnectionMode ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed, + contextValue: AzureResourceItemType.synapseWorkspace, + payload: { + id: generateGuid(), + connectionName: undefined, + serverName: synapseWorkspace.fullName, + databaseName: synapseWorkspace.defaultDatabaseName, + userName: synapseWorkspace.loginName, + password: '', + authenticationType: '', + savePassword: true, + groupFullName: '', + groupId: '', + providerName: mssqlProvider, + saveProfile: false, + options: {}, + azureAccount: account.key.accountId, + azureTenantId: synapseWorkspace.tenant, + azureResourceId: synapseWorkspace.id, + azurePortalEndpoint: account.properties.providerSettings.settings.portalEndpoint + }, + childProvider: mssqlProvider, + type: ExtensionNodeType.Server + }; + } + + public async getRootChildren(): Promise { + return [{ + id: AzureResourceSynapseWorkspaceTreeDataProvider.containerId, + label: AzureResourceSynapseWorkspaceTreeDataProvider.containerLabel, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/folder.svg') + }, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextValue: AzureResourceItemType.synapseWorkspaceContainer + }]; + } +} diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts index 56965dc916..e3e4565be5 100644 --- a/extensions/azurecore/src/azurecore.d.ts +++ b/extensions/azurecore/src/azurecore.d.ts @@ -337,7 +337,7 @@ declare module 'azurecore' { export namespace azureResource { /** - * AzureCore core extension supports following resource types of Azure Resource Graph. + * AzureCore extension supports following resource types of Azure Resource Graph. * To add more resources, please refer this guide: https://docs.microsoft.com/en-us/azure/governance/resource-graph/reference/supported-tables-resources */ export const enum AzureResourceType { @@ -345,6 +345,9 @@ declare module 'azurecore' { sqlServer = 'microsoft.sql/servers', sqlDatabase = 'microsoft.sql/servers/databases', sqlManagedInstance = 'microsoft.sql/managedinstances', + sqlSynapseWorkspace = 'microsoft.synapse/workspaces', // (Synapse Analytics workspace) + sqlSynapseSqlPool = 'microsoft.synapse/workspaces/sqlpools', // (Dedicated SQL pools) + sqlSynapseSqlDatabase = 'microsoft.synapse/workspaces/sqldatabases', // (Synapse SQL databases) azureArcSqlManagedInstance = 'microsoft.azuredata/sqlmanagedinstances', virtualMachines = 'microsoft.compute/virtualmachines', kustoClusters = 'microsoft.kusto/clusters', @@ -491,6 +494,7 @@ declare module 'azurecore' { fullName: string; defaultDatabaseName: string; } + export interface BlobContainer extends AzureResource { } export interface FileShare extends AzureResource { } diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index cb2100ca30..8723dee367 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -50,6 +50,10 @@ import { AzureResourceGroupService } from './azureResource/providers/resourceGro import { Logger } from './utils/Logger'; import { ConnectionDialogTreeProvider } from './azureResource/tree/connectionDialogTreeProvider'; import { AzureDataGridProvider } from './azureDataGridProvider'; +import { AzureResourceSynapseSqlPoolProvider } from './azureResource/providers/synapseSqlPool/synapseSqlPoolProvider'; +import { AzureResourceSynapseWorkspaceProvider } from './azureResource/providers/synapseWorkspace/synapseWorkspaceProvider'; +import { AzureResourceSynapseWorkspaceService } from './azureResource/providers/synapseWorkspace/synapseWorkspaceService'; +import { AzureResourceSynapseService } from './azureResource/providers/synapseSqlPool/synapseSqlPoolService'; let extensionContext: vscode.ExtensionContext; @@ -139,6 +143,8 @@ export async function activate(context: vscode.ExtensionContext): Promise