Skip to content

Commit 3e5b6cb

Browse files
authored
[Resources] Fix issue preventing deployment of Template Specs located outside of the current subscription context (Azure#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
1 parent d688ae5 commit 3e5b6cb

File tree

3 files changed

+163
-23
lines changed

3 files changed

+163
-23
lines changed

src/Resources/ResourceManager/Implementation/CmdletBase/ResourceWithParameterCmdletBase.cs

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
using System.Collections.Generic;
1818
using System.Linq;
1919
using System.Management.Automation;
20+
using Microsoft.Azure.Commands.Common.Authentication;
2021
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
2122
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Components;
22-
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkClient;
23-
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels;
2423
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities;
24+
using Microsoft.Azure.Management.ResourceManager;
2525
using Microsoft.WindowsAzure.Commands.Utilities.Common;
2626
using Newtonsoft.Json.Linq;
2727

@@ -59,6 +59,8 @@ public abstract class ResourceWithParameterCmdletBase : ResourceManagerCmdletBas
5959

6060
private string templateSpecId;
6161

62+
private ITemplateSpecsClient templateSpecsClient;
63+
6264
protected ResourceWithParameterCmdletBase()
6365
{
6466
dynamicParameters = new RuntimeDefinedParameterDictionary();
@@ -142,30 +144,35 @@ protected ResourceWithParameterCmdletBase()
142144
"to provide a better error message in the case where not all required parameters are satisfied.")]
143145
public SwitchParameter SkipTemplateParameterPrompt { get; set; }
144146

145-
private TemplateSpecsSdkClient templateSpecsSdkClient;
146-
147147
/// <summary>
148-
/// Gets or sets the Template Specs Azure sdk client wrapper
148+
/// Gets or sets the Template Specs Azure SDK client
149149
/// </summary>
150-
public TemplateSpecsSdkClient TemplateSpecsSdkClient
150+
public ITemplateSpecsClient TemplateSpecsClient
151151
{
152152
get
153153
{
154-
if (this.templateSpecsSdkClient == null)
154+
if (this.templateSpecsClient == null)
155155
{
156-
this.templateSpecsSdkClient = new TemplateSpecsSdkClient(DefaultContext);
156+
this.templateSpecsClient =
157+
AzureSession.Instance.ClientFactory.CreateArmClient<TemplateSpecsClient>(
158+
DefaultContext,
159+
AzureEnvironment.Endpoint.ResourceManager
160+
);
157161
}
158162

159-
return this.templateSpecsSdkClient;
163+
return this.templateSpecsClient;
160164
}
161165

162-
set { this.templateSpecsSdkClient = value; }
166+
set { this.templateSpecsClient = value; }
163167
}
164168

165169
public object GetDynamicParameters()
166170
{
167171
if (!this.IsParameterBound(c => c.SkipTemplateParameterPrompt))
168172
{
173+
// Resolve the static parameter names for this cmdlet:
174+
string[] staticParameterNames = this.GetStaticParameterNames();
175+
169176
if (TemplateObject != null && TemplateObject != templateObject)
170177
{
171178
templateObject = TemplateObject;
@@ -175,15 +182,15 @@ public object GetDynamicParameters()
175182
TemplateObject,
176183
TemplateParameterObject,
177184
this.ResolvePath(TemplateParameterFile),
178-
MyInvocation.MyCommand.Parameters.Keys.ToArray());
185+
staticParameterNames);
179186
}
180187
else
181188
{
182189
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
183190
TemplateObject,
184191
TemplateParameterObject,
185192
TemplateParameterUri,
186-
MyInvocation.MyCommand.Parameters.Keys.ToArray());
193+
staticParameterNames);
187194
}
188195
}
189196
else if (!string.IsNullOrEmpty(TemplateFile) &&
@@ -196,15 +203,15 @@ public object GetDynamicParameters()
196203
this.ResolvePath(TemplateFile),
197204
TemplateParameterObject,
198205
this.ResolvePath(TemplateParameterFile),
199-
MyInvocation.MyCommand.Parameters.Keys.ToArray());
206+
staticParameterNames);
200207
}
201208
else
202209
{
203210
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
204211
this.ResolvePath(TemplateFile),
205212
TemplateParameterObject,
206213
TemplateParameterUri,
207-
MyInvocation.MyCommand.Parameters.Keys.ToArray());
214+
staticParameterNames);
208215
}
209216
}
210217
else if (!string.IsNullOrEmpty(TemplateUri) &&
@@ -217,15 +224,15 @@ public object GetDynamicParameters()
217224
TemplateUri,
218225
TemplateParameterObject,
219226
this.ResolvePath(TemplateParameterFile),
220-
MyInvocation.MyCommand.Parameters.Keys.ToArray());
227+
staticParameterNames);
221228
}
222229
else
223230
{
224231
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
225232
TemplateUri,
226233
TemplateParameterObject,
227234
TemplateParameterUri,
228-
MyInvocation.MyCommand.Parameters.Keys.ToArray());
235+
staticParameterNames);
229236
}
230237
}
231238
else if (!string.IsNullOrEmpty(TemplateSpecId) &&
@@ -238,28 +245,41 @@ public object GetDynamicParameters()
238245
throw new PSArgumentException("No version found in Resource ID");
239246
}
240247

