diff --git a/sqltoolsservice.sln b/sqltoolsservice.sln index 6dca984b..a6d67b4c 100644 --- a/sqltoolsservice.sln +++ b/sqltoolsservice.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26510.0 +VisualStudioVersion = 15.0.26730.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2BBD7364-054F-4693-97CD-1C395E3E84A9}" EndProject @@ -72,6 +72,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.ServiceL EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.Serialization", "src\Microsoft.SqlTools.Serialization\Microsoft.SqlTools.Serialization.csproj", "{75E1A89F-9DF6-4DA3-9EF1-5FD966331E06}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.ResourceProvider", "src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj", "{6FEE7E14-8A1D-454E-8F7C-B63597801787}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.ResourceProvider.Core", "src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj", "{70E63BC1-2C82-41C0-89D6-272FD3C7B0C9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.ResourceProvider.DefaultImpl", "src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj", "{EFB39C03-F7D2-4E8D-BE51-09121CD71973}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -145,6 +151,24 @@ Global {75E1A89F-9DF6-4DA3-9EF1-5FD966331E06}.Integration|Any CPU.Build.0 = Debug|Any CPU {75E1A89F-9DF6-4DA3-9EF1-5FD966331E06}.Release|Any CPU.ActiveCfg = Release|Any CPU {75E1A89F-9DF6-4DA3-9EF1-5FD966331E06}.Release|Any CPU.Build.0 = Release|Any CPU + {6FEE7E14-8A1D-454E-8F7C-B63597801787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FEE7E14-8A1D-454E-8F7C-B63597801787}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FEE7E14-8A1D-454E-8F7C-B63597801787}.Integration|Any CPU.ActiveCfg = Debug|Any CPU + {6FEE7E14-8A1D-454E-8F7C-B63597801787}.Integration|Any CPU.Build.0 = Debug|Any CPU + {6FEE7E14-8A1D-454E-8F7C-B63597801787}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FEE7E14-8A1D-454E-8F7C-B63597801787}.Release|Any CPU.Build.0 = Release|Any CPU + {70E63BC1-2C82-41C0-89D6-272FD3C7B0C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70E63BC1-2C82-41C0-89D6-272FD3C7B0C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70E63BC1-2C82-41C0-89D6-272FD3C7B0C9}.Integration|Any CPU.ActiveCfg = Debug|Any CPU + {70E63BC1-2C82-41C0-89D6-272FD3C7B0C9}.Integration|Any CPU.Build.0 = Debug|Any CPU + {70E63BC1-2C82-41C0-89D6-272FD3C7B0C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70E63BC1-2C82-41C0-89D6-272FD3C7B0C9}.Release|Any CPU.Build.0 = Release|Any CPU + {EFB39C03-F7D2-4E8D-BE51-09121CD71973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFB39C03-F7D2-4E8D-BE51-09121CD71973}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFB39C03-F7D2-4E8D-BE51-09121CD71973}.Integration|Any CPU.ActiveCfg = Debug|Any CPU + {EFB39C03-F7D2-4E8D-BE51-09121CD71973}.Integration|Any CPU.Build.0 = Debug|Any CPU + {EFB39C03-F7D2-4E8D-BE51-09121CD71973}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFB39C03-F7D2-4E8D-BE51-09121CD71973}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -163,6 +187,9 @@ Global {501DB3B2-AF92-41CF-82F6-780F9C37C219} = {AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4} {2C290C58-C98D-46B2-BCED-44D9B67F6D31} = {AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4} {75E1A89F-9DF6-4DA3-9EF1-5FD966331E06} = {2BBD7364-054F-4693-97CD-1C395E3E84A9} + {6FEE7E14-8A1D-454E-8F7C-B63597801787} = {2BBD7364-054F-4693-97CD-1C395E3E84A9} + {70E63BC1-2C82-41C0-89D6-272FD3C7B0C9} = {2BBD7364-054F-4693-97CD-1C395E3E84A9} + {EFB39C03-F7D2-4E8D-BE51-09121CD71973} = {2BBD7364-054F-4693-97CD-1C395E3E84A9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B31CDF4B-2851-45E5-8C5F-BE97125D9DD8} diff --git a/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj b/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj index 5b8ec632..961fecaa 100644 --- a/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj +++ b/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj @@ -26,13 +26,14 @@ - + - + + diff --git a/src/Microsoft.SqlTools.Hosting/Extensibility/ExportStandardMetadataAttribute.cs b/src/Microsoft.SqlTools.Hosting/Extensibility/ExportStandardMetadataAttribute.cs new file mode 100644 index 00000000..3c90a1a7 --- /dev/null +++ b/src/Microsoft.SqlTools.Hosting/Extensibility/ExportStandardMetadataAttribute.cs @@ -0,0 +1,44 @@ +// +// 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.Composition; + +namespace Microsoft.SqlTools.Extensibility +{ + /// + /// Base attribute class for all export definitions. + /// + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public abstract class ExportStandardMetadataAttribute : ExportAttribute, IStandardMetadata + { + /// + /// Base class for DAC extensibility exports + /// + protected ExportStandardMetadataAttribute(Type contractType, string id, string displayName = null) + : base(contractType) + { + Id = id; + DisplayName = displayName; + } + + + /// + /// The version of this extension + /// + public string Version { get; set; } + + /// + /// The id of the extension + /// + public string Id { get; private set; } + + /// + /// The display name for the extension + /// + public virtual string DisplayName { get; private set; } + } +} diff --git a/src/Microsoft.SqlTools.Hosting/Extensibility/IStandardMetadata.cs b/src/Microsoft.SqlTools.Hosting/Extensibility/IStandardMetadata.cs new file mode 100644 index 00000000..a9e65748 --- /dev/null +++ b/src/Microsoft.SqlTools.Hosting/Extensibility/IStandardMetadata.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.Extensibility +{ + /// + /// Standard Metadata needed for extensions. + /// + public interface IStandardMetadata + { + /// + /// Extension version. Should be in the format "1.0.0.0" or similar + /// + string Version { get; } + + /// + /// Unique Id used to identify the export. + /// + string Id { get; } + + /// + /// Optional Display name describing the export type + /// + string DisplayName { get; } + } +} diff --git a/src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj b/src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj index ac92d32c..d0aecb44 100644 --- a/src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj +++ b/src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj @@ -1,6 +1,6 @@  - netcoreapp2.0 + netstandard2.0 false false false @@ -10,11 +10,9 @@ true true portable - win7-x64;win7-x86;ubuntu.14.04-x64;ubuntu.16.04-x64;centos.7-x64;rhel.7.2-x64;debian.8-x64;fedora.23-x64;opensuse.13.2-x64;osx.10.11-x64 - @@ -25,6 +23,7 @@ + diff --git a/src/Microsoft.SqlTools.Hosting/Utility/AutoLock.cs b/src/Microsoft.SqlTools.Hosting/Utility/AutoLock.cs new file mode 100644 index 00000000..06dff105 --- /dev/null +++ b/src/Microsoft.SqlTools.Hosting/Utility/AutoLock.cs @@ -0,0 +1,55 @@ +// +// 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.Threading; + +namespace Microsoft.SqlTools.Utility +{ + /// + /// A wrapper around the ReaderWriterLock to make sure the locks are released even if the action fails + /// + public class AutoLock + { + private readonly ReaderWriterLock _lock; + private readonly bool _isWriteLocked; + + /// + /// Creates new lock given type of lock and timeout + /// + public AutoLock(ReaderWriterLock lockObj, bool isWriteLock, TimeSpan timeOut, Action action, out Exception exception) + { + exception = null; + try + { + _lock = lockObj; + _isWriteLocked = isWriteLock; + if (_isWriteLocked) + { + _lock.AcquireWriterLock(timeOut); + } + else + { + _lock.AcquireReaderLock(timeOut); + } + action(); + } + catch (Exception ex) + { + exception = ex; + } + finally + { + if (_isWriteLocked && _lock.IsWriterLockHeld) + { + _lock.ReleaseWriterLock(); + } + else if (!_isWriteLocked && _lock.IsReaderLockHeld) + { + _lock.ReleaseReaderLock(); + } + } + } + } +} diff --git a/src/Microsoft.SqlTools.Hosting/Utility/CommandOptions.cs b/src/Microsoft.SqlTools.Hosting/Utility/CommandOptions.cs index 7967b07c..bc56d254 100644 --- a/src/Microsoft.SqlTools.Hosting/Utility/CommandOptions.cs +++ b/src/Microsoft.SqlTools.Hosting/Utility/CommandOptions.cs @@ -50,7 +50,7 @@ namespace Microsoft.SqlTools.Hosting.Utility ShouldExit = true; return; default: - ErrorMessage += String.Format("Unknown argument \"{0}\"" + Environment.NewLine, argName); + ErrorMessage += string.Format("Unknown argument \"{0}\"" + Environment.NewLine, argName); break; } } diff --git a/src/Microsoft.SqlTools.Hosting/Utility/ConcurrentCache.cs b/src/Microsoft.SqlTools.Hosting/Utility/ConcurrentCache.cs new file mode 100644 index 00000000..aace98f3 --- /dev/null +++ b/src/Microsoft.SqlTools.Hosting/Utility/ConcurrentCache.cs @@ -0,0 +1,82 @@ +// +// 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.Threading; + +namespace Microsoft.SqlTools.Utility +{ + public class ConcurrentCache + { + private readonly Dictionary _cache = new Dictionary(); + private readonly ReaderWriterLock _readerWriterLock = new ReaderWriterLock(); + private readonly TimeSpan _timeout = TimeSpan.FromHours(1); + + public void ClearCache(IEnumerable keys) + { + Exception exception; + new AutoLock(_readerWriterLock, true, _timeout, () => + { + { + foreach (var key in keys) + { + if (_cache.ContainsKey(key)) + { + _cache.Remove(key); + } + } + } + }, out exception); + if (exception != null) + { + throw exception; + } + } + + public T Get(string key) + { + T result = default(T); + Exception exception; + new AutoLock(_readerWriterLock, false, _timeout, () => + { + if (_cache.ContainsKey(key)) + { + result = _cache[key]; + } + }, out exception); + if (exception != null) + { + throw exception; + } + + return result; + } + + public T UpdateCache(string key, T newValue) + { + T result = newValue; + + Exception exception; + new AutoLock(_readerWriterLock, true, _timeout, () => + { + bool isDefined = _cache.ContainsKey(key); + if (!isDefined) + { + _cache.Add(key, newValue); + } + else + { + result = _cache[key]; + } + }, out exception); + if (exception != null) + { + throw exception; + } + + return result; + } + } +} diff --git a/src/Microsoft.SqlTools.Hosting/packages.config b/src/Microsoft.SqlTools.Hosting/packages.config index 77fd4961..0e62f811 100644 --- a/src/Microsoft.SqlTools.Hosting/packages.config +++ b/src/Microsoft.SqlTools.Hosting/packages.config @@ -1,7 +1,6 @@  - diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAccountManager.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAccountManager.cs new file mode 100644 index 00000000..f35eb926 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAccountManager.cs @@ -0,0 +1,57 @@ +// +// 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.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Authentication +{ + /// + /// An account manager has the information of currently logged in user and can authenticate the user + /// Implementing classes must add a + /// to the class in order to be found by the extension manager, + /// and to define the type and category supported + /// + public interface IAccountManager : IExportable + { + /// + /// Returns true is user needs reauthentication + /// + Task GetUserNeedsReauthenticationAsync(); + + /// + /// Authenticates the user + /// + Task AuthenticateAsync(); + + /// + /// Prompt the login dialog to login to a new use that has not been cached + /// + Task AddUserAccountAsync(); + + /// + /// Set the current loaged in user in the cache + /// + Task SetCurrentAccountAsync(object account); + + /// + /// Returns the current logged in user + /// + Task GetCurrentAccountAsync(); + + /// + /// Returns true if the API supports a login control + /// + bool HasLoginDialog + { + get; + } + + /// + /// Event to raise when the current logged in user changed + /// + event EventHandler CurrentAccountChanged; + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureAuthenticationManager.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureAuthenticationManager.cs new file mode 100644 index 00000000..ed18238b --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureAuthenticationManager.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Authentication +{ + /// + /// Provides functionality to authenticate to Azure and discover associated accounts and subscriptions + /// + public interface IAzureAuthenticationManager : IAccountManager + { + /// + /// User accounts associated to the logged in user + /// + IEnumerable UserAccounts + { + get; + } + + /// + /// Azure subscriptions associated to the logged in user + /// + Task> GetSubscriptionsAsync(); + + /// + /// Returns user's azure subscriptions + /// + Task> GetSelectedSubscriptionsAsync(); + + /// + /// Finds a subscription given subscription id + /// + bool TryParseSubscriptionIdentifier(string value, out IAzureSubscriptionIdentifier subscription); + + /// + /// Stores the selected subscriptions given the ids + /// + Task SetSelectedSubscriptionsAsync(IEnumerable subscriptionIds); + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureSubscriptionContext.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureSubscriptionContext.cs new file mode 100644 index 00000000..2384edcc --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureSubscriptionContext.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Authentication +{ + /// Contains information about an Azure subscription + public interface IAzureSubscriptionContext : IEquatable + { + /// + /// Subscription Identifier + /// + IAzureSubscriptionIdentifier Subscription + { + get; + } + + /// + /// Subscription name + /// + string SubscriptionName + { + get; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureSubscriptionIdentifier.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureSubscriptionIdentifier.cs new file mode 100644 index 00000000..2fcb8056 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureSubscriptionIdentifier.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Authentication +{ + /// + /// Contains information about an azure subscription identifier + /// + public interface IAzureSubscriptionIdentifier : IEquatable + { + IAzureUserAccount UserAccount + { + get; + } + + /// + /// Service endpoint + /// + Uri ServiceManagementEndpoint + { + get; + } + + /// + /// Subscription id + /// + string SubscriptionId + { + get; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccount.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccount.cs new file mode 100644 index 00000000..c02e049d --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccount.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Authentication +{ + /// + /// Contains information about an Azure user account + /// + public interface IAzureUserAccount : IEquatable, IUserAccount + { + /// + /// User Account Display Info + /// + IAzureUserAccountDisplayInfo DisplayInfo + { + get; + } + + /// + /// Tenant Id + /// + string TenantId + { + get; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccountDisplayInfo.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccountDisplayInfo.cs new file mode 100644 index 00000000..d6dfab5d --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccountDisplayInfo.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Authentication +{ + /// + /// Contains information about an Azure user display + /// + public interface IAzureUserAccountDisplayInfo : IEquatable + { + /// + /// Account Display Name + /// + string AccountDisplayName + { + get; + } + + /// + /// Account Logo + /// + byte[] AccountLogo + { + get; + } + + /// + /// Provider Dislay Name + /// + string ProviderDisplayName + { + get; + } + + /// + /// Provider Logo + /// + byte[] ProviderLogo + { + get; + } + + /// + /// User Name + /// + string UserName + { + get; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccountSubscriptionContext.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccountSubscriptionContext.cs new file mode 100644 index 00000000..588096a0 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IAzureUserAccountSubscriptionContext.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Authentication +{ + /// + /// Contains information about an Azure user account subscription + /// + public interface IAzureUserAccountSubscriptionContext : + IAzureSubscriptionContext, IEquatable + { + /// + /// User Account + /// + IAzureUserAccount UserAccount + { + get; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IUserAccount.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IUserAccount.cs new file mode 100644 index 00000000..c1c5e497 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Authentication/IUserAccount.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ResourceProvider.Core.Authentication +{ + /// + /// User account authentication information to be used by + /// + public interface IUserAccount + { + /// + /// The unique Id for the user + /// + string UniqueId + { + get; + } + + /// + /// Returns true if user needs reauthentication + /// + bool NeedsReauthentication + { + get; + } + + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Common/CommonUtil.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/CommonUtil.cs new file mode 100644 index 00000000..f090c87b --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/CommonUtil.cs @@ -0,0 +1,102 @@ +// +// 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 Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Utilities used by resource provider related code + /// + public static class CommonUtil + { + private const int KeyValueNameLength = 1024; // 1024 should be enough for registry key value name. + + //******************************************************************************************** + /// + /// Throw an exception if the object is null. + /// + /// the object to check + /// the variable or parameter name to display + //******************************************************************************************** + public static void CheckForNull(Object var, string varName) + { + if (var == null) + { + throw new ArgumentNullException(varName); + } + } + + //******************************************************************************************** + /// + /// Throw an exception if a string is null or empty. + /// + /// string to check + /// the variable or parameter name to display + //******************************************************************************************** + public static void CheckStringForNullOrEmpty(string stringVar, string stringVarName) + { + CheckStringForNullOrEmpty(stringVar, stringVarName, false); + } + + //******************************************************************************************** + /// + /// Throw an exception if a string is null or empty. + /// + /// string to check + /// the variable or parameter name to display + /// If true, will trim the string after it is determined not to be null + //******************************************************************************************** + public static void CheckStringForNullOrEmpty(string stringVar, string stringVarName, bool trim) + { + CheckForNull(stringVar, stringVarName); + if (trim == true) + { + stringVar = stringVar.Trim(); + } + if (stringVar.Length == 0) + { + throw new ArgumentException("EmptyStringNotAllowed", stringVarName); + } + } + + public static bool SameString(string value1, string value2) + { + return (value1 == null && value2 == null) || (value2 != null && value2.Equals(value1)); + } + + public static bool SameUri(Uri value1, Uri value2) + { + return (value1 == null && value2 == null) || (value2 != null && value2.Equals(value1)); + } + + public static bool SameSubscriptionIdentifier(IAzureSubscriptionIdentifier value1, + IAzureSubscriptionIdentifier value2) + { + return (value1 == null && value2 == null) || (value2 != null && value2.Equals(value1)); + } + + public static bool SameUserAccount(IAzureUserAccount value1, IAzureUserAccount value2) + { + return (value1 == null && value2 == null) || (value2 != null && value2.Equals(value1)); + } + + public static string GetExceptionMessage(Exception e) + { + string message; + +#if DEBUG + string nl2 = Environment.NewLine + Environment.NewLine; + message = e.Message + nl2 + "DEBUG ONLY:" + nl2 + e.ToString(); +#else + message = e.Message; +#endif + + return message; + } + + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ConnectionConstants.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ConnectionConstants.cs new file mode 100644 index 00000000..478ae03f --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ConnectionConstants.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Telemetry constants for Connection Dialog + /// + internal static class ConnectionConstants + { + #region connection value constants + public static int SqlAzureEngineEditionId = 5; + /// + /// Constant value for master database name + /// + public const string MasterDatabaseName = "master"; + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ExceptionUtil.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ExceptionUtil.cs new file mode 100644 index 00000000..2a5207df --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ExceptionUtil.cs @@ -0,0 +1,69 @@ +// +// 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.Data.Common; +using System.Data.SqlClient; +using System.Linq; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Extension methods and utils for exceptions + /// + internal static class ExceptionUtil + { + /// + /// Returns true if given exception if any of the inner exceptions is UserNeedsAuthenticationException + /// + internal static bool IsUserNeedsReauthenticateException(this Exception ex) + { + return ex.IsExceptionType(typeof(UserNeedsAuthenticationException)); + } + + /// + /// Returns true if given exception if any of the inner exceptions is sql exception + /// + internal static bool IsDbException(this Exception ex) + { + return ex.IsExceptionType(typeof (DbException)); + } + + /// + /// Returns true if given exception if any of the inner exceptions is same type of given type + /// + internal static bool IsExceptionType(this Exception ex, Type type) + { + if (ex == null) + { + return false; + } + if (ex is AggregateException) + { + var aggregateException = (AggregateException)ex; + return aggregateException.InnerExceptions != null && + aggregateException.InnerExceptions.Any(inner => inner.IsExceptionType(type)); + } + else if (type.IsAssignableFrom(ex.GetType()) || (ex.InnerException != null && ex.InnerException.IsExceptionType(type))) + { + return true; + } + return false; + } + + internal static string GetExceptionMessage(this Exception ex) + { + string errorMessage = string.Empty; + if (ex != null) + { + errorMessage = ex.Message; + if (ex.InnerException != null) + { + errorMessage += " " + ex.InnerException.Message; + } + } + return errorMessage; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ServiceExceptionBase.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ServiceExceptionBase.cs new file mode 100644 index 00000000..7640c8b2 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ServiceExceptionBase.cs @@ -0,0 +1,90 @@ +// +// 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.Net; +using System.Runtime.Serialization; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Base class for service exceptions + /// + public abstract class ServiceExceptionBase : Exception + { + /// + /// Initializes a new instance of the AuthenticationFailedException class. + /// + protected ServiceExceptionBase() + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + protected ServiceExceptionBase(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// The Http error code. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public ServiceExceptionBase(string message, HttpStatusCode httpStatusCode, Exception innerException = null) + : this(message, (int)httpStatusCode, innerException) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// The Http error code. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public ServiceExceptionBase(string message, int httpStatusCode, Exception innerException) + : base(message, innerException) + { + HttpStatusCode = (HttpStatusCode)httpStatusCode; + + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + protected ServiceExceptionBase(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + protected ServiceExceptionBase(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// The Http status code included in the exception + /// + public HttpStatusCode HttpStatusCode + { + get; set; + } + + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ServiceFailedException.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ServiceFailedException.cs new file mode 100644 index 00000000..bbe98c7b --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/ServiceFailedException.cs @@ -0,0 +1,67 @@ +// +// 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.Globalization; +using System.Runtime.Serialization; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// The exception is used if any operation inside a service or provider fails + /// + [Serializable] + public class ServiceFailedException : ServiceExceptionBase + { + /// + /// Initializes a new instance of the ServiceFailedException class. + /// + public ServiceFailedException() + { + } + + /// + /// Initializes a new instance of the ServiceFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public ServiceFailedException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the ServiceFailedException class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public ServiceFailedException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the ServiceFailedException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + public ServiceFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Creates a new instance of ServiceFailedException by adding the server definition info to the given message + /// + internal static ServiceFailedException CreateException(string message, ServerDefinition serverDefinition, Exception innerException) + { + return new ServiceFailedException( + string.Format(CultureInfo.CurrentCulture, message, + serverDefinition != null ? serverDefinition.ServerType : string.Empty, + serverDefinition != null ? serverDefinition.Category : string.Empty), innerException); + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Common/UserNeedsAuthenticationException.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/UserNeedsAuthenticationException.cs new file mode 100644 index 00000000..a3049e73 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Common/UserNeedsAuthenticationException.cs @@ -0,0 +1,53 @@ +// +// 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.Runtime.Serialization; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// The exception is used if any operation fails becauase user needs to reauthenticate + /// + public class UserNeedsAuthenticationException : ServiceExceptionBase + { + /// + /// Initializes a new instance of the ServiceFailedException class. + /// + public UserNeedsAuthenticationException() + { + } + + /// + /// Initializes a new instance of the ServiceFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public UserNeedsAuthenticationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the ServiceFailedException class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public UserNeedsAuthenticationException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the ServiceFailedException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + public UserNeedsAuthenticationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureDatabaseDiscoveryProvider.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureDatabaseDiscoveryProvider.cs new file mode 100644 index 00000000..37ffb1ca --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureDatabaseDiscoveryProvider.cs @@ -0,0 +1,328 @@ +// +// 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.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Default implementation for for Azure Sql databases. + /// A discovery provider capable of finding Sql Azure databases for a specific Azure user account. + /// + + [Exportable( + ServerTypes.SqlServer, + Categories.Azure, + typeof(IDatabaseDiscoveryProvider), + "Microsoft.SqlServer.ConnectionServices.Azure.AzureDatabaseDiscoveryProvider")] + public class AzureDatabaseDiscoveryProvider : ExportableBase, IDatabaseDiscoveryProvider, ISecureService, ICacheable> + { + private IAzureResourceManager _azureResourceManagerWrapper; + private IAzureAuthenticationManager _azureAccountManager; + private IDatabaseDiscoveryProvider _defaultDatabaseDiscoveryProvider; + private readonly ConcurrentCache> _cache = new ConcurrentCache>(); + + public AzureDatabaseDiscoveryProvider() + { + // Duplicate the exportable attribute as at present we do not support filtering using extensiondescriptor. + // The attribute is preserved in order to simplify ability to backport into existing tools + Metadata = new ExportableMetadata( + ServerTypes.SqlServer, + Categories.Azure, + "Microsoft.SqlServer.ConnectionServices.Azure.AzureDatabaseDiscoveryProvider"); + } + + /// + /// the event to raise when a database is found + /// + public event EventHandler DatabaseFound; + + /// + /// Updates the cache for current selected subscriptions + /// + /// The new cached data + public async Task> RefreshCacheAsync(CancellationToken cancellationToken) + { + ServiceResponse result = new ServiceResponse(); + + if (await ClearCacheAsync()) + { + result = await GetDatabaseInstancesAsync(serverName: null, cancellationToken: cancellationToken); + } + + return result; + } + + /// + /// Clears the cache for current selected subscriptions + /// + /// True if cache refreshed successfully. Otherwise returns false + public async Task ClearCacheAsync() + { + bool result = false; + + if (AzureResourceManager != null && AccountManager != null && AzureAccountManager != null) + { + try + { + IEnumerable subscriptions = await GetSubscriptionsAsync(); + _cache.ClearCache(subscriptions.Select(x => x.Subscription.SubscriptionId)); + result = true; + } + catch (Exception ex) + { + TraceException(TraceEventType.Error, TraceId.AzureResource, ex, "Failed to refresh the cache"); + result = false; + } + } + + return result; + } + + /// + /// Returns the databases for given connection info. + /// The connection info should be used to make the connection for getting databases not the account manager + /// + //public async Task> GetDatabaseInstancesAsync(UIConnectionInfo uiConnectionInfo, CancellationToken cancellationToken) + //{ + // ServiceResponse result = null; + // if (DefaultDatabaseDiscoveryProvider != null && DefaultDatabaseDiscoveryProvider != this) + // { + // result = await DefaultDatabaseDiscoveryProvider.GetDatabaseInstancesAsync(uiConnectionInfo, cancellationToken); + // } + // else + // { + // result = new ServiceResponse(); //TODO: add error that we couldn't find any default database provider + // } + // return result; + //} + + /// + /// Returns the databases for given server name. Using the account manager to get the databases + /// + public async Task> GetDatabaseInstancesAsync(string serverName, CancellationToken cancellationToken) + { + ServiceResponse result = null; + if (AzureResourceManager != null && AccountManager != null && AzureAccountManager != null) + { + try + { + //if connection is passed, we need to search all subscriptions not selected ones + IEnumerable subscriptions = await GetSubscriptionsAsync(string.IsNullOrEmpty(serverName)); + if (!cancellationToken.IsCancellationRequested) + { + result = await AzureUtil.ExecuteGetAzureResourceAsParallel(null, subscriptions, serverName, cancellationToken, + GetDatabaseForSubscriptionAsync); + } + + } + catch (Exception ex) + { + result = new ServiceResponse(ex); + } + } + + result = result ?? new ServiceResponse(); + return result; + } + + /// + /// Returns the resource manager that has same metadata as this class + /// + public IAzureResourceManager AzureResourceManager + { + get + { + return (_azureResourceManagerWrapper = _azureResourceManagerWrapper ?? GetService()); + } + set + { + _azureResourceManagerWrapper = value; + } + } + + /// + /// Returns the account manager that has same metadata as this class + /// + public IAzureAuthenticationManager AzureAccountManager + { + get + { + return (_azureAccountManager = _azureAccountManager ?? GetService()); + } + } + + /// + /// Returns the account manager that has same metadata as this class + /// + public IDatabaseDiscoveryProvider DefaultDatabaseDiscoveryProvider + { + get + { + return (_defaultDatabaseDiscoveryProvider = _defaultDatabaseDiscoveryProvider ?? GetService(null)); + } + } + + /// + /// Account Manager + /// + public IAccountManager AccountManager + { + get + { + return AzureAccountManager; + + } + set + { + _azureAccountManager = value as IAzureAuthenticationManager; + } + } + + /// + /// Returns azure subscriptions. + /// + private async Task> GetSubscriptionsAsync(bool selectedOnly = true) + { + try + { + return selectedOnly ? await AzureAccountManager.GetSelectedSubscriptionsAsync() : await AzureAccountManager.GetSubscriptionsAsync(); + } + catch (Exception ex) + { + throw new ServiceFailedException(string.Format(CultureInfo.CurrentCulture, SR.AzureSubscriptionFailedErrorMessage, ex)); + } + } + + /// + /// There was a wired nullReferencedException was running the tasks parallel. It only got fixed when I put the getting from cache insed an async method + /// + private Task> GetFromCacheAsync(string key) + { + return Task.Factory.StartNew(() => _cache.Get(key)); + } + + /// + /// Returns a list of Azure sql databases for given subscription + /// + private async Task> GetDatabaseForSubscriptionAsync(IAzureResourceManagementSession parentSession, + IAzureUserAccountSubscriptionContext input, string serverName, + CancellationToken cancellationToken, CancellationToken internalCancellationToken) + { + ServiceResponse result = null; + bool shouldFilter = !string.IsNullOrEmpty(serverName); + try + { + string key = input.Subscription.SubscriptionId; + + //when the data was coming from cache and no async mthod was called the parallel tasks running crashed so I had to call this line async to fix it + result = await GetFromCacheAsync(key); + if (result == null) + { + //this will only get the databases for the given server name + result = await GetDatabaseForSubscriptionFromServiceAsync(input, serverName, cancellationToken, internalCancellationToken); + } + else if (shouldFilter) + { + //we should filter the result because the cached data includes databases for all servers + result = new ServiceResponse(result.Data.Where(x => x.ServerInstanceInfo.FullyQualifiedDomainName == serverName), + result.Errors); + } + + //only update the cache if server name is not passes so the result is not filtered. The cache data supposed to be the data for all server + if (!shouldFilter && !cancellationToken.IsCancellationRequested) + { + result = _cache.UpdateCache(key, result); + } + } + catch (Exception ex) + { + result = new ServiceResponse(ex); + } + + return result; + } + + /// + /// Returns a list of Azure sql databases for given subscription + /// + private async Task> GetDatabaseForSubscriptionFromServiceAsync( + IAzureUserAccountSubscriptionContext input, string serverName, + CancellationToken cancellationToken, CancellationToken internalCancellationToken) + { + ServiceResponse result = null; + + try + { + if (!cancellationToken.IsCancellationRequested && !internalCancellationToken.IsCancellationRequested) + { + using (IAzureResourceManagementSession session = await AzureResourceManager.CreateSessionAsync(input)) + { + //find the server matches with the given servername which should be only one + bool shouldFilter = !string.IsNullOrEmpty(serverName); + IEnumerable sqlAzureServers = await AzureResourceManager.GetSqlServerAzureResourcesAsync(session); + IEnumerable filteredServers = !shouldFilter ? sqlAzureServers : sqlAzureServers.Where(x => + x.FullyQualifiedDomainName != null && + x.FullyQualifiedDomainName.Equals(serverName, + StringComparison.OrdinalIgnoreCase)); + + IList filteredServersList = filteredServers.ToList(); + result = await GetDatabasesForSubscriptionServersAsync(session, filteredServersList.ToList(), cancellationToken); + + //Set response Found to true to notify the other tasks to cancel + if (shouldFilter && filteredServersList.Any()) + { + result.Found = true; + } + } + } + } + catch (Exception ex) + { + result = new ServiceResponse(ex); + } + + return result ?? new ServiceResponse(); + } + + private async Task> GetDatabasesForSubscriptionServersAsync(IAzureResourceManagementSession session, + IList filteredServersList, CancellationToken cancellationToken) + { + ServiceResponse result = null; + AzureServerDatabaseDiscoveryProvider azureServerDatabaseDiscoveryProvider = new AzureServerDatabaseDiscoveryProvider(AzureResourceManager, session, ServerDefinition); + azureServerDatabaseDiscoveryProvider.DatabaseFound += AzureServerDatabaseDiscoveryProviderOnDatabaseFound; + if (filteredServersList.Any()) + { + result = await azureServerDatabaseDiscoveryProvider.GetDatabasesForServers(filteredServersList, cancellationToken); + } + + return result ?? new ServiceResponse(); + } + + private void AzureServerDatabaseDiscoveryProviderOnDatabaseFound(object sender, DatabaseInfoEventArgs databaseInfoEventArgs) + { + OnDatabaseFound(databaseInfoEventArgs.Database); + } + + /// + /// Raises DatabaseFound event with the given databases info + /// + private void OnDatabaseFound(DatabaseInstanceInfo databaseInfo) + { + if (DatabaseFound != null) + { + DatabaseFound(this, new DatabaseInfoEventArgs() { Database = databaseInfo }); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureServerDatabaseDiscoveryProvider.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureServerDatabaseDiscoveryProvider.cs new file mode 100644 index 00000000..fac48b6a --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureServerDatabaseDiscoveryProvider.cs @@ -0,0 +1,123 @@ +// +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + internal class AzureServerDatabaseDiscoveryProvider + { + private readonly IAzureResourceManagementSession _session; + /// + /// the event to raise when a database is found + /// + public event EventHandler DatabaseFound; + + public AzureServerDatabaseDiscoveryProvider(IAzureResourceManager azureResourceManager, IAzureResourceManagementSession session, ServerDefinition serverDefinition) + { + CommonUtil.CheckForNull(session, "session"); + CommonUtil.CheckForNull(azureResourceManager, "azureResourceManager"); + _session = session; + AzureResourceManager = azureResourceManager; + + ServerDefinition = serverDefinition ?? ServerDefinition.Default; + } + + /// + /// Returns the resource manager that has same metadata as this class + /// + internal IAzureResourceManager AzureResourceManager { get; set; } + + private ServerDefinition ServerDefinition { get; set; } + + public async Task> GetDatabasesForServers(IList serverResources, CancellationToken cancellationToken) + { + ServiceResponse < DatabaseInstanceInfo > result = new ServiceResponse(); + if (serverResources != null) + { + result = await AzureUtil.ExecuteGetAzureResourceAsParallel(_session, serverResources, null, cancellationToken, GetDatabasesForServerFromService); + } + return result; + } + + private async Task> GetDatabasesForServerFromService( + IAzureResourceManagementSession session, + IAzureSqlServerResource azureSqlServer, + string serverName, + CancellationToken cancellationToken, + CancellationToken internalCancellationToken) + { + try + { + if (cancellationToken.IsCancellationRequested) + { + return new ServiceResponse(); + } + ServerInstanceInfo serverInstanceInfo = new ServerInstanceInfo(ServerDefinition) + { + Name = azureSqlServer.Name, + FullyQualifiedDomainName = azureSqlServer.FullyQualifiedDomainName, + AdministratorLogin = azureSqlServer.AdministratorLogin + }; + OnDatabaseFound(new DatabaseInstanceInfo(serverInstanceInfo)); + IEnumerable databases = await AzureResourceManager.GetAzureDatabasesAsync( + session, + azureSqlServer.ResourceGroupName, + azureSqlServer.Name); + if (cancellationToken.IsCancellationRequested) + { + return new ServiceResponse(); + } + else + { + IEnumerable data = databases.Select(x => ConvertToModel(serverInstanceInfo, x)); + ServiceResponse result = new ServiceResponse(data); + foreach (var databaseInstance in result.Data) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + OnDatabaseFound(databaseInstance); + } + + return result; + } + } + catch (Exception ex) + { + return new ServiceResponse(ex); + } + } + + /// + /// Raises DatabaseFound event with the given databases info + /// + private void OnDatabaseFound(DatabaseInstanceInfo databaseInfo) + { + if (DatabaseFound != null) + { + DatabaseFound(this, new DatabaseInfoEventArgs() { Database = databaseInfo }); + } + } + + /// + /// Converts the resource to DatabaseInstanceInfo + /// + private DatabaseInstanceInfo ConvertToModel(ServerInstanceInfo serverInstanceInfo, IAzureResource azureResource) + { + DatabaseInstanceInfo databaseInstance = new DatabaseInstanceInfo(serverInstanceInfo) + { + Name = azureResource.Name.Replace(serverInstanceInfo.Name + "/", "") + }; + + return databaseInstance; + } + + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureSqlServerDiscoveryProvider.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureSqlServerDiscoveryProvider.cs new file mode 100644 index 00000000..79c9ae51 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureSqlServerDiscoveryProvider.cs @@ -0,0 +1,125 @@ +// +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Default implementation for for Azure Sql servers. + /// A discovery provider capable of finding Sql Azure servers for a specific Azure user account. + /// + [Exportable( + ServerTypes.SqlServer, + Categories.Azure, + typeof(IServerDiscoveryProvider), + "Microsoft.SqlServer.ConnectionServices.Azure.AzureServerDiscoveryProvider")] + internal class AzureSqlServerDiscoveryProvider : ExportableBase, IServerDiscoveryProvider, ISecureService + { + private IAzureResourceManager _azureResourceManagerWrapper; + private IAzureAuthenticationManager _azureAccountManager; + + public AzureSqlServerDiscoveryProvider() + { + // Duplicate the exportable attribute as at present we do not support filtering using extensiondescriptor. + // The attribute is preserved in order to simplify ability to backport into existing tools + Metadata = new ExportableMetadata( + ServerTypes.SqlServer, + Categories.Azure, + "Microsoft.SqlServer.ConnectionServices.Azure.AzureServerDiscoveryProvider"); + } + + public async Task> GetServerInstancesAsync() + { + ServiceResponse result = new ServiceResponse(); + List serverInstances = new List(); + + if (AccountManager != null && AzureAccountManager != null && AzureResourceManager != null) + { + try + { + IEnumerable subscriptions = + await AzureAccountManager.GetSelectedSubscriptionsAsync(); + if (subscriptions != null) + { + foreach (IAzureUserAccountSubscriptionContext subscription in subscriptions) + { + using (IAzureResourceManagementSession session = await AzureResourceManager.CreateSessionAsync(subscription)) + { + IEnumerable azureResources = + await AzureResourceManager.GetSqlServerAzureResourcesAsync(session); + serverInstances.AddRange( + azureResources.Select(x => + new ServerInstanceInfo(ServerDefinition) + { + Name = x.Name, + FullyQualifiedDomainName = x.FullyQualifiedDomainName, + AdministratorLogin = x.AdministratorLogin + })); + } + } + } + result = new ServiceResponse(serverInstances); + } + catch (Exception ex) + { + result = new ServiceResponse(serverInstances, new List() {ex}); + } + } + + return result; + } + + /// + /// Returns the resource manager that has same metadata as this class + /// + public IAzureResourceManager AzureResourceManager + { + get + { + return (_azureResourceManagerWrapper = + _azureResourceManagerWrapper ?? + GetService()); + } + internal set + { + _azureResourceManagerWrapper = value; + } + } + + /// + /// Returns the account manager that has same metadata as this class + /// + public IAzureAuthenticationManager AzureAccountManager + { + get + { + return (_azureAccountManager = + _azureAccountManager ?? + GetService()); + } + } + + /// + /// Account Manager + /// + public IAccountManager AccountManager + { + get + { + return AzureAccountManager; + + } + internal set + { + _azureAccountManager = value as IAzureAuthenticationManager; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureUtil.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureUtil.cs new file mode 100644 index 00000000..abde06a0 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/AzureUtil.cs @@ -0,0 +1,113 @@ +// +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + internal static class AzureUtil + { + /// + /// Execute an async action for each input in the a list of input in parallel. + /// If any task fails, adds the exeption message to the response errors + /// If cancellation token is set to cancel, returns empty response + /// + /// Resource management session to use to call the resource manager + /// List of inputs + /// server name to filter the result + /// Cancellation token + /// Async action + /// ServiceResponse including the list of data and errors + public static async Task> ExecuteGetAzureResourceAsParallel( + IAzureResourceManagementSession session, + IEnumerable inputs, + string serverName, + CancellationToken cancellationToken, + Func>> asyncAction + ) + { + List mergedResult = new List(); + List mergedErrors = new List(); + try + { + if (inputs == null) + { + return new ServiceResponse(mergedResult); + } + List inputList = inputs.ToList(); + + ServiceResponse[] resultList = new ServiceResponse[inputList.Count]; + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + var tasks = Enumerable.Range(0, inputList.Count()) + .Select(async i => + { + ServiceResponse result = await GetResult(session, inputList[i], serverName, cancellationToken, + cancellationTokenSource.Token, asyncAction); + //server name is used to filter the result and if the data is already found not, we need to cancel the other tasks + if (!string.IsNullOrEmpty(serverName) && result.Found) + { + cancellationTokenSource.Cancel(); + } + resultList[i] = result; + return result; + } + ); + + await Task.WhenAll(tasks); + + if (!cancellationToken.IsCancellationRequested) + { + foreach (ServiceResponse resultForEachInput in resultList) + { + mergedResult.AddRange(resultForEachInput.Data); + mergedErrors.AddRange(resultForEachInput.Errors); + } + } + } + catch (Exception ex) + { + mergedErrors.Add(ex); + return new ServiceResponse(mergedResult, mergedErrors); + } + return new ServiceResponse(mergedResult, mergedErrors); + } + + private static async Task> GetResult( + IAzureResourceManagementSession session, + TInput input, + string serverName, + CancellationToken cancellationToken, + CancellationToken internalCancellationToken, + Func>> asyncAction + ) + { + if (cancellationToken.IsCancellationRequested || internalCancellationToken.IsCancellationRequested) + { + return new ServiceResponse(); + } + try + { + return await asyncAction(session, input, serverName, cancellationToken, internalCancellationToken); + } + catch (Exception ex) + { + return new ServiceResponse(ex); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/DatabaseInfoEventArgs.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/DatabaseInfoEventArgs.cs new file mode 100644 index 00000000..fb3cf1b0 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/DatabaseInfoEventArgs.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Event arguments to use for database info events + /// + public class DatabaseInfoEventArgs : EventArgs + { + /// + /// Database Info + /// + public DatabaseInstanceInfo Database + { + get; set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/DatabaseInstanceInfo.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/DatabaseInstanceInfo.cs new file mode 100644 index 00000000..43e2e1e7 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/DatabaseInstanceInfo.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Includes the data for a discovered database + /// + public class DatabaseInstanceInfo + { + /// + /// Default constructor to initialize the instance + /// + /// + public DatabaseInstanceInfo(ServerInstanceInfo serverInstanceInfo) + { + ServerInstanceInfo = serverInstanceInfo; + } + + public DatabaseInstanceInfo(ServerDefinition serverDefinition, string serverName, string databaseName) + { + ServerInstanceInfo = new ServerInstanceInfo(serverDefinition) + { + Name = serverName + }; + Name = databaseName; + } + + /// + /// Server instance info associated to the database instance + /// + public ServerInstanceInfo ServerInstanceInfo + { + get; + private set; + } + + /// + /// Database Name + /// + public string Name + { + get; + set; + } + + /// + /// Returns true if the database is the master database + /// + public bool IsMaster + { + get { return ConnectionConstants.MasterDatabaseName.Equals(Name, StringComparison.OrdinalIgnoreCase); } + } + + public bool IsDefaultDatabase { get; set; } + + public bool IsSystemDatabase { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResource.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResource.cs new file mode 100644 index 00000000..84a3c1c2 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResource.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Interface for any implementation of azure resource + /// + public interface IAzureResource + { + /// + /// Azure Resource Name + /// + string Name { get; set; } + + /// + /// Azure Resource Type + /// + string Type { get; set; } + + /// + /// Azure Resource Id + /// + string Id { get; set; } + + /// + /// Resource Group Name + /// + string ResourceGroupName + { + get; + set; + } + + /// + /// Resource Location + /// + string Location { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResourceManagementSession.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResourceManagementSession.cs new file mode 100644 index 00000000..529b8991 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResourceManagementSession.cs @@ -0,0 +1,30 @@ +// +// 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 Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// A session used by . Includes all the clients that the resource management needs to get ther resources + /// + public interface IAzureResourceManagementSession : IDisposable + { + /// + /// Closes the session + /// + /// + bool CloseSession(); + + /// + /// Teh subscription for the current session + /// + IAzureUserAccountSubscriptionContext SubscriptionContext + { + get; + set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResourceManager.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResourceManager.cs new file mode 100644 index 00000000..507825fb --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureResourceManager.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.FirewallRule; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Provides functionality to get azure resources by making Http request to the Azure REST API + /// + public interface IAzureResourceManager : IExportable + { + /// + /// Returns a list of azure databases given subscription resource group name and server name + /// + /// Subscription Context which includes credentials to use in the resource manager + /// Resource Group Name + /// Server name + /// The list of databases + Task> GetAzureDatabasesAsync( + IAzureResourceManagementSession azureResourceManagementSession, + string resourceGroupName, + string serverName); + + /// + /// Returns a list of azure servers given subscription + /// + /// Subscription Context which includes credentials to use in the resource manager + /// The list of Sql server resources + Task> GetSqlServerAzureResourcesAsync( + IAzureResourceManagementSession azureResourceManagementSession); + + /// + /// Create new firewall rule given user subscription, Sql server resource and the firewall rule request + /// + /// Subscription Context which includes credentials to use in the resource manager + /// Sql server resource to create firewall rule for + /// Firewall rule request including the name and IP address range + /// + Task CreateFirewallRuleAsync( + IAzureResourceManagementSession azureResourceManagementSession, + IAzureSqlServerResource azureSqlServer, + FirewallRuleRequest firewallRuleRequest + ); + + Task CreateSessionAsync(IAzureUserAccountSubscriptionContext subscriptionContext); + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureSqlServerResource.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureSqlServerResource.cs new file mode 100644 index 00000000..0c5e192e --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IAzureSqlServerResource.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// An Azure Sql server resource + /// + public interface IAzureSqlServerResource : IAzureResource + { + /// + /// Fully qualified domain name + /// + string FullyQualifiedDomainName + { + get; + } + + /// + /// Administrator Login + /// + string AdministratorLogin + { + get; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ICacheable.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ICacheable.cs new file mode 100644 index 00000000..3cc2869b --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ICacheable.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Defines a class as cachable + /// + public interface ICacheable + { + /// + /// Clears the cache for current user + /// + /// True if cache refreshed successfully. Otherwise returns false + Task ClearCacheAsync(); + + /// + /// Updates the cache for current selected subscriptions + /// + /// The new cached data + Task RefreshCacheAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IDatabaseDiscoveryProvider.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IDatabaseDiscoveryProvider.cs new file mode 100644 index 00000000..6d8df6b6 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IDatabaseDiscoveryProvider.cs @@ -0,0 +1,38 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// A discovery provider capable of finding databases for a given server type and category. + /// For example: finding SQL Server databases in Azure, or on the local network. + /// Implementing classes must add a + /// to the class in order to be found by the extension manager, + /// and to define the type and category supported + /// + public interface IDatabaseDiscoveryProvider : IExportable + { + /// + /// Returns the databases for given server name. + /// + Task> GetDatabaseInstancesAsync(string serverName, CancellationToken cancellationToken); + + /// + /// Returns the databases for given connection info. + /// The connection info should be used to make the connection for getting databases not the account manager + /// + //Task> GetDatabaseInstancesAsync(UIConnectionInfo uiConnectionInfo, CancellationToken cancellationToken); + + /// + /// the event to raise when a database is found + /// + event EventHandler DatabaseFound; + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ISecureService.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ISecureService.cs new file mode 100644 index 00000000..4e2df4fd --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ISecureService.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Defines a class as secure which requires an account to function + /// + public interface ISecureService + { + /// + /// Gets the account manager instance + /// + IAccountManager AccountManager + { + get; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IServerDiscoveryProvider.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IServerDiscoveryProvider.cs new file mode 100644 index 00000000..db0c5712 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/IServerDiscoveryProvider.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// A discovery provider capable of finding servers for a given server type and category. + /// For example: finding SQL Servers in Azure, or on the local network. + /// Implementing classes must add a + /// to the class in order to be found by the extension manager, + /// and to define the type and category supported + /// + public interface IServerDiscoveryProvider : IExportable + { + /// + /// Discovers the server instances + /// + Task> GetServerInstancesAsync(); + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerDefinition.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerDefinition.cs new file mode 100644 index 00000000..7d7defe7 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerDefinition.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Defines a server grouping based on the type of server connection supported (SQL Server, Reporting Server, Analysis Server) + /// and a Category by which these should be shown to the user. + /// Built in examples of categories include Local, Network, and Azure and additional categories can be defined as needed. + /// Note that the Connection Dialog UI may require Category to be set for some resource types such as. + /// In addition a UI section matching that category may be required, or else the provider will not be used by any UI part and never be called. + /// + public interface IServerDefinition + { + /// + /// Category by which resources can be grouped. Built in examples of categories include Local, Network, and Azure and additional categories can be defined as needed. + /// + string Category + { + get; + } + + /// + /// The type of server connection supported. Examples include SQL Server, Reporting Server, Analysis Server. + /// + string ServerType + { + get; + } + } + + /// + /// The implementation of the server definition that implements the properties + /// + public sealed class ServerDefinition : IServerDefinition + { + private static ServerDefinition _default = new ServerDefinition(ServerTypes.SqlServer, string.Empty); + + public ServerDefinition(string serverType, string category) + { + ServerType = serverType; + Category = category; + } + + /// + /// + /// + public string ServerType { private set; get; } + + /// + /// + /// + public string Category { private set; get; } + + /// + /// Default value for the server definition + /// + public static ServerDefinition Default + { + get { return _default; } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerInstanceInfo.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerInstanceInfo.cs new file mode 100644 index 00000000..5f66ae91 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerInstanceInfo.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Contains information about a server discovered by + /// + public class ServerInstanceInfo + { + /// + /// Initializes the new instance with server and location + /// + public ServerInstanceInfo(IServerDefinition serverDefinition) + { + ServerDefinition = serverDefinition; + } + + public ServerInstanceInfo() + { + } + + public IServerDefinition ServerDefinition + { + get; private set; + } + + /// + /// Server Name + /// + public string Name + { + get; + set; + } + + /// + /// Fully qualified domain name + /// + public string FullyQualifiedDomainName + { + get; + set; + } + + /// + /// Administrator Login + /// + public string AdministratorLogin + { + get; + set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerTypes.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerTypes.cs new file mode 100644 index 00000000..cbdc626a --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServerTypes.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// List of built-in server types used in . + /// Defines a server grouping based on the type of server connection supported (SQL Server, Reporting Server, Analysis Server) + /// Additional server types can be defined as needed. + /// Note that the Connection Dialog UI may require server type to be set for some resource types such as. + /// In addition a UI section matching that category may be required, or else the provider will not be used by any UI part and never be called. + /// + public static class ServerTypes + { + /// + /// Sql server + /// + public const string SqlServer = "SqlServer"; + + /// + /// Reporting server + /// + public const string SqlReportingServer = "SqlReportingServer"; + + /// + /// Integration server + /// + public const string SqlIntegrationServer = "SqlIntegrationServer"; + + /// + /// Analysis server + /// + public const string SqlAnalysisServer = "SqlAnalysisServer"; + } + + /// + /// List of built-in categories used in + /// Defines a server grouping based on the category of server connection supported (Network, Local, Azure) + /// Additional categories can be defined as needed. + /// Note that the Connection Dialog UI may require Category to be set for some resource types such as. + /// In addition a UI section matching that category may be required, or else the provider will not be used by any UI part and never be called. + /// + public static class Categories + { + /// + /// Network category + /// + public const string Network = "network"; + + /// + /// Azure category + /// + public const string Azure = "azure"; + + /// + /// local category + /// + public const string Local = "local"; + + /// + /// local db category + /// + public const string LocalDb = "localdb"; + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServiceResponse.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServiceResponse.cs new file mode 100644 index 00000000..15619311 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Discovery/ServiceResponse.cs @@ -0,0 +1,93 @@ +// +// 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.Linq; + +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + /// + /// Contains the data that a service wants to returns plus the errors happened during getting some of the data + /// + /// + public class ServiceResponse + { + /// + /// Creates new instance given data and errors + /// + public ServiceResponse(IEnumerable data, IEnumerable errors) + { + Data = data; + Errors = errors; + } + + /// + /// Creates new instance given errors + /// + public ServiceResponse(IEnumerable errors) : this(Enumerable.Empty(), errors) + { + } + + /// + /// Creates new instance given data + /// + public ServiceResponse(IEnumerable data) : this(data, Enumerable.Empty()) + { + } + + /// + /// Creating new empry instance + /// + public ServiceResponse() : this(Enumerable.Empty(), Enumerable.Empty()) + { + } + + /// + /// Creates new instance given exception to create the error list + /// + public ServiceResponse(Exception ex) : this(Enumerable.Empty(), new List {ex}) + { + } + + /// + /// Information a service wants to returns + /// + public IEnumerable Data { get; private set; } + + /// + /// The errors that heppend during retrieving data + /// + public IEnumerable Errors { get; private set; } + + /// + /// Return true if the response includes errors + /// + public bool HasError + { + get { return Errors != null && Errors.Any(); } + } + + /// + /// Concatenates the messages into one error message + /// + public string ErrorMessage + { + get + { + string message = string.Empty; + if (HasError) + { + message = string.Join(Environment.NewLine, Errors.Select(x => x.GetExceptionMessage())); + } + return message; + } + } + + /// + /// Returns true if a response already found. it's used when we need to filter the responses + /// + public bool Found { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableAttribute.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableAttribute.cs new file mode 100644 index 00000000..e2bf82d5 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableAttribute.cs @@ -0,0 +1,112 @@ +// +// 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.Composition; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// Attribute defining a service export, and the metadata about that service. Implements IServiceMetadata, + /// which should be used on the importer side to ensure type consistency. Services and providers have to add this property + /// in order to be found by the extension manager + /// + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class ExportableAttribute : ExportAttribute, IExportableMetadata + { + /// + /// Default constructor to initialize the metadata of the exportable + /// + /// The server type supported by the exportable. If not set, exportable supports all server types + /// The category supported by the exportable. If not set, exportable supports all categories + /// The type of the exportable to be used by the extension manager to find the exportable + /// The unique id of the exportable. Used by the extension manager to pick only one from exportables with same id in the same assembly + /// The priority of the exportable. The extension manager will pick the exportable with the highest priority if multiple found + /// The display name of the exportable. This field is optional + public ExportableAttribute( + string serverType, + string category, + Type type, + string id, + int priority = 0, + string displayName = null) : base(type) + { + Category = category; + ServerType = serverType; + Id = id; + DisplayName = displayName; + Priority = priority; + } + + /// + /// The constructor to define an exportable by type, id and priority only. To be used by the exportables that support all server types and categories. + /// For example: the implementation of can be used for all server types and categories. + /// + /// The type of the exportable to be used by the extension manager to find the exportable + /// The unique id of the exportable. Used by the extension manager to pick only one from exportables with same id in the same assembly + /// The priority of the exportable. The extension manager will pick the exportable with the highest priority if multiple found + /// The display name of the exportable. This field is optional + public ExportableAttribute(Type type, string id, int priority = 0, + string displayName = null) : + this(String.Empty, String.Empty, type, id, priority, displayName) + { + } + + /// + /// Thye category of the service + /// + public string Category + { + get; + private set; + } + + /// + /// The server type of that the service supports + /// + public string ServerType + { + get; + private set; + } + + /// + /// The version of this extension + /// + public string Version + { + get; + set; + } + + /// + /// The id of the extension + /// + public string Id + { + get; + private set; + } + + /// + /// The display name for the extension + /// + public string DisplayName + { + get; + private set; + } + + /// + /// priority of the extension. Can be used to filter the extensions if multiple found + /// + public int Priority + { + get; + private set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableBase.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableBase.cs new file mode 100644 index 00000000..253e45cb --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableBase.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using Microsoft.SqlTools.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// The base class for all exportable classes. + /// + public abstract class ExportableBase : TraceableBase, IExportable, IComposableService + { + private ITrace _trace; + private ExportableStatus _exportableStatus = new ExportableStatus(); + + public void SetServiceProvider(IMultiServiceProvider provider) + { + ServiceProvider = provider; + } + + /// + /// The exportable metadata + /// + public IExportableMetadata Metadata + { + get; + set; + } + + + /// + /// Gets or sets the dependency manager to provider the dependencies of the class + /// + public IMultiServiceProvider ServiceProvider + { + get; + private set; + } + + public virtual ExportableStatus Status + { + get { return _exportableStatus; } + } + + /// + /// Finds a service of specific type which has the same metadata as class using the dependency manager. + /// If multiple services found, the one with the highest priority will be returned + /// + /// The type of the service + /// A service of type T or null if not found + protected T GetService() + where T : IExportable + { + return GetService(Metadata); + } + + /// + /// Finds a service of specific type which has the same metadata as class using the dependency manager. + /// If multiple services found, the one with the highest priority will be returned + /// + /// The type of the service + /// A service of type T or null if not found + protected T GetService(IExportableMetadata metadata) + where T : IExportable + { + //Don't try to find the service if it's the same service as current one with same metadata + if (ServiceProvider != null && (!(this is T) || metadata != Metadata)) + { + return ServiceProvider.GetService(metadata); + } + return default(T); + } + + /// + /// An instance of ITrace which is exported to the extension manager + /// + public override ITrace Trace + { + get + { + return (_trace = _trace ?? GetService()); + } + set + { + _trace = value; + } + } + + /// + /// ServerDefinition created from the metadata + /// + protected ServerDefinition ServerDefinition + { + get + { + return Metadata != null ? new ServerDefinition(Metadata.ServerType, Metadata.Category) : null; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableStatus.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableStatus.cs new file mode 100644 index 00000000..1521e5cc --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExportableStatus.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// Includes the status of the exportable - whether it failed to load and any error message + /// returned during loading + /// + public class ExportableStatus + { + /// + /// Returns true if the loading of the exportable failed + /// + public bool LoadingFailed + { + get; + set; + } + + /// + /// An error message if the loading failed + /// + public string ErrorMessage { get; set; } + + /// + /// An info link to navigate to + /// + public string InfoLink { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExtensionUtils.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExtensionUtils.cs new file mode 100644 index 00000000..163c49e4 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ExtensionUtils.cs @@ -0,0 +1,175 @@ +// +// 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.Globalization; +using System.Linq; +using Microsoft.SqlTools.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// Extension methods for exportable and service + /// + public static class ExtensionUtils + { + + /// + /// Finds a service of specific type which has the same metadata as class using the service provider. + /// If multiple services found, the one with the highest priority will be returned + /// + /// The type of the service + /// A service of type T or null if not found + public static T GetService(this IMultiServiceProvider provider, IServerDefinition serverDefinition) + where T : IExportable + { + return provider.GetServices() + .FilterExportables(serverDefinition) + .OrderByDescending(s => SortOrder(s)). + FirstOrDefault(); + } + + private static int SortOrder(T service) + { + IExportable exportable = service as IExportable; + if (exportable != null) + { + return exportable.Metadata.Priority; + } + return 0; + } + + public static IEnumerable FilterExportables(this IEnumerable exportables, IServerDefinition serverDefinition = null) + where T : IExportable + { + if (exportables == null) + { + return null; + } + //Get all the possible matches + IEnumerable allMatched = serverDefinition != null ? + exportables.Where(x => Match(x.Metadata, serverDefinition)).ToList() : exportables; + IList list = allMatched.ToList(); + + //If specific server type requested and the list has any item with that server type remove the others. + //for instance is there's server for all server types and one specifically for sql and give metadata is asking for sql then + //we should return the sql one even if the other service has higher priority + + IList withSameServerType = list.Where(x => serverDefinition.HasSameServerName(x.Metadata)).ToList(); + if (withSameServerType.Any()) + { + list = withSameServerType; + } + IList withSameCategory = list.Where(x => serverDefinition.HasSameCategory(x.Metadata)).ToList(); + if (withSameCategory.Any()) + { + list = withSameCategory; + } + return list; + } + + public static bool HasSameServerName(this IServerDefinition serverDefinition, IServerDefinition metadata) + { + if (serverDefinition != null && metadata != null) + { + // Note: this does not handle null <-> string.Empty equivalence. For now ignoring this as it should not matter + return string.Equals(serverDefinition.ServerType, metadata.ServerType, StringComparison.OrdinalIgnoreCase); + } + return false; + } + + public static bool HasSameCategory(this IServerDefinition serverDefinition, IServerDefinition metadata) + { + if (serverDefinition != null && metadata != null) + { + // Note: this does not handle null <-> string.Empty equivalence. For now ignoring this as it should not matter + return string.Equals(serverDefinition.Category, metadata.Category, StringComparison.OrdinalIgnoreCase); + } + return false; + } + + /// + /// Returns true if the given server definition is secure type. (i.e. Azure) + /// + internal static bool IsSecure(this IServerDefinition serverDefinition) + { + if (serverDefinition != null && serverDefinition.Category != null) + { + return serverDefinition.Category.Equals(Categories.Azure, StringComparison.OrdinalIgnoreCase); + } + return false; + } + + internal static string GetServerDefinitionKey(this IServerDefinition serverDefinition) + { + string key = string.Empty; + if (serverDefinition != null) + { + key = string.Format(CultureInfo.InvariantCulture, "{0}", GetKey(serverDefinition.Category)); + } + + return key; + } + + internal static bool EqualsServerDefinition(this IServerDefinition serverDefinition, IServerDefinition otherServerDefinition) + { + if (serverDefinition == null && otherServerDefinition == null) + { + return true; + } + if (serverDefinition != null && otherServerDefinition != null) + { + return (((string.IsNullOrEmpty(serverDefinition.Category) && string.IsNullOrEmpty(otherServerDefinition.Category)) || serverDefinition.HasSameCategory(otherServerDefinition)) && + ((string.IsNullOrEmpty(serverDefinition.ServerType) && string.IsNullOrEmpty(otherServerDefinition.ServerType)) || serverDefinition.HasSameServerName(otherServerDefinition))); + } + return false; + } + + internal static bool EmptyOrEqual(this string value1, string value2) + { + if (string.IsNullOrEmpty(value1) && string.IsNullOrEmpty(value2)) + { + return true; + } + return value1 == value2; + } + + private static string GetKey(string name) + { + return string.IsNullOrEmpty(name) ? string.Empty : name.ToUpperInvariant(); + } + + /// + /// Returns true if the metadata matches the given server definition + /// + public static bool Match(this IServerDefinition first, IServerDefinition other) + { + if (first == null) + { + // TODO should we handle this differently? + return false; + } + if (other == null) + { + return false; + } + return MatchMetaData(first.ServerType, other.ServerType) + && MatchMetaData(first.Category, other.Category); + } + + /// + /// Returns true if the metadata value matches the given value + /// + private static bool MatchMetaData(string metaData, string requestedMetaData) + { + if (string.IsNullOrEmpty(metaData) || string.IsNullOrEmpty(requestedMetaData)) + { + return true; + } + return (metaData.Equals(requestedMetaData, StringComparison.OrdinalIgnoreCase)); + } + + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/IExportable.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/IExportable.cs new file mode 100644 index 00000000..807e1d4d --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/IExportable.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// An interface to be implemented by any class that needs to be exportable + /// + public interface IExportable : IComposableService + { + /// + /// The metadata assigned to the exportable + /// + IExportableMetadata Metadata + { + set; get; + } + + /// + /// Returns the status of the exportable + /// + ExportableStatus Status + { + get; + } + + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/IExportableMetadata.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/IExportableMetadata.cs new file mode 100644 index 00000000..3c052daa --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/IExportableMetadata.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// The metadata describing an extension + /// + public interface IExportableMetadata : IStandardMetadata, IServerDefinition + { + /// + /// Exportable priority tobe used when multiple of same type found + /// + int Priority + { + get; + } + } + + public class ExportableMetadata : IExportableMetadata + { + /// + /// Default constructor to initialize the metadata of the exportable + /// + /// The server type supported by the exportable. If not set, exportable supports all server types + /// The category supported by the exportable. If not set, exportable supports all categories + /// The unique id of the exportable. Used by the extension manager to pick only one from exportables with same id in the same assembly + /// The priority of the exportable. The extension manager will pick the exportable with the highest priority if multiple found + /// The display name of the exportable. This field is optional + public ExportableMetadata( + string serverType, + string category, + string id, + int priority = 0, + string displayName = null) + { + Category = category; + ServerType = serverType; + Id = id; + DisplayName = displayName; + Priority = priority; + } + + /// + /// The constructor to define an exportable by type, id and priority only. To be used by the exportables that support all server types and categories. + /// For example: the implementation of can be used for all server types and categories. + /// + /// The unique id of the exportable. Used by the extension manager to pick only one from exportables with same id in the same assembly + /// The priority of the exportable. The extension manager will pick the exportable with the highest priority if multiple found + /// The display name of the exportable. This field is optional + public ExportableMetadata(string id, int priority = 0, + string displayName = null) : + this(string.Empty, string.Empty, id, priority, displayName) + { + } + + public int Priority + { + get; + set; + } + + public string Version + { + get; + set; + } + + public string Id + { + get; + set; + } + + public string DisplayName + { + get; + set; + } + + public string Category + { + get; + set; + } + + public string ServerType + { + get; + set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ITrace.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ITrace.cs new file mode 100644 index 00000000..a03cab6f --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/ITrace.cs @@ -0,0 +1,44 @@ +// +// 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.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// Provides facility to trace code execution through calls to Trace* methods. + /// Implementing classes must add a + /// to the class in order to be found by the extension manager + /// + public interface ITrace : IExportable + { + /// + /// Write a formatted trace event message to the underlying trace source. + /// + /// Event type that specifies the verbosity level of the trace. + /// The category of the caller's product feature. + /// Format string of the message to be traced along with the event. + /// Object array containing zero or more objects to format. + /// True if event was successfully written + bool TraceEvent(TraceEventType eventType, int traceId, string message, params object[] args); + + /// + /// Write a trace event with a message and exception details to the underlying trace source. + /// + /// Event type that specifies the verbosity level of the trace. + /// The category of the caller's product feature. + /// The exception to be logged. + /// String message to be traced along with the event. + /// Compile time property to trace the line number of the calling code. Used to trace location error occurred + /// Compile time property to trace the fileName of the calling code. Used to trace location error occurred + /// Compile time property to trace the name of the calling method. Used to trace location error occurred + /// True if event was successfully written + bool TraceException(TraceEventType eventType, int traceId, Exception exception, string message, + [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "", + [CallerMemberName] string memberName = ""); + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/TraceId.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/TraceId.cs new file mode 100644 index 00000000..28dee2b8 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/TraceId.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// Enumeration of values used as trace event identifiers that semantically represent the major categories of product features. + /// + public enum TraceId : int + { + /// + /// Trace Id for Azure Authentication traces + /// + AzureAuthentication = 0, + /// + /// Trace Id for azure resource traces + /// + AzureResource = 1, + /// + /// Trace Id for UI sections traces + /// + Sections = 2, + + /// + /// Trace Id for connection traces + /// + Connection = 3, + + /// + /// Trace Id for firewall rule traces + /// + FirewallRule = 4, + + /// + /// Trace Id for Azure browse traces + /// + AzureSection = 5, + + /// + /// Trace Id for network servers + /// + NetworkServers = 6, + + /// + /// Trace Id for local servers + /// + LocalServers = 7, + + /// + /// Trace Id for sql database discovery + /// + SqlDatabase = 8, + + /// + /// Trace Id for local browse traces + /// + LocalSection = 9, + + /// + /// Trace Id for account picker traces + /// + AccountPicker = 10, + + /// + /// Trace Id for network browse traces + /// + NetworkSection = 11, + + /// + /// Trace Id for main dialog traces + /// + UiInfra = 12, + + /// + /// Trace Id for hostory page traces + /// + HistoryPage = 13, + + /// + /// TraceId for Telemetry + /// + Telemetry = 14, + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/Traceable.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/Traceable.cs new file mode 100644 index 00000000..0a62c983 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/Traceable.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Extensibility; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// Enables tracing feature for classes + /// + internal class Traceable : TraceableBase, IComposableService + { + private IMultiServiceProvider _serviceProvider; + private ITrace _trace; + + public Traceable() + { + } + + public Traceable(ITrace trace) + { + _trace = trace; + } + + public override ITrace Trace + { + get + { + if (_trace == null) + { + if (_serviceProvider != null) + { + _trace = _serviceProvider.GetService(); + } + } + return _trace; + } + set + { + _trace = value; + } + } + + public void SetServiceProvider(IMultiServiceProvider provider) + { + _serviceProvider = provider; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/TraceableBase.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/TraceableBase.cs new file mode 100644 index 00000000..8eb9046d --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Extensibility/TraceableBase.cs @@ -0,0 +1,222 @@ +// +// 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.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Microsoft.SqlTools.ResourceProvider.Core.Extensibility +{ + /// + /// An abstract class to be used for classes that need to have trace feature + /// + public abstract class TraceableBase + { + /// + /// Returns the first implementation of trace in the catalog that has highest priority + /// + public abstract ITrace Trace + { + get; + set; + } + + /// + /// Write a trace event message to the underlying trace source. + /// + public bool TraceEvent(TraceEventType eventType, TraceId traceId, string format, params object[] args) + { + return TraceEvent(eventType, (int)traceId, format, args); + } + + /// + /// Write a trace event message to the underlying trace source. + /// + public bool TraceEvent(TraceEventType eventType, int traceId, string format, params object[] args) + { + return SafeTrace(eventType, traceId, format, args); + } + + /// + /// Write a formatted trace event message to the underlying trace source and issue a Debug.Fail() call + /// if condition is false. + /// + public bool AssertTraceEvent(bool condition, TraceEventType eventType, TraceId traceId, string message) + { + return AssertTraceEvent(condition, eventType, (int)traceId, message); + } + + /// + /// Write a formatted trace event message to the underlying trace source and issue a Debug.Fail() call + /// if condition is false. + /// + public bool AssertTraceEvent(bool condition, TraceEventType eventType, int traceId, string message) + { + if (!condition) + { + return DebugTraceEvent(eventType, traceId, message); + } + return false; + } + + /// + /// Write a trace event with a message and exception details to the underlying trace source and issue a + /// Debug.Fail() call if the condition is false. + /// + /// + /// Note: often the fact that an exception has been thrown by itself is enough to determine that the message + /// should be logged. If so please use DebugTraceException() instead. This method is for if the exception should + /// only be logged if some additional condition is also false. + /// + /// Must be false for Debug or Trace event to be issued. + /// Event type that specifies the verbosity level of the trace. + /// The category of the caller's product feature. + /// The exception to be logged. + /// Message to be traced along with the event. + /// True if event was successfully written or the condition was true + public bool AssertTraceException(bool condition, TraceEventType eventType, TraceId traceId, Exception exception, string message) + { + return AssertTraceException(condition, eventType, (int) traceId, exception, message); + } + + /// + /// Write a trace event with a message and exception details to the underlying trace source and issue a + /// Debug.Fail() call if the condition is false. + /// + /// + /// Note: often the fact that an exception has been thrown by itself is enough to determine that the message + /// should be logged. If so please use DebugTraceException() instead. This method is for if the exception should + /// only be logged if some additional condition is also false. + /// + /// Must be false for Debug or Trace event to be issued. + /// Event type that specifies the verbosity level of the trace. + /// The category of the caller's product feature. + /// The exception to be logged. + /// Message to be traced along with the event. + /// True if event was successfully written or the condition was true + public bool AssertTraceException2(bool condition, TraceEventType eventType, TraceId traceId, Exception exception, string message, + [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "", + [CallerMemberName] string memberName = "") + { + if (!condition) + { + return DebugTraceException2(eventType, (int) traceId, exception, message, lineNumber, fileName, memberName); + } + return true; + } + + /// + /// Write a trace event with a message and exception details to the underlying trace source and issue a + /// Debug.Fail() call if the condition is false. + /// + /// + /// Note: often the fact that an exception has been thrown by itself is enough to determine that the message + /// should be logged. If so please use DebugTraceException() instead. This method is for if the exception should + /// only be logged if some additional condition is also false. + /// + /// Must be false for Debug or Trace event to be issued. + /// Event type that specifies the verbosity level of the trace. + /// The category of the caller's product feature. + /// The exception to be logged. + /// Message to be traced along with the event. + /// True if event was successfully written or the condition was true + public bool AssertTraceException(bool condition, TraceEventType eventType, int traceId, Exception exception, string message) + { + if (!condition) + { + return DebugTraceException(eventType, traceId, exception, message); + } + return true; + } + + /// + /// Write a trace event with a message and exception details to the underlying trace source and issue a + /// Debug.Fail() call with the same message. + /// + /// Event type that specifies the verbosity level of the trace. + /// The category of the caller's product feature. + /// The exception to be logged. + /// Message to be traced along with the event. + /// True if event was successfully written + public bool DebugTraceException(TraceEventType eventType, int traceId, Exception exception, string message) + { + // Avoiding breaking change by not overloading this method. Passing default values to TraceException so this isn't + // reported as the callsite for majority of exceptions + bool success = TraceException(eventType, traceId, exception, message, 0, string.Empty, string.Empty); + Debug.Fail(message); + return success; + } + + public bool DebugTraceException2(TraceEventType eventType, int traceId, Exception exception, string message, + [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "", + [CallerMemberName] string memberName = "") + { + bool success = TraceException(eventType, traceId, exception, message, lineNumber, fileName, memberName); + Debug.Fail(message); + return success; + } + + /// + /// Write a trace event with a message and exception details to the underlying trace source. + /// + public bool TraceException(TraceEventType eventType, TraceId traceId, Exception exception, string message, + [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "", + [CallerMemberName] string memberName = "") + { + return TraceException(eventType, (int)traceId, exception, message, lineNumber, fileName, memberName); + } + + /// + /// Write a trace event with a message and exception details to the underlying trace source. + /// + public bool TraceException(TraceEventType eventType, int traceId, Exception exception, string message, + [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "", + [CallerMemberName] string memberName = "") + { + return SafeTraceException(eventType, traceId, exception, message, lineNumber, fileName, memberName); + } + + /// + /// Write a formatted trace event message to the underlying trace source and issue a Debug.Fail() with + /// the same message. + /// + /// Event type that specifies the verbosity level of the trace. + /// The category of the caller's product feature. + /// Message to be output via Debug.Fail() and traced along with the event. + /// True if event was successfully written + public bool DebugTraceEvent(TraceEventType eventType, int traceId, string message) + { + bool success = TraceEvent(eventType, traceId, message); + Debug.Fail(message); + return success; + } + + /// + /// Verifies ITrace instance is not null before tracing + /// + private bool SafeTrace(TraceEventType eventType, int traceId, string format, params object[] args) + { + if (Trace != null) + { + return Trace.TraceEvent(eventType, traceId, format, args); + } + return false; + } + + /// + /// Verifies ITrace instance is not null before tracing the exception + /// + private bool SafeTraceException(TraceEventType eventType, int traceId, Exception exception, string message, + [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "", + [CallerMemberName] string memberName = "") + { + if (Trace != null) + { + return Trace.TraceException(eventType, traceId, exception, message, lineNumber, fileName, memberName); + } + return false; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallErrorParser.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallErrorParser.cs new file mode 100644 index 00000000..44af4eae --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallErrorParser.cs @@ -0,0 +1,102 @@ +// +// 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.Data.SqlClient; +using System.Net; +using System.Text.RegularExpressions; + +namespace Microsoft.SqlTools.ResourceProvider.Core.FirewallRule +{ + internal interface IFirewallErrorParser + { + /// + /// Parses given error message and error code to see if it's firewall rule error + /// and finds the blocked ip address + /// + FirewallParserResponse ParseErrorMessage(string errorMessage, int errorCode); + + /// + /// Parses given error message and error code to see if it's firewall rule error + /// and finds the blocked ip address + /// + FirewallParserResponse ParseException(SqlException sqlException); + } + + /// + /// Parses an error to check for firewall rule error. Will include the blocked ip address if firewall rule error is detected + /// + public class FirewallErrorParser : IFirewallErrorParser + { + /// + /// Parses given error message and error code to see if it's firewall rule error + /// and finds the blocked ip address + /// + public FirewallParserResponse ParseException(SqlException sqlException) + { + CommonUtil.CheckForNull(sqlException, "sqlException"); + return ParseErrorMessage(sqlException.Message, sqlException.Number); + } + + /// + /// Parses given error message and error code to see if it's firewall rule error + /// and finds the blocked ip address + /// + public FirewallParserResponse ParseErrorMessage(string errorMessage, int errorCode) + { + CommonUtil.CheckForNull(errorMessage, "errorMessage"); + + FirewallParserResponse response = new FirewallParserResponse(); + if (IsSqlAzureFirewallBlocked(errorCode)) + { + // Connection failed due to blocked client IP + IPAddress clientIp; + if (TryParseClientIp(errorMessage, out clientIp)) + { + response = new FirewallParserResponse(true, clientIp); + } + } + return response; + } + + /// + /// Parses the given message to find the blocked ip address + /// + private static bool TryParseClientIp(string message, out IPAddress clientIp) + { + clientIp = null; + try + { + Regex regex = + new Regex( + @"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b", + RegexOptions.IgnoreCase); + Match match = regex.Match(message); + + if (match.Success) + { + string clientIpValue = match.Value; + return IPAddress.TryParse(clientIpValue, out clientIp); + } + + return false; + } + catch (Exception) + { + //TODO: trace? + return false; + } + } + + /// + /// Returns true if given error code is firewall rule blocked error code + /// + private bool IsSqlAzureFirewallBlocked(int errorCode) + { + return errorCode == SqlAzureFirewallBlockedErrorNumber; + } + + private const int SqlAzureFirewallBlockedErrorNumber = 40615; // http://msdn.microsoft.com/en-us/library/windowsazure/ff394106.aspx + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallParserResponse.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallParserResponse.cs new file mode 100644 index 00000000..724f7f39 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallParserResponse.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Net; + +namespace Microsoft.SqlTools.ResourceProvider.Core.FirewallRule +{ + /// + /// The response that's created by firewall rule parser + /// + public class FirewallParserResponse + { + public FirewallParserResponse(bool firewallRuleErrorDetected, IPAddress blockedIpAddress) + { + FirewallRuleErrorDetected = firewallRuleErrorDetected; + BlockedIpAddress = blockedIpAddress; + } + + public FirewallParserResponse() + { + FirewallRuleErrorDetected = false; + } + + /// + /// Returns true if firewall rule is detected, otherwise returns false. + /// + public bool FirewallRuleErrorDetected + { + get; private set; + } + + /// + /// Returns the blocked ip address if firewall rule is detected + /// + public IPAddress BlockedIpAddress + { + get; private set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleException.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleException.cs new file mode 100644 index 00000000..772854b5 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleException.cs @@ -0,0 +1,79 @@ +// +// 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.Net; +using System.Runtime.Serialization; + +namespace Microsoft.SqlTools.ResourceProvider.Core.FirewallRule +{ + /// + /// Exception used by firewall service to indicate when firewall rule operation fails + /// + public class FirewallRuleException : ServiceExceptionBase + { + /// + /// Initializes a new instance of the AuthenticationFailedException class. + /// + public FirewallRuleException() + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public FirewallRuleException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// The Http error code. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public FirewallRuleException(string message, HttpStatusCode httpStatusCode, Exception innerException = null) + : base(message, httpStatusCode, innerException) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// The Http error code. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public FirewallRuleException(string message, int httpStatusCode, Exception innerException = null) + : base(message, httpStatusCode, innerException) + { + + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public FirewallRuleException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + public FirewallRuleException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleRequest.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleRequest.cs new file mode 100644 index 00000000..f0a46325 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleRequest.cs @@ -0,0 +1,40 @@ +// +// 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.Globalization; +using System.Net; + +namespace Microsoft.SqlTools.ResourceProvider.Core.FirewallRule +{ + /// + /// Includes all the information needed to create a firewall rule + /// + public class FirewallRuleRequest + { + /// + /// Start IP address + /// + public IPAddress StartIpAddress { get; set; } + + /// + /// End IP address + /// + public IPAddress EndIpAddress { get; set; } + + /// + /// Firewall rule name + /// + public string FirewallRuleName + { + get + { + DateTime now = DateTime.UtcNow; + + return string.Format(CultureInfo.InvariantCulture, "ClientIPAddress_{0}", + now.ToString("yyyy-MM-dd_hh:mm:ss", CultureInfo.CurrentCulture)); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleResource.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleResource.cs new file mode 100644 index 00000000..6681385e --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleResource.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.Core.FirewallRule +{ + /// + /// Includes azure resource and subscription needed to create firewall rule + /// + public class FirewallRuleResource + { + /// + /// Azure resource + /// + public IAzureSqlServerResource AzureResource { get; set; } + + /// + /// Azure Subscription + /// + public IAzureUserAccountSubscriptionContext SubscriptionContext { get; set; } + + + /// + /// Returns true if the resource and subscription are not null + /// + public bool IsValid + { + get + { + return AzureResource != null && SubscriptionContext != null; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleResponse.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleResponse.cs new file mode 100644 index 00000000..c90a2461 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleResponse.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ResourceProvider.Core.FirewallRule +{ + /// + /// The response that's created when the firewall rule creation request is complete + /// + public class FirewallRuleResponse + { + /// + /// End IP address + /// + public string EndIpAddress + { + get; + set; + } + + /// + /// Start IP address + /// + public string StartIpAddress + { + get; + set; + } + + /// + /// Indicates whether the firewall rule created successfully or not + /// + public bool Created { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleService.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleService.cs new file mode 100644 index 00000000..9acf18a6 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/FirewallRule/FirewallRuleService.cs @@ -0,0 +1,257 @@ +// +// 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.Globalization; +using System.Net; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.Core.FirewallRule +{ + + public interface IFirewallRuleService + { + /// + /// Creates firewall rule for given server name and IP address range. Throws exception if operation fails + /// + Task CreateFirewallRuleAsync(string serverName, string startIpAddressValue, string endIpAddressValue); + + + /// + /// Creates firewall rule for given server name and IP address range. Throws exception if operation fails + /// + Task CreateFirewallRuleAsync(string serverName, IPAddress startIpAddress, IPAddress endIpAddress); + + + /// + /// Sets and gets azure resource manager instance. can be used by unit tests + /// + IAzureResourceManager ResourceManager + { + get; set; + } + + /// + /// Sets and gets authentication manager instance. can be used by unit tests + /// + IAzureAuthenticationManager AuthenticationManager + { + get; set; + } + } + + /// + /// Service to be used by the controls to create firewall rule + /// + public class FirewallRuleService : IFirewallRuleService + { + /// + /// Creates firewall rule for given server name and IP address range. Throws exception if operation fails + /// + public async Task CreateFirewallRuleAsync(string serverName, string startIpAddressValue, string endIpAddressValue) + { + IPAddress startIpAddress = ConvertToIpAddress(startIpAddressValue); + IPAddress endIpAddress = ConvertToIpAddress(endIpAddressValue); + return await CreateFirewallRuleAsync(serverName, startIpAddress, endIpAddress); + } + + /// + /// Creates firewall rule for given server name and IP address range. Throws exception if operation fails + /// + public async Task CreateFirewallRuleAsync(string serverName, IPAddress startIpAddress, IPAddress endIpAddress) + { + try + { + FirewallRuleResponse firewallRuleResponse = new FirewallRuleResponse() { Created = false }; + CommonUtil.CheckStringForNullOrEmpty(serverName, "serverName"); + CommonUtil.CheckForNull(startIpAddress, "startIpAddress"); + CommonUtil.CheckForNull(endIpAddress, "endIpAddress"); + + IAzureAuthenticationManager authenticationManager = AuthenticationManager; + + if (authenticationManager != null && !await authenticationManager.GetUserNeedsReauthenticationAsync()) + { + FirewallRuleResource firewallRuleResource = await FindAzureResourceAsync(serverName); + firewallRuleResponse = await CreateFirewallRule(firewallRuleResource, startIpAddress, endIpAddress); + } + if (firewallRuleResponse == null || !firewallRuleResponse.Created) + { + throw new FirewallRuleException(SR.FirewallRuleCreationFailed); + } + return firewallRuleResponse; + } + catch (ServiceExceptionBase) + { + throw; + } + catch (Exception ex) + { + throw new FirewallRuleException(SR.FirewallRuleCreationFailed, ex); + } + } + + /// + /// Sets and gets azure resource manager instance. can be used by unit tests + /// + public IAzureResourceManager ResourceManager { get; set; } + + /// + /// Sets and gets authentication manager instance. can be used by unit tests + /// + public IAzureAuthenticationManager AuthenticationManager { get; set; } + + /// + /// Creates firewall rule for given subscription and IP address range + /// + private async Task CreateFirewallRule(FirewallRuleResource firewallRuleResource, IPAddress startIpAddress, IPAddress endIpAddress) + { + CommonUtil.CheckForNull(firewallRuleResource, "firewallRuleResource"); + + try + { + if (firewallRuleResource.IsValid) + { + + FirewallRuleRequest request = new FirewallRuleRequest() + { + StartIpAddress = startIpAddress, + EndIpAddress = endIpAddress + }; + using (IAzureResourceManagementSession session = await ResourceManager.CreateSessionAsync(firewallRuleResource.SubscriptionContext)) + { + return await ResourceManager.CreateFirewallRuleAsync( + session, + firewallRuleResource.AzureResource, + request); + } + } + } + catch (ServiceExceptionBase) + { + throw; + } + catch (Exception ex) + { + throw new FirewallRuleException(SR.FirewallRuleCreationFailed, ex); + } + + return new FirewallRuleResponse() + { + Created = false + }; + } + + /// + /// Finds an azure resource for given server name under user's subscriptions + /// + private async Task FindAzureResourceAsync(string serverName) + { + try + { + IEnumerable subscriptions = await AuthenticationManager.GetSubscriptionsAsync(); + + if (subscriptions == null) + { + throw new FirewallRuleException(SR.FirewallRuleCreationFailed); + } + + foreach (IAzureUserAccountSubscriptionContext subscription in subscriptions) + { + using (IAzureResourceManagementSession session = await ResourceManager.CreateSessionAsync(subscription)) + { + IAzureSqlServerResource azureSqlServer = await FindAzureResourceForSubscriptionAsync(serverName, session); + + if (azureSqlServer != null) + { + return new FirewallRuleResource() + { + SubscriptionContext = subscription, + AzureResource = azureSqlServer + }; + } + } + } + var currentUser = await AuthenticationManager.GetCurrentAccountAsync(); + + throw new FirewallRuleException(string.Format(CultureInfo.CurrentCulture, SR.AzureServerNotFound, serverName, currentUser != null ? currentUser.UniqueId : string.Empty)); + } + catch (ServiceExceptionBase) + { + throw; + } + catch (Exception ex) + { + throw new FirewallRuleException(SR.FirewallRuleCreationFailed, ex); + } + } + + /// + /// Throws a firewallRule exception based on give status code + /// + private void HandleError(ServiceExceptionBase exception, string serverName, + IAzureUserAccountSubscriptionContext subscription) + { + var accountName = subscription != null && subscription.UserAccount != null && + subscription.UserAccount.DisplayInfo != null + ? subscription.UserAccount.DisplayInfo.UserName + : string.Empty; + + switch (exception.HttpStatusCode) + { + case HttpStatusCode.NotFound: + throw new FirewallRuleException(string.Format(CultureInfo.CurrentCulture, SR.AzureServerNotFound, serverName, accountName), exception); + case HttpStatusCode.Forbidden: + throw new FirewallRuleException(string.Format(CultureInfo.CurrentCulture, SR.FirewallRuleAccessForbidden, accountName), exception); + default: + throw new FirewallRuleException(SR.FirewallRuleCreationFailed, exception.HttpStatusCode, exception); + } + } + + /// + /// Finds Azure resource for the given subscription and server name + /// + private async Task FindAzureResourceForSubscriptionAsync( + string serverName, + IAzureResourceManagementSession session) + { + try + { + IEnumerable resources = await ResourceManager.GetSqlServerAzureResourcesAsync(session); + + if (resources == null) + { + return null; + } + foreach (IAzureSqlServerResource resource in resources) + { + if (serverName.Equals(resource.FullyQualifiedDomainName, StringComparison.OrdinalIgnoreCase)) + { + return resource; + } + } + } + catch (ServiceExceptionBase ex) + { + HandleError(ex, serverName, session.SubscriptionContext); + } + catch (Exception ex) + { + throw new FirewallRuleException(SR.FirewallRuleCreationFailed, ex); + } + return null; + } + + private IPAddress ConvertToIpAddress(string ipAddressValue) + { + IPAddress ipAddress; + if (!IPAddress.TryParse(ipAddressValue, out ipAddress)) + { + throw new FirewallRuleException(SR.InvalidIpAddress); + } + return ipAddress; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.cs new file mode 100644 index 00000000..1533eed7 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.cs @@ -0,0 +1,150 @@ +// WARNING: +// This file was generated by the Microsoft DataWarehouse String Resource Tool 1.37.0.0 +// from information in sr.strings +// DO NOT MODIFY THIS FILE'S CONTENTS, THEY WILL BE OVERWRITTEN +// +namespace Microsoft.SqlTools.ResourceProvider.Core +{ + using System; + using System.Reflection; + using System.Resources; + using System.Globalization; + + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SR + { + protected SR() + { } + + public static CultureInfo Culture + { + get + { + return Keys.Culture; + } + set + { + Keys.Culture = value; + } + } + + + public static string AzureServerNotFound + { + get + { + return Keys.GetString(Keys.AzureServerNotFound); + } + } + + public static string AzureSubscriptionFailedErrorMessage + { + get + { + return Keys.GetString(Keys.AzureSubscriptionFailedErrorMessage); + } + } + + public static string DatabaseDiscoveryFailedErrorMessage + { + get + { + return Keys.GetString(Keys.DatabaseDiscoveryFailedErrorMessage); + } + } + + public static string FirewallRuleAccessForbidden + { + get + { + return Keys.GetString(Keys.FirewallRuleAccessForbidden); + } + } + + public static string FirewallRuleCreationFailed + { + get + { + return Keys.GetString(Keys.FirewallRuleCreationFailed); + } + } + + public static string InvalidIpAddress + { + get + { + return Keys.GetString(Keys.InvalidIpAddress); + } + } + + public static string InvalidServerTypeErrorMessage + { + get + { + return Keys.GetString(Keys.InvalidServerTypeErrorMessage); + } + } + + public static string LoadingExportableFailedGeneralErrorMessage + { + get + { + return Keys.GetString(Keys.LoadingExportableFailedGeneralErrorMessage); + } + } + + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Keys + { + static ResourceManager resourceManager = new ResourceManager("Microsoft.SqlTools.ResourceProvider.Core.Localization.SR", typeof(SR).GetTypeInfo().Assembly); + + static CultureInfo _culture = null; + + + public const string AzureServerNotFound = "AzureServerNotFound"; + + + public const string AzureSubscriptionFailedErrorMessage = "AzureSubscriptionFailedErrorMessage"; + + + public const string DatabaseDiscoveryFailedErrorMessage = "DatabaseDiscoveryFailedErrorMessage"; + + + public const string FirewallRuleAccessForbidden = "FirewallRuleAccessForbidden"; + + + public const string FirewallRuleCreationFailed = "FirewallRuleCreationFailed"; + + + public const string InvalidIpAddress = "InvalidIpAddress"; + + + public const string InvalidServerTypeErrorMessage = "InvalidServerTypeErrorMessage"; + + + public const string LoadingExportableFailedGeneralErrorMessage = "LoadingExportableFailedGeneralErrorMessage"; + + + private Keys() + { } + + public static CultureInfo Culture + { + get + { + return _culture; + } + set + { + _culture = value; + } + } + + public static string GetString(string key) + { + return resourceManager.GetString(key, _culture); + } + + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.resx b/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.resx new file mode 100644 index 00000000..865b7923 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.resx @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The server you specified {0} does not exist in any subscription in {1}. Either you have signed in with an incorrect account or your server was removed from subscription(s) in this account. Please check your account and try again. + + + + An error occurred while getting Azure subscriptions + + + + An error occurred while getting databases from servers of type {0} from {1} + + + + {0} does not have permission to change the server firewall rule. Try again with a different account that is an Owner or Contributor of the Azure subscription or the server. + + + + An error occurred while creating a new firewall rule. + + + + Invalid IP address + + + + Server Type is invalid. + + + + A required dll cannot be loaded. Please repair your application. + + + diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.strings b/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.strings new file mode 100644 index 00000000..ba1bed62 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.strings @@ -0,0 +1,32 @@ +# String resource file +# +# When processed by the String Resource Tool, this file generates +# both a .CS and a .RESX file with the same name as the file. +# The .CS file contains a class which can be used to access these +# string resources, including the ability to format in +# parameters, which are identified with the .NET {x} format +# (see String.Format help). +# +# Comments below assume the file name is SR.strings. +# +# Lines starting with a semicolon ";" are also treated as comments, but +# in a future version they will be extracted and made available in LocStudio +# Put your comments to localizers _before_ the string they apply to. +# +# SMO build specific comment +# after generating the .resx file, run srgen on it and get the .resx file +# please remember to also check that .resx in, along with the +# .strings and .cs files + +[strings] + +############################################################################ +# Azure Core DLL +AzureServerNotFound = The server you specified {0} does not exist in any subscription in {1}. Either you have signed in with an incorrect account or your server was removed from subscription(s) in this account. Please check your account and try again. +AzureSubscriptionFailedErrorMessage = An error occurred while getting Azure subscriptions +DatabaseDiscoveryFailedErrorMessage = An error occurred while getting databases from servers of type {0} from {1} +FirewallRuleAccessForbidden = {0} does not have permission to change the server firewall rule. Try again with a different account that is an Owner or Contributor of the Azure subscription or the server. +FirewallRuleCreationFailed = An error occurred while creating a new firewall rule. +InvalidIpAddress = Invalid IP address +InvalidServerTypeErrorMessage = Server Type is invalid. +LoadingExportableFailedGeneralErrorMessage = A required dll cannot be loaded. Please repair your application. diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.xlf b/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.xlf new file mode 100644 index 00000000..3ab7cbb5 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Localization/sr.xlf @@ -0,0 +1,47 @@ + + + + + + The server you specified {0} does not exist in any subscription in {1}. Either you have signed in with an incorrect account or your server was removed from subscription(s) in this account. Please check your account and try again. + The server you specified {0} does not exist in any subscription in {1}. Either you have signed in with an incorrect account or your server was removed from subscription(s) in this account. Please check your account and try again. + + + + An error occurred while getting Azure subscriptions + An error occurred while getting Azure subscriptions + + + + An error occurred while getting databases from servers of type {0} from {1} + An error occurred while getting databases from servers of type {0} from {1} + + + + {0} does not have permission to change the server firewall rule. Try again with a different account that is an Owner or Contributor of the Azure subscription or the server. + {0} does not have permission to change the server firewall rule. Try again with a different account that is an Owner or Contributor of the Azure subscription or the server. + + + + Invalid IP address + Invalid IP address + + + + Server Type is invalid. + Server Type is invalid. + + + + A required dll cannot be loaded. Please repair your application. + A required dll cannot be loaded. Please repair your application. + + + + An error occurred while creating a new firewall rule. + An error occurred while creating a new firewall rule. + + + + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Microsoft.SqlTools.ResourceProvider.Core.csproj b/src/Microsoft.SqlTools.ResourceProvider.Core/Microsoft.SqlTools.ResourceProvider.Core.csproj new file mode 100644 index 00000000..b7b58a82 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Microsoft.SqlTools.ResourceProvider.Core.csproj @@ -0,0 +1,26 @@ + + + netstandard2.0 + Microsoft.SqlTools.ResourceProvider.Core + Microsoft.SqlTools.ResourceProvider.Core + false + + Library + + Provides Resource Provider and control plane services for SqlTools applications. + � Microsoft Corporation. All rights reserved. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider.Core/Properties/AssemblyInfo.cs b/src/Microsoft.SqlTools.ResourceProvider.Core/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..03deec20 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: InternalsVisibleTo("Microsoft.SqlTools.ServiceLayer.UnitTests")] +[assembly: InternalsVisibleTo("Microsoft.SqlTools.ServiceLayer.IntegrationTests")] +[assembly: InternalsVisibleTo("Microsoft.SqlTools.ServiceLayer.Test.Common")] + +// Allowing internals visible access to Moq library to help testing +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceFailedException.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceFailedException.cs new file mode 100644 index 00000000..0a4882ba --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceFailedException.cs @@ -0,0 +1,81 @@ +// +// 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.Net; +using System.Runtime.Serialization; +using Microsoft.SqlTools.ResourceProvider.Core; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Exception to be raised if azure resource manager operation fails + /// + public class AzureResourceFailedException : ServiceExceptionBase + { + /// + /// Initializes a new instance of the AuthenticationFailedException class. + /// + public AzureResourceFailedException() + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public AzureResourceFailedException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// The Http error code. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public AzureResourceFailedException(string message, HttpStatusCode httpStatusCode, Exception innerException = null) + : base(message, httpStatusCode, innerException) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// The Http error code. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public AzureResourceFailedException(string message, int httpStatusCode, Exception innerException = null) + : base(message, httpStatusCode, innerException) + { + + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference + /// (Nothing in Visual Basic) if no inner exception is specified + public AzureResourceFailedException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the AuthenticationFailedException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + public AzureResourceFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceManagementSession.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceManagementSession.cs new file mode 100644 index 00000000..4d5a30c0 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceManagementSession.cs @@ -0,0 +1,93 @@ +// +// 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 Microsoft.Azure.Management.ResourceManager; +using Microsoft.Azure.Management.Sql; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// VS session used by . + /// Includes all the clients that the resource management needs to get ther resources + /// + public class AzureResourceManagementSession : IAzureResourceManagementSession + { + /// + /// Creates the new session for given clients + /// + /// Sql Management Client + /// Resource Management Client + /// Subscription Context + public AzureResourceManagementSession(SqlManagementClient sqlManagementClient, + ResourceManagementClient resourceManagementClient, + IAzureUserAccountSubscriptionContext subscriptionContext) + { + SqlManagementClient = sqlManagementClient; + ResourceManagementClient = resourceManagementClient; + SubscriptionContext = subscriptionContext; + } + + /// + /// Disposes the session + /// + public void Dispose() + { + CloseSession(); + } + + /// + /// Closes the session by disposing the clients + /// + /// + public bool CloseSession() + { + try + { + if (ResourceManagementClient != null) + { + ResourceManagementClient.Dispose(); + } + + if (SqlManagementClient != null) + { + SqlManagementClient.Dispose(); + } + return true; + } + catch (Exception) + { + //TODO: trace + return false; + } + } + + /// + /// Subscription Context + /// + public IAzureUserAccountSubscriptionContext SubscriptionContext + { + get; + set; + } + + /// + /// Resource Management Client + /// + public ResourceManagementClient ResourceManagementClient + { + get; set; + } + + /// + /// Sql Management Client + /// + public SqlManagementClient SqlManagementClient + { + get; set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceManager.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceManager.cs new file mode 100644 index 00000000..af7dbb0d --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceManager.cs @@ -0,0 +1,258 @@ +// +// 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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Management.ResourceManager; +using Microsoft.Azure.Management.ResourceManager.Models; +using Microsoft.Azure.Management.Sql; +using Microsoft.Azure.Management.Sql.Models; +using RestFirewallRule = Microsoft.Azure.Management.Sql.Models.FirewallRule; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.FirewallRule; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; +using Microsoft.SqlTools.Utility; +using Microsoft.Rest; +using System.Globalization; +using Microsoft.Rest.Azure; +using Microsoft.SqlTools.ResourceProvider.Core; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Default implementation for + /// Provides functionality to get azure resources by making Http request to the Azure REST API + /// + [Exportable( + ServerTypes.SqlServer, + Categories.Azure, + typeof(IAzureResourceManager), + "Microsoft.SqlServer.ConnectionServices.Azure.Impl.VsAzureResourceManager", + 1) + ] + public class AzureResourceManager : ExportableBase, IAzureResourceManager + { + private readonly Uri _resourceManagementUri = new Uri("https://management.azure.com/"); + + public AzureResourceManager() + { + // Duplicate the exportable attribute as at present we do not support filtering using extensiondescriptor. + // The attribute is preserved in order to simplify ability to backport into existing tools + Metadata = new ExportableMetadata( + ServerTypes.SqlServer, + Categories.Azure, + "Microsoft.SqlServer.ConnectionServices.Azure.Impl.VsAzureResourceManager"); + } + + public async Task CreateSessionAsync(IAzureUserAccountSubscriptionContext subscriptionContext) + { + CommonUtil.CheckForNull(subscriptionContext, "subscriptionContext"); + try + { + ServiceClientCredentials credentials = await CreateCredentialsAsync(subscriptionContext); + SqlManagementClient sqlManagementClient = new SqlManagementClient(_resourceManagementUri, credentials); + ResourceManagementClient resourceManagementClient = new ResourceManagementClient(_resourceManagementUri, credentials); + return new AzureResourceManagementSession(sqlManagementClient, resourceManagementClient, subscriptionContext); + } + catch (Exception ex) + { + Logger.Write(LogLevel.Error, string.Format(CultureInfo.CurrentCulture, "Failed to get databases {0}", ex)); + throw; + } + } + + /// + /// Returns a list of azure databases given subscription resource group name and server name + /// + /// Subscription Context which includes credentials to use in the resource manager + /// Resource Group Name + /// Server name + /// The list of databases + public async Task> GetAzureDatabasesAsync( + IAzureResourceManagementSession azureResourceManagementSession, + string resourceGroupName, + string serverName) + { + CommonUtil.CheckForNull(azureResourceManagementSession, "azureResourceManagerSession"); + try + { + AzureResourceManagementSession vsAzureResourceManagementSession = azureResourceManagementSession as AzureResourceManagementSession; + + if (vsAzureResourceManagementSession != null) + { + try + { + IEnumerable databaseListResponse = await vsAzureResourceManagementSession.SqlManagementClient.Databases.ListByServerAsync(resourceGroupName, serverName); + return databaseListResponse.Select( + x => new AzureResourceWrapper(x) { ResourceGroupName = resourceGroupName }); + } + catch(HttpOperationException ex) + { + throw new AzureResourceFailedException(SR.FailedToGetAzureDatabasesErrorMessage, ex.Response.StatusCode); + } + } + } + catch (Exception ex) + { + Logger.Write(LogLevel.Error, string.Format(CultureInfo.CurrentCulture, "Failed to get databases {0}", ex.Message)); + throw; + } + + return null; + } + + /// + /// Returns a list of azure servers given subscription + /// + /// Subscription Context which includes credentials to use in the resource manager + /// The list of Sql server resources + public async Task> GetSqlServerAzureResourcesAsync( + IAzureResourceManagementSession azureResourceManagementSession) + { + CommonUtil.CheckForNull(azureResourceManagementSession, "azureResourceManagerSession"); + List sqlServers = new List(); + try + { + AzureResourceManagementSession vsAzureResourceManagementSession = azureResourceManagementSession as AzureResourceManagementSession; + if(vsAzureResourceManagementSession != null) + { + IEnumerable resourceGroupNames = await GetResourceGroupsAsync(vsAzureResourceManagementSession); + if (resourceGroupNames != null) + { + foreach (ResourceGroup resourceGroupExtended in resourceGroupNames) + { + try + { + IServersOperations serverOperations = vsAzureResourceManagementSession.SqlManagementClient.Servers; + IPage servers = await serverOperations.ListByResourceGroupAsync(resourceGroupExtended.Name); + if (servers != null) + { + sqlServers.AddRange(servers.Select(x => + new SqlAzureResource(x) { ResourceGroupName = resourceGroupExtended.Name })); + } + } + catch (HttpOperationException ex) + { + throw new AzureResourceFailedException(SR.FailedToGetAzureSqlServersErrorMessage, ex.Response.StatusCode); + } + } + } + } + } + catch(Exception ex) + { + TraceException(TraceEventType.Error, (int) TraceId.AzureResource, ex, "Failed to get servers"); + throw; + } + + return sqlServers; + } + + public async Task CreateFirewallRuleAsync( + IAzureResourceManagementSession azureResourceManagementSession, + IAzureSqlServerResource azureSqlServer, + FirewallRuleRequest firewallRuleRequest) + { + CommonUtil.CheckForNull(azureResourceManagementSession, "azureResourceManagerSession"); + CommonUtil.CheckForNull(firewallRuleRequest, "firewallRuleRequest"); + CommonUtil.CheckForNull(azureSqlServer, "azureSqlServer"); + + try + { + AzureResourceManagementSession vsAzureResourceManagementSession = azureResourceManagementSession as AzureResourceManagementSession; + + if (vsAzureResourceManagementSession != null) + { + try + { + var firewallRule = new RestFirewallRule() + { + EndIpAddress = firewallRuleRequest.EndIpAddress.ToString(), + StartIpAddress = firewallRuleRequest.StartIpAddress.ToString() + }; + IFirewallRulesOperations firewallRuleOperations = vsAzureResourceManagementSession.SqlManagementClient.FirewallRules; + var firewallRuleResponse = await firewallRuleOperations.CreateOrUpdateAsync( + azureSqlServer.ResourceGroupName, + azureSqlServer.Name, + firewallRuleRequest.FirewallRuleName, + firewallRule); + return new FirewallRuleResponse() + { + StartIpAddress = firewallRuleResponse.StartIpAddress, + EndIpAddress = firewallRuleResponse.EndIpAddress, + Created = true + }; + } + catch (HttpOperationException ex) + { + throw new AzureResourceFailedException(SR.FirewallRuleCreationFailed, ex.Response.StatusCode); + } + } + // else respond with failure case + return new FirewallRuleResponse() + { + Created = false + }; + } + catch (Exception ex) + { + TraceException(TraceEventType.Error, (int) TraceId.AzureResource, ex, "Failed to get databases"); + throw; + } + } + + /// + /// Returns the azure resource groups for given subscription + /// + private async Task> GetResourceGroupsAsync(AzureResourceManagementSession vsAzureResourceManagementSession) + { + try + { + if (vsAzureResourceManagementSession != null) + { + try + { + IResourceGroupsOperations resourceGroupOperations = vsAzureResourceManagementSession.ResourceManagementClient.ResourceGroups; + IPage resourceGroupList = await resourceGroupOperations.ListAsync(); + if (resourceGroupList != null) + { + return resourceGroupList.AsEnumerable(); + } + + } + catch (HttpOperationException ex) + { + throw new AzureResourceFailedException(SR.FailedToGetAzureResourceGroupsErrorMessage, ex.Response.StatusCode); + } + } + + return Enumerable.Empty(); + } + catch (Exception ex) + { + TraceException(TraceEventType.Error, (int)TraceId.AzureResource, ex, "Failed to get azure resource groups"); + throw; + } + } + + + /// + /// Creates credential instance for given subscription + /// + private Task CreateCredentialsAsync(IAzureUserAccountSubscriptionContext subscriptionContext) + { + AzureUserAccountSubscriptionContext azureUserSubContext = + subscriptionContext as AzureUserAccountSubscriptionContext; + + if (azureUserSubContext != null) + { + return Task.FromResult(azureUserSubContext.Credentials); + } + throw new NotSupportedException("This uses an unknown subscription type"); + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceWrapper.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceWrapper.cs new file mode 100644 index 00000000..6b0f5bdb --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureResourceWrapper.cs @@ -0,0 +1,103 @@ +// +// 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 Microsoft.Azure.Management.Sql.Models; +using Microsoft.SqlTools.ResourceProvider.Core; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Implementation for using VS services. + /// Provides information about an Azure resource + /// + public class AzureResourceWrapper : IAzureResource + { + /// + /// Initializes the resource + /// + public AzureResourceWrapper(TrackedResource azureResource) + { + CommonUtil.CheckForNull(azureResource, nameof(azureResource)); + AzureResource = azureResource; + } + + /// + /// Resource name + /// + public string Name + { + get + { + return AzureResource != null ? AzureResource.Name : string.Empty; + } + set + { + throw new NotSupportedException(); + } + } + + /// + /// Resource type + /// + public string Type + { + get + { + return AzureResource != null ? AzureResource.Type : string.Empty; + } + set + { + throw new NotSupportedException(); + } + } + + /// + /// Resource id + /// + public string Id + { + get + { + return AzureResource != null ? AzureResource.Id : string.Empty; + } + set + { + throw new NotSupportedException(); + } + } + + /// + /// Resource Group Name + /// + public string ResourceGroupName { get; set; } + + /// + /// Resource Location + /// + public string Location + { + get + { + return AzureResource != null ? AzureResource.Location : string.Empty; + } + set + { + if (AzureResource != null) + { + AzureResource.Location = value; + } + } + } + + /// + /// The resource wrapped by this class + /// + protected TrackedResource AzureResource + { + get; + set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureSubscriptionContext.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureSubscriptionContext.cs new file mode 100644 index 00000000..d2a9dc00 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureSubscriptionContext.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Implementation for using VS services + /// Contains information about an Azure subscription + /// + public class AzureSubscriptionContext : IAzureSubscriptionContext + { + private readonly IAzureSubscriptionIdentifier _azureSubscriptionIdentifier; + + /// + /// Default constructor to initialize the subscription + /// + public AzureSubscriptionContext(IAzureSubscriptionIdentifier azureSubscriptionIdentifier) + { + _azureSubscriptionIdentifier = azureSubscriptionIdentifier; + } + + /// + /// Returns true if given subscription equals this class + /// + public bool Equals(IAzureSubscriptionContext other) + { + return (other == null && Subscription == null) || (other != null && other.Subscription.Equals(Subscription)); + } + + /// + /// Returns the wraper for the subscription identifier + /// + public IAzureSubscriptionIdentifier Subscription + { + get + { + return _azureSubscriptionIdentifier; + } + } + + /// + /// Returns subscription name + /// + public string SubscriptionName + { + get + { + return _azureSubscriptionIdentifier != null ? + _azureSubscriptionIdentifier.SubscriptionId : string.Empty; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureSubscriptionIdentifier.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureSubscriptionIdentifier.cs new file mode 100644 index 00000000..52b2b949 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureSubscriptionIdentifier.cs @@ -0,0 +1,61 @@ +// +// 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 Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Implementation for + /// Contains information about an Azure subscription identifier + /// + public class AzureSubscriptionIdentifier : IAzureSubscriptionIdentifier + { + /// + /// Default constructor to initialize the subscription identifier + /// + public AzureSubscriptionIdentifier(IAzureUserAccount userAccount, string subscriptionId, Uri serviceManagementEndpoint) + { + UserAccount = userAccount; + SubscriptionId = subscriptionId; + ServiceManagementEndpoint = serviceManagementEndpoint; + } + + /// + /// Returns true if given subscription identifier equals this class + /// + public bool Equals(IAzureSubscriptionIdentifier other) + { + return other != null && + CommonUtil.SameString(SubscriptionId, other.SubscriptionId) && + CommonUtil.SameUri(ServiceManagementEndpoint, other.ServiceManagementEndpoint); + } + + public IAzureUserAccount UserAccount + { + get; + private set; + } + + /// + /// Returns the endpoint url used by the identifier + /// + public Uri ServiceManagementEndpoint + { + get; + private set; + } + + /// + /// Subscription id + /// + public string SubscriptionId + { + get; + private set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccount.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccount.cs new file mode 100644 index 00000000..075dd9ca --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccount.cs @@ -0,0 +1,93 @@ +// +// 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 Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Implementation for using VS services + /// Contains information about an Azure account + /// + public class AzureUserAccount : IAzureUserAccount + { + private string uniqueId; + + /// + /// Default constructor to initializes user session + /// + public AzureUserAccount() + { + } + + /// + /// Default constructor to initializes user session + /// + public AzureUserAccount(IAzureUserAccount azureUserAccount) + { + CopyFrom(azureUserAccount); + } + + private void CopyFrom(IAzureUserAccount azureUserAccount) + { + this.DisplayInfo = new AzureUserAccountDisplayInfo(azureUserAccount.DisplayInfo); + this.NeedsReauthentication = azureUserAccount.NeedsReauthentication; + this.TenantId = azureUserAccount.TenantId; + this.UniqueId = azureUserAccount.UniqueId; + } + /// + /// Returns true if given user account equals this class + /// + public bool Equals(IAzureUserAccount other) + { + return other != null && + CommonUtil.SameString(other.UniqueId, UniqueId) && + CommonUtil.SameString(other.TenantId, TenantId); + } + + /// + /// Unique Id + /// + public string UniqueId + { + get + { + return uniqueId == null ? string.Empty : uniqueId; + } + set + { + this.uniqueId = value; + } + } + + /// + /// Returns true if user needs reauthentication + /// + public bool NeedsReauthentication + { + get; + set; + } + + /// + /// User display info + /// + public IAzureUserAccountDisplayInfo DisplayInfo + { + get; + set; + } + + /// + /// Tenant Id + /// + public string TenantId + { + get; + set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccountDisplayInfo.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccountDisplayInfo.cs new file mode 100644 index 00000000..ac4d40e2 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccountDisplayInfo.cs @@ -0,0 +1,110 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Implementation for using VS services + /// Contains information about an Azure account display info + /// + public class AzureUserAccountDisplayInfo : IAzureUserAccountDisplayInfo + { + private string userName; + private string accountDisplayName; + + /// + /// Creating the instance using + /// + public AzureUserAccountDisplayInfo(IAzureUserAccountDisplayInfo azureUserAccountDisplayInfo) + { + CopyFrom(azureUserAccountDisplayInfo); + } + + /// + /// Creating empty instance + /// + public AzureUserAccountDisplayInfo() + { + } + + private void CopyFrom(IAzureUserAccountDisplayInfo azureUserAccountDisplayInfo) + { + this.AccountDisplayName = azureUserAccountDisplayInfo.AccountDisplayName; + this.AccountLogo = azureUserAccountDisplayInfo.AccountLogo; + this.ProviderDisplayName = azureUserAccountDisplayInfo.ProviderDisplayName; + this.ProviderLogo = azureUserAccountDisplayInfo.ProviderLogo; + this.UserName = azureUserAccountDisplayInfo.UserName; + } + + /// + /// Returns true if given user account equals this class + /// + public bool Equals(IAzureUserAccountDisplayInfo other) + { + return other != null && + ((other.AccountDisplayName == null && AccountDisplayName == null ) || (other.AccountDisplayName != null && other.AccountDisplayName.Equals(AccountDisplayName))) && + ((other.UserName == null && UserName == null ) || (other.UserName != null && other.UserName.Equals(UserName))); + } + + /// + /// Account display name + /// + public string AccountDisplayName + { + get + { + return accountDisplayName != null ? accountDisplayName : string.Empty; + } + set + { + accountDisplayName = value; + } + } + + /// + /// Account lego + /// + public byte[] AccountLogo + { + get; + set; + } + + /// + /// Provider display name + /// + public string ProviderDisplayName + { + get; + set; + } + + /// + /// Provider lego + /// + public byte[] ProviderLogo + { + get; + set; + } + + /// + /// User name + /// + public string UserName + { + get + { + return userName != null ? userName : string.Empty; + } + set + { + userName = value; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccountSubscriptionContext.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccountSubscriptionContext.cs new file mode 100644 index 00000000..6c03c946 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/AzureUserAccountSubscriptionContext.cs @@ -0,0 +1,92 @@ +// +// 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 Microsoft.Rest; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Implementation for using built-in services + /// Contains information about an Azure account subscription + /// + public class AzureUserAccountSubscriptionContext : IAzureUserAccountSubscriptionContext + { + + /// + /// Default constructor to initializes user account and subscription + /// + public AzureUserAccountSubscriptionContext(AzureSubscriptionIdentifier subscription, ServiceClientCredentials credentials) + { + CommonUtil.CheckForNull(subscription, nameof(subscription)); + CommonUtil.CheckForNull(credentials, nameof(credentials)); + Subscription = subscription; + Credentials = credentials; + } + + /// + /// Creates a subscription context for connecting with a known access token. This creates a object for use + /// in a request + /// + public static AzureUserAccountSubscriptionContext CreateStringTokenContext(AzureSubscriptionIdentifier subscription, string accessToken) + { + CommonUtil.CheckForNull(subscription, nameof(subscription)); + CommonUtil.CheckStringForNullOrEmpty(accessToken, nameof(accessToken)); + TokenCredentials credentials = new TokenCredentials(accessToken); + return new AzureUserAccountSubscriptionContext(subscription, credentials); + } + + public bool Equals(IAzureSubscriptionContext other) + { + return other != null && other.Equals(this); + } + + /// + /// Returns the wraper for the subscription identifier + /// + public IAzureSubscriptionIdentifier Subscription + { + get; + private set; + } + + /// + /// Subscription name + /// + public string SubscriptionName + { + get { return Subscription != null ? Subscription.SubscriptionId : string.Empty; } + } + + /// + /// + /// + public bool Equals(IAzureUserAccountSubscriptionContext other) + { + return other != null && + CommonUtil.SameSubscriptionIdentifier(Subscription, other.Subscription) && + CommonUtil.SameUserAccount(UserAccount, other.UserAccount); + } + + /// + /// User Account + /// + public IAzureUserAccount UserAccount + { + get + { + return Subscription != null ? + new AzureUserAccount(Subscription.UserAccount) : null; + } + } + + public ServiceClientCredentials Credentials + { + get; + private set; + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.cs new file mode 100644 index 00000000..b052d9e7 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.cs @@ -0,0 +1,106 @@ +// WARNING: +// This file was generated by the Microsoft DataWarehouse String Resource Tool 1.37.0.0 +// from information in sr.strings +// DO NOT MODIFY THIS FILE'S CONTENTS, THEY WILL BE OVERWRITTEN +// +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + using System; + using System.Reflection; + using System.Resources; + using System.Globalization; + + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SR + { + protected SR() + { } + + public static CultureInfo Culture + { + get + { + return Keys.Culture; + } + set + { + Keys.Culture = value; + } + } + + + public static string FailedToGetAzureDatabasesErrorMessage + { + get + { + return Keys.GetString(Keys.FailedToGetAzureDatabasesErrorMessage); + } + } + + public static string FailedToGetAzureResourceGroupsErrorMessage + { + get + { + return Keys.GetString(Keys.FailedToGetAzureResourceGroupsErrorMessage); + } + } + + public static string FailedToGetAzureSqlServersErrorMessage + { + get + { + return Keys.GetString(Keys.FailedToGetAzureSqlServersErrorMessage); + } + } + + public static string FirewallRuleCreationFailed + { + get + { + return Keys.GetString(Keys.FirewallRuleCreationFailed); + } + } + + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Keys + { + static ResourceManager resourceManager = new ResourceManager("Microsoft.SqlTools.ResourceProvider.DefaultImpl.Localization.SR", typeof(SR).GetTypeInfo().Assembly); + + static CultureInfo _culture = null; + + + public const string FailedToGetAzureDatabasesErrorMessage = "FailedToGetAzureDatabasesErrorMessage"; + + + public const string FailedToGetAzureResourceGroupsErrorMessage = "FailedToGetAzureResourceGroupsErrorMessage"; + + + public const string FailedToGetAzureSqlServersErrorMessage = "FailedToGetAzureSqlServersErrorMessage"; + + + public const string FirewallRuleCreationFailed = "FirewallRuleCreationFailed"; + + + private Keys() + { } + + public static CultureInfo Culture + { + get + { + return _culture; + } + set + { + _culture = value; + } + } + + public static string GetString(string key) + { + return resourceManager.GetString(key, _culture); + } + + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.resx b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.resx new file mode 100644 index 00000000..d44a779c --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An error occurred while getting Azure databases + + + + An error occurred while getting Azure resource groups + + + + An error occurred while getting Azure Sql Servers + + + + An error occurred while creating a new firewall rule. + + + diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.strings b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.strings new file mode 100644 index 00000000..3e87c835 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.strings @@ -0,0 +1,28 @@ +# String resource file +# +# When processed by the String Resource Tool, this file generates +# both a .CS and a .RESX file with the same name as the file. +# The .CS file contains a class which can be used to access these +# string resources, including the ability to format in +# parameters, which are identified with the .NET {x} format +# (see String.Format help). +# +# Comments below assume the file name is SR.strings. +# +# Lines starting with a semicolon ";" are also treated as comments, but +# in a future version they will be extracted and made available in LocStudio +# Put your comments to localizers _before_ the string they apply to. +# +# SMO build specific comment +# after generating the .resx file, run srgen on it and get the .resx file +# please remember to also check that .resx in, along with the +# .strings and .cs files + +[strings] + +############################################################################ +# Azure Core DLL +FailedToGetAzureDatabasesErrorMessage = An error occurred while getting Azure databases +FailedToGetAzureResourceGroupsErrorMessage = An error occurred while getting Azure resource groups +FailedToGetAzureSqlServersErrorMessage = An error occurred while getting Azure Sql Servers +FirewallRuleCreationFailed = An error occurred while creating a new firewall rule. \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.xlf b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.xlf new file mode 100644 index 00000000..0c54fb77 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Localization/sr.xlf @@ -0,0 +1,27 @@ + + + + + + An error occurred while getting Azure databases + An error occurred while getting Azure databases + + + + An error occurred while getting Azure resource groups + An error occurred while getting Azure resource groups + + + + An error occurred while getting Azure Sql Servers + An error occurred while getting Azure Sql Servers + + + + An error occurred while creating a new firewall rule. + An error occurred while creating a new firewall rule. + + + + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj new file mode 100644 index 00000000..0c4a8845 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj @@ -0,0 +1,31 @@ + + + netstandard2.0 + Microsoft.SqlTools.ResourceProvider.DefaultImpl + Microsoft.SqlTools.ResourceProvider.DefaultImpl + false + + Library + + Provides the default for SqlTools applications. + � Microsoft Corporation. All rights reserved. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/SqlAzureResource.cs b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/SqlAzureResource.cs new file mode 100644 index 00000000..47929de1 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider.DefaultImpl/SqlAzureResource.cs @@ -0,0 +1,50 @@ +// +// 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 Microsoft.SqlTools.ResourceProvider.Core; +using Models = Microsoft.Azure.Management.Sql.Models; + +namespace Microsoft.SqlTools.ResourceProvider.DefaultImpl +{ + /// + /// Implementation for using VS services + /// Provides information about an Azure Sql Server resource + /// + public class SqlAzureResource : AzureResourceWrapper, IAzureSqlServerResource + { + private readonly Models.Server _azureSqlServerResource; + + /// + /// Initializes the resource + /// + public SqlAzureResource(Models.Server azureResource) : base(azureResource) + { + CommonUtil.CheckForNull(azureResource, nameof(azureResource)); + _azureSqlServerResource = azureResource; + } + + /// + /// Fully qualified domain name + /// + public string FullyQualifiedDomainName + { + get + { + return _azureSqlServerResource != null ? _azureSqlServerResource.FullyQualifiedDomainName : string.Empty; + } + } + + /// + /// Administrator User + /// + public string AdministratorLogin + { + get + { + return _azureSqlServerResource != null ? _azureSqlServerResource.AdministratorLogin : string.Empty; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.cs b/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.cs new file mode 100644 index 00000000..8db98dd4 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.cs @@ -0,0 +1,62 @@ +// WARNING: +// This file was generated by the Microsoft DataWarehouse String Resource Tool 1.37.0.0 +// from information in sr.strings +// DO NOT MODIFY THIS FILE'S CONTENTS, THEY WILL BE OVERWRITTEN +// +namespace Microsoft.SqlTools.ResourceProvider +{ + using System; + using System.Reflection; + using System.Resources; + using System.Globalization; + + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SR + { + protected SR() + { } + + public static CultureInfo Culture + { + get + { + return Keys.Culture; + } + set + { + Keys.Culture = value; + } + } + + + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Keys + { + static ResourceManager resourceManager = new ResourceManager("Microsoft.SqlTools.ResourceProvider.Localization.SR", typeof(SR).GetTypeInfo().Assembly); + + static CultureInfo _culture = null; + + + private Keys() + { } + + public static CultureInfo Culture + { + get + { + return _culture; + } + set + { + _culture = value; + } + } + + public static string GetString(string key) + { + return resourceManager.GetString(key, _culture); + } + + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.resx b/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.resx new file mode 100644 index 00000000..09ac8ab9 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.strings b/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.strings new file mode 100644 index 00000000..aca2b7d0 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.strings @@ -0,0 +1,24 @@ +# String resource file +# +# When processed by the String Resource Tool, this file generates +# both a .CS and a .RESX file with the same name as the file. +# The .CS file contains a class which can be used to access these +# string resources, including the ability to format in +# parameters, which are identified with the .NET {x} format +# (see String.Format help). +# +# Comments below assume the file name is SR.strings. +# +# Lines starting with a semicolon ";" are also treated as comments, but +# in a future version they will be extracted and made available in LocStudio +# Put your comments to localizers _before_ the string they apply to. +# +# SMO build specific comment +# after generating the .resx file, run srgen on it and get the .resx file +# please remember to also check that .resx in, along with the +# .strings and .cs files + +[strings] + +############################################################################ +# Azure Service DLL \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.xlf b/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.xlf new file mode 100644 index 00000000..c5319993 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider/Localization/sr.xlf @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider/Microsoft.SqlTools.ResourceProvider.csproj b/src/Microsoft.SqlTools.ResourceProvider/Microsoft.SqlTools.ResourceProvider.csproj new file mode 100644 index 00000000..d1e9e31e --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider/Microsoft.SqlTools.ResourceProvider.csproj @@ -0,0 +1,28 @@ + + + netcoreapp2.0 + SqlToolsResourceProviderService + SqlToolsResourceProviderService + Exe + Microsoft + Sql Tools Service for Resource Provider services + Provides Resource Provider and control plane support. + � Microsoft Corporation. All rights reserved. + true + true + portable + false + win7-x64;win7-x86;ubuntu.14.04-x64;ubuntu.16.04-x64;centos.7-x64;rhel.7.2-x64;debian.8-x64;fedora.23-x64;opensuse.13.2-x64;osx.10.11-x64 + + + TRACE;DEBUG;NETCOREAPP1_0;NETCOREAPP2_0 + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ResourceProvider/Program.cs b/src/Microsoft.SqlTools.ResourceProvider/Program.cs new file mode 100644 index 00000000..aa2d2d42 --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider/Program.cs @@ -0,0 +1,63 @@ +// +// 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.IO; +using Microsoft.SqlTools.Hosting.Utility; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ResourceProvider +{ + /// + /// Main application class for Credentials Service Host executable + /// + internal class Program + { + private const string ServiceName = "SqlToolsAzure.exe"; + + /// + /// Main entry point into the Credentials Service Host + /// + internal static void Main(string[] args) + { + try + { + // read command-line arguments + CommandOptions commandOptions = new CommandOptions(args, ServiceName); + if (commandOptions.ShouldExit) + { + return; + } + + string logFilePath = "sqltoolsazure"; + if (!string.IsNullOrWhiteSpace(commandOptions.LoggingDirectory)) + { + logFilePath = Path.Combine(commandOptions.LoggingDirectory, logFilePath); + } + + // turn on Verbose logging during early development + // we need to switch to Normal when preparing for public preview + Logger.Initialize(logFilePath: logFilePath, minimumLogLevel: LogLevel.Verbose, isEnabled: commandOptions.EnableLogging); + Logger.Write(LogLevel.Normal, "Starting SqlTools Azure Provider"); + + // set up the host details and profile paths + var hostDetails = new HostDetails( + name: "SqlTools Azure Provider", + profileId: "Microsoft.SqlTools.ResourceProvider", + version: new Version(1, 0)); + + SqlToolsContext sqlToolsContext = new SqlToolsContext(hostDetails); + UtilityServiceHost serviceHost = ResourceProviderHostLoader.CreateAndStartServiceHost(sqlToolsContext); + + serviceHost.WaitForExit(); + } + catch (Exception e) + { + Logger.Write(LogLevel.Error, string.Format("An unhandled exception occurred: {0}", e)); + Environment.Exit(1); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ResourceProvider/ResourceProviderHostLoader.cs b/src/Microsoft.SqlTools.ResourceProvider/ResourceProviderHostLoader.cs new file mode 100644 index 00000000..24fc22ea --- /dev/null +++ b/src/Microsoft.SqlTools.ResourceProvider/ResourceProviderHostLoader.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SqlTools.Extensibility; +using Microsoft.SqlTools.Hosting; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ResourceProvider +{ + /// + /// Provides support for starting up a service host. This is a common responsibility + /// for both the main service program and test driver that interacts with it + /// + public static class ResourceProviderHostLoader + { + private static object lockObject = new object(); + private static bool isLoaded; + + internal static UtilityServiceHost CreateAndStartServiceHost(SqlToolsContext sqlToolsContext) + { + UtilityServiceHost serviceHost = UtilityServiceHost.Instance; + lock (lockObject) + { + if (!isLoaded) + { + // Grab the instance of the service host + serviceHost.Initialize(); + + InitializeRequestHandlersAndServices(serviceHost, sqlToolsContext); + + // Start the service only after all request handlers are setup. This is vital + // as otherwise the Initialize event can be lost - it's processed and discarded before the handler + // is hooked up to receive the message + serviceHost.Start().Wait(); + isLoaded = true; + } + } + return serviceHost; + } + + private static void InitializeRequestHandlersAndServices(UtilityServiceHost serviceHost, SqlToolsContext sqlToolsContext) + { + // Load extension provider, which currently finds all exports in current DLL. Can be changed to find based + // on directory or assembly list quite easily in the future + ExtensionServiceProvider serviceProvider = ExtensionServiceProvider.CreateDefaultServiceProvider(); + serviceProvider.RegisterSingleService(sqlToolsContext); + serviceProvider.RegisterSingleService(serviceHost); + + // CredentialService.Instance.InitializeService(serviceHost); + // serviceProvider.RegisterSingleService(CredentialService.Instance); + + InitializeHostedServices(serviceProvider, serviceHost); + + serviceHost.InitializeRequestHandlers(); + } + + /// + /// Internal to support testing. Initializes instances in the service, + /// and registers them for their preferred service type + /// + internal static void InitializeHostedServices(RegisteredServiceProvider provider, IProtocolEndpoint host) + { + // Pre-register all services before initializing. This ensures that if one service wishes to reference + // another one during initialization, it will be able to safely do so + foreach (IHostedService service in provider.GetServices()) + { + provider.RegisterSingleService(service.ServiceType, service); + } + + foreach (IHostedService service in provider.GetServices()) + { + // Initialize all hosted services, and register them in the service provider for their requested + // service type. This ensures that when searching for the ConnectionService you can get it without + // searching for an IHostedService of type ConnectionService + service.InitializeService(host); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj index 9178e6cb..14e38096 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj +++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj @@ -30,6 +30,7 @@ + diff --git a/test/CodeCoverage/codecoverage.bat b/test/CodeCoverage/codecoverage.bat index c0415805..c44db427 100644 --- a/test/CodeCoverage/codecoverage.bat +++ b/test/CodeCoverage/codecoverage.bat @@ -12,12 +12,18 @@ REM backup current CSPROJ files COPY /Y %REPOROOT%\src\Microsoft.SqlTools.Credentials\Microsoft.SqlTools.Credentials.csproj %REPOROOT%\src\Microsoft.SqlTools.Credentials\Microsoft.SqlTools.Credentials.csproj.BAK COPY /Y %REPOROOT%\src\Microsoft.SqlTools.Hosting\Microsoft.SqlTools.Hosting.csproj %REPOROOT%\src\Microsoft.SqlTools.Hosting\Microsoft.SqlTools.Hosting.csproj.BAK COPY /Y %REPOROOT%\src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.csproj %REPOROOT%\src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.csproj.BAK +COPY /Y %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj.BAK +COPY /Y %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj.BAK +COPY /Y %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj.BAK REM switch PDB type to Full since that is required by OpenCover for now REM we should remove this step on OpenCover supports portable PDB cscript /nologo ReplaceText.vbs %REPOROOT%\src\Microsoft.SqlTools.Credentials\Microsoft.SqlTools.Credentials.csproj portable full cscript /nologo ReplaceText.vbs %REPOROOT%\src\Microsoft.SqlTools.Hosting\Microsoft.SqlTools.Hosting.csproj portable full cscript /nologo ReplaceText.vbs %REPOROOT%\src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.csproj portable full +cscript /nologo ReplaceText.vbs %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj portable full +cscript /nologo ReplaceText.vbs %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj portable full +cscript /nologo ReplaceText.vbs %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj portable full REM rebuild the SqlToolsService project dotnet restore %REPOROOT%\src\Microsoft.SqlTools.Credentials\Microsoft.SqlTools.Credentials.csproj @@ -26,6 +32,12 @@ dotnet restore %REPOROOT%\src\Microsoft.SqlTools.Hosting\Microsoft.SqlTools.Host dotnet build %REPOROOT%\src\Microsoft.SqlTools.Hosting\Microsoft.SqlTools.Hosting.csproj %DOTNETCONFIG% dotnet restore %REPOROOT%\src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.csproj dotnet build %REPOROOT%\src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.csproj %DOTNETCONFIG% -r win7-x64 +dotnet restore %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj +dotnet build %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj %DOTNETCONFIG% -r win7-x64 +dotnet restore %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj +dotnet build %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj %DOTNETCONFIG% -r win7-x64 +dotnet restore %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj +dotnet build %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj %DOTNETCONFIG% -r win7-x64 REM run the tests through OpenCover and generate a report dotnet restore %REPOROOT%\test\Microsoft.SqlTools.ServiceLayer.TestDriver\Microsoft.SqlTools.ServiceLayer.TestDriver.csproj @@ -66,5 +78,11 @@ COPY /Y %REPOROOT%\src\Microsoft.SqlTools.Credentials\Microsoft.SqlTools.Credent DEL %REPOROOT%\src\Microsoft.SqlTools.Credentials\Microsoft.SqlTools.Credentials.csproj.BAK COPY /Y %REPOROOT%\src\Microsoft.SqlTools.Hosting\Microsoft.SqlTools.Hosting.csproj.BAK %REPOROOT%\src\Microsoft.SqlTools.Hosting\Microsoft.SqlTools.Hosting.csproj DEL %REPOROOT%\src\Microsoft.SqlTools.Hosting\Microsoft.SqlTools.Hosting.csproj.BAK +COPY /Y %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj.BAK %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj +DEL %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider\Microsoft.SqlTools.ResourceProvider.csproj.BAK +COPY /Y %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj.BAK %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj +DEL %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.Core\Microsoft.SqlTools.ResourceProvider.Core.csproj.BAK +COPY /Y %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj.BAK %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj +DEL %REPOROOT%\src\Microsoft.SqlTools.ResourceProvider.DefaultImpl\Microsoft.SqlTools.ResourceProvider.DefaultImpl.csproj.BAK EXIT diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj index d7c35110..b20c3156 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj @@ -16,19 +16,21 @@ - - ../../bin/ref/Newtonsoft.Json.dll - - - ../../bin/ref/Moq.dll - - - ../../bin/ref/Castle.Core.dll - - - - - + + ../../bin/ref/Newtonsoft.Json.dll + + + ../../bin/ref/Moq.dll + + + ../../bin/ref/Castle.Core.dll + + + + + + + diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureDatabaseDiscoveryProviderTest.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureDatabaseDiscoveryProviderTest.cs new file mode 100644 index 00000000..3baef9c4 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureDatabaseDiscoveryProviderTest.cs @@ -0,0 +1,204 @@ +// +// 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.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure +{ + /// + /// Tests for AzureDatabaseDiscoveryProvider to verify getting azure databases using azure resource manager + /// + public class AzureDatabaseDiscoveryProviderTest + { + [Fact] + public async Task GetShouldReturnDatabasesSuccessfully() + { + string databaseName1 = "server/db1"; + string databaseName2 = "db2"; + string databaseName3 = "server/"; + string databaseName4 = "/db4"; + List databasesForSubscription = new List() + { + databaseName1, + databaseName2 + }; + + Dictionary> subscriptionToDatabaseMap = new Dictionary>(); + subscriptionToDatabaseMap.Add(Guid.NewGuid().ToString(), databasesForSubscription); + subscriptionToDatabaseMap.Add(Guid.NewGuid().ToString(), new List() + { + databaseName3, + databaseName4, + }); + + AzureDatabaseDiscoveryProvider databaseDiscoveryProvider = FakeDataFactory.CreateAzureDatabaseDiscoveryProvider(subscriptionToDatabaseMap); + ServiceResponse response = await databaseDiscoveryProvider.GetDatabaseInstancesAsync(serverName: null, cancellationToken: new CancellationToken()); + List list = response.Data.ToList(); + Assert.NotNull(list); + Assert.True(list.Any(x => x.Name == "db1" && x.ServerInstanceInfo.Name == "server")); + Assert.False(list.Any(x => x.Name == "db2" && x.ServerInstanceInfo.Name == "")); + Assert.True(list.Any(x => x.Name == "" && x.ServerInstanceInfo.Name == "server")); + Assert.False(list.Any(x => x.Name == "db4" && x.ServerInstanceInfo.Name == "")); + Assert.True(list.Count() == 2); + } + + [Fact] + public async Task GetShouldReturnDatabasesEvenIfFailsForOneServer() + { + string databaseName1 = "server1/db1"; + string databaseName2 = "server1/db2"; + string databaseName3 = "error/db3"; + string databaseName4 = "server2/db4"; + List databasesForSubscription = new List() + { + databaseName1, + databaseName2 + }; + + Dictionary> subscriptionToDatabaseMap = new Dictionary>(); + subscriptionToDatabaseMap.Add(Guid.NewGuid().ToString(), databasesForSubscription); + subscriptionToDatabaseMap.Add(Guid.NewGuid().ToString(), new List() + { + databaseName3, + databaseName4, + }); + + AzureDatabaseDiscoveryProvider databaseDiscoveryProvider = FakeDataFactory.CreateAzureDatabaseDiscoveryProvider(subscriptionToDatabaseMap); + ServiceResponse response = await databaseDiscoveryProvider.GetDatabaseInstancesAsync(serverName: null, cancellationToken: new CancellationToken()); + List list = response.Data.ToList(); + Assert.NotNull(list); + Assert.True(list.Any(x => x.Name == "db1" && x.ServerInstanceInfo.Name == "server1")); + Assert.True(list.Any(x => x.Name == "db2" && x.ServerInstanceInfo.Name == "server1")); + Assert.True(list.Any(x => x.Name == "db4" && x.ServerInstanceInfo.Name == "server2")); + Assert.False(list.Any(x => x.Name == "db3" && x.ServerInstanceInfo.Name == "error")); + Assert.True(list.Count() == 3); + Assert.NotNull(response.Errors); + Assert.True(response.Errors.Count() == 1); + } + + [Fact] + public async Task GetShouldReturnDatabasesFromCacheIfGetCalledTwice() + { + Dictionary> subscriptionToDatabaseMap = CreateSubscriptonMap(2); + AddDatabases(subscriptionToDatabaseMap, 3); + + AzureDatabaseDiscoveryProvider databaseDiscoveryProvider = FakeDataFactory.CreateAzureDatabaseDiscoveryProvider(subscriptionToDatabaseMap); + ServiceResponse response = await databaseDiscoveryProvider.GetDatabaseInstancesAsync(serverName: null, cancellationToken: new CancellationToken()); + List list = response.Data.ToList(); + ValidateResult(subscriptionToDatabaseMap, list); + + Dictionary> subscriptionToDatabaseMap2 = CopySubscriptonMap(subscriptionToDatabaseMap); + AddDatabases(subscriptionToDatabaseMap2, 5); + AzureTestContext testContext = new AzureTestContext(subscriptionToDatabaseMap2); + databaseDiscoveryProvider.AccountManager = testContext.AzureAccountManager; + databaseDiscoveryProvider.AzureResourceManager = testContext.AzureResourceManager; + response = await databaseDiscoveryProvider.GetDatabaseInstancesAsync(serverName: null, cancellationToken: new CancellationToken()); + list = response.Data.ToList(); + //the provider should get the databases from cache for the list should match the first created data + ValidateResult(subscriptionToDatabaseMap, list); + } + + [Fact] + public async Task GetShouldReturnDatabasesFromServiceIfGetCalledTwiceButRefreshed() + { + Dictionary> subscriptionToDatabaseMap = CreateSubscriptonMap(2); + AddDatabases(subscriptionToDatabaseMap, 3); + + AzureDatabaseDiscoveryProvider databaseDiscoveryProvider = FakeDataFactory.CreateAzureDatabaseDiscoveryProvider(subscriptionToDatabaseMap); + ServiceResponse response = await databaseDiscoveryProvider.GetDatabaseInstancesAsync(serverName: null, cancellationToken: new CancellationToken()); + List list = response.Data.ToList(); + ValidateResult(subscriptionToDatabaseMap, list); + + Dictionary> subscriptionToDatabaseMap2 = CopySubscriptonMap(subscriptionToDatabaseMap); + AddDatabases(subscriptionToDatabaseMap2, 5); + AzureTestContext testContext = new AzureTestContext(subscriptionToDatabaseMap2); + databaseDiscoveryProvider.AccountManager = testContext.AzureAccountManager; + databaseDiscoveryProvider.AzureResourceManager = testContext.AzureResourceManager; + await databaseDiscoveryProvider.ClearCacheAsync(); + response = await databaseDiscoveryProvider.GetDatabaseInstancesAsync(serverName: null, cancellationToken: new CancellationToken()); + list = response.Data.ToList(); + //the provider should get the databases from cache for the list should match the first created data + ValidateResult(subscriptionToDatabaseMap2, list); + } + + private void ValidateResult(Dictionary> subscriptionToDatabaseMap, List result) + { + Assert.NotNull(result); + int numberOfDatabases = 0; + foreach (KeyValuePair> item in subscriptionToDatabaseMap) + { + numberOfDatabases += item.Value.Count; + foreach (string databaseFullName in item.Value) + { + string serverName = AzureTestContext.GetServerName(databaseFullName); + string databaseName = databaseFullName.Replace(serverName + @"/", ""); + Assert.True(result.Any(x => x.Name == databaseName && x.ServerInstanceInfo.Name == serverName)); + } + } + Assert.True(result.Count() == numberOfDatabases); + } + + private void AddDatabases(Dictionary> subscriptionToDatabaseMap, int numberOfDatabases) + { + foreach (string key in subscriptionToDatabaseMap.Keys.ToList()) + { + List databases = CreateDatabases(numberOfDatabases); + subscriptionToDatabaseMap[key] = databases; + } + } + + private List CreateDatabases(int numberOfDatabases) + { + List databases = new List(); + for (int j = 0; j < numberOfDatabases; j++) + { + databases.Add(string.Format(CultureInfo.InvariantCulture, @"{0}/{1}", Guid.NewGuid().ToString(), Guid.NewGuid().ToString())); + } + return databases; + } + + private Dictionary> CreateSubscriptonMap(int numberOfSubscriptions) + { + Dictionary> subscriptionToDatabaseMap = new Dictionary>(); + for (int i = 0; i < numberOfSubscriptions; i++) + { + string id = Guid.NewGuid().ToString(); + subscriptionToDatabaseMap.Add(id, null); + } + return subscriptionToDatabaseMap; + } + + private Dictionary> CopySubscriptonMap(Dictionary> subscriptionToDatabaseMap) + { + Dictionary> subscriptionToDatabaseMapCopy = new Dictionary>(); + foreach (KeyValuePair> pair in subscriptionToDatabaseMap) + { + subscriptionToDatabaseMapCopy.Add(pair.Key, null); + } + return subscriptionToDatabaseMapCopy; + } + + [Fact] + public async Task GetShouldReturnEmptyGivenNotSubscriptionFound() + { + Dictionary> subscriptionToDatabaseMap = new Dictionary>(); + + AzureDatabaseDiscoveryProvider databaseDiscoveryProvider = + FakeDataFactory.CreateAzureDatabaseDiscoveryProvider(subscriptionToDatabaseMap); + ServiceResponse response = + await databaseDiscoveryProvider.GetDatabaseInstancesAsync(serverName: null, cancellationToken: new CancellationToken()); + Assert.NotNull(response); + Assert.NotNull(response.Data); + Assert.False(response.Data.Any()); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureSqlServerDiscoveryProviderTest.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureSqlServerDiscoveryProviderTest.cs new file mode 100644 index 00000000..820041b4 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureSqlServerDiscoveryProviderTest.cs @@ -0,0 +1,60 @@ +// +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure +{ + /// + /// Tests for AzureServerDiscoveryProvider to verify getting azure servers using azure resource manager + /// + public class AzureSqlServerDiscoveryProviderTest + { + [Fact] + public async Task GetShouldReturnServersSuccessfully() + { + string serverName = "server"; + List serversForSubscription = new List() + { + Guid.NewGuid().ToString(), + serverName + }; + + Dictionary> subscriptionToDatabaseMap = new Dictionary>(); + subscriptionToDatabaseMap.Add(Guid.NewGuid().ToString(), serversForSubscription); + subscriptionToDatabaseMap.Add(Guid.NewGuid().ToString(), new List() + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + }); + + AzureSqlServerDiscoveryProvider discoveryProvider = + FakeDataFactory.CreateAzureServerDiscoveryProvider(subscriptionToDatabaseMap); + ServiceResponse response = await discoveryProvider.GetServerInstancesAsync(); + IEnumerable servers = response.Data; + Assert.NotNull(servers); + Assert.True(servers.Any(x => x.Name == serverName)); + Assert.True(servers.Count() == 4); + } + + [Fact] + public async Task GetShouldReturnEmptyGivenNotSubscriptionFound() + { + Dictionary> subscriptionToDatabaseMap = new Dictionary>(); + + AzureSqlServerDiscoveryProvider discoveryProvider = + FakeDataFactory.CreateAzureServerDiscoveryProvider(subscriptionToDatabaseMap); + ServiceResponse response = await discoveryProvider.GetServerInstancesAsync(); + IEnumerable servers = response.Data; + Assert.NotNull(servers); + Assert.False(servers.Any()); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureSubscriptionContextTest.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureSubscriptionContextTest.cs new file mode 100644 index 00000000..12dc138d --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureSubscriptionContextTest.cs @@ -0,0 +1,34 @@ +// +// 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 Microsoft.SqlTools.ResourceProvider.DefaultImpl; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure +{ + /// + /// Tests for AzureSubscriptionContextWrapper to verify the wrapper on azure subscription class + /// + public class AzureSubscriptionContextTest + { + [Fact] + public void SubscriptionNameShouldReturnNullGivenNullSubscription() + { + AzureSubscriptionContext subscriptionContext = new AzureSubscriptionContext(null); + Assert.True(subscriptionContext.SubscriptionName == String.Empty); + Assert.True(subscriptionContext.Subscription == null); + } + + [Fact] + public void SubscriptionNameShouldReturnCorrectValueGivenValidSubscription() + { + string name = Guid.NewGuid().ToString(); + + AzureSubscriptionContext subscriptionContext = new AzureSubscriptionContext(new AzureSubscriptionIdentifier(null, name, null)); + Assert.True(subscriptionContext.SubscriptionName == name); + Assert.True(subscriptionContext.Subscription != null); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureTestContext.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureTestContext.cs new file mode 100644 index 00000000..91eea47e --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/AzureTestContext.cs @@ -0,0 +1,121 @@ +// +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Management.Sql.Models; +using Microsoft.Rest; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.DefaultImpl; +using Moq; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure +{ + /// + /// A container to create test data and mock classes to test azure services and providers + /// + internal class AzureTestContext + { + public AzureTestContext(Dictionary> subscriptionToDatabaseMap) + { + AzureAccountManagerMock = new Mock(); + List accountSubscriptions = new List + (); + AzureResourceManagerMock = new Mock(); + + foreach (string subscriptionName in subscriptionToDatabaseMap.Keys) + { + var azureAccount = new AzureUserAccount(); + AzureSubscriptionIdentifier subId = new AzureSubscriptionIdentifier(azureAccount, subscriptionName, null); + var subscription = new AzureUserAccountSubscriptionContext(subId, new TokenCredentials("dummy")); + accountSubscriptions.Add(subscription); + + var sessionMock = new Mock(); + IAzureResourceManagementSession session = sessionMock.Object; + sessionMock.Setup(x => x.SubscriptionContext).Returns(subscription); + AzureResourceManagerMock.Setup(x => x.CreateSessionAsync(subscription)).Returns(Task.FromResult(session)); + MockServersAndDatabases(subscriptionToDatabaseMap[subscriptionName], session); + } + AzureAccountManagerMock.Setup(x => x.GetSelectedSubscriptionsAsync()).Returns + (Task.FromResult(accountSubscriptions as IEnumerable)); + } + + private void MockServersAndDatabases(List resourceNames, IAzureResourceManagementSession session) + { + IEnumerable azureResources = resourceNames.Select( + x => new AzureResourceWrapper(new TrackedResource(Guid.NewGuid().ToString(), "id", x, "type")) { ResourceGroupName = Guid.NewGuid().ToString()} + ).ToList(); + + List servers = new List(); + foreach (var azureResourceWrapper in azureResources.ToList()) + { + var serverName = GetServerName(azureResourceWrapper.Name); + if (string.IsNullOrEmpty(serverName) || servers.Any(x => x.Name == serverName)) + { + continue; + } + + var databases = azureResources.Where(x => x.Name.StartsWith(serverName + "/")); + if (serverName.Equals("error", StringComparison.OrdinalIgnoreCase)) + { + AzureResourceManagerMock.Setup(x => x.GetAzureDatabasesAsync(session, azureResourceWrapper.ResourceGroupName, serverName)) + .Throws(new ApplicationException(serverName)); + } + else + { + AzureResourceManagerMock.Setup(x => x.GetAzureDatabasesAsync(session, azureResourceWrapper.ResourceGroupName, serverName)) + .Returns(Task.FromResult(databases)); + } + + Server azureSqlServer = new Server(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), serverName, null, null, null, null, null, null, null, null, fullyQualifiedDomainName: serverName + ".database.windows.net"); + servers.Add(new SqlAzureResource(azureSqlServer) + { + ResourceGroupName = azureResourceWrapper.ResourceGroupName + }); + } + AzureResourceManagerMock.Setup(x => x.GetSqlServerAzureResourcesAsync(session)) + .Returns(Task.FromResult(servers as IEnumerable)); + } + + internal static string GetServerName(string name) + { + string azureResourceName = name; + int separatorIndex = azureResourceName.IndexOf("/", StringComparison.OrdinalIgnoreCase); + if (separatorIndex >= 0) + { + return azureResourceName.Substring(0, separatorIndex); + } + else + { + return azureResourceName; + } + } + + public Mock AzureAccountManagerMock + { + get; + set; + } + + public IAzureAuthenticationManager AzureAccountManager + { + get { return AzureAccountManagerMock.Object; } + } + + public IAzureResourceManager AzureResourceManager + { + get { return AzureResourceManagerMock.Object; } + } + + public Mock AzureResourceManagerMock + { + get; + set; + } + + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/FakeAzureServerDiscoveryProvider.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/FakeAzureServerDiscoveryProvider.cs new file mode 100644 index 00000000..fce524e0 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Azure/FakeAzureServerDiscoveryProvider.cs @@ -0,0 +1,48 @@ +// +// 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.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure +{ + /// + /// A fake server discovery class + /// + [Exportable(ServerTypes.SqlServer, Categories.Azure + , typeof(IServerDiscoveryProvider), + "Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure.FakeAzureServerDiscoveryProvider", 1)] + public class FakeAzureServerDiscoveryProvider : ExportableBase, IServerDiscoveryProvider, ISecureService + { + public FakeAzureServerDiscoveryProvider() + { + Metadata = new ExportableMetadata(ServerTypes.SqlServer, Categories.Azure, + "Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure.FakeAzureServerDiscoveryProvider", 1); + } + public Task> GetServerInstancesAsync() + { + throw new NotImplementedException(); + } + + public IAccountManager AccountManager + { + get; + set; + } + + /// + /// This should always return null otherwise there's going to be a infinite loop + /// + public IServerDiscoveryProvider ServerDiscoveryProvider + { + get + { + return GetService(); + } + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/DependencyManagerTest.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/DependencyManagerTest.cs new file mode 100644 index 00000000..e7f71304 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/DependencyManagerTest.cs @@ -0,0 +1,295 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// TODO Ideally would reenable these but using ExtensionServiceProvider + +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Threading.Tasks; +//using Microsoft.SqlTools.ResourceProvider.Core; +//using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; +//using Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes; +//using Moq; +//using Xunit; + +//namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider +//{ +// /// +// /// Tests for DependencyManager to verify the services and providers can be created given different types of catalogs +// /// +// public class DependencyManagerTest +// { +// private ExtensionProperties _serviceProperties; +// private DependencyManager _dependencyManager; +// private IList> _providers; + +// private readonly List _localSqlServers = new List() +// { +// new ServerInstanceInfo(), +// new ServerInstanceInfo(), +// }; + +// public DependencyManagerTest() +// { +// var provider1 = new Mock(); +// var provider2 = new Mock(); +// provider1.Setup(x => x.GetServerInstancesAsync()).Returns(Task.FromResult(new ServiceResponse(_localSqlServers.AsEnumerable()))); +// _providers = new List>() +// { +// new Lazy(() => provider1.Object, +// new ExportableAttribute("SqlServer", "Local", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// new Lazy(() => provider2.Object, +// new ExportableAttribute("SqlServer", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// }; + +// _serviceProperties = FakeDataFactory.CreateServiceProperties(_providers); +// _dependencyManager = new DependencyManager(_serviceProperties); +// } + +// [Fact] +// public void GetShouldReturnProvidersFromTheCatalog() +// { +// IEnumerable> providers = +// _dependencyManager.GetServiceDescriptors(); +// Assert.NotNull(providers); +// } + +// [Fact] +// public void GetShouldReturnEmptyListGivenInvalidCategory() +// { +// Assert.False(_dependencyManager.GetServiceDescriptors(new ServerDefinition(null, "invalid category")).Any()); +// } + +// [Fact] +// public void GetShouldReturnEmptyListGivenInvalidServerType() +// { +// Assert.False(_dependencyManager.GetServiceDescriptors(new ServerDefinition("invalid server type", null)).Any()); +// } + +// [Fact] +// public void GetShouldReturnAllProvidersGivenNoParameter() +// { +// IEnumerable> providers = +// _dependencyManager.GetServiceDescriptors(); +// Assert.NotNull(providers); +// Assert.True(providers.Count() == _providers.Count()); +// } + +// [Fact] +// public void GetShouldReturnProvidersGivenServerType() +// { +// var serverType = "sqlServer"; +// IEnumerable> providers = +// _dependencyManager.GetServiceDescriptors(new ServerDefinition(serverType, null)); +// Assert.NotNull(providers); +// Assert.True(providers.Any()); +// Assert.True(providers.Count() == _providers.Count(x => x.Metadata.ServerType.Equals(serverType, StringComparison.OrdinalIgnoreCase))); +// } + +// [Fact] +// public void GetShouldReturnProvidersGivenCategory() +// { +// IEnumerable> providers = +// _dependencyManager.GetServiceDescriptors(new ServerDefinition(null, "local")); +// Assert.NotNull(providers); +// Assert.True(providers.Count() == 1); +// } + +// [Fact] +// public void GetShouldReturnProviderForEmptyCategoryGivenEmptyCategory() +// { +// // Given choice of 2 providers, one with empty category and other with specified one + +// IServerDiscoveryProvider provider1 = new Mock(); +// IServerDiscoveryProvider provider2 = new Mock(); +// provider1.Setup(x => x.GetServerInstancesAsync()).Returns(Task.FromResult(new ServiceResponse(_localSqlServers.AsEnumerable()))); +// var providers = new List>() +// { +// new Lazy(() => provider1, +// new ExportableAttribute("SqlServer", "Azure", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// new Lazy(() => provider2, +// new ExportableAttribute("SqlServer", "", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// }; + +// var serviceProperties = FakeDataFactory.CreateServiceProperties(providers); +// var dependencyManager = new DependencyManager(serviceProperties); + +// // When getting the correct descriptor + +// IEnumerable> foundProviders = +// dependencyManager.GetServiceDescriptors(new ServerDefinition("SqlServer", "")); + +// // Then expect only the provider with the empty categorty to be returned +// Assert.NotNull(foundProviders); +// Assert.True(foundProviders.Count() == 1); +// } + +// [Fact] +// public void GetShouldReturnProviderGivenServerTypeAndLocationWithValidProvider() +// { +// IEnumerable> providers = +// _dependencyManager.GetServiceDescriptors(new ServerDefinition("SqlServer", "local")); +// Assert.NotNull(providers); +// Assert.True(providers.Count() == 1); +// } + +// [Fact] + +// public void GetShouldReturnTheServiceWithTheHighestPriorityIdMultipleFound() +// { +// IServerDiscoveryProvider expectedProvider = new Mock(); +// List> providers = new List>() +// { +// new Lazy(() => new Mock(), +// new ExportableAttribute("SqlServer", "Local", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// new Lazy(() => new Mock(), +// new ExportableAttribute("SqlServer", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 1)), +// new Lazy(() => expectedProvider, +// new ExportableAttribute("SqlServer", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 2)) +// }; + +// ExtensionProperties serviceProperties = FakeDataFactory.CreateServiceProperties(providers); +// DependencyManager dependencyManager = new DependencyManager(serviceProperties); + +// IEnumerable> descriptors = +// dependencyManager.GetServiceDescriptors(); +// Assert.NotNull(descriptors); + +// ExportableDescriptor descriptor = descriptors.FindMatchedDescriptor(new ServerDefinition("SqlServer", "network")); +// Assert.NotNull(descriptor); +// Assert.True(descriptor.Exportable == expectedProvider); +// } + +// [Fact] +// public void GetShouldReturnTheServiceEvenIfTheServerTypeNotSet() +// { +// IServerDiscoveryProvider expectedProvider = new Mock(); +// List> providers = new List>() +// { +// new Lazy(() => new Mock(), +// new ExportableAttribute("SqlServer", "", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// new Lazy(() => new Mock(), +// new ExportableAttribute("SqlServer", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 1)), +// new Lazy(() => expectedProvider, +// new ExportableAttribute("", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 2)) +// }; + +// ExtensionProperties serviceProperties = FakeDataFactory.CreateServiceProperties(providers); +// DependencyManager dependencyManager = new DependencyManager(serviceProperties); + +// IEnumerable> descriptors = +// dependencyManager.GetServiceDescriptors(); +// Assert.NotNull(descriptors); + +// ExportableDescriptor descriptor = descriptors.FindMatchedDescriptor(new ServerDefinition("", "network")); +// Assert.NotNull(descriptor); +// Assert.True(descriptor.Exportable == expectedProvider); +// } + +// [Fact] +// public void GetShouldReturnTheServiceThatMatchedExactlyIfServerTypeSpecified() +// { +// IServerDiscoveryProvider expectedProvider = new Mock(); +// List> providers = new List>() +// { +// new Lazy(() => new Mock(), +// new ExportableAttribute("SqlServer", "", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// new Lazy(() => expectedProvider, +// new ExportableAttribute("SqlServer", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 1)), +// new Lazy(() => new Mock(), +// new ExportableAttribute("", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 2)) +// }; + +// ExtensionProperties serviceProperties = FakeDataFactory.CreateServiceProperties(providers); +// DependencyManager dependencyManager = new DependencyManager(serviceProperties); + +// IEnumerable> descriptors = +// dependencyManager.GetServiceDescriptors(); +// Assert.NotNull(descriptors); + +// ExportableDescriptor descriptor = descriptors.FindMatchedDescriptor(new ServerDefinition("SqlServer", "network")); +// Assert.NotNull(descriptor); +// Assert.True(descriptor.Exportable == expectedProvider); +// } + +// [Fact] +// public void GetShouldReturnTheServiceThatMatchedExactlyIfCategorySpecified() +// { +// IServerDiscoveryProvider expectedProvider = new Mock(); +// List> providers = new List>() +// { +// new Lazy(() => new Mock(), +// new ExportableAttribute("SqlServer", "", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// new Lazy(() => expectedProvider, +// new ExportableAttribute("SqlServer", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 1)), +// new Lazy(() => new Mock(), +// new ExportableAttribute("", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 2)) +// }; + +// ExtensionProperties serviceProperties = FakeDataFactory.CreateServiceProperties(providers); +// DependencyManager dependencyManager = new DependencyManager(serviceProperties); + +// IEnumerable> descriptors = +// dependencyManager.GetServiceDescriptors(); +// Assert.NotNull(descriptors); + +// ExportableDescriptor descriptor = descriptors.FindMatchedDescriptor(new ServerDefinition("SqlServer", "network")); +// Assert.NotNull(descriptor); +// Assert.True(descriptor.Exportable == expectedProvider); +// } + +// [Fact] + +// public void GetShouldReturnTheServiceEvenIfTheCategoryNotSet() +// { +// IServerDiscoveryProvider expectedProvider = new Mock(); +// List> providers = new List>() +// { +// new Lazy(() => new Mock(), +// new ExportableAttribute("SqlServer", "Local", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString())), +// new Lazy(() => new Mock(), +// new ExportableAttribute("SqlServer", "Network", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 1)), +// new Lazy(() => expectedProvider, +// new ExportableAttribute("SqlServer", "", typeof(IServerDiscoveryProvider), Guid.NewGuid().ToString(), 2)) +// }; + +// ExtensionProperties serviceProperties = FakeDataFactory.CreateServiceProperties(providers); +// DependencyManager dependencyManager = new DependencyManager(serviceProperties); + +// IEnumerable> descriptors = +// dependencyManager.GetServiceDescriptors(); +// Assert.NotNull(descriptors); + +// ExportableDescriptor descriptor = descriptors.FindMatchedDescriptor(new ServerDefinition("SqlServer", "")); +// Assert.NotNull(descriptor); +// Assert.True(descriptor.Exportable == expectedProvider); +// } + +// [Fact] +// public void GetShouldReturnProvidersGivenServerTypeAndMoreThanOneLocation() +// { +// var serverType = "sqlServer"; +// IEnumerable> providers = +// _dependencyManager.GetServiceDescriptors(new ServerDefinition(serverType, null)); +// Assert.NotNull(providers); +// Assert.True(providers.Count() == _providers.Count(x => x.Metadata.ServerType.Equals(serverType, StringComparison.OrdinalIgnoreCase))); +// } + +// [Fact] +// public async Task ProviderCreatedByFactoryShouldReturnServersSuccessfully() +// { +// List expectedServers = _localSqlServers; +// IEnumerable> providers = +// _dependencyManager.GetServiceDescriptors(new ServerDefinition("SqlServer", +// "local")); +// ExportableDescriptor provider = providers.First(); +// Assert.NotNull(provider); +// ServiceResponse result = await provider.Exportable.GetServerInstancesAsync(); +// var servers = result.Data; +// Assert.NotNull(servers); +// Assert.Equal(expectedServers, servers); +// } +// } +//} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/ExceptionUtilTest.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/ExceptionUtilTest.cs new file mode 100644 index 00000000..f0896e31 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/ExceptionUtilTest.cs @@ -0,0 +1,72 @@ +// +// 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.Data.Common; +using Microsoft.SqlTools.ResourceProvider.Core; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider +{ + /// + /// Tests for ExceptionUtil to verify the helper and extension methods + /// + public class ExceptionUtilTest + { + [Fact] + public void IsSqlExceptionShouldReturnFalseGivenNullException() + { + Exception exception = null; + bool expected = false; + bool actual = exception.IsDbException(); + Assert.Equal(expected, actual); + } + + [Fact] + public void IsSqlExceptionShouldReturnFalseGivenNonSqlException() + { + Exception exception = new ApplicationException(); + bool expected = false; + bool actual = exception.IsDbException(); + Assert.Equal(expected, actual); + } + + [Fact] + public void IsSqlExceptionShouldReturnFalseGivenNonSqlExceptionWithInternalException() + { + Exception exception = new ApplicationException("Exception message", new ServiceFailedException()); + bool expected = false; + bool actual = exception.IsDbException(); + Assert.Equal(expected, actual); + } + + [Fact] + public void IsSqlExceptionShouldReturnTrueGivenSqlException() + { + Exception exception = CreateDbException(); + Assert.NotNull(exception); + + bool expected = true; + bool actual = exception.IsDbException(); + Assert.Equal(expected, actual); + } + + [Fact] + public void IsSqlExceptionShouldReturnTrueGivenExceptionWithInnerSqlException() + { + Exception exception = new ApplicationException("", CreateDbException()); + Assert.NotNull(exception); + + bool expected = true; + bool actual = exception.IsDbException(); + Assert.Equal(expected, actual); + } + + private Exception CreateDbException() + { + return new Mock().Object; + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeAccountManager.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeAccountManager.cs new file mode 100644 index 00000000..702033c9 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeAccountManager.cs @@ -0,0 +1,83 @@ +// +// 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.Threading.Tasks; +using Microsoft.SqlTools.Extensibility; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + [Exportable(ServerTypes.SqlServer, Categories.Azure, + typeof(IAccountManager), + "Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes.FakeAccountManager", 1)] + public class FakeAccountManager : IAccountManager + { + public FakeAccountManager(IExportableMetadata metadata) + { + Metadata = metadata; + } + + public ITrace Trace { get; set; } + public Task GetUserNeedsReauthenticationAsync() + { + throw new NotImplementedException(); + } + + public Task AuthenticateAsync() + { + throw new System.NotImplementedException(); + } + + public bool IsCachExpired { get; private set; } + public bool SessionIsCached { get; private set; } + public void ResetSession() + { + throw new System.NotImplementedException(); + } + + public Task AddUserAccountAsync() + { + throw new System.NotImplementedException(); + } + + public Task SetCurrentAccountAsync(object account) + { + throw new System.NotImplementedException(); + } + + public Task SetCurrentAccountFromLoginDialogAsync() + { + throw new NotImplementedException(); + } + + public Task GetCurrentAccountAsync() + { + throw new System.NotImplementedException(); + } + + public bool HasLoginDialog { get; } + + public event EventHandler CurrentAccountChanged; + + public IUserAccount SetCurrentAccount(object account) + { + if (CurrentAccountChanged != null) + { + CurrentAccountChanged(this, null); + } + return null; + } + + public void SetServiceProvider(IMultiServiceProvider provider) + { + throw new NotImplementedException(); + } + + public IExportableMetadata Metadata { get; set; } + public ExportableStatus Status { get; } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeAccountManager2.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeAccountManager2.cs new file mode 100644 index 00000000..e652f984 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeAccountManager2.cs @@ -0,0 +1,81 @@ +// +// 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.Threading.Tasks; +using Microsoft.SqlTools.Extensibility; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + [Exportable("SqlServer", "Network", + typeof(IAccountManager), "Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes.FakeAccountManager2", 2)] + public class FakeAccountManager2 : IAccountManager + { + public FakeAccountManager2(IExportableMetadata metadata) + { + Metadata = metadata; + } + + public ITrace Trace { get; set; } + public Task GetUserNeedsReauthenticationAsync() + { + throw new NotImplementedException(); + } + + public Task AuthenticateAsync() + { + throw new System.NotImplementedException(); + } + + public bool IsCachExpired { get; private set; } + public bool SessionIsCached { get; private set; } + public void ResetSession() + { + throw new System.NotImplementedException(); + } + + public Task AddUserAccountAsync() + { + throw new System.NotImplementedException(); + } + + public Task SetCurrentAccountAsync(object account) + { + throw new System.NotImplementedException(); + } + + public Task SetCurrentAccountFromLoginDialogAsync() + { + throw new NotImplementedException(); + } + + public Task GetCurrentAccountAsync() + { + throw new System.NotImplementedException(); + } + + public bool HasLoginDialog { get; } + + public event EventHandler CurrentAccountChanged; + + public IUserAccount SetCurrentAccount(object account) + { + if (CurrentAccountChanged != null) + { + CurrentAccountChanged(this, null); + } + return null; + } + + public void SetServiceProvider(IMultiServiceProvider provider) + { + throw new NotImplementedException(); + } + + public IExportableMetadata Metadata { get; set; } + public ExportableStatus Status { get; } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDataFactory.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDataFactory.cs new file mode 100644 index 00000000..2c89f579 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDataFactory.cs @@ -0,0 +1,193 @@ +// +// 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.SqlClient; +using System.Diagnostics; +using System.Linq; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; +using Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure; +using Moq; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + internal static class FakeDataFactory + { + //public static ExtensionProperties CreateServiceProperties(IList> exports) + //{ + // FakeExportProvider fakeProvider = new FakeExportProvider(f => ((FakeInstanceExportDefinition)f).Instance); + // foreach (var export in exports) + // { + // var metadata = new Dictionary() + // { + // {"ServerType", export.Metadata.ServerType}, + // {"Category", export.Metadata.Category}, + // {"Id", export.Metadata.Id }, + // {"Priority", export.Metadata.Priority} + // }; + + // var definition = new FakeInstanceExportDefinition(typeof(IServerDiscoveryProvider), export.Value, metadata); + // fakeProvider.AddExportDefinitions(definition); + // } + // var trace = new Mock(); + // trace.Setup(x => x.TraceEvent(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + // .Returns(true); + + // var metadata2 = new Dictionary() + // { + // {"Id", Guid.NewGuid().ToString()}, + // {"Priority", 0} + // }; + // var traceDefinition = new FakeInstanceExportDefinition(typeof(ITrace), trace, metadata2); + // fakeProvider.AddExportDefinitions(traceDefinition); + + // ExtensionProperties serviceProperties = new ExtensionProperties(false); + // serviceProperties.Providers = new ExportProvider[] { fakeProvider }; + // TypeCatalog typeCatalog = new TypeCatalog(typeof(FakeTrace)); + // serviceProperties.AddCatalog(typeCatalog); + // return serviceProperties; + //} + + internal static AzureDatabaseDiscoveryProvider CreateAzureDatabaseDiscoveryProvider( + Dictionary> subscriptionToDatabaseMap) + { + AzureTestContext testContext = new AzureTestContext(subscriptionToDatabaseMap); + + AzureDatabaseDiscoveryProvider databaseDiscoveryProvider = new AzureDatabaseDiscoveryProvider(); + databaseDiscoveryProvider.AccountManager = testContext.AzureAccountManager; + databaseDiscoveryProvider.AzureResourceManager = testContext.AzureResourceManager; + + return databaseDiscoveryProvider; + } + + internal static AzureSqlServerDiscoveryProvider CreateAzureServerDiscoveryProvider(Dictionary> subscriptionToDatabaseMap) + { + AzureTestContext testContext = new AzureTestContext(subscriptionToDatabaseMap); + + AzureSqlServerDiscoveryProvider serverDiscoveryProvider = new AzureSqlServerDiscoveryProvider(); + serverDiscoveryProvider.AccountManager = testContext.AzureAccountManager; + serverDiscoveryProvider.AzureResourceManager = testContext.AzureResourceManager; + + return serverDiscoveryProvider; + } + + //internal static IDependencyManager AddDependencyProvider(T provider, + // ServerDefinition serverDefinition, IDependencyManager existingDependencyManager = null) + // where T : IExportable + //{ + // return AddDependencyProviders(new Dictionary() {{ provider, serverDefinition}}, existingDependencyManager); + //} + + //internal static IDependencyManager AddDependencyProviders(Dictionary providers, IDependencyManager existingDependencyManager = null) + // where T : IExportable + //{ + // IDependencyManager dependencyManager = existingDependencyManager ?? new Mock(); + + // IEnumerable> exportableDescriptors = + // providers.Select(x => new ExportableDescriptorImpl( + // new ExtensionDescriptor( + // new Lazy( + // () => x.Key, + // new ExportableAttribute(x.Value.ServerType, x.Value.Category, + // typeof (T), Guid.NewGuid().ToString()))))); + + // dependencyManager.Setup(x => x.GetServiceDescriptors()).Returns(exportableDescriptors); + + // return dependencyManager; + //} + + internal static ServiceResponse CreateServerInstanceResponse(int numberOfServers, ServerDefinition serverDefinition, Exception exception = null) + { + List servers = new List(); + for (int i = 0; i < numberOfServers; i++) + { + servers.Add(new ServerInstanceInfo(serverDefinition) + { + Name = Guid.NewGuid().ToString(), + FullyQualifiedDomainName = Guid.NewGuid().ToString() + }); + } + ServiceResponse response; + if (exception != null) + { + response = new ServiceResponse(servers, new List { exception }); + } + else + { + response = new ServiceResponse(servers); + } + + return response; + } + + internal static ServiceResponse CreateDatabaseInstanceResponse(int numberOfServers, ServerDefinition serverDefinition = null, + string serverName = "", Exception exception = null) + { + serverDefinition = serverDefinition ?? ServerDefinition.Default; + List databases = new List(); + for (int i = 0; i < numberOfServers; i++) + { + databases.Add(new DatabaseInstanceInfo(serverDefinition, serverName, Guid.NewGuid().ToString())); + } + ServiceResponse response; + if (exception != null) + { + response = new ServiceResponse(databases, new List { exception }); + } + else + { + response = new ServiceResponse(databases); + } + + return response; + } + + //internal static UIConnectionInfo CreateUiConnectionInfo(string baseDbName) + //{ + // SqlConnectionStringBuilder connectionStringBuilder = CreateConnectionStringBuilder(baseDbName); + // return CreateUiConnectionInfo(connectionStringBuilder); + //} + //internal static UIConnectionInfo CreateUiConnectionInfo(SqlConnectionStringBuilder connectionStringBuilder) + //{ + // UIConnectionInfo ci = UIConnectionInfoUtil.GetUIConnectionInfoFromConnectionString(connectionStringBuilder.ConnectionString, (new SqlServerType())); + // ci.PersistPassword = connectionStringBuilder.PersistSecurityInfo; + // return ci; + //} + + + //internal static SqlConnectionStringBuilder CreateConnectionStringBuilder(string baseDbName) + //{ + // return CreateConnectionStringBuilder(baseDbName, InstanceManager.DefaultSql2011); + //} + + + //internal static SqlConnectionStringBuilder CreateConnectionStringBuilder(string baseDbName, InstanceInfo dbInstance) + //{ + // string dbName = ConnectionDialogHelper.CreateTestDatabase(baseDbName, dbInstance); + // string dbConnectionString = dbInstance.BuildConnectionString(dbName); + // SqlConnectionStringBuilder connectionStringBuilder = new SqlConnectionStringBuilder(dbConnectionString); + // connectionStringBuilder.ApplicationName = Guid.NewGuid().ToString(); + // connectionStringBuilder.ConnectTimeout = 123; + // connectionStringBuilder.Encrypt = true; + // connectionStringBuilder.ApplicationIntent = ApplicationIntent.ReadWrite; + // connectionStringBuilder.AsynchronousProcessing = true; + // connectionStringBuilder.MaxPoolSize = 45; + // connectionStringBuilder.MinPoolSize = 3; + // connectionStringBuilder.PacketSize = 600; + // connectionStringBuilder.Pooling = true; + // connectionStringBuilder.TrustServerCertificate = false; + // return connectionStringBuilder; + //} + + //internal static ConnectionInfo CreateConnectionInfo(string baseDbName, IEventsChannel eventsChannel = null) + //{ + // ConnectionInfo connectionInfo = new ConnectionInfo(eventsChannel); + // UIConnectionInfo uiConnectionInfo = CreateUiConnectionInfo(baseDbName); + // connectionInfo.UpdateConnectionInfo(uiConnectionInfo); + // return connectionInfo; + //} + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDatabaseDiscoveryProvider.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDatabaseDiscoveryProvider.cs new file mode 100644 index 00000000..1c696584 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDatabaseDiscoveryProvider.cs @@ -0,0 +1,74 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.SqlTools.Extensibility; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + /// + /// A fake database discovery class which generates db names for 5 seconds or until it gets canceled + /// + public class FakeDatabaseDiscoveryProvider : IDatabaseDiscoveryProvider + { + private TimeSpan _timeout = TimeSpan.FromSeconds(5); + + public IExportableMetadata Metadata { get; set; } + public ExportableStatus Status { get; } + IExportableMetadata IExportable.Metadata { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + ExportableStatus IExportable.Status => throw new NotImplementedException(); + + public Task> GetDatabaseInstancesAsync(string serverName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + //public Task> GetDatabaseInstancesAsync(UIConnectionInfo uiConnectionInfo, CancellationToken cancellationToken) + //{ + // return Task.Factory.StartNew(() => GetDatabaseInstances(uiConnectionInfo, cancellationToken), cancellationToken); + //} + + //private ServiceResponse GetDatabaseInstances(UIConnectionInfo uiConnectionInfo, CancellationToken cancellationToken) + //{ + // List databases = new List(); + // DateTime startTime = DateTime.UtcNow; + // while (!cancellationToken.IsCancellationRequested) + // { + // DateTime now = DateTime.UtcNow; + // if (now.Subtract(startTime).TotalMilliseconds >= _timeout.TotalMilliseconds) + // { + // break; + // } + // databases.Add(new DatabaseInstanceInfo(ServerDefinition.Default, uiConnectionInfo.ServerName, uiConnectionInfo.ServerName + "" + Guid.NewGuid().ToString())); + // } + + // return new ServiceResponse(databases); + //} + + private static void TimerCallback(object state) + { + + } + + private void OnDatabaseFound(DatabaseInstanceInfo databaseInfo) + { + if (DatabaseFound != null) + { + DatabaseFound(this, new DatabaseInfoEventArgs() { Database = databaseInfo }); + } + } + + public void SetServiceProvider(IMultiServiceProvider provider) + { + throw new NotImplementedException(); + } + + public event EventHandler DatabaseFound; + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDatabaseResourceManager.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDatabaseResourceManager.cs new file mode 100644 index 00000000..d4754205 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeDatabaseResourceManager.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + public interface IDatabaseResourceManager + { + } + + [Exportable(FakeServerDiscoveryProvider.ServerTypeValue, FakeServerDiscoveryProvider.CategoryValue + , typeof(IDatabaseResourceManager), "Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes.FakeDatabaseResourceManager")] + public class FakeDatabaseResourceManager : IDatabaseResourceManager + { + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeSecureServerDiscoveryProvider.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeSecureServerDiscoveryProvider.cs new file mode 100644 index 00000000..c8a8043c --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeSecureServerDiscoveryProvider.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + [Exportable("SqlServer", "azure", typeof(IServerDiscoveryProvider), + "Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes.FakeSecureServerDiscoveryProvider")] + public class FakeSecureServerDiscoveryProvider : ExportableBase, IServerDiscoveryProvider, ISecureService + { + public FakeSecureServerDiscoveryProvider(IExportableMetadata metadata) + { + Metadata = metadata; + } + + public async Task> GetServerInstancesAsync() + { + return await Task.Run(() => new ServiceResponse()); + } + + public IDatabaseResourceManager DatabaseResourceManager + { + get; + set; + } + + public IAccountManager AccountManager + { + get + { + return GetService(); + } + set + { + + } + } + + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeServerDiscoveryProvider.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeServerDiscoveryProvider.cs new file mode 100644 index 00000000..abc6fd5c --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeServerDiscoveryProvider.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + [Exportable(ServerTypeValue, CategoryValue + , typeof(IServerDiscoveryProvider), + "Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes.FakeServerDiscoveryProvider")] + public class FakeServerDiscoveryProvider : ExportableBase, IServerDiscoveryProvider + { + public FakeServerDiscoveryProvider(IExportableMetadata metadata) + { + Metadata = metadata; + } + + public async Task> GetServerInstancesAsync() + { + return await Task.Run(() => new ServiceResponse()); + } + + + public IDatabaseResourceManager DatabaseResourceManager + { + get; + set; + } + + public const string ServerTypeValue = "FakeServerType"; + public const string CategoryValue = "FakeCategory"; + } +} + diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeServerDiscoveryProvider2.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeServerDiscoveryProvider2.cs new file mode 100644 index 00000000..a5d9a0e3 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeServerDiscoveryProvider2.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + public class FakeServerDiscoveryProvider2 : ExportableBase, IServerDiscoveryProvider + { + public FakeServerDiscoveryProvider2(IExportableMetadata metadata) + { + Metadata = metadata; + } + + public async Task> GetServerInstancesAsync() + { + return await Task.Run(() => new ServiceResponse()); + } + + + public IDatabaseResourceManager DatabaseResourceManager + { + get; + set; + } + + public const string ServerTypeValue = "FakeServerType"; + public const string CategoryValue = "FakeCategory"; + } +} + diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeTrace.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeTrace.cs new file mode 100644 index 00000000..6ee09707 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/Fakes/FakeTrace.cs @@ -0,0 +1,46 @@ +// +// 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.Diagnostics; +using Microsoft.SqlTools.Extensibility; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes +{ + [Exportable(typeof(ITrace), "Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes.FakeTrace") +] + public class FakeTrace : ITrace + { + private readonly List _traces = new List(); + public bool TraceEvent(TraceEventType eventType, int traceId, string message, params object[] args) + { + _traces.Add(message); + return true; + } + + public bool TraceException(TraceEventType eventType, int traceId, Exception exception, string message, int lineNumber = 0, + string fileName = "", string memberName = "") + { + return true; + } + + public void SetServiceProvider(IMultiServiceProvider provider) + { + throw new NotImplementedException(); + } + + public IEnumerable Traces + { + get + { + return _traces; + } + } + + public IExportableMetadata Metadata { get; set; } + public ExportableStatus Status { get; } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/FirewallErrorParserTest.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/FirewallErrorParserTest.cs new file mode 100644 index 00000000..804d0545 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/FirewallErrorParserTest.cs @@ -0,0 +1,71 @@ +// +// 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 Microsoft.SqlTools.ResourceProvider.Core.FirewallRule; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider +{ + /// + /// Tests to verify FirewallErrorParser + /// + public class FirewallErrorParserTest + { + private const int SqlAzureFirewallBlockedErrorNumber = 40615; + private const int SqlAzureLoginFailedErrorNumber = 18456; + private string _errorMessage = "error Message with 1.2.3.4 as IP address"; + private FirewallErrorParser _firewallErrorParser = new FirewallErrorParser(); + + + [Fact] + public void ParseExceptionShouldThrowExceptionGivenNullErrorMessage() + { + string errorMessage = null; + int errorCode = SqlAzureFirewallBlockedErrorNumber; + + Assert.Throws("errorMessage", () => + { + FirewallParserResponse response = _firewallErrorParser.ParseErrorMessage(errorMessage, errorCode); + Assert.False(response.FirewallRuleErrorDetected); + }); + } + + [Fact] + public void ParseExceptionShouldReturnFireWallRuleNotDetectedGivenDifferentError() + { + int errorCode = 123; + + FirewallParserResponse response = _firewallErrorParser.ParseErrorMessage(_errorMessage, errorCode); + Assert.False(response.FirewallRuleErrorDetected); + } + + [Fact] + public void ParseExceptionShouldReturnFireWallRuleNotDetectedGivenLoginFailedError() + { + int errorCode = SqlAzureLoginFailedErrorNumber; + + FirewallParserResponse response = _firewallErrorParser.ParseErrorMessage(_errorMessage, errorCode); + Assert.False(response.FirewallRuleErrorDetected); + } + + [Fact] + public void ParseExceptionShouldReturnFireWallRuleNotDetectedGivenInvalidErrorMessage() + { + int errorCode = SqlAzureFirewallBlockedErrorNumber; + string errorMessage = "error Message with no IP address"; + FirewallParserResponse response = _firewallErrorParser.ParseErrorMessage(errorMessage, errorCode); + Assert.False(response.FirewallRuleErrorDetected); + } + + [Fact] + public void ParseExceptionShouldReturnFireWallRuleDetectedGivenValidErrorMessage() + { + int errorCode = SqlAzureFirewallBlockedErrorNumber; + FirewallParserResponse response = _firewallErrorParser.ParseErrorMessage(_errorMessage, errorCode); + Assert.True(response.FirewallRuleErrorDetected); + Assert.Equal(response.BlockedIpAddress.ToString(), "1.2.3.4"); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/FirewallRuleServiceTest.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/FirewallRuleServiceTest.cs new file mode 100644 index 00000000..e43f7627 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/FirewallRuleServiceTest.cs @@ -0,0 +1,527 @@ +// +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.FirewallRule; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider +{ + /// + /// Tests to verify FirewallRuleService by mocking the azure authentication and resource managers + /// + public class FirewallRuleServiceTest + { + [Fact] + public async Task CreateShouldThrowExceptionGivenNullServerName() + { + string serverName = null; + + ServiceTestContext testContext = new ServiceTestContext(); + + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, serverName)); + } + + [Fact] + public async Task CreateShouldThrowExceptionGivenNullStartIp() + { + string serverName = "serverName"; + + ServiceTestContext testContext = new ServiceTestContext(); + testContext.StartIpAddress = null; + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, serverName)); + } + + [Fact] + public async Task CreateShouldThrowExceptionGivenInvalidEndIp() + { + string serverName = "serverName"; + + ServiceTestContext testContext = new ServiceTestContext(); + testContext.EndIpAddress = "invalid ip"; + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, serverName)); + } + + [Fact] + public async Task CreateShouldThrowExceptionGivenInvalidStartIp() + { + string serverName = "serverName"; + + ServiceTestContext testContext = new ServiceTestContext(); + testContext.StartIpAddress = "invalid ip"; + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, serverName)); + } + + [Fact] + public async Task CreateShouldThrowExceptionGivenNullEndIp() + { + ServiceTestContext testContext = new ServiceTestContext(); + testContext.EndIpAddress = null; + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, testContext.ServerName)); + } + + [Fact] + public async Task CreateShouldThrowExceptionIfUserIsNotLoggedIn() + { + var applicationAuthenticationManagerMock = new Mock(); + applicationAuthenticationManagerMock.Setup(x => x.GetUserNeedsReauthenticationAsync()).Throws(new ApplicationException()); + var azureResourceManagerMock = new Mock(); + + ServiceTestContext testContext = new ServiceTestContext(); + testContext.ApplicationAuthenticationManagerMock = applicationAuthenticationManagerMock; + testContext.AzureResourceManagerMock = azureResourceManagerMock; + + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, testContext.ServerName)); + azureResourceManagerMock.Verify(x => x.CreateFirewallRuleAsync( + It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task CreateShouldThrowExceptionIfUserDoesNotHaveSubscriptions() + { + var applicationAuthenticationManagerMock = + new Mock(); + applicationAuthenticationManagerMock.Setup(x => x.GetUserNeedsReauthenticationAsync()).Returns(Task.FromResult(false)); + applicationAuthenticationManagerMock.Setup(x => x.GetSubscriptionsAsync()) + .Returns(Task.FromResult(Enumerable.Empty())); + var azureResourceManagerMock = new Mock(); + + ServiceTestContext testContext = new ServiceTestContext(); + testContext.ApplicationAuthenticationManagerMock = applicationAuthenticationManagerMock; + testContext.AzureResourceManagerMock = azureResourceManagerMock; + + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, testContext.ServerName)); + azureResourceManagerMock.Verify(x => x.CreateFirewallRuleAsync( + It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task CreateShouldThrowExceptionIfAuthenticationManagerFailsToReturnSubscription() + { + var applicationAuthenticationManagerMock = new Mock(); + applicationAuthenticationManagerMock.Setup(x => x.GetUserNeedsReauthenticationAsync()).Returns(Task.FromResult(false)); + applicationAuthenticationManagerMock.Setup(x => x.GetSubscriptionsAsync()).Throws(new Exception()); + var azureResourceManagerMock = new Mock(); + + ServiceTestContext testContext = new ServiceTestContext(); + testContext.ApplicationAuthenticationManagerMock = applicationAuthenticationManagerMock; + testContext.AzureResourceManagerMock = azureResourceManagerMock; + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, "invalid server")); + + azureResourceManagerMock.Verify(x => x.CreateFirewallRuleAsync( + It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task CreateShouldThrowExceptionGivenNoSubscriptionFound() + { + ServiceTestContext testContext = new ServiceTestContext(); + testContext = CreateMocks(testContext); + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, "invalid server")); + + } + + [Fact] + public async Task CreateShouldCreateFirewallSuccessfullyGivenValidUserAccount() + { + ServiceTestContext testContext = new ServiceTestContext(); + testContext = CreateMocks(testContext); + + await VerifyCreateAsync(testContext, testContext.ServerName); + } + + [Fact] + public async Task CreateShouldFindTheRightSubscriptionGivenValidSubscriptionInFirstPlace() + { + ServiceTestContext testContext = new ServiceTestContext(); + testContext.Subscriptions = new List + { + testContext.ValidSubscription, + ServiceTestContext.CreateSubscriptionContext(), + ServiceTestContext.CreateSubscriptionContext(), + }; + + testContext = CreateMocks(testContext); + + await VerifyCreateAsync(testContext, testContext.ServerName); + } + + [Fact] + public async Task CreateShouldFindTheRightSubscriptionGivenValidSubscriptionInSecondPlace() + { + ServiceTestContext testContext = new ServiceTestContext(); + testContext.Subscriptions = new List + { + ServiceTestContext.CreateSubscriptionContext(), + testContext.ValidSubscription, + ServiceTestContext.CreateSubscriptionContext(), + }; + testContext.Initialize(); + testContext = CreateMocks(testContext); + await VerifyCreateAsync(testContext, testContext.ServerName); + } + + [Fact] + public async Task CreateShouldFindTheRightSubscriptionGivenValidSubscriptionInLastPlace() + { + ServiceTestContext testContext = new ServiceTestContext(); + testContext.Subscriptions = new List + { + ServiceTestContext.CreateSubscriptionContext(), + ServiceTestContext.CreateSubscriptionContext(), + testContext.ValidSubscription + }; + testContext.Initialize(); + + testContext = CreateMocks(testContext); + + await VerifyCreateAsync(testContext, testContext.ServerName); + } + + [Fact] + public async Task CreateShouldFindTheRightResourceGivenValidResourceInLastPlace() + { + ServiceTestContext testContext = new ServiceTestContext(); + var resources = new List + { + ServiceTestContext.CreateAzureSqlServer(Guid.NewGuid().ToString()), + ServiceTestContext.CreateAzureSqlServer(testContext.ServerName), + }; + testContext.SubscriptionToResourcesMap[testContext.ValidSubscription.Subscription.SubscriptionId] = resources; + + testContext = CreateMocks(testContext); + + await VerifyCreateAsync(testContext, testContext.ServerName); + } + + [Fact] + public async Task CreateShouldFindTheRightResourceGivenValidResourceInFirstPlace() + { + ServiceTestContext testContext = new ServiceTestContext(); + var resources = new List + { + ServiceTestContext.CreateAzureSqlServer(testContext.ServerName), + ServiceTestContext.CreateAzureSqlServer(Guid.NewGuid().ToString()), + }; + testContext.SubscriptionToResourcesMap[testContext.ValidSubscription.Subscription.SubscriptionId] = resources; + + testContext = CreateMocks(testContext); + + await VerifyCreateAsync(testContext, testContext.ServerName); + } + + [Fact] + public async Task CreateShouldFindTheRightResourceGivenValidResourceInMiddle() + { + ServiceTestContext testContext = new ServiceTestContext(); + var resources = new List + { + ServiceTestContext.CreateAzureSqlServer(Guid.NewGuid().ToString()), + ServiceTestContext.CreateAzureSqlServer(testContext.ServerName), + ServiceTestContext.CreateAzureSqlServer(Guid.NewGuid().ToString()) + }; + testContext.SubscriptionToResourcesMap[testContext.ValidSubscription.Subscription.SubscriptionId] = resources; + + testContext = CreateMocks(testContext); + + await VerifyCreateAsync(testContext, testContext.ServerName); + } + + [Fact] + public async Task CreateThrowExceptionIfResourceNotFound() + { + ServiceTestContext testContext = new ServiceTestContext(); + var resources = new List + { + ServiceTestContext.CreateAzureSqlServer(Guid.NewGuid().ToString()), + ServiceTestContext.CreateAzureSqlServer(Guid.NewGuid().ToString()), + }; + testContext.SubscriptionToResourcesMap[testContext.ValidSubscription.Subscription.SubscriptionId] = resources; + + testContext = CreateMocks(testContext); + + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, testContext.ServerName)); + } + + [Fact] + public async Task CreateThrowExceptionIfResourcesIsEmpty() + { + ServiceTestContext testContext = new ServiceTestContext(); + + testContext.SubscriptionToResourcesMap[testContext.ValidSubscription.Subscription.SubscriptionId] = new List(); + testContext = CreateMocks(testContext); + + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, testContext.ServerName, false)); + } + + [Fact] + public async Task CreateShouldThrowExceptionIfThereIsNoSubscriptionForUser() + { + ServiceTestContext testContext = new ServiceTestContext(); + testContext.Subscriptions = new List(); + + testContext = CreateMocks(testContext); + + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, testContext.ServerName, false)); + } + + + [Fact] + public async Task CreateShouldThrowExceptionIfSubscriptionIsInAnotherAccount() + { + ServiceTestContext testContext = new ServiceTestContext(); + testContext.Subscriptions = new List + { + ServiceTestContext.CreateSubscriptionContext(), + ServiceTestContext.CreateSubscriptionContext(), + }; + + testContext = CreateMocks(testContext); + await Assert.ThrowsAsync(() => VerifyCreateAsync(testContext, testContext.ServerName, false)); + } + + [Fact] + public async Task CreateShouldCreateFirewallForTheRightServerFullyQualifiedName() + { + ServiceTestContext testContext = new ServiceTestContext(); + string serverNameWithDifferentDomain = testContext.ServerNameWithoutDomain + ".myaliased.domain.name"; + + testContext.ServerName = serverNameWithDifferentDomain; + testContext.Initialize(); + testContext = CreateMocks(testContext); + + await VerifyCreateAsync(testContext, testContext.ServerName); + } + + private async Task VerifyCreateAsync(ServiceTestContext testContext, string serverName, bool verifyFirewallRuleCreated = true) + { + try + { + FirewallRuleService service = new FirewallRuleService(); + service.AuthenticationManager = testContext.ApplicationAuthenticationManager; + service.ResourceManager = testContext.AzureResourceManager; + FirewallRuleResponse response = await service.CreateFirewallRuleAsync(serverName, testContext.StartIpAddress, testContext.EndIpAddress); + if (verifyFirewallRuleCreated) + { + testContext.AzureResourceManagerMock.Verify(x => x.CreateFirewallRuleAsync( + It.Is(s => s.SubscriptionContext.Subscription.SubscriptionId == testContext.ValidSubscription.Subscription.SubscriptionId), + It.Is(r => r.FullyQualifiedDomainName == serverName), + It.Is(y => y.EndIpAddress.ToString().Equals(testContext.EndIpAddress) && y.StartIpAddress.ToString().Equals(testContext.StartIpAddress))), + Times.AtLeastOnce); + } + else + { + testContext.AzureResourceManagerMock.Verify(x => x.CreateFirewallRuleAsync( + It.Is(s => s.SubscriptionContext.Subscription.SubscriptionId == testContext.ValidSubscription.Subscription.SubscriptionId), + It.Is(r => r.FullyQualifiedDomainName == serverName), + It.Is(y => y.EndIpAddress.ToString().Equals(testContext.EndIpAddress) && y.StartIpAddress.ToString().Equals(testContext.StartIpAddress))), + Times.Never); + } + + return response; + } + catch (Exception ex) + { + if (ex is FirewallRuleException) + { + Assert.True(ex.InnerException == null || !(ex.InnerException is FirewallRuleException)); + } + throw; + } + } + + private ServiceTestContext CreateMocks(ServiceTestContext testContext) + { + var accountMock = new Mock(); + accountMock.Setup(x => x.UniqueId).Returns(Guid.NewGuid().ToString()); + var applicationAuthenticationManagerMock = new Mock(); + applicationAuthenticationManagerMock.Setup(x => x.GetUserNeedsReauthenticationAsync()) + .Returns(Task.FromResult(false)); + applicationAuthenticationManagerMock.Setup(x => x.GetCurrentAccountAsync()).Returns(Task.FromResult(accountMock.Object)); + applicationAuthenticationManagerMock.Setup(x => x.GetSubscriptionsAsync()).Returns(Task.FromResult(testContext.Subscriptions as IEnumerable)); + + var azureResourceManagerMock = new Mock(); + + CreateMocksForResources(testContext, azureResourceManagerMock); + + testContext.ApplicationAuthenticationManagerMock = applicationAuthenticationManagerMock; + testContext.AzureResourceManagerMock = azureResourceManagerMock; + return testContext; + } + + private void CreateMocksForResources( + ServiceTestContext testContext, + Mock azureResourceManagerMock) + { + foreach (IAzureUserAccountSubscriptionContext subscription in testContext.Subscriptions) + { + var sessionMock = new Mock(); + sessionMock.Setup(x => x.SubscriptionContext).Returns(subscription); + azureResourceManagerMock.Setup(x => x.CreateSessionAsync(subscription)).Returns(Task.FromResult(sessionMock.Object)); + + List resources; + if (testContext.SubscriptionToResourcesMap.TryGetValue(subscription.Subscription.SubscriptionId, + out resources)) + { + azureResourceManagerMock.Setup(x => x.GetSqlServerAzureResourcesAsync(It.Is( + m => m.SubscriptionContext.Subscription.SubscriptionId == subscription.Subscription.SubscriptionId))) + .Returns(Task.FromResult(resources as IEnumerable)); + } + else + { + azureResourceManagerMock.Setup(x => x.GetSqlServerAzureResourcesAsync( + It.Is(m => m.SubscriptionContext.Subscription.SubscriptionId == subscription.Subscription.SubscriptionId))) + .Returns(Task.FromResult>(null)); + } + } + + azureResourceManagerMock + .Setup(x => x.CreateFirewallRuleAsync( + It.IsAny(), + It.IsAny(), + It.Is( + y => y.EndIpAddress.ToString().Equals(testContext.EndIpAddress) + && y.StartIpAddress.ToString().Equals(testContext.StartIpAddress)))) + .Returns(Task.FromResult(new FirewallRuleResponse() {Created = true})); + } + } + + internal class ServiceTestContext + { + private string _validServerName = "validServerName.database.windows.net"; + private string _startIpAddressValue = "1.2.3.6"; + private string _endIpAddressValue = "1.2.3.6"; + private Dictionary> _subscriptionToResourcesMap; + + public ServiceTestContext() + { + StartIpAddress = _startIpAddressValue; + EndIpAddress = _endIpAddressValue; + ServerName = _validServerName; + Initialize(); + } + + internal void Initialize() + { + CreateSubscriptions(); + CreateAzureResources(); + } + + internal static IAzureUserAccountSubscriptionContext CreateSubscriptionContext() + { + var subscriptionContext = new Mock(); + var subscriptionMock = new Mock(); + subscriptionMock.Setup(x => x.SubscriptionId).Returns(Guid.NewGuid().ToString()); + subscriptionContext.Setup(x => x.Subscription).Returns(subscriptionMock.Object); + return subscriptionContext.Object; + } + + private void CreateSubscriptions() + { + if (Subscriptions == null || Subscriptions.Count == 0) + { + + ValidSubscriptionMock = new Mock(); + var subscriptionMock = new Mock(); + subscriptionMock.Setup(x => x.SubscriptionId).Returns(Guid.NewGuid().ToString()); + ValidSubscriptionMock.Setup(x => x.Subscription).Returns(subscriptionMock.Object); + + Subscriptions = new List + { + ValidSubscription, + CreateSubscriptionContext(), + CreateSubscriptionContext() + }; + } + } + + internal void CreateAzureResources(Dictionary> subscriptionToResourcesMap = null) + { + _subscriptionToResourcesMap = new Dictionary>(); + + if (subscriptionToResourcesMap == null) + { + foreach (var subscriptionDetails in Subscriptions) + { + if (subscriptionDetails.Subscription.SubscriptionId == ValidSubscription.Subscription.SubscriptionId) + { + var resources = new List(); + resources.Add(CreateAzureSqlServer(Guid.NewGuid().ToString())); + resources.Add(CreateAzureSqlServer(Guid.NewGuid().ToString())); + resources.Add(CreateAzureSqlServer(ServerName)); + _subscriptionToResourcesMap.Add(ValidSubscription.Subscription.SubscriptionId, resources); + } + else + { + var resources = new List(); + resources.Add(CreateAzureSqlServer(Guid.NewGuid().ToString())); + resources.Add(CreateAzureSqlServer(Guid.NewGuid().ToString())); + _subscriptionToResourcesMap.Add(subscriptionDetails.Subscription.SubscriptionId, resources); + } + } + } + else + { + _subscriptionToResourcesMap = subscriptionToResourcesMap; + } + } + + internal static IAzureSqlServerResource CreateAzureSqlServer(string serverName) + { + var azureSqlServer = + new Mock(); + azureSqlServer.Setup(x => x.Name).Returns(GetServerNameWithoutDomain(serverName)); + azureSqlServer.Setup(x => x.FullyQualifiedDomainName).Returns(serverName); + return azureSqlServer.Object; + } + + internal Dictionary> SubscriptionToResourcesMap + { + get { return _subscriptionToResourcesMap; } + } + + internal static string GetServerNameWithoutDomain(string serverName) + { + int index = serverName.IndexOf('.'); + if (index > 0) + { + return serverName.Substring(0, index); + } + return serverName; + } + + internal string StartIpAddress { get; set; } + + internal string EndIpAddress { get; set; } + + internal IList Subscriptions { get; set; } + + internal Mock ValidSubscriptionMock { get; set; } + internal IAzureUserAccountSubscriptionContext ValidSubscription { get { return ValidSubscriptionMock.Object; } } + + internal string ServerName { get; set; } + + internal string ServerNameWithoutDomain + { + get { return GetServerNameWithoutDomain(ServerName); } + } + + internal Mock ApplicationAuthenticationManagerMock { get; set; } + internal IAzureAuthenticationManager ApplicationAuthenticationManager { get { return ApplicationAuthenticationManagerMock?.Object; } } + + internal Mock AzureResourceManagerMock { get; set; } + + internal IAzureResourceManager AzureResourceManager { get { return AzureResourceManagerMock?.Object; } } + } + +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/ServiceManagerTest.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/ServiceManagerTest.cs new file mode 100644 index 00000000..c7e1d9ac --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ResourceProvider/ServiceManagerTest.cs @@ -0,0 +1,168 @@ +// +// 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.Reflection; +using Microsoft.SqlTools.Extensibility; +using Microsoft.SqlTools.ResourceProvider.Core; +using Microsoft.SqlTools.ResourceProvider.Core.Authentication; +using Microsoft.SqlTools.ResourceProvider.Core.Extensibility; +using Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Azure; +using Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider.Fakes; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ResourceProvider +{ + /// + /// Tests for ServiceManager to verify finding services and providers for specific type correctly + /// + public class ServiceManagerTest + { + private IList _providers; + private IList _accountManagers; + + public ServiceManagerTest() + { + _providers = new List() + { + new FakeServerDiscoveryProvider2(new ExportableMetadata("SqlServer", "Local", Guid.NewGuid().ToString())), + new FakeSecureServerDiscoveryProvider(new ExportableMetadata("SqlServer", "Azure", Guid.NewGuid().ToString())), + new FakeServerDiscoveryProvider(new ExportableMetadata("SqlServer", "Network", Guid.NewGuid().ToString())) + }; + + _accountManagers = new List() + { + new FakeAccountManager(new ExportableMetadata("SqlServer", "Azure", Guid.NewGuid().ToString())), + new FakeAccountManager2(new ExportableMetadata("SqlServer", "Network", Guid.NewGuid().ToString())) + }; + } + + [Fact] + public void GetServiceShouldReturnTheServiceThatHasGivenMetadataCorrectly() + { + //given + var serverDefinition = new ServerDefinition("SqlServer", "Azure"); + IMultiServiceProvider provider = CreateServiceProvider(); + + //when + IServerDiscoveryProvider service = ExtensionUtils.GetService(provider, serverDefinition); + + //then + Assert.NotNull(service); + Assert.True(service.GetType() == typeof(FakeSecureServerDiscoveryProvider)); + } + + [Fact] + public void GetServiceShouldReturnNullGivenInvalidMetadata() + { + //given + var serverDefinition = new ServerDefinition(Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + IMultiServiceProvider provider = CreateServiceProvider(); + + //when + IServerDiscoveryProvider service = ExtensionUtils.GetService(provider, serverDefinition); + + //then + Assert.Null(service); + } + + [Fact] + public void GetServiceShouldReturnNullGivenUnSupportedMetadata() + { + //given + var serverDefinition = new ServerDefinition("SqlServer", "Local"); + IMultiServiceProvider provider = CreateServiceProvider(); + + //when + IAccountManager service = ExtensionUtils.GetService(provider, serverDefinition); + + //then + Assert.Null(service); + } + + [Fact] + public void RequiresUserAccountShouldReturnFalseGivenNotSecuredService() + { + //given + var serverDefinition = new ServerDefinition("SqlServer", "Local"); + IMultiServiceProvider provider = CreateServiceProvider(); + + //when + IServerDiscoveryProvider service = ExtensionUtils.GetService(provider, serverDefinition); + + //then + Assert.NotNull(service); + } + + [Fact] + public void GetShouldReturnDefaultAzureServiceGivenDefaultCatalog() + { + // given + ExtensionServiceProvider provider = ExtensionServiceProvider.Create(new Assembly[] + { + typeof(IAccountManager).Assembly, + typeof(IAzureResourceManager).Assembly + }); + var serverDefinition = new ServerDefinition("sqlserver", "azure"); + + // when I query each provider + IServerDiscoveryProvider serverDiscoveryProvider = ExtensionUtils.GetService(provider, serverDefinition); + // Then I get a valid provider back + Assert.NotNull(serverDiscoveryProvider); + Assert.True(serverDiscoveryProvider is AzureSqlServerDiscoveryProvider); + + IDatabaseDiscoveryProvider databaseDiscoveryProvider = + ExtensionUtils.GetService(provider, serverDefinition); + + // TODO Verify account manager is detected as soon as the account manager has a real implementation + //IAccountManager accountManager = ((AzureSqlServerDiscoveryProvider)serverDiscoveryProvider).AccountManager; + //Assert.NotNull(accountManager); + //Assert.True(accountManager is IAzureAuthenticationManager); + + Assert.NotNull(databaseDiscoveryProvider); + Assert.True(databaseDiscoveryProvider is AzureDatabaseDiscoveryProvider); + } + + + [Fact] + public void GetShouldReturnImplementedAzureServiceIfFoundInCatalog() + { + //given + ExtensionServiceProvider provider = ExtensionServiceProvider.Create(typeof(FakeAzureServerDiscoveryProvider).SingleItemAsEnumerable()); + + //when + IServerDiscoveryProvider serverDiscoveryProvider = ExtensionUtils.GetService(provider, new ServerDefinition("sqlserver", "azure")); + + Assert.NotNull(serverDiscoveryProvider); + Assert.True(serverDiscoveryProvider is FakeAzureServerDiscoveryProvider); + } + + [Fact] + public void GetGetServiceOfExportableShouldReturnNullGivenSameTypeAsExportable() + { + //given + ExtensionServiceProvider provider = ExtensionServiceProvider.Create(typeof(FakeAzureServerDiscoveryProvider).SingleItemAsEnumerable()); + + //when + IServerDiscoveryProvider serverDiscoveryProvider = ExtensionUtils.GetService(provider, new ServerDefinition("sqlserver", "azure")); + + Assert.NotNull(serverDiscoveryProvider); + FakeAzureServerDiscoveryProvider fakeAzureServerDiscovery = serverDiscoveryProvider as FakeAzureServerDiscoveryProvider; + Assert.NotNull(fakeAzureServerDiscovery); + Assert.Null(fakeAzureServerDiscovery.ServerDiscoveryProvider); + } + + private IMultiServiceProvider CreateServiceProvider() + { + var providerMock = new Mock(); + + providerMock.Setup(x => x.GetServices()).Returns(_providers); + providerMock.Setup(x => x.GetServices()).Returns(_accountManagers); + + return providerMock.Object; + } + } +}