|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Azure.Provisioning;
using Azure.Provisioning.AppContainers;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.Resources;
namespace Aspire.Hosting.Azure;
internal abstract class BaseContainerAppContext(IResource resource, ContainerAppEnvironmentContext containerAppEnvironmentContext)
{
protected readonly ContainerAppEnvironmentContext _containerAppEnvironmentContext = containerAppEnvironmentContext;
public IResource Resource => resource;
/// <summary>
/// The normalized container app name (lowercase) that will be used consistently
/// throughout the container app creation process for both the resource identifier
/// and endpoint mapping host names.
/// </summary>
public string NormalizedContainerAppName => resource.Name.ToLowerInvariant();
protected record struct EndpointMapping(string Scheme, string Host, int Port, int? TargetPort, bool IsHttpIngress, bool External);
protected readonly Dictionary<string, EndpointMapping> _endpointMapping = [];
// Resolved environment variables and command line args
// These contain the values that need to be further transformed into
// bicep compatible values
public Dictionary<string, object> EnvironmentVariables { get; } = [];
public List<object> Args { get; } = [];
public Dictionary<string, (ContainerMountAnnotation, BicepOutputReference)> Volumes { get; } = [];
// Bicep build state
protected ProvisioningParameter? _containerRegistryUrlParameter;
protected ProvisioningParameter? _containerRegistryManagedIdentityIdParameter;
protected AzureResourceInfrastructure? _infrastructure;
public AzureResourceInfrastructure Infra => _infrastructure ?? throw new InvalidOperationException("Infra is not set");
public abstract void BuildContainerApp(AzureResourceInfrastructure infra);
protected void AddVolumes(BicepList<ContainerAppVolume> volumes, ContainerAppContainer containerAppContainer)
{
foreach (var (volumeName, (volume, storageName)) in Volumes)
{
var containerAppVolume = new ContainerAppVolume
{
Name = volumeName,
StorageType = ContainerAppStorageType.AzureFile,
StorageName = storageName.AsProvisioningParameter(Infra),
};
volumes.Add(containerAppVolume);
var containerAppVolumeMount = new ContainerAppVolumeMount
{
VolumeName = volumeName,
MountPath = volume.Target,
};
containerAppContainer.VolumeMounts.Add(containerAppVolumeMount);
}
}
protected void AddAzureClientId(IAppIdentityResource? appIdentityResource, BicepList<ContainerAppEnvironmentVariable> env)
{
if (appIdentityResource is not null)
{
env.Add(new ContainerAppEnvironmentVariable
{
Name = "AZURE_CLIENT_ID",
Value = AllocateParameter(appIdentityResource.ClientId)
});
}
}
protected static bool TryGetContainerImageName(IResource resource, out string? containerImageName)
{
// If the resource has a Dockerfile build annotation, we don't have the image name
// it will come as a parameter
if (resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out _))
{
containerImageName = null;
return false;
}
return resource.TryGetContainerImageName(out containerImageName);
}
public async Task ProcessResourceAsync(CancellationToken cancellationToken)
{
ProcessEndpoints();
ProcessVolumes();
await ProcessEnvironmentAsync(cancellationToken).ConfigureAwait(false);
await ProcessArgumentsAsync(cancellationToken).ConfigureAwait(false);
}
protected virtual void ProcessEndpoints() { }
private async Task ProcessArgumentsAsync(CancellationToken cancellationToken)
{
if (resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var commandLineArgsCallbackAnnotations))
{
var context = new CommandLineArgsCallbackContext(Args, resource, cancellationToken: cancellationToken)
{
ExecutionContext = _containerAppEnvironmentContext.ExecutionContext,
};
foreach (var c in commandLineArgsCallbackAnnotations)
{
await c.Callback(context).ConfigureAwait(false);
}
}
}
private async Task ProcessEnvironmentAsync(CancellationToken cancellationToken)
{
if (resource.TryGetAnnotationsOfType<EnvironmentCallbackAnnotation>(out var environmentCallbacks))
{
var context = new EnvironmentCallbackContext(_containerAppEnvironmentContext.ExecutionContext, resource, EnvironmentVariables, cancellationToken: cancellationToken);
foreach (var c in environmentCallbacks)
{
await c.Callback(context).ConfigureAwait(false);
}
}
}
private static BicepValue<string> ResolveValue(object val)
{
return val switch
{
BicepValue<string> s => s,
string s => s,
ProvisioningParameter p => p,
FormattableString fs => BicepFunction.Interpolate(fs),
_ => throw new NotSupportedException("Unsupported value type " + val.GetType())
};
}
private void ProcessVolumes()
{
if (resource.TryGetContainerMounts(out var mounts))
{
var bindMountIndex = 0;
var volumeIndex = 0;
foreach (var volume in mounts)
{
var (index, volumeName) = volume.Type switch
{
ContainerMountType.BindMount => (bindMountIndex, $"bm{bindMountIndex}"),
ContainerMountType.Volume => (volumeIndex, $"v{volumeIndex}"),
_ => throw new NotSupportedException()
};
var storageName = _containerAppEnvironmentContext.Environment.GetVolumeStorage(resource, volume, index);
Volumes[volumeName] = (volume, storageName);
if (volume.Type == ContainerMountType.BindMount)
{
bindMountIndex++;
}
else
{
volumeIndex++;
}
}
}
}
private BicepValue<string> GetValue(EndpointMapping mapping, EndpointProperty property)
{
var (scheme, host, port, targetPort, isHttpIngress, external) = mapping;
BicepValue<string> GetHostValue(string? prefix = null, string? suffix = null)
{
if (isHttpIngress)
{
var domain = AllocateParameter(_containerAppEnvironmentContext.Environment.ContainerAppDomain);
return external ? BicepFunction.Interpolate($$"""{{prefix}}{{host}}.{{domain}}{{suffix}}""") : BicepFunction.Interpolate($$"""{{prefix}}{{host}}.internal.{{domain}}{{suffix}}""");
}
return $"{prefix}{host}{suffix}";
}
return property switch
{
EndpointProperty.Url => GetHostValue($"{scheme}://", suffix: isHttpIngress ? null : $":{port}"),
EndpointProperty.Host or EndpointProperty.IPV4Host => GetHostValue(),
EndpointProperty.Port => port.ToString(CultureInfo.InvariantCulture),
EndpointProperty.HostAndPort => GetHostValue(suffix: $":{port}"),
EndpointProperty.TargetPort => targetPort is null ? AllocateContainerPortParameter() : $"{targetPort}",
EndpointProperty.Scheme => scheme,
_ => throw new NotSupportedException(),
};
}
private (object, SecretType) ProcessValue(object value, SecretType secretType = SecretType.None, object? parent = null)
{
if (value is string s)
{
return (s, secretType);
}
if (value is EndpointReference ep)
{
var context = ep.Resource == resource
? this
: _containerAppEnvironmentContext.GetContainerAppContext(ep.Resource);
var mapping = context._endpointMapping[ep.EndpointName];
var url = GetValue(mapping, EndpointProperty.Url);
return (url, secretType);
}
if (value is ParameterResource param)
{
var st = param.Secret ? SecretType.Normal : secretType;
return (AllocateParameter(param, secretType: st), st);
}
if (value is ConnectionStringReference cs)
{
return ProcessValue(cs.Resource.ConnectionStringExpression, secretType: secretType, parent: parent);
}
if (value is IResourceWithConnectionString csrs)
{
return ProcessValue(csrs.ConnectionStringExpression, secretType: secretType, parent: parent);
}
if (value is BicepOutputReference output)
{
return (AllocateParameter(output, secretType: secretType), secretType);
}
#pragma warning disable CS0618 // Type or member is obsolete
if (value is BicepSecretOutputReference)
{
throw new NotSupportedException("Automatic Key vault generation is not supported in this environment. Please create a key vault resource directly.");
}
#pragma warning restore CS0618 // Type or member is obsolete
if (value is IAzureKeyVaultSecretReference vaultSecretReference)
{
if (parent is null)
{
return (AllocateKeyVaultSecretUriReference(vaultSecretReference), SecretType.KeyVault);
}
return (AllocateParameter(vaultSecretReference, secretType: SecretType.KeyVault), SecretType.KeyVault);
}
if (value is EndpointReferenceExpression epExpr)
{
var context = epExpr.Endpoint.Resource == resource
? this
: _containerAppEnvironmentContext.GetContainerAppContext(epExpr.Endpoint.Resource);
var mapping = context._endpointMapping[epExpr.Endpoint.EndpointName];
var val = GetValue(mapping, epExpr.Property);
return (val, secretType);
}
if (value is ReferenceExpression expr)
{
// Special case simple expressions
if (expr.Format == "{0}" && expr.ValueProviders.Count == 1)
{
return ProcessValue(expr.ValueProviders[0], secretType, parent: parent);
}
var args = new object[expr.ValueProviders.Count];
var index = 0;
var finalSecretType = SecretType.None;
foreach (var vp in expr.ValueProviders)
{
var (val, secret) = ProcessValue(vp, secretType, parent: expr);
if (secret != SecretType.None)
{
finalSecretType = SecretType.Normal;
}
args[index++] = val;
}
return (FormattableStringFactory.Create(expr.Format, args), finalSecretType);
}
if (value is IManifestExpressionProvider manifestExpressionProvider)
{
return (AllocateParameter(manifestExpressionProvider, secretType), secretType);
}
throw new NotSupportedException("Unsupported value type " + value.GetType());
}
private BicepValue<string> AllocateKeyVaultSecretUriReference(IAzureKeyVaultSecretReference secretOutputReference)
{
var secret = secretOutputReference.AsKeyVaultSecret(Infra);
return secret.Properties.SecretUri;
}
protected ProvisioningParameter AllocateContainerImageParameter()
=> AllocateParameter(new ContainerImageReference(resource));
protected BicepValue<string> AllocateContainerPortParameter()
=> AllocateParameter(new ContainerPortReference(resource));
protected static BicepValue<int> AsInt(BicepValue<string> value)
{
return new FunctionCallExpression(new IdentifierExpression("int"), value.Compile());
}
protected void AllocateContainerRegistryParameters()
{
_containerRegistryUrlParameter ??= AllocateParameter(_containerAppEnvironmentContext.Environment.ContainerRegistryUrl);
_containerRegistryManagedIdentityIdParameter ??= AllocateParameter(_containerAppEnvironmentContext.Environment.ContainerRegistryManagedIdentityId);
}
protected ProvisioningParameter AllocateParameter(IManifestExpressionProvider parameter, SecretType secretType = SecretType.None)
{
return parameter.AsProvisioningParameter(Infra, isSecure: secretType != SecretType.None);
}
protected void SetEntryPoint(ContainerAppContainer container)
{
if (Resource is ContainerResource containerResource && containerResource.Entrypoint is { } entrypoint)
{
container.Command = [entrypoint];
}
}
protected void AddEnvironmentVariablesAndCommandLineArgs(
ContainerAppContainer container,
Func<BicepList<ContainerAppWritableSecret>> getContainerAppConfigurationSecrets,
BicepValue<string>? containerAppIdentityId)
{
if (EnvironmentVariables.Count > 0)
{
container.Env = [];
foreach (var kv in EnvironmentVariables)
{
var (val, secretType) = ProcessValue(kv.Value);
var argValue = ResolveValue(val);
if (secretType != SecretType.None)
{
var secretName = kv.Key.Replace("_", "-").ToLowerInvariant();
var secrets = getContainerAppConfigurationSecrets();
// Get or add the secret
var secret = secrets.FirstOrDefault(s => s.Value?.Name.Value == secretName)
?.Value;
if (secret is null)
{
secret = new ContainerAppWritableSecret()
{
Name = secretName
};
if (secretType == SecretType.KeyVault)
{
// TODO: this should be able to use ToUri(), but it hit an issue
secret.KeyVaultUri = new BicepValue<Uri>(((BicepExpression?)argValue)!);
if (containerAppIdentityId is not null)
{
secret.Identity = containerAppIdentityId;
}
}
else
{
secret.Value = argValue;
}
secrets.Add(secret);
}
// The value is the secret name
val = secretName;
}
container.Env.Add(secretType switch
{
SecretType.None => new ContainerAppEnvironmentVariable { Name = kv.Key, Value = argValue },
SecretType.Normal or SecretType.KeyVault => new ContainerAppEnvironmentVariable { Name = kv.Key, SecretRef = (string)val },
_ => throw new NotSupportedException()
});
}
}
if (Args.Count > 0)
{
container.Args = [];
foreach (var arg in Args)
{
var (val, _) = ProcessValue(arg);
var argValue = ResolveValue(val);
container.Args.Add(argValue);
}
}
}
protected void AddContainerRegistryManagedIdentity(ManagedServiceIdentity identity)
{
if (_containerRegistryManagedIdentityIdParameter is null)
{
return;
}
// REVIEW: This is is a little hacky, we should probably have a better way to do this
var id = BicepFunction.Interpolate($"{_containerRegistryManagedIdentityIdParameter}").Compile().ToString();
identity.ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned;
identity.UserAssignedIdentities[id] = new UserAssignedIdentityDetails();
}
protected void AddContainerRegistryParameters(Action<BicepList<ContainerAppRegistryCredentials>> setRegistries)
{
if (_containerRegistryUrlParameter is null || _containerRegistryManagedIdentityIdParameter is null)
{
return;
}
setRegistries([
new ContainerAppRegistryCredentials
{
Server = _containerRegistryUrlParameter,
Identity = _containerRegistryManagedIdentityIdParameter
}
]);
}
protected enum SecretType
{
None,
Normal,
KeyVault,
}
}
|