241-
var templateSpecVersion = TemplateSpecsSdkClient.GetTemplateSpec(
242-
ResourceIdUtility.GetResourceName(templateSpecId).Split('/')[0],
248+
if (!string.IsNullOrEmpty(resourceIdentifier.Subscription) &&
249+
TemplateSpecsClient.SubscriptionId != resourceIdentifier.Subscription)
250+
{
251+
// The template spec is in a different subscription than our default
252+
// context. Force the client to use that subscription:
253+
TemplateSpecsClient.SubscriptionId = resourceIdentifier.Subscription;
254+
}
255+
256+
var templateSpecVersion = TemplateSpecsClient.TemplateSpecVersions.Get(
243257
ResourceIdUtility.GetResourceGroupName(templateSpecId),
244-
resourceIdentifier.ResourceName).Versions.Single();
258+
ResourceIdUtility.GetResourceName(templateSpecId).Split('/')[0],
259+
resourceIdentifier.ResourceName);
260+
261+
if (!(templateSpecVersion.Template is JObject))
262+
{
263+
throw new InvalidOperationException("Unexpected type."); // Sanity check
264+
}
245265

246-
var templateObj = JObject.Parse(templateSpecVersion.Template);
266+
JObject templateObj = (JObject)templateSpecVersion.Template;
247267

248268
if (string.IsNullOrEmpty(TemplateParameterUri))
249269
{
250270
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
251271
templateObj,
252272
TemplateParameterObject,
253273
this.ResolvePath(TemplateParameterFile),
254-
MyInvocation.MyCommand.Parameters.Keys.ToArray());
274+
staticParameterNames);
255275
}
256276
else
257277
{
258278
dynamicParameters = TemplateUtility.GetTemplateParametersFromFile(
259279
templateObj,
260280
TemplateParameterObject,
261281
TemplateParameterUri,
262-
MyInvocation.MyCommand.Parameters.Keys.ToArray());
282+
staticParameterNames);
263283
}
264284
}
265285
}
@@ -345,5 +365,39 @@ protected string GetDeploymentDebugLogLevel(string deploymentDebugLogLevel)
345365

346366
return debugSetting;
347367
}
368+
369+
/// <summary>
370+
/// Gets the names of the static parameters defined for this cmdlet.
371+
/// </summary>
372+
protected string[] GetStaticParameterNames()
373+
{
374+
if (MyInvocation.MyCommand != null)
375+
{
376+
// We're running inside the shell... parameter information will already
377+
// be resolved for us:
378+
return MyInvocation.MyCommand.Parameters.Keys.ToArray();
379+
}
380+
381+
// This invocation is internal (e.g: through a unit test), fallback to
382+
// collecting the command/parameter info explicitly from our current type:
383+
384+
CmdletAttribute cmdletAttribute = (CmdletAttribute)this.GetType()
385+
.GetCustomAttributes(typeof(CmdletAttribute), true)
386+
.FirstOrDefault();
387+
388+
if (cmdletAttribute == null)
389+
{
390+
throw new InvalidOperationException(
391+
$"Expected type '{this.GetType().Name}' to have CmdletAttribute."
392+
);
393+
}
394+
395+
// The command name we provide for the temporary CmdletInfo isn't consumed
396+
// anywhere other than instantiation, but let's resolve it anyway:
397+
string commandName = $"{cmdletAttribute.VerbName}-{cmdletAttribute.NounName}";
398+
399+
CmdletInfo cmdletInfo = new CmdletInfo(commandName, this.GetType());
400+
return cmdletInfo.Parameters.Keys.ToArray();
401+
}
348402
}
349403
}

src/Resources/Resources.Test/ResourceGroupDeployments/NewAzureResourceGroupDeploymentCommandTests.cs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
using System.Collections;
3131
using FluentAssertions;
3232
using ProvisioningState = Microsoft.Azure.Commands.ResourceManager.Cmdlets.Entities.ProvisioningState;
33+
using Microsoft.Azure.Management.ResourceManager;
34+
using Newtonsoft.Json.Linq;
35+
using System.Threading;
36+
using System.Threading.Tasks;
37+
using Microsoft.Rest.Azure;
38+
using Newtonsoft.Json;
3339

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

4046
private Mock<ResourceManagerSdkClient> resourcesClientMock;
4147

48+
private Mock<ITemplateSpecsClient> templateSpecsClientMock;
49+
50+
private Mock<ITemplateSpecVersionsOperations> templateSpecsVersionOperationsMock;
51+
4252
private Mock<ICommandRuntime> commandRuntimeMock;
4353

