diff --git a/src/Resources/ResourceManager/Formatters/ColoredStringBuilder.cs b/src/Resources/ResourceManager/Formatters/ColoredStringBuilder.cs index 9c34a1192f74..afbb8fd1e46b 100644 --- a/src/Resources/ResourceManager/Formatters/ColoredStringBuilder.cs +++ b/src/Resources/ResourceManager/Formatters/ColoredStringBuilder.cs @@ -24,6 +24,8 @@ public class ColoredStringBuilder private readonly Stack colorStack = new Stack(); + private readonly List indentStack = new List(); + public override string ToString() { return stringBuilder.ToString(); @@ -89,6 +91,78 @@ public AnsiColorScope NewColorScope(Color color) return new AnsiColorScope(this, color); } + public void Insert(int index, string value) + { + if (index >= 0 && index <= this.stringBuilder.Length) + { + this.stringBuilder.Insert(index, value); + } + } + + public void InsertLine(int index, string value, Color color) + { + if (color != Color.Reset) + { + this.Insert(index, color.ToString()); + } + this.Insert(index, value + Environment.NewLine); + if (color != Color.Reset) + { + this.Insert(index, Color.Reset.ToString()); + } + } + + public int GetCurrentIndex() + { + return this.stringBuilder.Length; + } + + public void PushIndent(string indent) + { + this.indentStack.Add(indent); + } + + public void PopIndent() + { + if (this.indentStack.Count > 0) + { + this.indentStack.RemoveAt(this.indentStack.Count - 1); + } + } + + public void EnsureNumNewLines(int numNewLines) + { + if (this.stringBuilder.Length == 0) + { + for (int i = 0; i < numNewLines; i++) + { + this.stringBuilder.AppendLine(); + } + return; + } + + string currentText = this.stringBuilder.ToString(); + int existingNewlines = 0; + + for (int i = currentText.Length - 1; i >= 0 && currentText[i] == '\n'; i--) + { + existingNewlines++; + } + + int remainingNewlines = numNewLines - existingNewlines; + for (int i = 0; i < remainingNewlines; i++) + { + this.stringBuilder.AppendLine(); + } + } + + public void Clear() + { + this.stringBuilder.Clear(); + this.colorStack.Clear(); + this.indentStack.Clear(); + } + private void PushColor(Color color) { this.colorStack.Push(color); @@ -101,7 +175,7 @@ private void PopColor() this.stringBuilder.Append(this.colorStack.Count > 0 ? this.colorStack.Peek() : Color.Reset); } - public class AnsiColorScope: IDisposable + public class AnsiColorScope : IDisposable { private readonly ColoredStringBuilder builder; @@ -117,4 +191,4 @@ public void Dispose() } } } -} +} \ No newline at end of file diff --git a/src/Resources/ResourceManager/Formatters/DeploymentStackWhatIfFormatter.cs b/src/Resources/ResourceManager/Formatters/DeploymentStackWhatIfFormatter.cs new file mode 100644 index 000000000000..5696163f3bf3 --- /dev/null +++ b/src/Resources/ResourceManager/Formatters/DeploymentStackWhatIfFormatter.cs @@ -0,0 +1,617 @@ +// ---------------------------------------------------------------------------------- +// +// 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.Cmdlets.Formatters +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments; + using Newtonsoft.Json; + + /// + /// Formatter for Deployment Stack What-If operation results. + /// Produces output matching Azure CLI format exactly. + /// + public class DeploymentStackWhatIfFormatter + { + private const int IndentSize = 2; + + private static readonly string[] AllWhatIfTopLevelChangeTypes = new[] + { + "Create", + "Unsupported", + "Modify", + "Delete", + "NoChange", + "Detach" + }; + + private static readonly Dictionary ChangeTypeSymbols = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Array", "~" }, + { "Create", "+" }, + { "Delete", "-" }, + { "Detach", "v" }, + { "Modify", "~" }, + { "NoChange", "=" }, + { "NoEffect", "=" }, + { "Unsupported", "!" } + }; + + private static readonly Dictionary ChangeTypeColors = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Array", Color.Purple }, + { "Create", Color.Green }, + { "Delete", Color.Red }, + { "Detach", Color.Blue }, + { "Modify", Color.Purple } + }; + + private readonly ColoredStringBuilder builder; + private PSDeploymentStackWhatIfResult whatIfResult; + private DeploymentStackWhatIfProperties whatIfProps; + private DeploymentStackWhatIfChanges whatIfChanges; + + public DeploymentStackWhatIfFormatter(ColoredStringBuilder builder) + { + this.builder = builder ?? new ColoredStringBuilder(); + } + + /// + /// Formats a Deployment Stack What-If result. + /// + public static string Format(PSDeploymentStackWhatIfResult result) + { + if (result == null) + { + return null; + } + + var builder = new ColoredStringBuilder(); + var formatter = new DeploymentStackWhatIfFormatter(builder); + + return formatter.FormatInternal(result); + } + + private string FormatInternal(PSDeploymentStackWhatIfResult result) + { + this.whatIfResult = result; + this.whatIfProps = result.Properties; + this.whatIfChanges = this.whatIfProps?.Changes; + + if (FormatChangeTypeLegend()) + { + this.builder.EnsureNumNewLines(2); + } + + if (FormatStackChanges()) + { + this.builder.EnsureNumNewLines(2); + } + + if (FormatResourceChangesAndDeletionSummary()) + { + this.builder.EnsureNumNewLines(2); + } + + FormatDiagnostics(); + + string output = this.builder.ToString(); + + this.whatIfResult = null; + this.whatIfProps = null; + this.whatIfChanges = null; + + return output; + } + + private bool FormatChangeTypeLegend() + { + const int changeTypeMaxLength = 20; + + this.builder.AppendLine("Resource and property changes are indicated with these symbols:"); + this.builder.PushIndent(new string(' ', IndentSize)); + + for (int i = 0; i < AllWhatIfTopLevelChangeTypes.Length; i++) + { + string changeType = AllWhatIfTopLevelChangeTypes[i]; + var (symbol, color) = GetChangeTypeFormatting(changeType); + + this.builder.Append(symbol, color).Append(" "); + this.builder.Append(changeType.PadRight(changeTypeMaxLength - symbol.Length)); + + if (i % 2 == 0 && i < AllWhatIfTopLevelChangeTypes.Length - 1) + { + this.builder.Append(" "); + } + else if (i < AllWhatIfTopLevelChangeTypes.Length - 1) + { + this.builder.AppendLine(); + } + } + + this.builder.PopIndent(); + this.builder.AppendLine(); + + return true; + } + + private bool FormatStackChanges() + { + if (this.whatIfChanges == null) + { + return false; + } + + bool printed = false; + int titleIndex = this.builder.GetCurrentIndex(); + + if (this.whatIfChanges.DeploymentScopeChange != null) + { + if (FormatPrimitiveChange(this.whatIfChanges.DeploymentScopeChange, "DeploymentScope")) + { + printed = true; + } + } + + if (this.whatIfChanges.DenySettingsChange != null) + { + if (FormatDenySettingsChange(this.whatIfChanges.DenySettingsChange)) + { + printed = true; + } + } + + if (printed) + { + this.builder.InsertLine(titleIndex, + $"Changes to Stack {this.whatIfProps.DeploymentStackResourceId}:", + Color.DarkYellow); + } + + return printed; + } + + private bool FormatDenySettingsChange(DeploymentStackChangeDeltaRecord denySettingsChange) + { + if (denySettingsChange?.Delta == null || denySettingsChange.Delta.Count == 0) + { + return false; + } + + bool printed = false; + + foreach (var change in denySettingsChange.Delta) + { + string fullPath = $"DenySettings.{change.Path}"; + + if (string.Equals(change.ChangeType, "Array", StringComparison.OrdinalIgnoreCase)) + { + if (FormatArrayChange(change, fullPath)) + { + printed = true; + } + } + else + { + if (FormatPrimitiveChange(change, fullPath)) + { + printed = true; + } + } + } + + return printed; + } + + private bool FormatArrayChange(DeploymentStackPropertyChange arrayChange, string path) + { + var (symbol, color) = GetChangeTypeFormatting("Modify"); + + this.builder.Append(symbol, color).Append(" ").Append(path).AppendLine(": ", color); + this.builder.PushIndent(new string(' ', IndentSize)); + + if (arrayChange.Children != null && arrayChange.Children.Count > 0) + { + bool hasIndices = arrayChange.Children.All(c => !string.IsNullOrEmpty(c.Path)); + + var sortedChildren = hasIndices + ? arrayChange.Children.OrderBy(c => int.TryParse(c.Path, out int idx) ? idx : int.MaxValue).ToList() + : arrayChange.Children.ToList(); + + foreach (var child in sortedChildren) + { + if (hasIndices) + { + var (childSymbol, childColor) = GetChangeTypeFormatting(child.ChangeType); + this.builder.Append(childSymbol, childColor).AppendLine($" {child.Path}:"); + this.builder.PushIndent(new string(' ', IndentSize)); + + FormatPrimitiveValue(child); + + this.builder.PopIndent(); + } + else + { + FormatPrimitiveValue(child); + } + } + } + + this.builder.PopIndent(); + + return true; + } + + private void FormatPrimitiveValue(DeploymentStackPropertyChange change) + { + var (symbol, color) = GetChangeTypeFormatting(change.ChangeType); + this.builder.Append(symbol, color).Append(" ").AppendLine(FormatValue(change.After), color); + } + + private bool FormatResourceChangesAndDeletionSummary() + { + if (this.whatIfChanges?.ResourceChanges == null || this.whatIfChanges.ResourceChanges.Count == 0) + { + return false; + } + + bool printed = false; + var resourceChangesSorted = SortResourceChanges(this.whatIfChanges.ResourceChanges); + + if (FormatResourceChanges(resourceChangesSorted)) + { + printed = true; + } + + if (FormatResourceDeletionsSummary(resourceChangesSorted)) + { + printed = true; + } + + return printed; + } + + private List SortResourceChanges( + IList resourceChanges) + { + return resourceChanges + .OrderBy(x => string.IsNullOrEmpty(x.Id) ? 1 : 0) + .ThenBy(x => GetChangeCertaintyPriority(x.ChangeCertainty)) + .ThenBy(x => x.Id?.ToLowerInvariant() ?? "") + .ThenBy(x => x.Extension?.Name ?? "") + .ThenBy(x => x.Extension?.Version ?? "") + .ToList(); + } + + private int GetChangeCertaintyPriority(string certainty) + { + return string.Equals(certainty, "Definite", StringComparison.OrdinalIgnoreCase) ? 0 : 1; + } + + private bool FormatResourceChanges(List resourceChangesSorted) + { + if (resourceChangesSorted == null || resourceChangesSorted.Count == 0) + { + return false; + } + + this.builder.AppendLine("Changes to Managed Resources:", Color.DarkYellow); + this.builder.AppendLine(); + + string lastGroup = null; + bool hasPotentialChanges = false; + + foreach (var change in resourceChangesSorted) + { + string group = FormatResourceClassHeader(change); + + if (group != lastGroup) + { + lastGroup = group; + hasPotentialChanges = false; + this.builder.AppendLine(group); + } + + if (!hasPotentialChanges && + string.Equals(change.ChangeCertainty, "Potential", StringComparison.OrdinalIgnoreCase)) + { + this.builder.Append(">> ").AppendLine( + "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", + Color.Purple); + hasPotentialChanges = true; + } + + FormatResourceChange(change); + } + + return true; + } + + private void FormatResourceChange(DeploymentStackResourceChange resourceChange) + { + FormatResourceHeadingLine(resourceChange); + + this.builder.PushIndent(new string(' ', IndentSize)); + + if (resourceChange.ManagementStatusChange != null) + { + FormatPrimitiveChange(resourceChange.ManagementStatusChange, "Management Status"); + } + + if (resourceChange.DenyStatusChange != null) + { + FormatPrimitiveChange(resourceChange.DenyStatusChange, "Deny Status"); + } + + if (resourceChange.ResourceConfigurationChanges?.Delta != null) + { + foreach (var delta in resourceChange.ResourceConfigurationChanges.Delta) + { + FormatPrimitiveChange(delta, delta.Path); + } + } + + this.builder.PopIndent(); + } + + private void FormatResourceHeadingLine(DeploymentStackResourceChange resourceChange) + { + var (symbol, color) = GetChangeTypeFormatting(resourceChange.ChangeType); + bool isPotential = string.Equals(resourceChange.ChangeCertainty, "Potential", StringComparison.OrdinalIgnoreCase); + + if (isPotential) + { + this.builder.Append("?", Color.Cyan); + } + + this.builder.Append(symbol, color).Append(" "); + + if (isPotential) + { + this.builder.Append("[Potential] ", Color.Cyan); + } + + string resourceId = !string.IsNullOrEmpty(resourceChange.Id) + ? resourceChange.Id + : $"{resourceChange.Type} {FormatExtResourceIdentifiers(resourceChange.Identifiers)}"; + + this.builder.AppendLine(resourceId, color); + } + + private bool FormatResourceDeletionsSummary(List resourceChangesSorted) + { + var deleteChanges = resourceChangesSorted + .Where(x => string.Equals(x.ChangeType, "Delete", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (deleteChanges.Count == 0) + { + return false; + } + + this.builder.Append("Deleting - ", Color.Red); + this.builder.AppendLine($"Resources Marked for Deletion {deleteChanges.Count} total:"); + this.builder.AppendLine(); + + string lastGroup = null; + bool hasPotentialDeletions = false; + + foreach (var change in deleteChanges) + { + string group = FormatResourceClassHeader(change); + + if (group != lastGroup) + { + lastGroup = group; + hasPotentialDeletions = false; + this.builder.AppendLine(group); + } + + if (!hasPotentialDeletions && + string.Equals(change.ChangeCertainty, "Potential", StringComparison.OrdinalIgnoreCase)) + { + int numPotential = deleteChanges.Skip(deleteChanges.IndexOf(change)) + .TakeWhile(c => string.Equals(c.ChangeCertainty, "Potential", StringComparison.OrdinalIgnoreCase)) + .Count(); + + this.builder.Append(">> ").AppendLine( + $"Potential Deletions {numPotential} total (Learn more at https://aka.ms/whatIfPotentialChanges)", + Color.Red); + hasPotentialDeletions = true; + } + + FormatResourceHeadingLine(change); + } + + return true; + } + + private void FormatDiagnostics() + { + if (this.whatIfProps?.Diagnostics == null || this.whatIfProps.Diagnostics.Count == 0) + { + return; + } + + var diagnosticsSorted = this.whatIfProps.Diagnostics + .OrderBy(x => GetDiagnosticLevelPriority(x.Level)) + .ThenBy(x => x.Code ?? "") + .ToList(); + + this.builder.AppendLine($"Diagnostics ({diagnosticsSorted.Count}):"); + + foreach (var diagnostic in diagnosticsSorted) + { + Color color = GetDiagnosticColor(diagnostic.Level); + this.builder.AppendLine( + $"{diagnostic.Level?.ToUpperInvariant()}: [{diagnostic.Code}] {diagnostic.Message}", + color); + } + } + + private bool FormatPrimitiveChange(object change, string path) + { + var baseChange = change as DeploymentStackChangeBase; + var propertyChange = change as DeploymentStackPropertyChange; + + if (baseChange == null && propertyChange == null) + { + return false; + } + + object before = baseChange?.Before ?? propertyChange?.Before; + object after = baseChange?.After ?? propertyChange?.After; + string changeType = baseChange?.ChangeType ?? propertyChange?.ChangeType; + + if (changeType == null) + { + changeType = Equals(before, after) ? "NoEffect" : "Modify"; + } + + var (symbol, color) = GetChangeTypeFormatting(changeType); + + this.builder.Append(symbol, color).Append(" "); + this.builder.Append(path).Append(": "); + + if (string.Equals(changeType, "Modify", StringComparison.OrdinalIgnoreCase)) + { + this.builder.AppendLine($"{FormatValue(before)} => {FormatValue(after)}", color); + } + else + { + object value = string.Equals(changeType, "Delete", StringComparison.OrdinalIgnoreCase) ? before : after; + this.builder.AppendLine(FormatValue(value)); + } + + return true; + } + + private static string FormatValue(object value) + { + if (value == null) + { + return "null"; + } + + if (value is string strValue) + { + return $"\"{strValue}\""; + } + + if (value is bool boolValue) + { + return $"\"{(boolValue ? "True" : "False")}\""; + } + + return value.ToString(); + } + + private static (string symbol, Color color) GetChangeTypeFormatting(string changeType) + { + if (changeType == null) + { + return (null, Color.Reset); + } + + string symbol = ChangeTypeSymbols.GetValueOrDefault(changeType, "?"); + Color color = ChangeTypeColors.GetValueOrDefault(changeType, Color.Reset); + + return (symbol, color); + } + + private static string FormatResourceClassHeader(DeploymentStackResourceChange change) + { + if (!string.IsNullOrEmpty(change.Id)) + { + return "Azure"; + } + + if (change.Extension == null) + { + return "Unknown"; + } + + string result = $"{change.Extension.Name}@{change.Extension.Version}"; + + if (change.Extension.Config != null && change.Extension.Config.Count > 0) + { + var configParts = new List(); + + foreach (var kvp in change.Extension.Config.OrderBy(c => c.Value?.KeyVaultReference != null).ThenBy(c => c.Key)) + { + if (kvp.Value == null) + { + continue; + } + + if (kvp.Value.KeyVaultReference != null) + { + string secretName = kvp.Value.KeyVaultReference.SecretName; + string secretVersion = kvp.Value.KeyVaultReference.SecretVersion; + string kvId = kvp.Value.KeyVaultReference.KeyVault?.Id; + string versionSuffix = !string.IsNullOrEmpty(secretVersion) ? $"@{secretVersion}" : ""; + + configParts.Add($"{kvp.Key}="); + } + else if (kvp.Value.Value != null) + { + string jsonValue = JsonConvert.SerializeObject(kvp.Value.Value); + configParts.Add($"{kvp.Key}={jsonValue}"); + } + } + + if (configParts.Count > 0) + { + result += $" {string.Join(", ", configParts)}"; + } + } + + return result; + } + + private static string FormatExtResourceIdentifiers(IDictionary identifiers) + { + if (identifiers == null || identifiers.Count == 0) + { + return string.Empty; + } + + return string.Join(", ", identifiers.OrderBy(kvp => kvp.Key) + .Select(kvp => $"{kvp.Key}={JsonConvert.SerializeObject(kvp.Value)}")); + } + + private static int GetDiagnosticLevelPriority(string level) + { + return level?.ToLowerInvariant() switch + { + "info" => 1, + "warning" => 2, + "error" => 3, + _ => 0 + }; + } + + private static Color GetDiagnosticColor(string level) + { + return level?.ToLowerInvariant() switch + { + "warning" => Color.DarkYellow, + "error" => Color.Red, + _ => Color.Reset + }; + } + } +} \ No newline at end of file diff --git a/src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStackWhatIfCmdlet.cs b/src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStackWhatIfCmdlet.cs new file mode 100644 index 000000000000..d3c4decfb984 --- /dev/null +++ b/src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStackWhatIfCmdlet.cs @@ -0,0 +1,69 @@ +// ---------------------------------------------------------------------------------- +// +// 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.Cmdlets.Implementation.CmdletBase +{ + using System; + using System.Management.Automation; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments; + + /// + /// Base class for Deployment Stack What-If cmdlets. + /// + public abstract class DeploymentStackWhatIfCmdlet : DeploymentStacksCreateCmdletBase + { + /// + /// It's important not to call this function more than once during an invocation, as it can call the Bicep CLI. + /// This is slow, and can also cause diagnostics to be emitted multiple times. + /// + protected abstract PSDeploymentStackWhatIfParameters BuildWhatIfParameters(); + + protected override void OnProcessRecord() + { + PSDeploymentStackWhatIfResult whatIfResult = this.ExecuteWhatIf(); + + // The ToString() method on PSDeploymentStackWhatIfResult calls the formatter + this.WriteObject(whatIfResult); + } + + protected PSDeploymentStackWhatIfResult ExecuteWhatIf() + { + const string statusMessage = "Getting the latest status of all resources..."; + var clearMessage = new string(' ', statusMessage.Length); + var information = new HostInformationMessage { Message = statusMessage, NoNewLine = true }; + var clearInformation = new HostInformationMessage { Message = $"\r{clearMessage}\r", NoNewLine = true }; + var tags = new[] { "PSHOST" }; + + try + { + // Write status message. + this.WriteInformation(information, tags); + + var parameters = this.BuildWhatIfParameters(); + var whatIfResult = DeploymentStacksSdkClient.ExecuteDeploymentStackWhatIf(parameters); + + // Clear status before returning result. + this.WriteInformation(clearInformation, tags); + + return whatIfResult; + } + catch (Exception) + { + // Clear status on exception. + this.WriteInformation(clearInformation, tags); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStacksCmdletBase.cs b/src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStacksCmdletBase.cs index 2b275d0edb58..067d28b6289b 100644 --- a/src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStacksCmdletBase.cs +++ b/src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStacksCmdletBase.cs @@ -16,6 +16,7 @@ using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkClient; using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities; using Microsoft.WindowsAzure.Commands.Utilities.Common; +using Newtonsoft.Json; using System.Collections; using System.Collections.Generic; using System.IO; diff --git a/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzManagementGroupDeploymentStackWhatIf.cs b/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzManagementGroupDeploymentStackWhatIf.cs new file mode 100644 index 000000000000..18352865fbbc --- /dev/null +++ b/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzManagementGroupDeploymentStackWhatIf.cs @@ -0,0 +1,107 @@ +// ---------------------------------------------------------------------------------- +// +// 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.Cmdlets.Implementation.DeploymentStacks +{ + using System.Management.Automation; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.CmdletBase; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.DeploymentStacks; + using Microsoft.Azure.Commands.ResourceManager.Common; + + /// + /// Cmdlet to preview changes for a Management Group Deployment Stack. + /// + [Cmdlet("Get", AzureRMConstants.AzureRMPrefix + "ManagementGroupDeploymentStackWhatIf", + DefaultParameterSetName = ParameterlessTemplateFileParameterSetName)] + [OutputType(typeof(PSDeploymentStackWhatIfResult))] + public class GetAzManagementGroupDeploymentStackWhatIf : DeploymentStackWhatIfCmdlet + { + #region Cmdlet Parameters + + [Alias("StackName")] + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The name of the DeploymentStack to preview changes for.")] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The ID of the target management group.")] + [ValidateNotNullOrEmpty] + public string ManagementGroupId { get; set; } + + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The location to store deployment data.")] + [ValidateNotNullOrEmpty] + public string Location { get; set; } + + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, + HelpMessage = "Description for the stack.")] + public string Description { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "The scope for the deployment stack. Determines where managed resources can be deployed.")] + public string DeploymentScope { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Action to take on resources that become unmanaged. Possible values include: " + + "'detachAll', 'deleteResources', and 'deleteAll'.")] + public PSActionOnUnmanage ActionOnUnmanage { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Mode for DenySettings. Possible values include: 'denyDelete', 'denyWriteAndDelete', and 'none'.")] + public PSDenySettingsMode DenySettingsMode { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "List of AAD principal IDs excluded from the lock. Up to 5 principals are permitted.")] + public string[] DenySettingsExcludedPrincipal { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "List of role-based management operations excluded from the denySettings. Up to 200 actions are permitted.")] + public string[] DenySettingsExcludedAction { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Apply to child scopes.")] + public SwitchParameter DenySettingsApplyToChildScopes { get; set; } + + #endregion + + #region Cmdlet Implementation + + protected override PSDeploymentStackWhatIfParameters BuildWhatIfParameters() + { + var shouldDeleteResources = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll || ActionOnUnmanage is PSActionOnUnmanage.DeleteResources); + var shouldDeleteResourceGroups = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll); + var shouldDeleteManagementGroups = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll); + + return new PSDeploymentStackWhatIfParameters + { + StackName = Name, + ManagementGroupId = ManagementGroupId, + Location = Location, + TemplateFile = TemplateFile, + TemplateUri = !string.IsNullOrEmpty(protectedTemplateUri) ? protectedTemplateUri : TemplateUri, + TemplateSpecId = TemplateSpecId, + TemplateObject = TemplateObject, + TemplateParameterUri = TemplateParameterUri, + TemplateParameterObject = GetTemplateParameterObject(), + Description = Description, + DeploymentScope = DeploymentScope, + ResourcesCleanupAction = shouldDeleteResources ? "delete" : "detach", + ResourceGroupsCleanupAction = shouldDeleteResourceGroups ? "delete" : "detach", + ManagementGroupsCleanupAction = shouldDeleteManagementGroups ? "delete" : "detach", + DenySettingsMode = DenySettingsMode?.ToString(), + DenySettingsExcludedPrincipals = DenySettingsExcludedPrincipal, + DenySettingsExcludedActions = DenySettingsExcludedAction, + DenySettingsApplyToChildScopes = DenySettingsApplyToChildScopes.IsPresent + }; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzResourceGroupDeploymentStackWhatIf.cs b/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzResourceGroupDeploymentStackWhatIf.cs new file mode 100644 index 000000000000..b974e5378a06 --- /dev/null +++ b/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzResourceGroupDeploymentStackWhatIf.cs @@ -0,0 +1,99 @@ +// ---------------------------------------------------------------------------------- +// +// 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.Cmdlets.Implementation.DeploymentStacks +{ + using System.Management.Automation; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.CmdletBase; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.DeploymentStacks; + using Microsoft.Azure.Commands.ResourceManager.Common; + using Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters; + + /// + /// Cmdlet to preview changes for a Resource Group Deployment Stack. + /// + [Cmdlet("Get", AzureRMConstants.AzureRMPrefix + "ResourceGroupDeploymentStackWhatIf", + DefaultParameterSetName = ParameterlessTemplateFileParameterSetName)] + [OutputType(typeof(PSDeploymentStackWhatIfResult))] + public class GetAzResourceGroupDeploymentStackWhatIf : DeploymentStackWhatIfCmdlet + { + #region Cmdlet Parameters + + [Alias("StackName")] + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The name of the DeploymentStack to preview changes for.")] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The name of the ResourceGroup.")] + [ResourceGroupCompleter] + [ValidateNotNullOrEmpty] + public string ResourceGroupName { get; set; } + + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, + HelpMessage = "Description for the stack.")] + public string Description { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Action to take on resources that become unmanaged. Possible values include: " + + "'detachAll', 'deleteResources', and 'deleteAll'.")] + public PSActionOnUnmanage ActionOnUnmanage { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Mode for DenySettings. Possible values include: 'denyDelete', 'denyWriteAndDelete', and 'none'.")] + public PSDenySettingsMode DenySettingsMode { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "List of AAD principal IDs excluded from the lock. Up to 5 principals are permitted.")] + public string[] DenySettingsExcludedPrincipal { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "List of role-based management operations excluded from the denySettings. Up to 200 actions are permitted.")] + public string[] DenySettingsExcludedAction { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Apply to child scopes.")] + public SwitchParameter DenySettingsApplyToChildScopes { get; set; } + + #endregion + + #region Cmdlet Implementation + + protected override PSDeploymentStackWhatIfParameters BuildWhatIfParameters() + { + var shouldDeleteResources = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll || ActionOnUnmanage is PSActionOnUnmanage.DeleteResources); + var shouldDeleteResourceGroups = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll); + var shouldDeleteManagementGroups = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll); + + return new PSDeploymentStackWhatIfParameters + { + StackName = Name, + ResourceGroupName = ResourceGroupName, + TemplateFile = TemplateFile, + TemplateUri = !string.IsNullOrEmpty(protectedTemplateUri) ? protectedTemplateUri : TemplateUri, + TemplateSpecId = TemplateSpecId, + TemplateObject = TemplateObject, + TemplateParameterUri = TemplateParameterUri, + TemplateParameterObject = GetTemplateParameterObject(), + Description = Description, + ResourcesCleanupAction = shouldDeleteResources ? "delete" : "detach", + ResourceGroupsCleanupAction = shouldDeleteResourceGroups ? "delete" : "detach", + ManagementGroupsCleanupAction = shouldDeleteManagementGroups ? "delete" : "detach", + DenySettingsMode = DenySettingsMode?.ToString(), + DenySettingsExcludedPrincipals = DenySettingsExcludedPrincipal, + DenySettingsExcludedActions = DenySettingsExcludedAction, + DenySettingsApplyToChildScopes = DenySettingsApplyToChildScopes.IsPresent + }; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzSubscriptionDeploymentStackWhatIf.cs b/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzSubscriptionDeploymentStackWhatIf.cs new file mode 100644 index 000000000000..d88f4f8cf9ba --- /dev/null +++ b/src/Resources/ResourceManager/Implementation/DeploymentStacks/GetAzSubscriptionDeploymentStackWhatIf.cs @@ -0,0 +1,101 @@ +// ---------------------------------------------------------------------------------- +// +// 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.Cmdlets.Implementation.DeploymentStacks +{ + using System.Management.Automation; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.CmdletBase; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.DeploymentStacks; + using Microsoft.Azure.Commands.ResourceManager.Common; + + /// + /// Cmdlet to preview changes for a Subscription Deployment Stack. + /// + [Cmdlet("Get", AzureRMConstants.AzureRMPrefix + "SubscriptionDeploymentStackWhatIf", + DefaultParameterSetName = ParameterlessTemplateFileParameterSetName)] + [OutputType(typeof(PSDeploymentStackWhatIfResult))] + public class GetAzSubscriptionDeploymentStackWhatIf : DeploymentStackWhatIfCmdlet + { + #region Cmdlet Parameters + + [Alias("StackName")] + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The name of the DeploymentStack to preview changes for.")] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, + HelpMessage = "The location to store deployment data.")] + [ValidateNotNullOrEmpty] + public string Location { get; set; } + + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, + HelpMessage = "Description for the stack.")] + public string Description { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "The scope for the deployment stack. Determines where managed resources can be deployed.")] + public string DeploymentScope { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Action to take on resources that become unmanaged. Possible values include: " + + "'detachAll', 'deleteResources', and 'deleteAll'.")] + public PSActionOnUnmanage ActionOnUnmanage { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Mode for DenySettings. Possible values include: 'denyDelete', 'denyWriteAndDelete', and 'none'.")] + public PSDenySettingsMode DenySettingsMode { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "List of AAD principal IDs excluded from the lock. Up to 5 principals are permitted.")] + public string[] DenySettingsExcludedPrincipal { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "List of role-based management operations excluded from the denySettings. Up to 200 actions are permitted.")] + public string[] DenySettingsExcludedAction { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Apply to child scopes.")] + public SwitchParameter DenySettingsApplyToChildScopes { get; set; } + + #endregion + + #region Cmdlet Implementation + + protected override PSDeploymentStackWhatIfParameters BuildWhatIfParameters() + { + var shouldDeleteResources = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll || ActionOnUnmanage is PSActionOnUnmanage.DeleteResources); + var shouldDeleteResourceGroups = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll); + var shouldDeleteManagementGroups = (ActionOnUnmanage is PSActionOnUnmanage.DeleteAll); + + return new PSDeploymentStackWhatIfParameters + { + StackName = Name, + Location = Location, + TemplateFile = TemplateFile, + TemplateUri = !string.IsNullOrEmpty(protectedTemplateUri) ? protectedTemplateUri : TemplateUri, + TemplateSpecId = TemplateSpecId, + TemplateObject = TemplateObject, + TemplateParameterUri = TemplateParameterUri, + TemplateParameterObject = GetTemplateParameterObject(), + Description = Description, + DeploymentScope = DeploymentScope, + ResourcesCleanupAction = shouldDeleteResources ? "delete" : "detach", + ResourceGroupsCleanupAction = shouldDeleteResourceGroups ? "delete" : "detach", + ManagementGroupsCleanupAction = shouldDeleteManagementGroups ? "delete" : "detach", + DenySettingsMode = DenySettingsMode?.ToString(), + DenySettingsExcludedPrincipals = DenySettingsExcludedPrincipal, + DenySettingsExcludedActions = DenySettingsExcludedAction, + DenySettingsApplyToChildScopes = DenySettingsApplyToChildScopes.IsPresent + }; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Resources/ResourceManager/SdkClient/DeploymentStacksSdkClient.cs b/src/Resources/ResourceManager/SdkClient/DeploymentStacksSdkClient.cs index 6135e11e029b..a4a3b0bd6943 100644 --- a/src/Resources/ResourceManager/SdkClient/DeploymentStacksSdkClient.cs +++ b/src/Resources/ResourceManager/SdkClient/DeploymentStacksSdkClient.cs @@ -1148,5 +1148,390 @@ private IList ConvertCloudErrorListToErrorDetailList(IList + /// Executes a what-if operation for a deployment stack. + /// Main entry point that determines scope and routes to appropriate method. + /// + public PSDeploymentStackWhatIfResult ExecuteDeploymentStackWhatIf(PSDeploymentStackWhatIfParameters parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + // Determine scope and call appropriate method + if (!string.IsNullOrEmpty(parameters.ResourceGroupName)) + { + return ExecuteResourceGroupDeploymentStackWhatIf( + parameters.StackName, + parameters.ResourceGroupName, + parameters.TemplateFile, + parameters.TemplateUri, + parameters.TemplateSpecId, + parameters.TemplateObject, + parameters.TemplateParameterUri, + parameters.TemplateParameterObject, + parameters.Description, + parameters.ResourcesCleanupAction, + parameters.ResourceGroupsCleanupAction, + parameters.ManagementGroupsCleanupAction, + parameters.DenySettingsMode, + parameters.DenySettingsExcludedPrincipals, + parameters.DenySettingsExcludedActions, + parameters.DenySettingsApplyToChildScopes, + false); + } + else if (!string.IsNullOrEmpty(parameters.ManagementGroupId)) + { + return ExecuteManagementGroupDeploymentStackWhatIf( + parameters.StackName, + parameters.ManagementGroupId, + parameters.Location, + parameters.TemplateFile, + parameters.TemplateUri, + parameters.TemplateSpecId, + parameters.TemplateObject, + parameters.TemplateParameterUri, + parameters.TemplateParameterObject, + parameters.Description, + parameters.ResourcesCleanupAction, + parameters.ResourceGroupsCleanupAction, + parameters.ManagementGroupsCleanupAction, + parameters.DeploymentScope, + parameters.DenySettingsMode, + parameters.DenySettingsExcludedPrincipals, + parameters.DenySettingsExcludedActions, + parameters.DenySettingsApplyToChildScopes, + false); + } + else + { + // Subscription scope + return ExecuteSubscriptionDeploymentStackWhatIf( + parameters.StackName, + parameters.Location, + parameters.TemplateFile, + parameters.TemplateUri, + parameters.TemplateSpecId, + parameters.TemplateObject, + parameters.TemplateParameterUri, + parameters.TemplateParameterObject, + parameters.Description, + parameters.ResourcesCleanupAction, + parameters.ResourceGroupsCleanupAction, + parameters.ManagementGroupsCleanupAction, + parameters.DeploymentScope, + parameters.DenySettingsMode, + parameters.DenySettingsExcludedPrincipals, + parameters.DenySettingsExcludedActions, + parameters.DenySettingsApplyToChildScopes, + false); + } + } + + /// + /// Executes a what-if operation for a deployment stack at resource group scope. + /// + public PSDeploymentStackWhatIfResult ExecuteResourceGroupDeploymentStackWhatIf( + string deploymentStackName, + string resourceGroupName, + string templateFile, + string templateUri, + string templateSpec, + Hashtable templateObject, + string parameterUri, + Hashtable parameters, + string description, + string resourcesCleanupAction, + string resourceGroupsCleanupAction, + string managementGroupsCleanupAction, + string denySettingsMode, + string[] denySettingsExcludedPrincipals, + string[] denySettingsExcludedActions, + bool denySettingsApplyToChildScopes, + bool bypassStackOutOfSyncError) + { + // Create the deployment stack model + var deploymentStackModel = CreateDeploymentStackModel( + location: null, + templateFile, + templateUri, + templateSpec, + templateObject, + parameterUri, + parameters, + description, + resourcesCleanupAction, + resourceGroupsCleanupAction, + managementGroupsCleanupAction, + deploymentScope: null, + denySettingsMode, + denySettingsExcludedPrincipals, + denySettingsExcludedActions, + denySettingsApplyToChildScopes, + tags: null, + bypassStackOutOfSyncError); + + WriteVerbose($"Starting what-if operation for deployment stack '{deploymentStackName}' in resource group '{resourceGroupName}'"); + + // Call the what-if API - this returns a long-running operation + var whatIfOperation = DeploymentStacksClient.DeploymentStacks.BeginWhatIfAtResourceGroup( + resourceGroupName, + deploymentStackName, + deploymentStackModel); + + WriteVerbose("What-if operation started, waiting for completion..."); + + // Poll for completion + var whatIfResult = WaitForWhatIfCompletion( + () => DeploymentStacksClient.DeploymentStacks.GetWhatIfResultAtResourceGroup(resourceGroupName, deploymentStackName)); + + WriteVerbose("What-if operation completed"); + + return ConvertToDeploymentStackWhatIfResult(whatIfResult); + } + + /// + /// Executes a what-if operation for a deployment stack at subscription scope. + /// + public PSDeploymentStackWhatIfResult ExecuteSubscriptionDeploymentStackWhatIf( + string deploymentStackName, + string location, + string templateFile, + string templateUri, + string templateSpec, + Hashtable templateObject, + string parameterUri, + Hashtable parameters, + string description, + string resourcesCleanupAction, + string resourceGroupsCleanupAction, + string managementGroupsCleanupAction, + string deploymentScope, + string denySettingsMode, + string[] denySettingsExcludedPrincipals, + string[] denySettingsExcludedActions, + bool denySettingsApplyToChildScopes, + bool bypassStackOutOfSyncError) + { + var deploymentStackModel = CreateDeploymentStackModel( + location, + templateFile, + templateUri, + templateSpec, + templateObject, + parameterUri, + parameters, + description, + resourcesCleanupAction, + resourceGroupsCleanupAction, + managementGroupsCleanupAction, + deploymentScope, + denySettingsMode, + denySettingsExcludedPrincipals, + denySettingsExcludedActions, + denySettingsApplyToChildScopes, + tags: null, + bypassStackOutOfSyncError); + + WriteVerbose($"Starting what-if operation for deployment stack '{deploymentStackName}' at subscription scope"); + + var whatIfOperation = DeploymentStacksClient.DeploymentStacks.BeginWhatIfAtSubscription( + deploymentStackName, + deploymentStackModel); + + WriteVerbose("What-if operation started, waiting for completion..."); + + var whatIfResult = WaitForWhatIfCompletion( + () => DeploymentStacksClient.DeploymentStacks.GetWhatIfResultAtSubscription(deploymentStackName)); + + WriteVerbose("What-if operation completed"); + + return ConvertToDeploymentStackWhatIfResult(whatIfResult); + } + + /// + /// Executes a what-if operation for a deployment stack at management group scope. + /// + public PSDeploymentStackWhatIfResult ExecuteManagementGroupDeploymentStackWhatIf( + string deploymentStackName, + string managementGroupId, + string location, + string templateFile, + string templateUri, + string templateSpec, + Hashtable templateObject, + string parameterUri, + Hashtable parameters, + string description, + string resourcesCleanupAction, + string resourceGroupsCleanupAction, + string managementGroupsCleanupAction, + string deploymentScope, + string denySettingsMode, + string[] denySettingsExcludedPrincipals, + string[] denySettingsExcludedActions, + bool denySettingsApplyToChildScopes, + bool bypassStackOutOfSyncError) + { + var deploymentStackModel = CreateDeploymentStackModel( + location, + templateFile, + templateUri, + templateSpec, + templateObject, + parameterUri, + parameters, + description, + resourcesCleanupAction, + resourceGroupsCleanupAction, + managementGroupsCleanupAction, + deploymentScope, + denySettingsMode, + denySettingsExcludedPrincipals, + denySettingsExcludedActions, + denySettingsApplyToChildScopes, + tags: null, + bypassStackOutOfSyncError); + + WriteVerbose($"Starting what-if operation for deployment stack '{deploymentStackName}' at management group '{managementGroupId}'"); + + var whatIfOperation = DeploymentStacksClient.DeploymentStacks.BeginWhatIfAtManagementGroup( + managementGroupId, + deploymentStackName, + deploymentStackModel); + + WriteVerbose("What-if operation started, waiting for completion..."); + + var whatIfResult = WaitForWhatIfCompletion( + () => DeploymentStacksClient.DeploymentStacks.GetWhatIfResultAtManagementGroup(managementGroupId, deploymentStackName)); + + WriteVerbose("What-if operation completed"); + + return ConvertToDeploymentStackWhatIfResult(whatIfResult); + } + + /// + /// Waits for a what-if operation to complete by polling. + /// + private DeploymentStackWhatIfResult WaitForWhatIfCompletion( + Func>> getWhatIfResult) + { + const int counterUnit = 1000; + int step = 5; + int phaseOne = 400; + + DeploymentStackWhatIfResult result = null; + + do + { + TestMockSupport.Delay(step * counterUnit); + + if (phaseOne > 0) + phaseOne -= step; + + var getResultTask = getWhatIfResult(); + + using (var getResult = getResultTask.ConfigureAwait(false).GetAwaiter().GetResult()) + { + result = getResult.Body; + var response = getResult.Response; + + if (response != null && response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta.HasValue) + { + step = response.Headers.RetryAfter.Delta.Value.Seconds; + } + else + { + step = phaseOne > 0 ? 5 : 60; + } + } + + if (result?.Properties?.ProvisioningState != null) + { + WriteVerbose($"What-if operation status: {result.Properties.ProvisioningState}"); + } + + } while (!IsWhatIfComplete(result)); + + return result; + } + + /// + /// Checks if the what-if operation has completed. + /// + private bool IsWhatIfComplete(DeploymentStackWhatIfResult result) + { + if (result?.Properties?.ProvisioningState == null) + { + return false; + } + + var state = result.Properties.ProvisioningState; + return string.Equals(state, "Succeeded", StringComparison.OrdinalIgnoreCase) || + string.Equals(state, "Failed", StringComparison.OrdinalIgnoreCase) || + string.Equals(state, "Canceled", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Converts the SDK what-if result to PowerShell model. + /// + private PSDeploymentStackWhatIfResult ConvertToDeploymentStackWhatIfResult(DeploymentStackWhatIfResult sdkResult) + { + if (sdkResult == null) + { + return null; + } + + // The SDK result should already be in the correct format + // Just deserialize and re-serialize to convert to our PS model + var json = JsonConvert.SerializeObject(sdkResult); + return JsonConvert.DeserializeObject(json); + } + + /// + /// Converts the SDK what-if result to PowerShell model. + /// + private PSDeploymentStackWhatIfResult ConvertToDeploymentStackWhatIfResult(DeploymentStackWhatIfResult sdkResult) + { + if (sdkResult == null) + { + return null; + } + + // The SDK result should already be in the correct format + // Just deserialize and re-serialize to convert to our PS model + var json = JsonConvert.SerializeObject(sdkResult); + return JsonConvert.DeserializeObject(json); + } + + #region Temporary What-If Stubs (TODO: Replace with actual SDK types) + + /// + /// Temporary stub for DeploymentStackWhatIfResult. + /// TODO: Replace with actual Azure SDK type when What-If API is fully released. + /// The Azure SDK team is working on adding this type to the Microsoft.Azure.Management.Resources package. + /// + internal class DeploymentStackWhatIfResult + { + public string Id { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public DeploymentStackWhatIfProperties Properties { get; set; } + } + + /// + /// Temporary stub for DeploymentStackWhatIfProperties. + /// TODO: Replace with actual Azure SDK type when What-If API is fully released. + /// + internal class DeploymentStackWhatIfProperties + { + public string ProvisioningState { get; set; } + } + + #endregion + } +} } } \ No newline at end of file diff --git a/src/Resources/ResourceManager/SdkModels/Deployments/PSDeploymentStackWhatIfParameters.cs b/src/Resources/ResourceManager/SdkModels/Deployments/PSDeploymentStackWhatIfParameters.cs new file mode 100644 index 000000000000..38cffd684d1c --- /dev/null +++ b/src/Resources/ResourceManager/SdkModels/Deployments/PSDeploymentStackWhatIfParameters.cs @@ -0,0 +1,62 @@ +// ---------------------------------------------------------------------------------- +// +// 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.Cmdlets.SdkModels.Deployments +{ + using System.Collections; + + /// + /// Parameters for Deployment Stack What-If operation. + /// + public class PSDeploymentStackWhatIfParameters + { + public string StackName { get; set; } + + public string ResourceGroupName { get; set; } + + public string ManagementGroupId { get; set; } + + public string Location { get; set; } + + public string TemplateFile { get; set; } + + public string TemplateUri { get; set; } + + public string TemplateSpecId { get; set; } + + public Hashtable TemplateObject { get; set; } + + public string TemplateParameterUri { get; set; } + + public Hashtable TemplateParameterObject { get; set; } + + public string Description { get; set; } + + public string DeploymentScope { get; set; } + + public string ResourcesCleanupAction { get; set; } + + public string ResourceGroupsCleanupAction { get; set; } + + public string ManagementGroupsCleanupAction { get; set; } + + public string DenySettingsMode { get; set; } + + public string[] DenySettingsExcludedPrincipals { get; set; } + + public string[] DenySettingsExcludedActions { get; set; } + + public bool DenySettingsApplyToChildScopes { get; set; } + } +} \ No newline at end of file diff --git a/src/Resources/ResourceManager/SdkModels/Deployments/PSDeploymentStackWhatIfResult.cs b/src/Resources/ResourceManager/SdkModels/Deployments/PSDeploymentStackWhatIfResult.cs new file mode 100644 index 000000000000..070a566c0eb7 --- /dev/null +++ b/src/Resources/ResourceManager/SdkModels/Deployments/PSDeploymentStackWhatIfResult.cs @@ -0,0 +1,303 @@ +// ---------------------------------------------------------------------------------- +// +// 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.Cmdlets.SdkModels.Deployments +{ + using System; + using System.Collections.Generic; + using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Formatters; + using Newtonsoft.Json; + + /// + /// Represents the result of a Deployment Stack What-If operation. + /// Maps to the API response from Azure Resource Manager. + /// + public class PSDeploymentStackWhatIfResult + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + public DeploymentStackWhatIfProperties Properties { get; set; } + + public override string ToString() + { + return DeploymentStackWhatIfFormatter.Format(this); + } + } + + public class DeploymentStackWhatIfProperties + { + [JsonProperty("deploymentStackResourceId")] + public string DeploymentStackResourceId { get; set; } + + [JsonProperty("retentionInterval")] + public string RetentionInterval { get; set; } + + [JsonProperty("provisioningState")] + public string ProvisioningState { get; set; } + + [JsonProperty("deploymentStackLastModified")] + public DateTime? DeploymentStackLastModified { get; set; } + + [JsonProperty("deploymentExtensions")] + public IList DeploymentExtensions { get; set; } + + [JsonProperty("changes")] + public DeploymentStackWhatIfChanges Changes { get; set; } + + [JsonProperty("diagnostics")] + public IList Diagnostics { get; set; } + + [JsonProperty("correlationId")] + public string CorrelationId { get; set; } + + [JsonProperty("actionOnUnmanage")] + public ActionOnUnmanage ActionOnUnmanage { get; set; } + + [JsonProperty("deploymentScope")] + public string DeploymentScope { get; set; } + + [JsonProperty("denySettings")] + public DenySettings DenySettings { get; set; } + + [JsonProperty("parametersLink")] + public ParametersLink ParametersLink { get; set; } + + [JsonProperty("templateLink")] + public TemplateLink TemplateLink { get; set; } + + [JsonProperty("bypassStackOutOfSyncError")] + public bool? BypassStackOutOfSyncError { get; set; } + } + + public class DeploymentExtension + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + + [JsonProperty("configId")] + public string ConfigId { get; set; } + + [JsonProperty("config")] + public IDictionary Config { get; set; } + } + + public class DeploymentStackWhatIfChanges + { + [JsonProperty("resourceChanges")] + public IList ResourceChanges { get; set; } + + [JsonProperty("deploymentScopeChange")] + public DeploymentStackChangeBase DeploymentScopeChange { get; set; } + + [JsonProperty("denySettingsChange")] + public DeploymentStackChangeDeltaRecord DenySettingsChange { get; set; } + } + + public class DeploymentStackChangeBase + { + [JsonProperty("changeType")] + public string ChangeType { get; set; } + + [JsonProperty("before")] + public object Before { get; set; } + + [JsonProperty("after")] + public object After { get; set; } + } + + public class DeploymentStackResourceChange + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("changeType")] + public string ChangeType { get; set; } + + [JsonProperty("changeCertainty")] + public string ChangeCertainty { get; set; } + + [JsonProperty("apiVersion")] + public string ApiVersion { get; set; } + + [JsonProperty("managementStatusChange")] + public DeploymentStackChangeBase ManagementStatusChange { get; set; } + + [JsonProperty("denyStatusChange")] + public DeploymentStackChangeBase DenyStatusChange { get; set; } + + [JsonProperty("resourceConfigurationChanges")] + public ResourceConfigurationChanges ResourceConfigurationChanges { get; set; } + + [JsonProperty("extension")] + public DeploymentStackExtensionInfo Extension { get; set; } + + [JsonProperty("identifiers")] + public IDictionary Identifiers { get; set; } + } + + public class ResourceConfigurationChanges + { + [JsonProperty("before")] + public object Before { get; set; } + + [JsonProperty("after")] + public object After { get; set; } + + [JsonProperty("delta")] + public IList Delta { get; set; } + } + + public class DeploymentStackChangeDeltaRecord + { + [JsonProperty("before")] + public object Before { get; set; } + + [JsonProperty("after")] + public object After { get; set; } + + [JsonProperty("delta")] + public IList Delta { get; set; } + } + + public class DeploymentStackPropertyChange + { + [JsonProperty("path")] + public string Path { get; set; } + + [JsonProperty("changeType")] + public string ChangeType { get; set; } + + [JsonProperty("before")] + public object Before { get; set; } + + [JsonProperty("after")] + public object After { get; set; } + + [JsonProperty("children")] + public IList Children { get; set; } + } + + public class DeploymentStackExtensionInfo + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + + [JsonProperty("configId")] + public string ConfigId { get; set; } + + [JsonProperty("config")] + public IDictionary Config { get; set; } + } + + public class DeploymentStackExtensionConfigItem + { + [JsonProperty("value")] + public object Value { get; set; } + + [JsonProperty("keyVaultReference")] + public DeploymentStackKeyVaultReference KeyVaultReference { get; set; } + } + + public class DeploymentStackKeyVaultReference + { + [JsonProperty("secretName")] + public string SecretName { get; set; } + + [JsonProperty("secretVersion")] + public string SecretVersion { get; set; } + + [JsonProperty("keyVault")] + public DeploymentStackKeyVaultInfo KeyVault { get; set; } + } + + public class DeploymentStackKeyVaultInfo + { + [JsonProperty("id")] + public string Id { get; set; } + } + + public class DeploymentStackDiagnostic + { + [JsonProperty("level")] + public string Level { get; set; } + + [JsonProperty("code")] + public string Code { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + + [JsonProperty("target")] + public string Target { get; set; } + } + + public class ActionOnUnmanage + { + [JsonProperty("resources")] + public string Resources { get; set; } + + [JsonProperty("resourceGroups")] + public string ResourceGroups { get; set; } + + [JsonProperty("managementGroups")] + public string ManagementGroups { get; set; } + + [JsonProperty("resourcesWithoutDeleteSupport")] + public string ResourcesWithoutDeleteSupport { get; set; } + } + + public class DenySettings + { + [JsonProperty("mode")] + public string Mode { get; set; } + + [JsonProperty("applyToChildScopes")] + public bool? ApplyToChildScopes { get; set; } + + [JsonProperty("excludedPrincipals")] + public IList ExcludedPrincipals { get; set; } + + [JsonProperty("excludedActions")] + public IList ExcludedActions { get; set; } + } + + public class ParametersLink + { + [JsonProperty("uri")] + public string Uri { get; set; } + } + + public class TemplateLink + { + [JsonProperty("uri")] + public string Uri { get; set; } + } +} \ No newline at end of file diff --git a/src/Resources/Resources/Az.Resources.psd1 b/src/Resources/Resources/Az.Resources.psd1 index e85365fbdb43..272ecac0961d 100644 --- a/src/Resources/Resources/Az.Resources.psd1 +++ b/src/Resources/Resources/Az.Resources.psd1 @@ -152,7 +152,8 @@ CmdletsToExport = 'Export-AzResourceGroup', 'Export-AzTemplateSpec', 'Get-AzManagedApplicationDefinition', 'Get-AzManagementGroup', 'Get-AzManagementGroupDeployment', 'Get-AzManagementGroupDeploymentOperation', - 'Get-AzManagementGroupDeploymentStack', + 'Get-AzManagementGroupDeploymentStack', + 'Get-AzManagementGroupDeploymentStackWhatIf', 'Get-AzManagementGroupDeploymentWhatIfResult', 'Get-AzManagementGroupEntity', 'Get-AzManagementGroupHierarchySetting', @@ -167,14 +168,15 @@ CmdletsToExport = 'Export-AzResourceGroup', 'Export-AzTemplateSpec', 'Get-AzResourceGroupDeploymentWhatIfResult', 'Get-AzResourceLock', 'Get-AzResourceManagementPrivateLink', 'Get-AzResourceProvider', 'Get-AzRoleAssignment', 'Get-AzRoleDefinition', - 'Get-AzSubscriptionDeploymentStack', 'Get-AzTag', - 'Get-AzTemplateSpec', 'Get-AzTenantBackfillStatus', + 'Get-AzSubscriptionDeploymentStack', 'Get-AzSubscriptionDeploymentStackWhatIf', + 'Get-AzTag', 'Get-AzTemplateSpec', 'Get-AzTenantBackfillStatus', 'Get-AzTenantDeployment', 'Get-AzTenantDeploymentOperation', 'Get-AzTenantDeploymentWhatIfResult', 'Invoke-AzResourceAction', 'Move-AzResource', 'New-AzDeployment', 'New-AzManagedApplication', 'New-AzManagedApplicationDefinition', 'New-AzManagementGroup', 'New-AzManagementGroupDeployment', - 'New-AzManagementGroupDeploymentStack', + 'New-AzManagementGroupDeploymentStack', + 'Get-AzResourceGroupDeploymentStackWhatIf', 'New-AzManagementGroupHierarchySetting', 'New-AzManagementGroupSubscription', 'New-AzPrivateLinkAssociation', 'New-AzResource', 'New-AzResourceGroup',