diff --git a/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net6.0.cs b/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net6.0.cs index b792aa38176d..17e27d68c9cd 100644 --- a/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net6.0.cs +++ b/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net6.0.cs @@ -5,7 +5,7 @@ public static partial class CdkExtensions public static T? GetSingleResourceInScope(this Azure.Provisioning.IConstruct construct) where T : Azure.Provisioning.Resource { throw null; } public static T? GetSingleResource(this Azure.Provisioning.IConstruct construct) where T : Azure.Provisioning.Resource { throw null; } } - public abstract partial class Construct : Azure.Provisioning.IConstruct, System.ClientModel.Primitives.IPersistableModel + public abstract partial class Construct : Azure.Provisioning.IConstruct { protected Construct(Azure.Provisioning.IConstruct? scope, string name, Azure.Provisioning.ConstructScope constructScope = Azure.Provisioning.ConstructScope.ResourceGroup, System.Guid? tenantId = default(System.Guid?), System.Guid? subscriptionId = default(System.Guid?), string? envName = null, Azure.Provisioning.ResourceManager.ResourceGroup? resourceGroup = null) { } public Azure.Provisioning.ConstructScope ConstructScope { get { throw null; } } @@ -23,9 +23,6 @@ public void AddResource(Azure.Provisioning.Resource resource) { } public System.Collections.Generic.IEnumerable GetOutputs(bool recursive = true) { throw null; } public System.Collections.Generic.IEnumerable GetParameters(bool recursive = true) { throw null; } public System.Collections.Generic.IEnumerable GetResources(bool recursive = true) { throw null; } - Azure.Provisioning.Construct System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } - string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } - System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } protected T UseExistingResource(T? resource, System.Func create) where T : Azure.Provisioning.Resource { throw null; } } public enum ConstructScope @@ -55,7 +52,7 @@ public partial interface IConstruct } public abstract partial class Infrastructure : Azure.Provisioning.Construct { - public Infrastructure(Azure.Provisioning.ConstructScope constructScope = Azure.Provisioning.ConstructScope.Subscription, System.Guid? tenantId = default(System.Guid?), System.Guid? subscriptionId = default(System.Guid?), string? envName = null) : base (default(Azure.Provisioning.IConstruct), default(string), default(Azure.Provisioning.ConstructScope), default(System.Guid?), default(System.Guid?), default(string), default(Azure.Provisioning.ResourceManager.ResourceGroup)) { } + public Infrastructure(Azure.Provisioning.ConstructScope constructScope = Azure.Provisioning.ConstructScope.Subscription, System.Guid? tenantId = default(System.Guid?), System.Guid? subscriptionId = default(System.Guid?), string? envName = null, bool useAnonymousResourceGroup = false) : base (default(Azure.Provisioning.IConstruct), default(string), default(Azure.Provisioning.ConstructScope), default(System.Guid?), default(System.Guid?), default(string), default(Azure.Provisioning.ResourceManager.ResourceGroup)) { } public void Build(string? outputPath = null) { } } public partial class Output @@ -170,8 +167,9 @@ namespace Azure.Provisioning.ResourceManager { public partial class ResourceGroup : Azure.Provisioning.Resource { - public ResourceGroup(Azure.Provisioning.IConstruct scope, string name = "rg", string version = "2023-07-01", Azure.Core.AzureLocation? location = default(Azure.Core.AzureLocation?), Azure.Provisioning.ResourceManager.Subscription? parent = null) : base (default(Azure.Provisioning.IConstruct), default(Azure.Provisioning.Resource), default(string), default(Azure.Core.ResourceType), default(string), default(System.Func)) { } + public ResourceGroup(Azure.Provisioning.IConstruct scope, string? name = "rg", string version = "2023-07-01", Azure.Core.AzureLocation? location = default(Azure.Core.AzureLocation?), Azure.Provisioning.ResourceManager.Subscription? parent = null) : base (default(Azure.Provisioning.IConstruct), default(Azure.Provisioning.Resource), default(string), default(Azure.Core.ResourceType), default(string), default(System.Func)) { } protected override Azure.Provisioning.Resource? FindParentInScope(Azure.Provisioning.IConstruct scope) { throw null; } + protected override string GetAzureName(Azure.Provisioning.IConstruct scope, string resourceName) { throw null; } } public static partial class ResourceManagerExtensions { diff --git a/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs b/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs index b792aa38176d..17e27d68c9cd 100644 --- a/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs +++ b/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs @@ -5,7 +5,7 @@ public static partial class CdkExtensions public static T? GetSingleResourceInScope(this Azure.Provisioning.IConstruct construct) where T : Azure.Provisioning.Resource { throw null; } public static T? GetSingleResource(this Azure.Provisioning.IConstruct construct) where T : Azure.Provisioning.Resource { throw null; } } - public abstract partial class Construct : Azure.Provisioning.IConstruct, System.ClientModel.Primitives.IPersistableModel + public abstract partial class Construct : Azure.Provisioning.IConstruct { protected Construct(Azure.Provisioning.IConstruct? scope, string name, Azure.Provisioning.ConstructScope constructScope = Azure.Provisioning.ConstructScope.ResourceGroup, System.Guid? tenantId = default(System.Guid?), System.Guid? subscriptionId = default(System.Guid?), string? envName = null, Azure.Provisioning.ResourceManager.ResourceGroup? resourceGroup = null) { } public Azure.Provisioning.ConstructScope ConstructScope { get { throw null; } } @@ -23,9 +23,6 @@ public void AddResource(Azure.Provisioning.Resource resource) { } public System.Collections.Generic.IEnumerable GetOutputs(bool recursive = true) { throw null; } public System.Collections.Generic.IEnumerable GetParameters(bool recursive = true) { throw null; } public System.Collections.Generic.IEnumerable GetResources(bool recursive = true) { throw null; } - Azure.Provisioning.Construct System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } - string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } - System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } protected T UseExistingResource(T? resource, System.Func create) where T : Azure.Provisioning.Resource { throw null; } } public enum ConstructScope @@ -55,7 +52,7 @@ public partial interface IConstruct } public abstract partial class Infrastructure : Azure.Provisioning.Construct { - public Infrastructure(Azure.Provisioning.ConstructScope constructScope = Azure.Provisioning.ConstructScope.Subscription, System.Guid? tenantId = default(System.Guid?), System.Guid? subscriptionId = default(System.Guid?), string? envName = null) : base (default(Azure.Provisioning.IConstruct), default(string), default(Azure.Provisioning.ConstructScope), default(System.Guid?), default(System.Guid?), default(string), default(Azure.Provisioning.ResourceManager.ResourceGroup)) { } + public Infrastructure(Azure.Provisioning.ConstructScope constructScope = Azure.Provisioning.ConstructScope.Subscription, System.Guid? tenantId = default(System.Guid?), System.Guid? subscriptionId = default(System.Guid?), string? envName = null, bool useAnonymousResourceGroup = false) : base (default(Azure.Provisioning.IConstruct), default(string), default(Azure.Provisioning.ConstructScope), default(System.Guid?), default(System.Guid?), default(string), default(Azure.Provisioning.ResourceManager.ResourceGroup)) { } public void Build(string? outputPath = null) { } } public partial class Output @@ -170,8 +167,9 @@ namespace Azure.Provisioning.ResourceManager { public partial class ResourceGroup : Azure.Provisioning.Resource { - public ResourceGroup(Azure.Provisioning.IConstruct scope, string name = "rg", string version = "2023-07-01", Azure.Core.AzureLocation? location = default(Azure.Core.AzureLocation?), Azure.Provisioning.ResourceManager.Subscription? parent = null) : base (default(Azure.Provisioning.IConstruct), default(Azure.Provisioning.Resource), default(string), default(Azure.Core.ResourceType), default(string), default(System.Func)) { } + public ResourceGroup(Azure.Provisioning.IConstruct scope, string? name = "rg", string version = "2023-07-01", Azure.Core.AzureLocation? location = default(Azure.Core.AzureLocation?), Azure.Provisioning.ResourceManager.Subscription? parent = null) : base (default(Azure.Provisioning.IConstruct), default(Azure.Provisioning.Resource), default(string), default(Azure.Core.ResourceType), default(string), default(System.Func)) { } protected override Azure.Provisioning.Resource? FindParentInScope(Azure.Provisioning.IConstruct scope) { throw null; } + protected override string GetAzureName(Azure.Provisioning.IConstruct scope, string resourceName) { throw null; } } public static partial class ResourceManagerExtensions { diff --git a/sdk/provisioning/Azure.Provisioning/src/Construct.cs b/sdk/provisioning/Azure.Provisioning/src/Construct.cs index 442bb510858e..935a5a93531e 100644 --- a/sdk/provisioning/Azure.Provisioning/src/Construct.cs +++ b/sdk/provisioning/Azure.Provisioning/src/Construct.cs @@ -2,12 +2,9 @@ // Licensed under the MIT License. using System; -using System.ClientModel.Primitives; using System.Collections.Generic; -using System.IO; using System.Linq; using Azure.Provisioning.ResourceManager; -using Microsoft.Extensions.Azure; namespace Azure.Provisioning { @@ -15,7 +12,7 @@ namespace Azure.Provisioning /// Basic building block of a set of resources in Azure. /// #pragma warning disable AZC0012 // Avoid single word type names - public abstract class Construct : IConstruct, IPersistableModel + public abstract class Construct : IConstruct #pragma warning restore AZC0012 // Avoid single word type names { private List _parameters; @@ -67,11 +64,6 @@ internal Construct( Subscription? subscription = default, ResourceGroup? resourceGroup = default) { - if (scope is null && constructScope == ConstructScope.ResourceGroup) - { - throw new ArgumentException($"Scope cannot be null if construct scope is is {nameof(ConstructScope.ResourceGroup)}"); - } - Scope = scope; Scope?.AddConstruct(this); _resources = new List(); @@ -84,7 +76,7 @@ internal Construct( ConstructScope = constructScope; if (constructScope == ConstructScope.ResourceGroup) { - ResourceGroup = resourceGroup ?? scope!.ResourceGroup ?? scope.GetOrAddResourceGroup(); + ResourceGroup = resourceGroup ?? scope?.ResourceGroup ?? scope?.GetOrAddResourceGroup(); } if (constructScope == ConstructScope.Subscription) { @@ -124,6 +116,16 @@ public IEnumerable GetResources(bool recursive = true) return result; } + internal IEnumerable GetExistingResources(bool recursive = true) + { + IEnumerable result = _existingResources; + if (recursive) + { + result = result.Concat(GetConstructs(false).SelectMany(c => ((Construct)c).GetExistingResources(true))); + } + return result; + } + /// public IEnumerable GetConstructs(bool recursive = true) { @@ -180,219 +182,5 @@ public void AddOutput(Output output) { _outputs.Add(output); } - - private string GetScopeName() - { - return ResourceGroup?.Name ?? (Subscription != null ? $"subscription('{Subscription.Name}')" : "tenant()"); - } - - private BinaryData SerializeModuleReference(ModelReaderWriterOptions options) - { - using var stream = new MemoryStream(); - stream.WriteLine($"module {Name} './resources/{Name}/{Name}.bicep' = {{"); - stream.WriteLine($" name: '{Name}'"); - stream.WriteLine($" scope: {GetScopeName()}"); - - var parametersToWrite = new HashSet(); - var outputs = new HashSet(GetOutputs()); - foreach (var p in GetParameters(false)) - { - if (!ShouldExposeParameter(p, outputs)) - { - continue; - } - parametersToWrite.Add(p); - } - if (parametersToWrite.Count > 0) - { - stream.WriteLine($" params: {{"); - foreach (var parameter in parametersToWrite) - { - stream.WriteLine($" {parameter.Name}: {parameter.GetParameterString(Scope!)}"); - } - stream.WriteLine($" }}"); - } - stream.WriteLine($"}}"); - - return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); - } - - private BinaryData SerializeModule(ModelReaderWriterOptions options) - { - using var stream = new MemoryStream(); - - WriteScopeLine(stream); - - WriteParameters(stream); - - WriteExistingResources(stream); - - foreach (var resource in GetResources(false)) - { - if (resource is Tenant) - { - continue; - } - stream.WriteLine(); - WriteLines(0, ModelReaderWriter.Write(resource, options), stream, resource); - } - - foreach (var construct in GetConstructs(false)) - { - stream.WriteLine(); - stream.Write(ModelReaderWriter.Write(construct, new ModelReaderWriterOptions("bicep-module")).ToArray()); - } - - WriteOutputs(stream); - - return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); - } - - private void WriteExistingResources(MemoryStream stream) - { - foreach (var resource in _existingResources) - { - stream.WriteLine(); - stream.WriteLine($"resource {resource.Name} '{resource.Id.ResourceType}@{resource.Version}' existing = {{"); - stream.WriteLine($" name: '{resource.Name}'"); - stream.WriteLine($"}}"); - } - } - - private void WriteScopeLine(MemoryStream stream) - { - if (ConstructScope != ConstructScope.ResourceGroup) - { - stream.WriteLine($"targetScope = '{ConstructScope.ToString().ToCamelCase()}'{Environment.NewLine}"); - } - } - - internal void WriteOutputs(MemoryStream stream) - { - if (GetOutputs().Any()) - { - stream.WriteLine(); - } - - var outputsToWrite = new HashSet(); - GetAllOutputsRecursive(this, outputsToWrite, false); - foreach (var output in outputsToWrite) - { - string value; - if (output.IsLiteral || ReferenceEquals(this, output.Resource.ModuleScope)) - { - value = output.IsLiteral ? $"'{output.Value}'" : output.Value; - } - else - { - value = $"{output.Resource.ModuleScope!.Name}.outputs.{output.Name}"; - } - string name = output.Name; - stream.WriteLine($"output {name} string = {value}"); - } - } - - private void GetAllOutputsRecursive(IConstruct construct, HashSet visited, bool isChild) - { - if (!isChild) - { - foreach (var output in construct.GetOutputs()) - { - if (!visited.Contains(output)) - { - visited.Add(output); - } - } - } - } - - private void WriteParameters(MemoryStream stream) - { - var parametersToWrite = new HashSet(); - GetAllParametersRecursive(this, parametersToWrite, false); - var outputs = new HashSet(GetOutputs()); - foreach (var parameter in parametersToWrite) - { - if (!ShouldExposeParameter(parameter, outputs)) - { - continue; - } - string defaultValue = parameter.DefaultValue is null ? string.Empty : $" = '{parameter.DefaultValue}'"; - - if (parameter.IsSecure) - stream.WriteLine($"@secure()"); - - stream.WriteLine($"@description('{parameter.Description}')"); - stream.WriteLine($"param {parameter.Name} string{defaultValue}{Environment.NewLine}"); - } - } - - private bool ShouldExposeParameter(Parameter parameter, HashSet outputs) - { - // Don't expose the parameter if the output that was used to create the parameter is already in scope. - return parameter.Output == null || !outputs.Contains(parameter.Output); - } - - private void GetAllParametersRecursive(IConstruct construct, HashSet visited, bool isChild) - { - if (!isChild) - { - foreach (var parameter in construct.GetParameters()) - { - if (!visited.Contains(parameter)) - { - visited.Add(parameter); - } - } - foreach (var child in construct.GetConstructs(false)) - { - GetAllParametersRecursive(child, visited, isChild); - } - } - } - - private static void WriteLines(int depth, BinaryData data, MemoryStream stream, Resource resource) - { - string indent = new string(' ', depth * 2); - string[] lines = data.ToString().Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < lines.Length; i++) - { - string lineToWrite = lines[i]; - - ReadOnlySpan line = lines[i].AsSpan(); - int start = 0; - while (line.Length > start && line[start] == ' ') - { - start++; - } - line = line.Slice(start); - int end = line.IndexOf(':'); - if (end > 0) - { - // foo: 1 - // foo: 'something.url' - string name = line.Slice(0, end).ToString(); - if (resource.ParameterOverrides.TryGetValue(name, out var value)) - { - lineToWrite = $"{new string(' ', start)}{name}: {value}"; - } - } - stream.WriteLine($"{indent}{lineToWrite}"); - } - } - - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => (options.Format) switch - { - "bicep" => SerializeModule(options), - "bicep-module" => SerializeModuleReference(options), - _ => throw new FormatException($"Unsupported format {options.Format}") - }; - - Construct IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "bicep"; } } diff --git a/sdk/provisioning/Azure.Provisioning/src/Infrastructure.cs b/sdk/provisioning/Azure.Provisioning/src/Infrastructure.cs index 8c3ef195da22..c17abd6c0caa 100644 --- a/sdk/provisioning/Azure.Provisioning/src/Infrastructure.cs +++ b/sdk/provisioning/Azure.Provisioning/src/Infrastructure.cs @@ -19,11 +19,15 @@ public abstract class Infrastructure : Construct /// The tenant id to use. If not passed in will try to load from AZURE_TENANT_ID environment variable. /// The subscription id to use. If not passed, the subscription will be loaded from the deployment context. /// The environment name to use. If not passed in will try to load from AZURE_ENV_NAME environment variable. - public Infrastructure(ConstructScope constructScope = ConstructScope.Subscription, Guid? tenantId = null, Guid? subscriptionId = null, string? envName = null) + /// Whether to use a single anonymous resource group. When deploying the resource group will need to be provided. + public Infrastructure(ConstructScope constructScope = ConstructScope.Subscription, Guid? tenantId = null, Guid? subscriptionId = null, string? envName = null, bool useAnonymousResourceGroup = false) : base(null, "default", constructScope, tenantId, subscriptionId, envName ?? Environment.GetEnvironmentVariable("AZURE_ENV_NAME") ?? throw new Exception("No environment variable found named 'AZURE_ENV_NAME'"), resourceGroup: null) { + UseAnonymousResourceGroup = useAnonymousResourceGroup; } + internal bool UseAnonymousResourceGroup { get; } + /// /// Converts the infrastructure to Bicep files. /// diff --git a/sdk/provisioning/Azure.Provisioning/src/ModuleConstruct.cs b/sdk/provisioning/Azure.Provisioning/src/ModuleConstruct.cs index ed60f095e2f0..791e86e14343 100644 --- a/sdk/provisioning/Azure.Provisioning/src/ModuleConstruct.cs +++ b/sdk/provisioning/Azure.Provisioning/src/ModuleConstruct.cs @@ -2,6 +2,10 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Linq; using Azure.Provisioning.ResourceManager; namespace Azure.Provisioning @@ -11,7 +15,7 @@ internal class ModuleConstruct : Construct public ModuleConstruct(ModuleResource resource) : base( resource.Scope, - GetScopeName(resource.Resource), + GetConstructName(resource.Resource), ResourceToConstructScope(resource.Resource), tenant: GetTenant(resource.Resource), subscription: GetSubscription(resource.Resource), @@ -19,12 +23,17 @@ public ModuleConstruct(ModuleResource resource) { } - private static string GetScopeName(Resource resource) + private static string GetConstructName(Resource resource) { var prefix = resource is Subscription ? resource.Name : resource.Id.Name.Replace('-', '_'); return $"{prefix}_module"; } + private string GetScopeName() + { + return ResourceGroup?.Name ?? (Subscription != null ? $"subscription('{Subscription.Name}')" : "tenant()"); + } + public bool IsRoot { get; set; } private static Tenant? GetTenant(Resource resource) @@ -52,12 +61,213 @@ private static ConstructScope ResourceToConstructScope(Resource resource) { return resource switch { - Tenant => ConstructScope.Tenant, + ResourceManager.Tenant => ConstructScope.Tenant, ResourceManager.Subscription => ConstructScope.Subscription, //TODO managementgroup support ResourceManager.ResourceGroup => ConstructScope.ResourceGroup, _ => throw new InvalidOperationException(), }; } + + public BinaryData SerializeModuleReference() + { + using var stream = new MemoryStream(); + stream.WriteLine($"module {Name} './resources/{Name}/{Name}.bicep' = {{"); + stream.WriteLine($" name: '{Name}'"); + stream.WriteLine($" scope: {GetScopeName()}"); + + var parametersToWrite = new HashSet(); + var outputs = new HashSet(GetOutputs()); + foreach (var p in GetParameters(false)) + { + if (!ShouldExposeParameter(p, outputs)) + { + continue; + } + parametersToWrite.Add(p); + } + if (parametersToWrite.Count > 0) + { + stream.WriteLine($" params: {{"); + foreach (var parameter in parametersToWrite) + { + stream.WriteLine($" {parameter.Name}: {parameter.GetParameterString(Scope!)}"); + } + stream.WriteLine($" }}"); + } + stream.WriteLine($"}}"); + + return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); + } + + public BinaryData SerializeModule() + { + using var stream = new MemoryStream(); + var options = new ModelReaderWriterOptions("bicep"); + + WriteScopeLine(stream); + + WriteParameters(stream); + + WriteExistingResources(stream); + + foreach (var resource in GetResources(false)) + { + if (resource is Tenant) + { + continue; + } + if (resource is ResourceGroup && resource.Id.Name == "resourceGroup()") + { + continue; + } + + stream.WriteLine(); + WriteLines(0, ModelReaderWriter.Write(resource, options), stream, resource); + } + + foreach (var construct in GetConstructs(false).Select(c => (ModuleConstruct)c)) + { + stream.WriteLine(); + stream.Write(construct.SerializeModuleReference().ToArray()); + } + + WriteOutputs(stream); + + return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); + } + + private void WriteExistingResources(MemoryStream stream) + { + foreach (var resource in GetExistingResources(false)) + { + stream.WriteLine(); + stream.WriteLine($"resource {resource.Name} '{resource.Id.ResourceType}@{resource.Version}' existing = {{"); + stream.WriteLine($" name: '{resource.Name}'"); + stream.WriteLine($"}}"); + } + } + + private void WriteScopeLine(MemoryStream stream) + { + if (ConstructScope != ConstructScope.ResourceGroup || IsRoot) + { + stream.WriteLine($"targetScope = '{ConstructScope.ToString().ToCamelCase()}'{Environment.NewLine}"); + } + } + + internal void WriteOutputs(MemoryStream stream) + { + if (GetOutputs().Any()) + { + stream.WriteLine(); + } + + var outputsToWrite = new HashSet(); + GetAllOutputsRecursive(this, outputsToWrite, false); + foreach (var output in outputsToWrite) + { + string value; + if (output.IsLiteral || ReferenceEquals(this, output.Resource.ModuleScope)) + { + value = output.IsLiteral ? $"'{output.Value}'" : output.Value; + } + else + { + value = $"{output.Resource.ModuleScope!.Name}.outputs.{output.Name}"; + } + string name = output.Name; + stream.WriteLine($"output {name} string = {value}"); + } + } + + private void GetAllOutputsRecursive(IConstruct construct, HashSet visited, bool isChild) + { + if (!isChild) + { + foreach (var output in construct.GetOutputs()) + { + if (!visited.Contains(output)) + { + visited.Add(output); + } + } + } + } + + private void WriteParameters(MemoryStream stream) + { + var parametersToWrite = new HashSet(); + GetAllParametersRecursive(this, parametersToWrite, false); + var outputs = new HashSet(GetOutputs()); + foreach (var parameter in parametersToWrite) + { + if (!ShouldExposeParameter(parameter, outputs)) + { + continue; + } + string defaultValue = parameter.DefaultValue is null ? string.Empty : $" = '{parameter.DefaultValue}'"; + + if (parameter.IsSecure) + stream.WriteLine($"@secure()"); + + stream.WriteLine($"@description('{parameter.Description}')"); + stream.WriteLine($"param {parameter.Name} string{defaultValue}{Environment.NewLine}"); + } + } + + private bool ShouldExposeParameter(Parameter parameter, HashSet outputs) + { + // Don't expose the parameter if the output that was used to create the parameter is already in scope. + return parameter.Output == null || !outputs.Contains(parameter.Output); + } + + private void GetAllParametersRecursive(IConstruct construct, HashSet visited, bool isChild) + { + if (!isChild) + { + foreach (var parameter in construct.GetParameters()) + { + if (!visited.Contains(parameter)) + { + visited.Add(parameter); + } + } + foreach (var child in construct.GetConstructs(false)) + { + GetAllParametersRecursive(child, visited, isChild); + } + } + } + + private static void WriteLines(int depth, BinaryData data, MemoryStream stream, Resource resource) + { + string indent = new string(' ', depth * 2); + string[] lines = data.ToString().Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < lines.Length; i++) + { + string lineToWrite = lines[i]; + + ReadOnlySpan line = lines[i].AsSpan(); + int start = 0; + while (line.Length > start && line[start] == ' ') + { + start++; + } + line = line.Slice(start); + int end = line.IndexOf(':'); + if (end > 0) + { + // foo: 1 + // foo: 'something.url' + string name = line.Slice(0, end).ToString(); + if (resource.ParameterOverrides.TryGetValue(name, out var value)) + { + lineToWrite = $"{new string(' ', start)}{name}: {value}"; + } + } + stream.WriteLine($"{indent}{lineToWrite}"); + } + } } } diff --git a/sdk/provisioning/Azure.Provisioning/src/ModuleInfrastructure.cs b/sdk/provisioning/Azure.Provisioning/src/ModuleInfrastructure.cs index f15694a97df5..81712c86c00b 100644 --- a/sdk/provisioning/Azure.Provisioning/src/ModuleInfrastructure.cs +++ b/sdk/provisioning/Azure.Provisioning/src/ModuleInfrastructure.cs @@ -13,16 +13,14 @@ namespace Azure.Provisioning internal class ModuleInfrastructure { private readonly Infrastructure _infrastructure; - private readonly ModuleConstruct _rootConstruct; + private ModuleConstruct? _rootConstruct; public ModuleInfrastructure(Infrastructure infrastructure) { _infrastructure = infrastructure; Dictionary> resourceTree = BuildResourceTree(); - ModuleConstruct? root = null; - BuildModuleConstructs(_infrastructure.Root, resourceTree, null, ref root); - _rootConstruct = root!; + BuildModuleConstructs(_infrastructure.Root, resourceTree, null); AddOutputsToModules(); } @@ -32,10 +30,10 @@ public void Write(string? outputPath = null) outputPath ??= $".\\{GetType().Name}"; outputPath = Path.GetFullPath(outputPath); - WriteBicepFile(_rootConstruct, outputPath); + WriteBicepFile(_rootConstruct!, outputPath); var queue = new Queue(); - queue.Enqueue(_rootConstruct); + queue.Enqueue(_rootConstruct!); WriteConstructsByLevel(queue, outputPath); } @@ -88,7 +86,7 @@ private void VisitResource(Resource resource, Dictionary> resourceTree, ModuleConstruct? parentScope, ref ModuleConstruct? root) + private void BuildModuleConstructs(Resource resource, Dictionary> resourceTree, ModuleConstruct? parentScope) { ModuleConstruct? construct = null; var moduleResource = new ModuleResource(resource, parentScope); @@ -99,8 +97,8 @@ private void BuildModuleConstructs(Resource resource, Dictionary> resourceTree) { - if (!(resource is Tenant || resource is Subscription || resource is ResourceGroup)) + if (resource is not (Tenant or Subscription or ResourceGroup)) { return false; } @@ -154,7 +152,19 @@ private bool NeedsModuleConstruct(Resource resource, Dictionary 1; } - if (resource is Subscription || resource is ResourceGroup) + + if (resource is Subscription) + { + foreach (var child in resourceTree[resource]) + { + if (child is not ResourceGroup || (child is ResourceGroup && child.Id.Name != ResourceGroup.AnonymousResourceGroupName)) + { + return true; + } + } + } + + if (resource is ResourceGroup) { // TODO add policy support return resourceTree[resource].Count > 0; @@ -187,9 +197,9 @@ private void WriteBicepFile(ModuleConstruct construct, string outputPath) { using var stream = new FileStream(GetFilePath(construct, outputPath), FileMode.Create); #if NET6_0_OR_GREATER - stream.Write(ModelReaderWriter.Write(construct, new ModelReaderWriterOptions("bicep"))); + stream.Write(construct.SerializeModule()); #else - BinaryData data = ModelReaderWriter.Write(construct, new ModelReaderWriterOptions("bicep")); + BinaryData data = construct.SerializeModule(); var buffer = data.ToArray(); stream.Write(buffer, 0, buffer.Length); #endif diff --git a/sdk/provisioning/Azure.Provisioning/src/resourcemanager/ResourceGroup.cs b/sdk/provisioning/Azure.Provisioning/src/resourcemanager/ResourceGroup.cs index c69be981062e..1c83014c082a 100644 --- a/sdk/provisioning/Azure.Provisioning/src/resourcemanager/ResourceGroup.cs +++ b/sdk/provisioning/Azure.Provisioning/src/resourcemanager/ResourceGroup.cs @@ -15,6 +15,7 @@ namespace Azure.Provisioning.ResourceManager public class ResourceGroup : Resource { internal static readonly ResourceType ResourceType = "Microsoft.Resources/resourceGroups"; + internal const string AnonymousResourceGroupName = "resourceGroup()"; /// /// Initializes a new instance of the . @@ -24,8 +25,8 @@ public class ResourceGroup : Resource /// The version of the resourceGroup. /// The location of the resourceGroup. /// The parent of the resourceGroup. - public ResourceGroup(IConstruct scope, string name = "rg", string version = "2023-07-01", AzureLocation? location = default, Subscription? parent = default) - : base(scope, parent, name, ResourceType, version, (name) => ResourceManagerModelFactory.ResourceGroupData( + public ResourceGroup(IConstruct scope, string? name = "rg", string version = "2023-07-01", AzureLocation? location = default, Subscription? parent = default) + : base(scope, parent, name!, ResourceType, version, (name) => ResourceManagerModelFactory.ResourceGroupData( name: name, resourceType: ResourceType, tags: new Dictionary { { "azd-env-name", scope.EnvironmentName } }, @@ -43,5 +44,11 @@ public ResourceGroup(IConstruct scope, string name = "rg", string version = "202 } return result; } + + /// + protected override string GetAzureName(IConstruct scope, string resourceName) + { + return resourceName == AnonymousResourceGroupName ? resourceName : base.GetAzureName(scope, resourceName); + } } } diff --git a/sdk/provisioning/Azure.Provisioning/src/resourcemanager/ResourceManagerExtensions.cs b/sdk/provisioning/Azure.Provisioning/src/resourcemanager/ResourceManagerExtensions.cs index dbeb126b90d1..e8212e124120 100644 --- a/sdk/provisioning/Azure.Provisioning/src/resourcemanager/ResourceManagerExtensions.cs +++ b/sdk/provisioning/Azure.Provisioning/src/resourcemanager/ResourceManagerExtensions.cs @@ -23,11 +23,11 @@ public static ResourceGroup AddResourceGroup(this IConstruct construct) throw new InvalidOperationException("ResourceGroup already exists on the construct"); } - return new ResourceGroup(construct, name: "rg"); + return new ResourceGroup(construct, name: construct is Infrastructure { UseAnonymousResourceGroup: true } ? ResourceGroup.AnonymousResourceGroupName : "rg"); } /// - /// Gets or adds a resource group to the construct. + /// Gets or adds the resource group of the construct. /// /// The construct. /// The see . diff --git a/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/StorageBlobDefaults/resources/rg_TEST_module/rg_TEST_module.bicep b/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/StorageBlobDefaults/resources/rg_TEST_module/rg_TEST_module.bicep index c75009f15d4c..e6d15ef26352 100644 --- a/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/StorageBlobDefaults/resources/rg_TEST_module/rg_TEST_module.bicep +++ b/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/StorageBlobDefaults/resources/rg_TEST_module/rg_TEST_module.bicep @@ -1,6 +1,6 @@ -resource storageAccount_0socNL5dP 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: 'photoacct74fc8bcc792b429' +resource storageAccount_NHn0GuaqX 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'photoacct84d98ba67edc4e3' location: 'westus' sku: { name: 'Premium_LRS' @@ -10,8 +10,8 @@ resource storageAccount_0socNL5dP 'Microsoft.Storage/storageAccounts@2022-09-01' } } -resource blobService_atCEUE42D 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = { - parent: storageAccount_0socNL5dP +resource blobService_58OA4T8kA 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = { + parent: storageAccount_NHn0GuaqX name: 'default' properties: { } diff --git a/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/StorageBlobDropDown/resources/rg_TEST_module/rg_TEST_module.bicep b/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/StorageBlobDropDown/resources/rg_TEST_module/rg_TEST_module.bicep index e85cb164c93e..834cdaa38678 100644 --- a/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/StorageBlobDropDown/resources/rg_TEST_module/rg_TEST_module.bicep +++ b/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/StorageBlobDropDown/resources/rg_TEST_module/rg_TEST_module.bicep @@ -1,6 +1,6 @@ -resource storageAccount_v68CLFjrL 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: 'photoacct17b6e77d8453479' +resource storageAccount_hOJq7fwUJ 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'photoacct98d7a228f178408' location: 'westus' sku: { name: 'Premium_LRS' @@ -10,8 +10,8 @@ resource storageAccount_v68CLFjrL 'Microsoft.Storage/storageAccounts@2022-09-01' } } -resource blobService_trxerMWWN 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = { - parent: storageAccount_v68CLFjrL +resource blobService_c88nS1we4 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = { + parent: storageAccount_hOJq7fwUJ name: 'default' properties: { deleteRetentionPolicy: { diff --git a/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/WebSiteUsingL3ResourceGroupScope/main.bicep b/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/WebSiteUsingL3ResourceGroupScope/main.bicep new file mode 100644 index 000000000000..963de69bb5db --- /dev/null +++ b/sdk/provisioning/Azure.Provisioning/tests/Infrastructure/WebSiteUsingL3ResourceGroupScope/main.bicep @@ -0,0 +1,248 @@ +targetScope = 'resourceGroup' + +@secure() +@description('SQL Server administrator password') +param sqlAdminPassword string + +@secure() +@description('Application user password') +param appUserPassword string + + +resource appServicePlan_PxkuWnuWL 'Microsoft.Web/serverfarms@2021-02-01' = { + name: 'appServicePlan-TEST' + location: 'westus' + sku: { + name: 'B1' + } + properties: { + reserved: true + } +} + +resource keyVault_zomsD2kWf 'Microsoft.KeyVault/vaults@2023-02-01' = { + name: 'kv-TEST' + location: 'westus' + tags: { + 'key': 'value' + } + properties: { + tenantId: '00000000-0000-0000-0000-000000000000' + sku: { + name: 'standard' + family: 'A' + } + enableRbacAuthorization: true + } +} + +resource keyVaultAddAccessPolicy_gnJ6YLPh4 'Microsoft.KeyVault/vaults/accessPolicies@2023-02-01' = { + parent: keyVault_zomsD2kWf + name: 'add' + properties: { + accessPolicies: [ + { + tenantId: '00000000-0000-0000-0000-000000000000' + objectId: webSite_IGuzwfciS.identity.principalId + permissions: { + secrets: [ + 'get' + 'list' + ] + } + } + ] + } +} + +resource keyVaultSecret_CBLh3EPfm 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_zomsD2kWf + name: 'sqlAdminPassword-TEST' + properties: { + value: sqlAdminPassword + } +} + +resource keyVaultSecret_QtRTwwecs 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_zomsD2kWf + name: 'appUserPassword-TEST' + properties: { + value: appUserPassword + } +} + +resource keyVaultSecret_YNErVycWe 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = { + parent: keyVault_zomsD2kWf + name: 'connectionString-TEST' + properties: { + value: 'Server=${sqlServer_2CRay8gJr.properties.fullyQualifiedDomainName}; Database=${sqlDatabase_P8xenywiS.name}; User=appUser; Password=${appUserPassword}' + } +} + +resource webSite_IGuzwfciS 'Microsoft.Web/sites@2021-02-01' = { + name: 'frontEnd-TEST' + location: 'westus' + kind: 'app,linux' + properties: { + serverFarmId: '/subscriptions/subscription()/resourceGroups/resourceGroup()/providers/Microsoft.Web/serverfarms/appServicePlan-TEST' + siteConfig: { + linuxFxVersion: 'node|18-lts' + alwaysOn: true + appCommandLine: './entrypoint.sh -o ./env-config.js && pm2 serve /home/site/wwwroot --no-daemon --spa' + cors: { + allowedOrigins: [ + 'https://portal.azure.com' + 'https://ms.portal.azure.com' + ] + } + minTlsVersion: '1.2' + ftpsState: 'FtpsOnly' + } + httpsOnly: true + } +} + +resource applicationSettingsResource_jW0EB5uhd 'Microsoft.Web/sites/config@2021-02-01' = { + parent: webSite_IGuzwfciS + name: 'appsettings' +} + +resource webSiteConfigLogs_GwVSHGFxS 'Microsoft.Web/sites/config@2021-02-01' = { + parent: webSite_IGuzwfciS + name: 'logs' + properties: { + applicationLogs: { + fileSystem: { + level: 'Verbose' + } + } + httpLogs: { + fileSystem: { + retentionInMb: 35 + retentionInDays: 1 + enabled: true + } + } + failedRequestsTracing: { + enabled: true + } + detailedErrorMessages: { + enabled: true + } + } +} + +resource sqlServer_2CRay8gJr 'Microsoft.Sql/servers@2022-08-01-preview' = { + name: 'sqlserver-TEST' + location: 'westus' + properties: { + administratorLogin: 'sqladmin' + administratorLoginPassword: sqlAdminPassword + version: '12.0' + minimalTlsVersion: '1.2' + publicNetworkAccess: 'Enabled' + } +} + +resource sqlDatabase_P8xenywiS 'Microsoft.Sql/servers/databases@2022-08-01-preview' = { + parent: sqlServer_2CRay8gJr + name: 'db-TEST' + location: 'westus' + properties: { + } +} + +resource sqlFirewallRule_MTg5B9jZr 'Microsoft.Sql/servers/firewallRules@2020-11-01-preview' = { + parent: sqlServer_2CRay8gJr + name: 'firewallRule-TEST' + properties: { + startIpAddress: '0.0.0.1' + endIpAddress: '255.255.255.254' + } +} + +resource deploymentScript_qloqQ8wU0 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: 'cliScript-TEST' + location: 'westus' + kind: 'AzureCLI' + properties: { + cleanupPreference: 'OnSuccess' + scriptContent: ''' +wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 +tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . +cat < ./initDb.sql +drop user ${APPUSERNAME} +go +create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' +go +alter role db_owner add member ${APPUSERNAME} +go +SCRIPT_END +./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql''' + environmentVariables: [ + { + name: 'APPUSERPASSWORD' + secureValue: appUserPassword + } + { + name: 'SQLCMDPASSWORD' + secureValue: sqlAdminPassword + } + { + name: 'DBSERVER' + value: sqlServer_2CRay8gJr.properties.fullyQualifiedDomainName + } + { + name: 'DBNAME' + value: 'db-TEST' + } + { + name: 'APPUSERNAME' + value: 'appUser' + } + { + name: 'SQLADMIN' + value: 'sqlAdmin' + } + ] + retentionInterval: 'PT1H' + timeout: 'PT5M' + azCliVersion: '2.37.0' + } +} + +resource webSite_TR8bo87ZZ 'Microsoft.Web/sites@2021-02-01' = { + name: 'backEnd-TEST' + location: 'westus' + kind: 'app,linux' + properties: { + serverFarmId: '/subscriptions/subscription()/resourceGroups/resourceGroup()/providers/Microsoft.Web/serverfarms/appServicePlan-TEST' + siteConfig: { + linuxFxVersion: 'dotnetcore|6.0' + alwaysOn: true + appCommandLine: '' + cors: { + allowedOrigins: [ + 'https://portal.azure.com' + 'https://ms.portal.azure.com' + ] + } + minTlsVersion: '1.2' + ftpsState: 'FtpsOnly' + } + httpsOnly: true + } +} + +resource applicationSettingsResource_FmsJom6FN 'Microsoft.Web/sites/config@2021-02-01' = { + parent: webSite_TR8bo87ZZ + name: 'appsettings' + properties: { + 'SCM_DO_BUILD_DURING_DEPLOYMENT': 'False' + 'ENABLE_ORYX_BUILD': 'True' + } +} + +output vaultUri string = keyVault_zomsD2kWf.properties.vaultUri +output SERVICE_API_IDENTITY_PRINCIPAL_ID string = webSite_IGuzwfciS.identity.principalId +output sqlServerName string = sqlServer_2CRay8gJr.properties.fullyQualifiedDomainName diff --git a/sdk/provisioning/Azure.Provisioning/tests/ProvisioningTests.cs b/sdk/provisioning/Azure.Provisioning/tests/ProvisioningTests.cs index 966032582923..e3bcbc645e5a 100644 --- a/sdk/provisioning/Azure.Provisioning/tests/ProvisioningTests.cs +++ b/sdk/provisioning/Azure.Provisioning/tests/ProvisioningTests.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Azure.Core; using Azure.Core.TestFramework; using Azure.Core.Tests.TestFramework; using Azure.Identity; @@ -170,6 +171,31 @@ await ValidateBicepAsync(BinaryData.FromObjectAsJson( })); } + [Test] + public async Task WebSiteUsingL3ResourceGroupScope() + { + var infra = new TestInfrastructure(scope: ConstructScope.ResourceGroup, useAnonymousResourceGroup: true); + infra.AddWebSiteWithSqlBackEnd(); + + infra.GetSingleResource()!.Properties.Tags.Add("key", "value"); + infra.GetSingleResourceInScope()!.Properties.Tags.Add("key", "value"); + + foreach (var website in infra.GetResources().Where(r => r is WebSite)) + { + Assert.AreEqual("subscription()", ((WebSite)website).Properties.AppServicePlanId.SubscriptionId); + Assert.AreEqual("resourceGroup()", ((WebSite)website).Properties.AppServicePlanId.ResourceGroupName); + } + + infra.Build(GetOutputPath()); + + await ValidateBicepAsync(BinaryData.FromObjectAsJson( + new + { + sqlAdminPassword = new { value = "password" }, + appUserPassword = new { value = "password" } + }), anonymousResourceGroup: true); + } + [Test] public async Task StorageBlobDefaults() { @@ -254,7 +280,7 @@ public async Task OutputsSpanningModules() await ValidateBicepAsync(); } - public async Task ValidateBicepAsync(BinaryData? parameters = null) + public async Task ValidateBicepAsync(BinaryData? parameters = null, bool anonymousResourceGroup = false) { if (TestEnvironment.GlobalIsRunningInCI) { @@ -262,6 +288,10 @@ public async Task ValidateBicepAsync(BinaryData? parameters = null) } var testPath = Path.Combine(_infrastructureRoot, TestContext.CurrentContext.Test.Name); + var client = new ArmClient(new DefaultAzureCredential()); + ResourceGroupResource? rg = null; + + SubscriptionResource subscription = await client.GetSubscriptions().GetAsync(Environment.GetEnvironmentVariable("SUBSCRIPTION_ID")); try { @@ -286,25 +316,40 @@ public async Task ValidateBicepAsync(BinaryData? parameters = null) } } - var client = new ArmClient(new DefaultAzureCredential()); - SubscriptionResource subscription = await client.GetSubscriptions().GetAsync(Environment.GetEnvironmentVariable("SUBSCRIPTION_ID")); - - var identifier = ArmDeploymentResource.CreateResourceIdentifier(subscription.Id, TestContext.CurrentContext.Test.Name); - var resource = client.GetArmDeploymentResource(identifier); - await resource.ValidateAsync(WaitUntil.Completed, - new ArmDeploymentContent( - new ArmDeploymentProperties(ArmDeploymentMode.Incremental) - { - Template = new BinaryData(File.ReadAllText(Path.Combine(testPath, "main.json"))), - Parameters = parameters - }) + ResourceIdentifier scope; + if (anonymousResourceGroup) + { + var rgs = subscription.GetResourceGroups(); + var data = new ResourceGroupData("westus"); + rg = (await rgs.CreateOrUpdateAsync(WaitUntil.Completed, TestContext.CurrentContext.Test.Name, data)).Value; + scope = ResourceGroupResource.CreateResourceIdentifier(subscription.Id.SubscriptionId, + TestContext.CurrentContext.Test.Name); + } + else + { + scope = subscription.Id; + } + + var resource = client.GetArmDeploymentResource(ArmDeploymentResource.CreateResourceIdentifier(scope, TestContext.CurrentContext.Test.Name)); + var content = new ArmDeploymentContent( + new ArmDeploymentProperties(ArmDeploymentMode.Incremental) { - Location = "westus" + Template = new BinaryData(File.ReadAllText(Path.Combine(testPath, "main.json"))), + Parameters = parameters }); + if (!anonymousResourceGroup) + { + content.Location = "westus"; + } + await resource.ValidateAsync(WaitUntil.Completed, content); } finally { File.Delete(Path.Combine(testPath, "main.json")); + if (rg != null) + { + await rg.DeleteAsync(WaitUntil.Completed); + } } } diff --git a/sdk/provisioning/Azure.Provisioning/tests/TestInfrastructure.cs b/sdk/provisioning/Azure.Provisioning/tests/TestInfrastructure.cs index 164ab5265213..fa4d8bde5c7d 100644 --- a/sdk/provisioning/Azure.Provisioning/tests/TestInfrastructure.cs +++ b/sdk/provisioning/Azure.Provisioning/tests/TestInfrastructure.cs @@ -7,8 +7,8 @@ namespace Azure.Provisioning.Tests { internal class TestInfrastructure : Infrastructure { - public TestInfrastructure(Guid? subscriptionId = null) - : base(ConstructScope.Subscription, Guid.Empty, subscriptionId, "TEST") + public TestInfrastructure(Guid? subscriptionId = null, ConstructScope scope = ConstructScope.Subscription, bool useAnonymousResourceGroup = false) + : base(scope, Guid.Empty, subscriptionId, "TEST", useAnonymousResourceGroup: useAnonymousResourceGroup) { } }