4454
private string resourceGroupName = "myResourceGroup";
@@ -54,13 +64,20 @@ public class NewAzureResourceGroupDeploymentCommandTests : RMTestBase
5464
public NewAzureResourceGroupDeploymentCommandTests(ITestOutputHelper output)
5565
{
5666
resourcesClientMock = new Mock<ResourceManagerSdkClient>();
67+
68+
templateSpecsClientMock = new Mock<ITemplateSpecsClient>();
69+
templateSpecsClientMock.SetupAllProperties();
70+
templateSpecsVersionOperationsMock = new Mock<ITemplateSpecVersionsOperations>();
71+
templateSpecsClientMock.Setup(m => m.TemplateSpecVersions).Returns(templateSpecsVersionOperationsMock.Object);
72+
5773
XunitTracingInterceptor.AddToContext(new XunitTracingInterceptor(output));
5874
commandRuntimeMock = new Mock<ICommandRuntime>();
5975
SetupConfirmation(commandRuntimeMock);
6076
cmdlet = new NewAzureResourceGroupDeploymentCmdlet()
6177
{
6278
CommandRuntime = commandRuntimeMock.Object,
63-
ResourceManagerSdkClient = resourcesClientMock.Object
79+
ResourceManagerSdkClient = resourcesClientMock.Object,
80+
TemplateSpecsClient = templateSpecsClientMock.Object
6481
};
6582
}
6683

@@ -271,5 +288,73 @@ public void CreatesNewPSResourceGroupDeploymentWithUserTemplateEmptyRollback()
271288

272289
commandRuntimeMock.Verify(f => f.WriteObject(expected), Times.Once());
273290
}
291+
292+
/// <summary>
293+
/// When deployments are created using a template spec, the dynamic parameters are
294+
/// resolved by reading the parameters from the template spec version's template body.
295+
/// Previously a bug was present that prevented successful dynamic parameter resolution
296+
/// if the template spec existed in a subscription outside the current subscription
297+
/// context. This test validates dynamic parameter resolution works for deployments using
298+
/// cross-subscription template specs.
299+
/// </summary>
300+
301+
[Fact]
302+
[Trait(Category.AcceptanceType, Category.CheckIn)]
303+
public void ResolvesDynamicParametersWithCrossSubTemplateSpec()
304+
{
305+
const string templateSpecSubscriptionId = "10000000-0000-0000-0000-000000000000";
306+
const string templateSpecRGName = "someRG";
307+
const string templateSpecName = "myTemplateSpec";
308+
const string templateSpecVersion = "v1";
309+
310+
string templateSpecId = $"/subscriptions/{templateSpecSubscriptionId}/" +
311+
$"resourceGroups/{templateSpecRGName}/providers/Microsoft.Resources/" +
312+
$"templateSpecs/{templateSpecName }/versions/{templateSpecVersion}";
313+
314+
// Ensure our template file path is normalized for the current system:
315+
var normalizedTemplateFilePath = (Path.DirectorySeparatorChar != '\\')
316+
? templateFile.Replace('\\', Path.DirectorySeparatorChar) // Other/Unix based
317+
: templateFile; // Windows based (already valid)
318+
319+
var templateContentForTest = File.ReadAllText(normalizedTemplateFilePath);
320+
var template = JsonConvert.DeserializeObject<TemplateFile>(templateContentForTest);
321+
322+
templateSpecsVersionOperationsMock.Setup(f => f.GetWithHttpMessagesAsync(
323+
templateSpecRGName,
324+
templateSpecName,
325+
templateSpecVersion,
326+
null,
327+
new CancellationToken()))
328+
.Returns(() => {
329+
330+
// We should only be getting this template spec from the expected subscription:
331+
Assert.Equal(templateSpecSubscriptionId, templateSpecsClientMock.Object.SubscriptionId);
332+
333+
var versionToReturn = new TemplateSpecVersion(
334+
location: "westus2",
335+
id: templateSpecId,
336+
name: templateSpecVersion,
337+
type: "Microsoft.Resources/templateSpecs/versions",
338+
template: JObject.Parse(templateContentForTest)
339+
);
340+
341+
return Task.Factory.StartNew(() =>
342+
new AzureOperationResponse<TemplateSpecVersion>()
343+
{
344+
Body = versionToReturn
345+
}
346+
);
347+
});
348+
349+
cmdlet.ResourceGroupName = resourceGroupName;
350+
cmdlet.Name = deploymentName;
351+
cmdlet.TemplateSpecId = templateSpecId;
352+
353+
var dynamicParams = cmdlet.GetDynamicParameters() as RuntimeDefinedParameterDictionary;
354+
355+
dynamicParams.Should().NotBeNull();
356+
dynamicParams.Count().Should().Be(template.Parameters.Count);
357+
dynamicParams.Keys.Should().Contain(template.Parameters.Keys);
358+
}
274359
}
275360
}

src/Resources/Resources/ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* Fixed an issue where What-If shows two resource group scopes with different casing
2424
* Updated `Export-AzResourceGroup` to use the SDK.
2525
* Added culture info to parse methods
26+
* Fixed issue where attempts to deploy template specs from a subscription outside of the current subscription context would fail
2627
* Changed Double parser for version parser
2728

2829
## Version 3.0.0

0 commit comments

Comments
 (0)