Skip to content

Commit 5ff512b

Browse files
authored
[Resources] Add cmdlets for management group and tenant level deployment What-If (#12548)
* Implement spliting tenant and MG level resource IDs * Update Microsoft.Azure.Management.ResourceManager to 3.9.0-preview * Remove a test that might fail due to new line in the resource string * Re-record deployment tests * Fix What-If array property change format * Add MG-level and tenant-level What-If cmdlets * Add tests for PSJsonSerializer * Refactoring round 1 * Refactoring round 2 * Add e2e test for MG and tenant level What-If and fix errors * Refactoring round 3 * Cleanup code * Generate help docs * Update ChangeLog * Fix build error * Fix online version links * Fix macOS build error * Fix macOS build error second trial * Fix test errors * Move parameters back to concrete cmdlet classes
1 parent 7a91c0d commit 5ff512b

File tree

83 files changed

+451165
-523511
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+451165
-523511
lines changed

src/Resources/ResourceManager/Components/ResourceIdUtility.cs

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Components
2626
/// </summary>
2727
public static class ResourceIdUtility
2828
{
29+
private static readonly Regex ManagementGroupRegex =
30+
new Regex(@"^\/?providers\/Microsoft.Management\/managementGroups\/(?<managementGroupId>[\w\d_\.\(\)-]+)", RegexOptions.IgnoreCase);
31+
2932
private static readonly Regex SubscriptionRegex =
30-
new Regex(@"^\/?subscriptions\/(?<subscriptionId>[a-f0-9-]+)", RegexOptions.IgnoreCase);
33+
new Regex(@"^\/?subscriptions\/(?<subscriptionId>[\w\d-]+)", RegexOptions.IgnoreCase);
3134

3235
private static readonly Regex ResourceGroupRegex =
3336
new Regex(@"^\/resourceGroups\/(?<resourceGroupName>[-\w\._\(\)]+)", RegexOptions.IgnoreCase);
@@ -308,47 +311,49 @@ public static string GetExtensionResourceName(string resourceId)
308311
}
309312

310313
/// <summary>
311-
/// Parses a fully qualified resource ID.
314+
/// Split a fully qualified resource identifier into two parts (resource scope, relative resource identifier).
312315
/// </summary>
313-
/// <param name="fullyQualifiedResourceId">The fully qualified resource ID to parse. Only subscription resource IDs are supported for now.</param>
314-
/// <returns>The resource scope and the relative resource ID (resource provider/name).</returns>
315-
public static (string scope, string relativeResourceId) ParseResourceId(string fullyQualifiedResourceId)
316+
/// <param name="fullyQualifiedResourceId">The fully qualified resource identifier to split.</param>
317+
/// <returns>The resource scope and the relative resource identifier.</returns>
318+
public static (string scope, string relativeResourceId) SplitResourceId(string fullyQualifiedResourceId)
316319
{
317320
string remaining = fullyQualifiedResourceId;
318321

319-
// Parse subscriptionId.
322+
Match managementGroupMatch = ManagementGroupRegex.Match(remaining);
323+
string managementGroupId = managementGroupMatch.Groups["managementGroupId"].Value;
324+
remaining = remaining.Substring(managementGroupMatch.Length);
325+
320326
Match subscriptionMatch = SubscriptionRegex.Match(remaining);
321327
string subscriptionId = subscriptionMatch.Groups["subscriptionId"].Value;
322328
remaining = remaining.Substring(subscriptionMatch.Length);
323329

324-
// Parse resourceGroupName.
325330
Match resourceGroupMatch = ResourceGroupRegex.Match(remaining);
326331
string resourceGroupName = resourceGroupMatch.Groups["resourceGroupName"].Value;
327332
remaining = remaining.Substring(resourceGroupMatch.Length);
328333

329-
// Parse relativeResourceId.
330334
Match relativeResourceIdMatch = RelativeResourceIdRegex.Match(remaining);
331335
string relativeResourceId = relativeResourceIdMatch.Groups["relativeResourceId"].Value;
332336

333-
// The resourceId represents a resource group as a resource with
334-
// the format /subscription/{subscriptionId}/resourceGroups/{resourceGroupName},
335-
// which is a subscription-level resource ID. The resourceGroupName should belong to
336-
// the relativePath but not the scope.
337-
if (subscriptionMatch.Success && resourceGroupMatch.Success && !relativeResourceIdMatch.Success)
337+
if (managementGroupMatch.Success)
338338
{
339-
relativeResourceId = $"resourceGroups/{resourceGroupName}";
340-
resourceGroupName = string.Empty;
339+
return relativeResourceIdMatch.Success
340+
? ($"/providers/Microsoft.Management/ManagementGroups/{managementGroupId}", relativeResourceId)
341+
: ("/", $"Microsoft.Management/ManagementGroups/{managementGroupId}");
341342
}
342343

343-
// Construct scope.
344-
string scope = $"/subscriptions/{subscriptionId.ToLowerInvariant()}";
345-
346-
if (!string.IsNullOrEmpty(resourceGroupName))
344+
if (subscriptionMatch.Success)
347345
{
348-
scope += $"/resourceGroups/{resourceGroupName}";
346+
if (resourceGroupMatch.Success)
347+
{
348+
return relativeResourceIdMatch.Success
349+
? ($"/subscriptions/{subscriptionId.ToLowerInvariant()}/resourceGroups/{resourceGroupName}", relativeResourceId)
350+
: ($"/subscriptions/{subscriptionId.ToLowerInvariant()}", $"resourceGroups/{resourceGroupName}");
351+
}
352+
353+
return ($"/subscriptions/{subscriptionId.ToLowerInvariant()}", relativeResourceId);
349354
}
350355

351-
return (scope, relativeResourceId);
356+
return ("/", relativeResourceId);
352357
}
353358

354359
/// <summary>

src/Resources/ResourceManager/Formatters/WhatIfOperationResultFormatter.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ private WhatIfOperationResultFormatter(ColoredStringBuilder builder)
3434

3535
public static string Format(PSWhatIfOperationResult result)
3636
{
37+
if (result == null)
38+
{
39+
return null;
40+
}
41+
3742
var builder = new ColoredStringBuilder();
3843
var formatter = new WhatIfOperationResultFormatter(builder);
3944

@@ -319,6 +324,7 @@ private void FormatPropertyChange(PSWhatIfPropertyChange propertyChange, int max
319324
break;
320325

321326
case PropertyChangeType.Array:
327+
this.FormatPropertyChangePath(propertyChangeType, path, null, children, maxPathLength, indentLevel);
322328
this.FormatPropertyArrayChange(propertyChange.Children, indentLevel + 1);
323329
break;
324330

@@ -338,7 +344,7 @@ private void FormatPropertyChangePath(
338344
int paddingWidth = maxPathLength - path.Length + 1;
339345
bool hasChildren = children != null && children.Count > 0;
340346

341-
if (valueAfterPath.IsNonEmptyArray())
347+
if (valueAfterPath.IsNonEmptyArray() || (propertyChangeType == PropertyChangeType.Array && hasChildren))
342348
{
343349
paddingWidth = 1;
344350
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.CmdletBase
16+
{
17+
using System;
18+
using System.Management.Automation;
19+
using Microsoft.Azure.Commands.Common.Strategies;
20+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Formatters;
21+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Properties;
22+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels;
23+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments;
24+
25+
public abstract class DeploymentCreateCmdlet: DeploymentWhatIfCmdlet
26+
{
27+
protected abstract ConfirmImpact ConfirmImpact { get; }
28+
29+
protected abstract PSDeploymentCmdletParameters DeploymentParameters { get; }
30+
31+
protected override void OnProcessRecord()
32+
{
33+
string whatIfMessage = null;
34+
string warningMessage = null;
35+
string captionMessage = null;
36+
37+
if (this.ShouldExecuteWhatIf())
38+
{
39+
PSWhatIfOperationResult whatIfResult = this.ExecuteWhatIf();
40+
string whatIfFormattedOutput = WhatIfOperationResultFormatter.Format(whatIfResult);
41+
string cursorUp = $"{(char)27}[1A";
42+
43+
// Use \r to override the built-in "What if:" in output.
44+
whatIfMessage = $"\r \r{Environment.NewLine}{whatIfFormattedOutput}{Environment.NewLine}";
45+
warningMessage = $"{Environment.NewLine}{Resources.ConfirmDeploymentMessage}";
46+
captionMessage = $"{cursorUp}{Color.Reset}{whatIfMessage}";
47+
}
48+
49+
if (this.ShouldProcess(whatIfMessage, warningMessage, captionMessage))
50+
{
51+
this.ExecuteDeployment();
52+
}
53+
}
54+
55+
protected void ExecuteDeployment()
56+
{
57+
if (!string.IsNullOrEmpty(this.DeploymentParameters.DeploymentDebugLogLevel))
58+
{
59+
WriteWarning(Resources.WarnOnDeploymentDebugSetting);
60+
}
61+
62+
if (this.DeploymentParameters.ScopeType == DeploymentScopeType.ResourceGroup)
63+
{
64+
WriteObject(this.ResourceManagerSdkClient.ExecuteResourceGroupDeployment(this.DeploymentParameters));
65+
}
66+
else
67+
{
68+
WriteObject(this.ResourceManagerSdkClient.ExecuteDeployment(this.DeploymentParameters));
69+
}
70+
}
71+
72+
protected bool ShouldExecuteWhatIf()
73+
{
74+
return ShouldProcessGivenCurrentWhatIfFlagAndPreference()
75+
|| ShouldProcessGivenCurrentConfirmFlagAndPreference();
76+
}
77+
78+
private bool ShouldProcessGivenCurrentWhatIfFlagAndPreference()
79+
{
80+
if (this.MyInvocation.BoundParameters.GetOrNull("WhatIf") is SwitchParameter whatIfFlag)
81+
{
82+
return whatIfFlag.IsPresent;
83+
}
84+
85+
if (this.SessionState == null)
86+
{
87+
return false;
88+
}
89+
90+
return (bool)this.SessionState.PSVariable.GetValue("WhatIfPreference");
91+
}
92+
93+
private bool ShouldProcessGivenCurrentConfirmFlagAndPreference()
94+
{
95+
if (this.MyInvocation.BoundParameters.GetOrNull("Confirm") is SwitchParameter confirmFlag)
96+
{
97+
return confirmFlag.IsPresent;
98+
}
99+
100+
if (this.SessionState == null)
101+
{
102+
return false;
103+
}
104+
105+
var confirmPreference = (ConfirmImpact)this.SessionState.PSVariable.GetValue("ConfirmPreference");
106+
107+
return this.ConfirmImpact >= confirmPreference;
108+
}
109+
}
110+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.CmdletBase
16+
{
17+
using System;
18+
using System.Management.Automation;
19+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments;
20+
21+
public abstract class DeploymentWhatIfCmdlet: ResourceWithParameterCmdletBase, IDynamicParameters
22+
{
23+
protected abstract PSDeploymentWhatIfCmdletParameters WhatIfParameters { get; }
24+
25+
protected override void OnProcessRecord()
26+
{
27+
PSWhatIfOperationResult whatIfResult = this.ExecuteWhatIf();
28+
29+
this.WriteObject(whatIfResult);
30+
}
31+
32+
protected PSWhatIfOperationResult ExecuteWhatIf()
33+
{
34+
const string statusMessage = "Getting the latest status of all resources...";
35+
var clearMessage = new string(' ', statusMessage.Length);
36+
var information = new HostInformationMessage { Message = statusMessage, NoNewLine = true };
37+
var clearInformation = new HostInformationMessage { Message = $"\r{clearMessage}\r", NoNewLine = true };
38+
var tags = new[] { "PSHOST" };
39+
40+
try
41+
{
42+
// Write status message.
43+
this.WriteInformation(information, tags);
44+
45+
PSWhatIfOperationResult whatIfResult = ResourceManagerSdkClient.ExecuteDeploymentWhatIf(this.WhatIfParameters);
46+
47+
// Clear status before returning result.
48+
this.WriteInformation(clearInformation, tags);
49+
50+
return whatIfResult;
51+
}
52+
catch (Exception)
53+
{
54+
// Clear status before on exception.
55+
this.WriteInformation(clearInformation, tags);
56+
throw;
57+
}
58+
}
59+
}
60+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation
16+
{
17+
using System.Management.Automation;
18+
using Common;
19+
using Common.ArgumentCompleters;
20+
using Management.ResourceManager.Models;
21+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Attributes;
22+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.CmdletBase;
23+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments;
24+
using WindowsAzure.Commands.Utilities.Common;
25+
26+
/// <summary>
27+
/// Gets What-If results for a tenant-level deployment.
28+
/// </summary>
29+
[Cmdlet(VerbsCommon.Get, AzureRMConstants.AzureRMPrefix + "ManagementGroupDeploymentWhatIfResult",
30+
DefaultParameterSetName = ParameterlessTemplateFileParameterSetName),
31+
OutputType(typeof(PSWhatIfOperationResult))]
32+
public class GetAzureManagementGroupDeploymentWhatIfResultCmdlet : DeploymentWhatIfCmdlet
33+
{
34+
[Alias("DeploymentName")]
35+
[Parameter(Mandatory = false, HelpMessage = "The name of the deployment it's going to create. If not specified, defaults to the template file name when a template file is provided; defaults to the current time when a template object is provided, e.g. \"20131223140835\".")]
36+
[ValidateNotNullOrEmpty]
37+
public string Name { get; set; }
38+
39+
[Parameter(Mandatory = true, HelpMessage = "The management group ID.")]
40+
[ValidateNotNullOrEmpty]
41+
public string ManagementGroupId { get; set; }
42+
43+
[Parameter(Mandatory = true, HelpMessage = "The location to store deployment data.")]
44+
[LocationCompleter("Microsoft.Resources/resourceGroups")]
45+
[ValidateNotNullOrEmpty]
46+
public string Location { get; set; }
47+
48+
[Parameter(Mandatory = false, HelpMessage = "The What-If result format.")]
49+
public WhatIfResultFormat ResultFormat { get; set; } = WhatIfResultFormat.FullResourcePayloads;
50+
51+
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of resource change types to be excluded from What-If results.")]
52+
[ChangeTypeCompleter]
53+
[ValidateChangeTypes]
54+
public string[] ExcludeChangeType { get; set; }
55+
56+
protected override PSDeploymentWhatIfCmdletParameters WhatIfParameters => new PSDeploymentWhatIfCmdletParameters(
57+
scopeType: DeploymentScopeType.ManagementGroup,
58+
managementGroupId: this.ManagementGroupId,
59+
deploymentName: this.Name,
60+
location: this.Location,
61+
mode: DeploymentMode.Incremental,
62+
templateUri: this.TemplateUri ?? this.TryResolvePath(this.TemplateFile),
63+
templateObject: this.TemplateObject,
64+
templateParametersUri: this.TemplateParameterUri,
65+
templateParametersObject: GetTemplateParameterObject(this.TemplateParameterObject),
66+
resultFormat: this.ResultFormat,
67+
excludeChangeTypes: this.ExcludeChangeType);
68+
}
69+
}
70+
71+

0 commit comments

Comments
 (0)