Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Resources] Fix issue preventing deployment of Template Specs located outside of the current subscription context #13483

Merged
merged 5 commits into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using Microsoft.Azure.Commands.Common.Authentication;
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Components;
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkClient;
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels;
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities;
using Microsoft.Azure.Management.ResourceManager;
using Microsoft.WindowsAzure.Commands.Utilities.Common;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -59,6 +59,8 @@ public abstract class ResourceWithParameterCmdletBase : ResourceManagerCmdletBas

private string templateSpecId;

private ITemplateSpecsClient templateSpecsClient;

protected ResourceWithParameterCmdletBase()
{
dynamicParameters = new RuntimeDefinedParameterDictionary();
Expand Down Expand Up @@ -142,30 +144,35 @@ protected ResourceWithParameterCmdletBase()
"to provide a better error message in the case where not all required parameters are satisfied.")]
public SwitchParameter SkipTemplateParameterPrompt { get; set; }

private TemplateSpecsSdkClient templateSpecsSdkClient;

/// <summary>
/// Gets or sets the Template Specs Azure sdk client wrapper
/// Gets or sets the Template Specs Azure SDK client
/// </summary>
public TemplateSpecsSdkClient TemplateSpecsSdkClient
public ITemplateSpecsClient TemplateSpecsClient
{
get
{
if (this.templateSpecsSdkClient == null)
if (this.templateSpecsClient == null)
{
this.templateSpecsSdkClient = new TemplateSpecsSdkClient(DefaultContext);
this.templateSpecsClient =
AzureSession.Instance.ClientFactory.CreateArmClient<TemplateSpecsClient>(
DefaultContext,
AzureEnvironment.Endpoint.ResourceManager
);
}

return this.templateSpecsSdkClient;
return this.templateSpecsClient;
}

set { this.templateSpecsSdkClient = value; }
set { this.templateSpecsClient = value; }
}

public object GetDynamicParameters()
{
if (!this.IsParameterBound(c => c.SkipTemplateParameterPrompt))
{
// Resolve the static parameter names for this cmdlet:
string[] staticParameterNames = this.GetStaticParameterNames();

if (TemplateObject != null && TemplateObject != templateObject)
{
templateObject = TemplateObject;
Expand All @@ -175,15 +182,15 @@ public object GetDynamicParameters()
TemplateObject,
TemplateParameterObject,
this.ResolvePath(TemplateParameterFile),
MyInvocation.MyCommand.Parameters.Keys.ToArray());
staticParameterNames);
}
else
{
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
TemplateObject,
TemplateParameterObject,
TemplateParameterUri,
MyInvocation.MyCommand.Parameters.Keys.ToArray());
staticParameterNames);
}
}
else if (!string.IsNullOrEmpty(TemplateFile) &&
Expand All @@ -196,15 +203,15 @@ public object GetDynamicParameters()
this.ResolvePath(TemplateFile),
TemplateParameterObject,
this.ResolvePath(TemplateParameterFile),
MyInvocation.MyCommand.Parameters.Keys.ToArray());
staticParameterNames);
}
else
{
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
this.ResolvePath(TemplateFile),
TemplateParameterObject,
TemplateParameterUri,
MyInvocation.MyCommand.Parameters.Keys.ToArray());
staticParameterNames);
}
}
else if (!string.IsNullOrEmpty(TemplateUri) &&
Expand All @@ -217,15 +224,15 @@ public object GetDynamicParameters()
TemplateUri,
TemplateParameterObject,
this.ResolvePath(TemplateParameterFile),
MyInvocation.MyCommand.Parameters.Keys.ToArray());
staticParameterNames);
}
else
{
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
TemplateUri,
TemplateParameterObject,
TemplateParameterUri,
MyInvocation.MyCommand.Parameters.Keys.ToArray());
staticParameterNames);
}
}
else if (!string.IsNullOrEmpty(TemplateSpecId) &&
Expand All @@ -238,28 +245,41 @@ public object GetDynamicParameters()
throw new PSArgumentException("No version found in Resource ID");
}

