Files
sqltoolsservice/src/Microsoft.Kusto.ServiceLayer/DataSource/Kusto/KustoClient.cs
Charles Gagnon c248400a6c Add linting for copyright and unused usings (#1416)
* Add linting for copyright and unused usings

* Add one more + comment

* Enforce in build and fix errors

* Fix build
2022-03-04 15:17:29 -08:00

271 lines
11 KiB
C#

//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Kusto.Cloud.Platform.Data;
using Kusto.Data;
using Kusto.Data.Common;
using Kusto.Data.Data;
using Kusto.Data.Exceptions;
using Kusto.Data.Net.Client;
using Kusto.Language;
using Kusto.Language.Editor;
using Microsoft.Kusto.ServiceLayer.Connection;
using Microsoft.Kusto.ServiceLayer.DataSource.Contracts;
using Microsoft.Kusto.ServiceLayer.Utility;
namespace Microsoft.Kusto.ServiceLayer.DataSource.Kusto
{
public class KustoClient : IKustoClient
{
private readonly string _ownerUri;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private ICslAdminProvider _kustoAdminProvider;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private ICslQueryProvider _kustoQueryProvider;
public string ClusterName { get; private set; }
public string DatabaseName { get; private set; }
public KustoClient(DataSourceConnectionDetails connectionDetails, string ownerUri)
{
_ownerUri = ownerUri;
Initialize(connectionDetails);
}
private void Initialize(DataSourceConnectionDetails connectionDetails)
{
var stringBuilder = GetKustoConnectionStringBuilder(connectionDetails);
_kustoQueryProvider = KustoClientFactory.CreateCslQueryProvider(stringBuilder);
_kustoAdminProvider = KustoClientFactory.CreateCslAdminProvider(stringBuilder);
if (DatabaseName != "NetDefaultDB")
{
return;
}
var dataReader = ExecuteQuery(".show databases | top 1 by DatabaseName | project DatabaseName", new CancellationToken());
var databaseName = dataReader.ToEnumerable().Select(row => row["DatabaseName"]).FirstOrDefault();
DatabaseName = databaseName?.ToString() ?? "";
}
private bool TryRefreshAuthToken()
{
if (!ConnectionService.Instance.TryRefreshAuthToken(_ownerUri, out string accountToken))
{
return false;
}
_kustoQueryProvider.Dispose();
_kustoAdminProvider.Dispose();
var connectionDetails = new DataSourceConnectionDetails
{
ServerName = ClusterName,
DatabaseName = DatabaseName,
UserToken = accountToken,
AuthenticationType = "AzureMFA"
};
Initialize(connectionDetails);
return true;
}
private KustoConnectionStringBuilder GetKustoConnectionStringBuilder(DataSourceConnectionDetails connectionDetails)
{
var stringBuilder = string.IsNullOrWhiteSpace(connectionDetails.ConnectionString)
? new KustoConnectionStringBuilder(connectionDetails.ServerName, connectionDetails.DatabaseName)
: new KustoConnectionStringBuilder(connectionDetails.ConnectionString);
ClusterName = stringBuilder.DataSource;
var databaseName = KustoQueryUtils.ParseDatabaseName(stringBuilder.InitialCatalog);
DatabaseName = databaseName;
stringBuilder.InitialCatalog = databaseName;
ValidationUtils.IsNotNull(ClusterName, nameof(ClusterName));
switch (connectionDetails.AuthenticationType)
{
case "AzureMFA": return stringBuilder.WithAadUserTokenAuthentication(connectionDetails.UserToken);
case "dstsAuth": return stringBuilder.WithDstsUserTokenAuthentication(connectionDetails.UserToken);
default:
return string.IsNullOrWhiteSpace(connectionDetails.UserName) && string.IsNullOrWhiteSpace(connectionDetails.Password)
? stringBuilder
: stringBuilder.WithKustoBasicAuthentication(connectionDetails.UserName, connectionDetails.Password);
}
}
private ClientRequestProperties GetClientRequestProperties(CancellationToken cancellationToken)
{
var clientRequestProperties = new ClientRequestProperties
{
ClientRequestId = Guid.NewGuid().ToString()
};
clientRequestProperties.SetOption(ClientRequestProperties.OptionNoTruncation, true);
cancellationToken.Register(() => CancelQuery(clientRequestProperties.ClientRequestId));
return clientRequestProperties;
}
public IDataReader ExecuteQuery(string query, CancellationToken cancellationToken, string databaseName = null, int retryCount = 1)
{
ValidationUtils.IsArgumentNotNullOrWhiteSpace(query, nameof(query));
var script = CodeScript.From(query, GlobalState.Default);
IDataReader[] origReaders = new IDataReader[script.Blocks.Count];
try
{
var numOfQueries = 0;
Parallel.ForEach(script.Blocks, (codeBlock, state, index) =>
{
var minimalQuery =
codeBlock.Service.GetMinimalText(MinimalTextKind.RemoveLeadingWhitespaceAndComments);
if (!string.IsNullOrEmpty(minimalQuery))
{
// Query is empty in case of comments
IDataReader origReader;
var clientRequestProperties = GetClientRequestProperties(cancellationToken);
if (minimalQuery.StartsWith(".") && !minimalQuery.StartsWith(".show"))
{
origReader = _kustoAdminProvider.ExecuteControlCommand(
KustoQueryUtils.IsClusterLevelQuery(minimalQuery) ? "" : databaseName,
minimalQuery,
clientRequestProperties);
}
else
{
origReader = _kustoQueryProvider.ExecuteQuery(
KustoQueryUtils.IsClusterLevelQuery(minimalQuery) ? "" : databaseName,
minimalQuery,
clientRequestProperties);
}
origReaders[index] = origReader;
numOfQueries++;
}
});
if (numOfQueries == 0 && origReaders.Length > 0) // Covers the scenario when user tries to run comments.
{
var clientRequestProperties = GetClientRequestProperties(cancellationToken);
origReaders[0] = _kustoQueryProvider.ExecuteQuery(
KustoQueryUtils.IsClusterLevelQuery(query) ? "" : databaseName,
query,
clientRequestProperties);
}
return new KustoResultsReader(origReaders);
}
catch (AggregateException exception)
when (retryCount > 0 &&
exception.InnerException is KustoRequestException innerException
&& innerException.FailureCode == 401) // Unauthorized
{
if (!TryRefreshAuthToken())
{
throw;
}
retryCount--;
return ExecuteQuery(query, cancellationToken, databaseName, retryCount);
}
}
/// <summary>
/// Executes a query or command against a kusto cluster and returns a sequence of result row instances.
/// </summary>
public async Task ExecuteControlCommandAsync(string command, bool throwOnError, int retryCount = 1)
{
ValidationUtils.IsArgumentNotNullOrWhiteSpace(command, nameof(command));
try
{
using (var adminOutput = await _kustoAdminProvider.ExecuteControlCommandAsync(DatabaseName, command))
{
}
}
catch (KustoRequestException exception) when (retryCount > 0 && exception.FailureCode == 401) // Unauthorized
{
if (!TryRefreshAuthToken())
{
throw;
}
retryCount--;
await ExecuteControlCommandAsync(command, throwOnError, retryCount);
}
catch (Exception) when (!throwOnError)
{
}
}
/// <summary>
/// Executes a query.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="cancellationToken"></param>
/// <param name="databaseName"></param>
/// <returns>The results.</returns>
public async Task<IEnumerable<T>> ExecuteQueryAsync<T>(string query, CancellationToken cancellationToken,
string databaseName = null)
{
var resultReader = ExecuteQuery(query, cancellationToken, databaseName);
var results = KustoDataReaderParser.ParseV1(resultReader, null);
var tableReader = results[WellKnownDataSet.PrimaryResult].Single().TableData.CreateDataReader();
return await Task.FromResult(new ObjectReader<T>(tableReader));
}
private void CancelQuery(string clientRequestId)
{
var query = $".cancel query \"{clientRequestId}\"";
ExecuteControlCommand(query);
}
/// <summary>
/// Executes a Kusto control command.
/// </summary>
/// <param name="command">The command.</param>
/// <param name="retryCount"></param>
public void ExecuteControlCommand(string command, int retryCount = 1)
{
ValidationUtils.IsArgumentNotNullOrWhiteSpace(command, nameof(command));
try
{
using (var adminOutput = _kustoAdminProvider.ExecuteControlCommand(command))
{
}
}
catch (KustoRequestException exception) when (retryCount > 0 && exception.FailureCode == 401) // Unauthorized
{
if (!TryRefreshAuthToken())
{
throw;
}
retryCount--;
ExecuteControlCommand(command, retryCount);
}
}
public void UpdateDatabase(string databaseName)
{
DatabaseName = databaseName;
}
public void Dispose()
{
_kustoQueryProvider.Dispose();
_kustoAdminProvider.Dispose();
}
}
}