Skip to content

Commit

Permalink
[Resources] Fix issue preventing deployment of Template Specs located…
Browse files Browse the repository at this point in the history
… outside of the current subscription context (#13483)

* Template Specs: Fixing issue where template specs from subscriptions not in context could not be deployed

* Adding mock test for covering recent cross-sub template spec deployment bug involving dynamic parameters.

* Template Specs: Fixing issue with test on Unix based systems

* Fix for test failure
  • Loading branch information
stuartko authored Nov 30, 2020
1 parent d688ae5 commit 3e5b6cb
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 23 deletions.
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

0 comments on commit 3e5b6cb

Please sign in to comment.