diff --git a/src/ResourceManager/Common/Commands.ResourceManager.Common/ArgumentCompleters/ResourceGroupCompleter.cs b/src/ResourceManager/Common/Commands.ResourceManager.Common/ArgumentCompleters/ResourceGroupCompleter.cs new file mode 100644 index 000000000000..447e3a7ba776 --- /dev/null +++ b/src/ResourceManager/Common/Commands.ResourceManager.Common/ArgumentCompleters/ResourceGroupCompleter.cs @@ -0,0 +1,158 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters +{ + using Commands.Common.Authentication; + using Commands.Common.Authentication.Abstractions; + using Management.Internal.Resources; + using Management.Internal.Resources.Models; + using Properties; + using Rest.Azure; + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + + /// + /// This attribute will allow the user to autocomplete the -ResourceGroup parameter of a cmdlet with valid resource groups + /// + public class ResourceGroupCompleterAttribute : ArgumentCompleterAttribute + { + private static IDictionary> _resourceGroupNamesDictionary = new ConcurrentDictionary>(); + private static readonly object _lock = new object(); + + protected static IList ResourceGroupNames + { + get + { + lock (_lock) + { + IAzureContext context = AzureRmProfileProvider.Instance.Profile.DefaultContext; + var contextHash = HashContext(context); + if (!_resourceGroupNamesDictionary.ContainsKey(contextHash)) + { + var tempResourceGroupList = new List(); + try + { + var instance = AzureSession.Instance; + var client = instance.ClientFactory.CreateCustomArmClient( + context.Environment.GetEndpointAsUri(AzureEnvironment.Endpoint.ResourceManager), + instance.AuthenticationFactory.GetServiceClientCredentials(context, AzureEnvironment.Endpoint.ResourceManager), + instance.ClientFactory.GetCustomHandlers()); + client.SubscriptionId = context.Subscription.Id; + // Retrieve only the first page of ResourceGroups to display to the user + var resourceGroups = client.ResourceGroups.ListAsync(); + if (resourceGroups.Wait(TimeSpan.FromSeconds(5))) + { + tempResourceGroupList = CreateResourceGroupList(resourceGroups.Result); + if (resourceGroups.Result != null) + { + _resourceGroupNamesDictionary[contextHash] = tempResourceGroupList; + } + } +#if DEBUG + else + { + throw new Exception("client.ResourceGroups call timed out"); + } +#endif + } + + catch (Exception ex) + { +#if DEBUG + throw ex; +#endif + } + + return tempResourceGroupList; + } + + else + { + return _resourceGroupNamesDictionary[contextHash]; + } + } + } + } + + /// + /// This class will provide a list of resource groups that are available to the user (with default resource group first if it exists). This will then be available to the user to tab through. + /// Example: [Parameter(ParameterSetName = ListByNameInTenantParameterSet, ValueFromPipelineByPropertyName = true, Mandatory = false), ResourceGroupCompleter()] + /// + /// + public ResourceGroupCompleterAttribute() : base(CreateScriptBlock()) + { + } + + public static string[] GetResourceGroups() + { + IAzureContext context = AzureRmProfileProvider.Instance.Profile.DefaultContext; + var resourceGroupNamesCopy = ResourceGroupNames; + if (context.IsPropertySet(Resources.DefaultResourceGroupKey)) + { + return GetResourceGroups(resourceGroupNamesCopy, context.ExtendedProperties[Resources.DefaultResourceGroupKey]); + } + return GetResourceGroups(resourceGroupNamesCopy, null); + } + + public static string[] GetResourceGroups(IList resourceGroupNames, string defaultResourceGroup) + { + if (defaultResourceGroup != null) + { + if (resourceGroupNames.Contains(defaultResourceGroup)) + { + resourceGroupNames.Remove(defaultResourceGroup); + } + resourceGroupNames.Insert(0, defaultResourceGroup); + } + return resourceGroupNames.ToArray(); + } + + private static ScriptBlock CreateScriptBlock() + { + string script = "param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)\n" + + "$locations = [Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters.ResourceGroupCompleterAttribute]::GetResourceGroups()\n" + + "$locations | Where-Object { $_ -Like \"$wordToComplete*\" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }"; + ScriptBlock scriptBlock = ScriptBlock.Create(script); + return scriptBlock; + } + + private static int HashContext(IAzureContext context) + { + return (context.Account.Id + context.Environment.Name + context.Subscription.Id + context.Tenant.Id).GetHashCode(); + } + + public static List CreateResourceGroupList(IPage result) + { + var tempResourceGroupList = new List(); + if (result != null) + { + foreach (ResourceGroup resourceGroup in result) + { + tempResourceGroupList.Add(resourceGroup.Name); + } + } +#if DEBUG + else + { + throw new Exception("Result from client.ResourceGroups is null"); + } +#endif + return tempResourceGroupList; + } + } +} \ No newline at end of file diff --git a/src/ResourceManager/Common/Commands.ResourceManager.Common/Commands.ResourceManager.Common.csproj b/src/ResourceManager/Common/Commands.ResourceManager.Common/Commands.ResourceManager.Common.csproj index 2571ef4028ef..37834ab7db84 100644 --- a/src/ResourceManager/Common/Commands.ResourceManager.Common/Commands.ResourceManager.Common.csproj +++ b/src/ResourceManager/Common/Commands.ResourceManager.Common/Commands.ResourceManager.Common.csproj @@ -185,6 +185,7 @@ True Resources.resx + diff --git a/src/ResourceManager/Common/Commands.ResourceManager.Common/Properties/Resources.Designer.cs b/src/ResourceManager/Common/Commands.ResourceManager.Common/Properties/Resources.Designer.cs index e302b959a4cb..abcfc8e6ae08 100644 --- a/src/ResourceManager/Common/Commands.ResourceManager.Common/Properties/Resources.Designer.cs +++ b/src/ResourceManager/Common/Commands.ResourceManager.Common/Properties/Resources.Designer.cs @@ -137,6 +137,15 @@ public static string DataCollectionSaveFileInformation { } } + /// + /// Looks up a localized string similar to Default Resource Group. + /// + public static string DefaultResourceGroupKey { + get { + return ResourceManager.GetString("DefaultResourceGroupKey", resourceCulture); + } + } + /// /// Looks up a localized string similar to No default subscription has been designated. Use Select-AzureSubscription -Default <subscriptionName> to set the default subscription.. /// diff --git a/src/ResourceManager/Common/Commands.ResourceManager.Common/Properties/Resources.resx b/src/ResourceManager/Common/Commands.ResourceManager.Common/Properties/Resources.resx index 458e32bcf32a..18733a6bb2bc 100644 --- a/src/ResourceManager/Common/Commands.ResourceManager.Common/Properties/Resources.resx +++ b/src/ResourceManager/Common/Commands.ResourceManager.Common/Properties/Resources.resx @@ -216,4 +216,7 @@ The data is collected by Microsoft. Use the Disable-AzureDataCollection cmdlet to turn the feature Off. The cmdlet can be found in the Azure module. To disable data collection: PS > Disable-AzureDataCollection. Use the Enable-AzureDataCollection cmdlet to turn the feature On. The cmdlet can be found in the Azure module. To enable data collection: PS > Enable-AzureDataCollection. + + Default Resource Group + \ No newline at end of file diff --git a/src/ResourceManager/Profile/Commands.Profile.Test/Commands.Profile.Test.csproj b/src/ResourceManager/Profile/Commands.Profile.Test/Commands.Profile.Test.csproj index 5597a6bf7702..046805ce092d 100644 --- a/src/ResourceManager/Profile/Commands.Profile.Test/Commands.Profile.Test.csproj +++ b/src/ResourceManager/Profile/Commands.Profile.Test/Commands.Profile.Test.csproj @@ -203,6 +203,7 @@ + @@ -286,4 +287,4 @@ - + \ No newline at end of file diff --git a/src/ResourceManager/Profile/Commands.Profile.Test/ResourceGroupCompleterUnitTests.cs b/src/ResourceManager/Profile/Commands.Profile.Test/ResourceGroupCompleterUnitTests.cs new file mode 100644 index 000000000000..0e090ece3fcb --- /dev/null +++ b/src/ResourceManager/Profile/Commands.Profile.Test/ResourceGroupCompleterUnitTests.cs @@ -0,0 +1,95 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters; +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Azure.Commands.Profile.Test +{ + public class ResourceGroupCompleterUnitTests + { + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ReturnsEmptyListWhenNoResourceGroupsExist() + { + IList resourceGroupsReturned = new List(); + Assert.Collection(ResourceGroupCompleterAttribute.GetResourceGroups(resourceGroupsReturned, null)); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void OneResourceGroupNoDefault() + { + IList resourceGroupsReturned = new List(); + resourceGroupsReturned.Add("test1"); + Assert.Collection(ResourceGroupCompleterAttribute.GetResourceGroups(resourceGroupsReturned, null), e1 => Assert.Equal("test1", e1)); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void OneResourceGroupWithDefault() + { + IList resourceGroupsReturned = new List(); + resourceGroupsReturned.Add("test1"); + Assert.Collection(ResourceGroupCompleterAttribute.GetResourceGroups(resourceGroupsReturned, "test1"), e1 => Assert.Equal("test1", e1)); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void OneResourceGroupWithInvalidDefault() + { + IList resourceGroupsReturned = new List(); + resourceGroupsReturned.Add("test1"); + Assert.Collection(ResourceGroupCompleterAttribute.GetResourceGroups(resourceGroupsReturned, "defaultOutOfPage"), e1 => Assert.Equal("defaultOutOfPage", e1), + e2 => Assert.Equal("test1", e2)); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void MultipleResourceGroupsNoDefault() + { + IList resourceGroupsReturned = new List(); + resourceGroupsReturned.Add("test1"); + resourceGroupsReturned.Add("test2"); + resourceGroupsReturned.Add("test3"); + resourceGroupsReturned.Add("test4"); + Assert.Collection(ResourceGroupCompleterAttribute.GetResourceGroups(resourceGroupsReturned, null), e1 => Assert.Equal("test1", e1), + e2 => Assert.Equal("test2", e2), e3 => Assert.Equal("test3", e3), e4 => Assert.Equal("test4", e4)); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void MultipleResourceGroupsWithDefault() + { + IList resourceGroupsReturned = new List(); + resourceGroupsReturned.Add("test1"); + resourceGroupsReturned.Add("test2"); + resourceGroupsReturned.Add("test3"); + resourceGroupsReturned.Add("test4"); + Assert.Collection(ResourceGroupCompleterAttribute.GetResourceGroups(resourceGroupsReturned, "test3"), e1 => Assert.Equal("test3", e1), + e2 => Assert.Equal("test1", e2), e3 => Assert.Equal("test2", e3), e4 => Assert.Equal("test4", e4)); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ThrowsErrorWhenResultNull() + { + var ex = Assert.Throws(() => ResourceGroupCompleterAttribute.CreateResourceGroupList(null)); + Assert.Equal(ex.Message, "Result from client.ResourceGroups is null"); + } + } +} \ No newline at end of file diff --git a/src/ResourceManager/Profile/Commands.Profile/Default/SetAzureRMDefault.cs b/src/ResourceManager/Profile/Commands.Profile/Default/SetAzureRMDefault.cs index 1992cd4c96d1..b3b4b6e23db0 100644 --- a/src/ResourceManager/Profile/Commands.Profile/Default/SetAzureRMDefault.cs +++ b/src/ResourceManager/Profile/Commands.Profile/Default/SetAzureRMDefault.cs @@ -19,6 +19,7 @@ using Microsoft.Azure.Commands.Profile.Models; using Microsoft.Azure.Commands.Profile.Properties; using Microsoft.Azure.Commands.ResourceManager.Common; +using Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters; using Microsoft.Azure.Management.Internal.Resources; using Microsoft.Azure.Management.Internal.Resources.Models; using System.Management.Automation; @@ -36,6 +37,7 @@ public class SetAzureRMDefaultCommand : AzureContextModificationCmdlet private const string ResourceGroupNameParameterSet = "ResourceGroupName"; [Parameter(ParameterSetName = ResourceGroupNameParameterSet, Mandatory = false, HelpMessage = "Name of the resource group being set as default", ValueFromPipelineByPropertyName = true)] + [ResourceGroupCompleter()] [ValidateNotNullOrEmpty] public string ResourceGroupName { get; set; }