var templateSpecVersion = TemplateSpecsSdkClient.GetTemplateSpec(
ResourceIdUtility.GetResourceName(templateSpecId).Split('/')[0],
if (!string.IsNullOrEmpty(resourceIdentifier.Subscription) &&
TemplateSpecsClient.SubscriptionId != resourceIdentifier.Subscription)
{
// The template spec is in a different subscription than our default
// context. Force the client to use that subscription:
TemplateSpecsClient.SubscriptionId = resourceIdentifier.Subscription;
}

var templateSpecVersion = TemplateSpecsClient.TemplateSpecVersions.Get(
ResourceIdUtility.GetResourceGroupName(templateSpecId),
resourceIdentifier.ResourceName).Versions.Single();
ResourceIdUtility.GetResourceName(templateSpecId).Split('/')[0],
resourceIdentifier.ResourceName);

if (!(templateSpecVersion.Template is JObject))
{
throw new InvalidOperationException("Unexpected type."); // Sanity check
}

var templateObj = JObject.Parse(templateSpecVersion.Template);
JObject templateObj = (JObject)templateSpecVersion.Template;

if (string.IsNullOrEmpty(TemplateParameterUri))
{
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
templateObj,
TemplateParameterObject,
this.ResolvePath(TemplateParameterFile),
MyInvocation.MyCommand.Parameters.Keys.ToArray());
staticParameterNames);
}
else
{
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
templateObj,
TemplateParameterObject,
TemplateParameterUri,
MyInvocation.MyCommand.Parameters.Keys.ToArray());
staticParameterNames);
}
}
}
Expand Down Expand Up @@ -345,5 +365,39 @@ protected string GetDeploymentDebugLogLevel(string deploymentDebugLogLevel)

return debugSetting;
}

