in sdk/provisioning/Azure.Provisioning/src/Infrastructure.cs [20:325]
public class Infrastructure(string bicepName = "main") : Provisionable
{
/// <summary>
/// A friendly name that can also be used as a file name if compiling to a
/// module.
/// </summary>
public string BicepName { get; private set; } = bicepName;
/// <summary>
/// Optional target scope for the infrastructure. If left empty, then
/// <see cref="DeploymentScope.ResourceGroup"/> is assumed.
/// </summary>
public DeploymentScope? TargetScope { get; set; }
// Placeholder until we get module splitting handled
private Infrastructure? _parent = null;
/// <inheritdoc/>
public override IEnumerable<Provisionable> GetProvisionableResources() => _resources;
private readonly List<Provisionable> _resources = [];
/// <summary>
/// Adds a provisionable construct to this Infrastructure.
/// </summary>
/// <param name="resource">The provisionable construct to add.</param>
/// <remarks>
/// This will remove any existing association between this provisionable
/// construct and other Infrastructure instances.
/// </remarks>
public virtual void Add(Provisionable resource)
{
if (resource is ProvisionableConstruct construct &&
construct.ParentInfrastructure != this)
{
// Don't parent expression references
if (((IBicepValue)construct).Kind == BicepValueKind.Expression) { return; }
// Remove it from any existing Infrastructure first
construct.ParentInfrastructure?.Remove(this);
// Add and associate the resource
_resources.Add(construct);
construct.ParentInfrastructure = this;
}
else if (resource is Infrastructure nested &&
nested._parent != this)
{
// Remove it from any existing Infrastructure first
nested._parent?.Remove(this);
// Add and associate the resource
_resources.Add(nested);
nested._parent = this;
}
// Ensure all cases are covered
Debug.Assert(
resource is ProvisionableConstruct || resource is Infrastructure,
$"{nameof(Infrastructure)} needs to be updated if you add a new fork in the hierarchy derived from {nameof(Provisionable)}!");
}
/// <summary>
/// Remove a provisionable construct from this Infrastructure.
/// </summary>
/// <param name="resource">The provisionable construct to remove.</param>
public virtual void Remove(Provisionable resource)
{
if (resource is ProvisionableConstruct construct &&
construct.ParentInfrastructure == this)
{
construct.ParentInfrastructure = null;
_resources.Remove(construct);
}
else if (resource is Infrastructure nested &&
nested._parent == this)
{
nested._parent = null;
_resources.Remove(nested);
}
}
private static bool IsAsciiLetterOrDigit(char ch) =>
'a' <= ch && ch <= 'z' ||
'A' <= ch && ch <= 'Z' ||
'0' <= ch && ch <= '9';
/// <summary>
/// Checks whether an name is a valid bicep identifier name comprised of
/// letters, digits, and underscores.
/// </summary>
/// <param name="bicepIdentifier">The proposed identifier.</param>
/// <returns>Whether the name is a valid bicep identifier.</returns>
public static bool IsValidBicepIdentifier(string? bicepIdentifier)
{
if (string.IsNullOrEmpty(bicepIdentifier)) { return false; }
if (char.IsDigit(bicepIdentifier![0])) { return false; }
foreach (char ch in bicepIdentifier)
{
if (!IsAsciiLetterOrDigit(ch) && ch != '_')
{
return false;
}
}
return true;
}
/// <summary>
/// Validates whether a given bicep identifier name is correctly formed of
/// letters, numbers, and underscores.
/// </summary>
/// <param name="bicepIdentifier">The proposed bicep identifier.</param>
/// <param name="paramName">Optional parameter name to use for exceptions.</param>
/// <exception cref="ArgumentNullException">Throws if null.</exception>
/// <exception cref="ArgumentException">Throws if empty or invalid.</exception>
public static void ValidateBicepIdentifier(string? bicepIdentifier, string? paramName = default)
{
paramName ??= nameof(bicepIdentifier);
if (bicepIdentifier is null)
{
throw new ArgumentNullException(paramName, $"{paramName} cannot be null.");
}
else if (bicepIdentifier.Length == 0)
{
throw new ArgumentException($"{paramName} cannot be empty.", paramName);
}
else if (char.IsDigit(bicepIdentifier[0]))
{
throw new ArgumentException($"{paramName} cannot start with a number: \"{bicepIdentifier}\"", paramName);
}
foreach (var ch in bicepIdentifier)
{
if (!IsAsciiLetterOrDigit(ch) && ch != '_')
{
throw new ArgumentException($"{paramName} should only contain letters, numbers, and underscores: \"{bicepIdentifier}\"", paramName);
}
}
}
/// <summary>
/// Normalizes a proposed bicep identifier name. Any invalid characters
/// will be replaced with underscores.
/// </summary>
/// <param name="bicepIdentifier">The proposed bicep identifier.</param>
/// <returns>A valid bicep identifier name.</returns>
/// <exception cref="ArgumentNullException">Throws if null.</exception>
/// <exception cref="ArgumentException">Throws if empty.</exception>
public static string NormalizeBicepIdentifier(string? bicepIdentifier)
{
if (IsValidBicepIdentifier(bicepIdentifier))
{
return bicepIdentifier!;
}
if (bicepIdentifier is null)
{
// TODO: This may be relaxed in the future to generate an automatic
// name rather than throwing
throw new ArgumentNullException(nameof(bicepIdentifier), $"{nameof(bicepIdentifier)} cannot be null.");
}
else if (bicepIdentifier.Length == 0)
{
throw new ArgumentException($"{nameof(bicepIdentifier)} cannot be empty.", nameof(bicepIdentifier));
}
StringBuilder builder = new(bicepIdentifier.Length);
// Digits are not allowed as the first character, so prepend an
// underscore if the bicepIdentifier starts with a digit
if (char.IsDigit(bicepIdentifier[0]))
{
builder.Append('_');
}
foreach (char ch in bicepIdentifier)
{
// TODO: Consider opening this up to other naming strategies if
// someone can do something more intelligent for their usage/domain
builder.Append(IsAsciiLetterOrDigit(ch) ? ch : '_');
}
return builder.ToString();
}
/// <inheritdoc/>
protected internal override void Validate(ProvisioningBuildOptions? options = null)
{
options ??= new();
base.Validate(options);
foreach (Provisionable resource in GetProvisionableResources()) { resource.Validate(options); }
}
/// <inheritdoc/>
protected internal override void Resolve(ProvisioningBuildOptions? options = default)
{
options ??= new();
base.Resolve(options);
Provisionable[] cached = [.. GetProvisionableResources()]; // Copy so Resolve can mutate
foreach (Provisionable resource in cached) { resource.Resolve(options); }
foreach (InfrastructureResolver resolver in options.InfrastructureResolvers)
{
resolver.ResolveInfrastructure(this, options);
}
}
/// <inheritdoc/>
protected internal override IEnumerable<BicepStatement> Compile() =>
CompileInternal(options: null);
/// <summary>
/// Compile this infrastructure into a set of bicep modules.
/// </summary>
/// <param name="options">Provisioning options.</param>
/// <returns>Dictionary mapping module names to module definitions.</returns>
protected internal IDictionary<string, IEnumerable<BicepStatement>> CompileModules(ProvisioningBuildOptions? options = default)
{
// This API shape will eventually help us grow into compiling multiple
// modules at once and automatically splitting resources across them.
options ??= new();
Dictionary<string, IEnumerable<BicepStatement>> modules = [];
modules.Add(BicepName, CompileInternal(options));
// Optionally add any nested modules
List<Infrastructure> nested = [];
foreach (InfrastructureResolver resolver in options.InfrastructureResolvers)
{
nested.AddRange(resolver.GetNestedInfrastructure(this, options));
}
foreach (Infrastructure infra in nested)
{
modules.Add(infra.BicepName, infra.CompileInternal(options));
}
return modules;
}
/// <inheritdoc/>
private List<BicepStatement> CompileInternal(ProvisioningBuildOptions? options)
{
List<BicepStatement> statements = [];
if (TargetScope is not null)
{
statements.Add(
new TargetScopeStatement(
TargetScope switch
{
DeploymentScope.ResourceGroup => "resourceGroup",
DeploymentScope.Subscription => "subscription",
DeploymentScope.ManagementGroup => "managementGroup",
DeploymentScope.Tenant => "tenant",
_ => throw new InvalidOperationException($"Unknown deployment scope: {TargetScope}")
}));
}
IEnumerable<Provisionable> resources = GetProvisionableResources();
// Optionally customize the resources with the extensibility hooks on
// ProvisioningBuildOptions.
if (options is not null)
{
foreach (InfrastructureResolver resolver in options.InfrastructureResolvers)
{
resources = resolver.ResolveResources(resources, options);
}
}
foreach (Provisionable resource in resources)
{
if (resource is ProvisionableConstruct construct)
{
statements.AddRange(construct.Compile());
}
else if (resource is Infrastructure nested)
{
// We'll eventually add support for auto module splitting and
// be able to do smart things here. We're going to intentionally
// fail for now though so we have more flexibility in the future.
// We fail here instead of Add so folks have a chance to move it
// around between different Infrastructure classes if they want
throw new NotSupportedException($"Nested {nameof(Infrastructure)} is not currently supported. Please build them separately and use {nameof(ModuleImport)} to link them together.");
}
}
return statements;
}
/// <summary>
/// Compose all the resources into a concrete <see cref="ProvisioningPlan"/>
/// that can be compiled into Bicep, deployed, etc.
/// </summary>
/// <param name="options">
/// The build options to use for composing resources.
/// </param>
/// <returns>
/// A <see cref="ProvisioningPlan"/> that can be compiled into Bicep,
/// deployed, etc.
/// </returns>
public virtual ProvisioningPlan Build(ProvisioningBuildOptions? options = default)
{
options ??= new();
Resolve(options);
Validate(options);
return new ProvisioningPlan(this, options);
}
}