/// <summary>
/// Gets the names of the static parameters defined for this cmdlet.
/// </summary>
protected string[] GetStaticParameterNames()
{
if (MyInvocation.MyCommand != null)
{
// We're running inside the shell... parameter information will already
// be resolved for us:
return MyInvocation.MyCommand.Parameters.Keys.ToArray();
}

// This invocation is internal (e.g: through a unit test), fallback to
// collecting the command/parameter info explicitly from our current type:

CmdletAttribute cmdletAttribute = (CmdletAttribute)this.GetType()
.GetCustomAttributes(typeof(CmdletAttribute), true)
.FirstOrDefault();

if (cmdletAttribute == null)
{
throw new InvalidOperationException(
$"Expected type '{this.GetType().Name}' to have CmdletAttribute."
);
}

// The command name we provide for the temporary CmdletInfo isn't consumed
// anywhere other than instantiation, but let's resolve it anyway:
string commandName = $"{cmdletAttribute.VerbName}-{cmdletAttribute.NounName}";

CmdletInfo cmdletInfo = new CmdletInfo(commandName, this.GetType());
return cmdletInfo.Parameters.Keys.ToArray();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
using System.Collections;
using FluentAssertions;
using ProvisioningState = Microsoft.Azure.Commands.ResourceManager.Cmdlets.Entities.ProvisioningState;
using Microsoft.Azure.Management.ResourceManager;
using Newtonsoft.Json.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Rest.Azure;
using Newtonsoft.Json;

namespace Microsoft.Azure.Commands.Resources.Test
{
Expand All @@ -39,6 +45,10 @@ public class NewAzureResourceGroupDeploymentCommandTests : RMTestBase

private Mock<ResourceManagerSdkClient> resourcesClientMock;

private Mock<ITemplateSpecsClient> templateSpecsClientMock;

private Mock<ITemplateSpecVersionsOperations> templateSpecsVersionOperationsMock;

private Mock<ICommandRuntime> commandRuntimeMock;

private string resourceGroupName = "myResourceGroup";
Expand All @@ -54,13 +64,20 @@ public class NewAzureResourceGroupDeploymentCommandTests : RMTestBase
public NewAzureResourceGroupDeploymentCommandTests(ITestOutputHelper output)
{
resourcesClientMock = new Mock<ResourceManagerSdkClient>();

templateSpecsClientMock = new Mock<ITemplateSpecsClient>();
templateSpecsClientMock.SetupAllProperties();
templateSpecsVersionOperationsMock = new Mock<ITemplateSpecVersionsOperations>();
templateSpecsClientMock.Setup(m => m.TemplateSpecVersions).Returns(templateSpecsVersionOperationsMock.Object);

XunitTracingInterceptor.AddToContext(new XunitTracingInterceptor(output));
commandRuntimeMock = new Mock<ICommandRuntime>();
SetupConfirmation(commandRuntimeMock);
cmdlet = new NewAzureResourceGroupDeploymentCmdlet()
{
CommandRuntime = commandRuntimeMock.Object,
ResourceManagerSdkClient = resourcesClientMock.Object
ResourceManagerSdkClient = resourcesClientMock.Object,
TemplateSpecsClient = templateSpecsClientMock.Object
};
}

Expand Down Expand Up @@ -271,5 +288,73 @@ public void CreatesNewPSResourceGroupDeploymentWithUserTemplateEmptyRollback()

commandRuntimeMock.Verify(f => f.WriteObject(expected), Times.Once());
}

/// <summary>
/// When deployments are created using a template spec, the dynamic parameters are
/// resolved by reading the parameters from the template spec version's template body.
/// Previously a bug was present that prevented successful dynamic parameter resolution
/// if the template spec existed in a subscription outside the current subscription
/// context. This test validates dynamic parameter resolution works for deployments using
/// cross-subscription template specs.
/// </summary>

[Fact]
[Trait(Category.AcceptanceType, Category.CheckIn)]
public void ResolvesDynamicParametersWithCrossSubTemplateSpec()
{
const string templateSpecSubscriptionId = "10000000-0000-0000-0000-000000000000";
const string templateSpecRGName = "someRG";
const string templateSpecName = "myTemplateSpec";
const string templateSpecVersion = "v1";

string templateSpecId = $"/subscriptions/{templateSpecSubscriptionId}/" +
$"resourceGroups/{templateSpecRGName}/providers/Microsoft.Resources/" +
$"templateSpecs/{templateSpecName }/versions/{templateSpecVersion}";

// Ensure our template file path is normalized for the current system:
var normalizedTemplateFilePath = (Path.DirectorySeparatorChar != '\\')
? templateFile.Replace('\\', Path.DirectorySeparatorChar) // Other/Unix based
: templateFile; // Windows based (already valid)

var templateContentForTest = File.ReadAllText(normalizedTemplateFilePath);
var template = JsonConvert.DeserializeObject<TemplateFile>(templateContentForTest);

templateSpecsVersionOperationsMock.Setup(f => f.GetWithHttpMessagesAsync(
templateSpecRGName,
templateSpecName,
templateSpecVersion,
null,
new CancellationToken()))
.Returns(() => {

// We should only be getting this template spec from the expected subscription:
Assert.Equal(templateSpecSubscriptionId, templateSpecsClientMock.Object.SubscriptionId);

var versionToReturn = new TemplateSpecVersion(
location: "westus2",
id: templateSpecId,
name: templateSpecVersion,
type: "Microsoft.Resources/templateSpecs/versions",
template: JObject.Parse(templateContentForTest)
);

return Task.Factory.StartNew(() =>
new AzureOperationResponse<TemplateSpecVersion>()
{
Body = versionToReturn
}
);
});

cmdlet.ResourceGroupName = resourceGroupName;
cmdlet.Name = deploymentName;
cmdlet.TemplateSpecId = templateSpecId;

var dynamicParams = cmdlet.GetDynamicParameters() as RuntimeDefinedParameterDictionary;

dynamicParams.Should().NotBeNull();
dynamicParams.Count().Should().Be(template.Parameters.Count);
dynamicParams.Keys.Should().Contain(template.Parameters.Keys);
}
}
}
1 change: 1 addition & 0 deletions src/Resources/Resources/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* Fixed an issue where What-If shows two resource group scopes with different casing
* Updated `Export-AzResourceGroup` to use the SDK.
* Added culture info to parse methods
* Fixed issue where attempts to deploy template specs from a subscription outside of the current subscription context would fail
* Changed Double parser for version parser

## Version 3.0.0
Expand Down