Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a8bad02
Add a new deferred value provider for tls connection properties
danegsta Mar 3, 2026
a5aa70a
Respond to PR feedback
danegsta Mar 3, 2026
19280d2
Update comment
danegsta Mar 3, 2026
9bde1bc
Update baselines for code generation tests
danegsta Mar 3, 2026
3fbf2ef
Update comments with links to redis scheme registrations
danegsta Mar 3, 2026
0ecdf86
Make DeferredValueProvider async, use appropriate string comparison
danegsta Mar 4, 2026
fc4a4b5
Relax scheme constraints in manifest schema
danegsta Mar 4, 2026
42a8315
Omit the manifest schema going forward. Keeps schema tests to avoid r…
danegsta Mar 4, 2026
6c1d3e9
Merge remote-tracking branch 'upstream/release/13.2' into danegsta/re…
danegsta Mar 6, 2026
c47ee79
Add ConditionalReferenceExpression with polyglot codegen support
danegsta Mar 7, 2026
c914fcf
Fix outdated code generation snapshots
danegsta Mar 7, 2026
c2a6b4b
Fix CRE marshaller test to use pattern matching for auto-generated name
danegsta Mar 7, 2026
f277a91
Merge remote-tracking branch 'upstream/release/13.2' into danegsta/re…
danegsta Mar 7, 2026
02747ac
Update TwoPassScanning snapshots after merge with release/13.2
danegsta Mar 7, 2026
af482ba
Merge ConditionalReferenceExpression into ReferenceExpression
danegsta Mar 7, 2026
b433616
Ensure snapshots get updated
danegsta Mar 7, 2026
a20d5b7
Add explicit matchValue parameter to CreateConditional
danegsta Mar 7, 2026
2a77de0
Reorder matchValue parameter in polyglot language definitions
danegsta Mar 7, 2026
c58d780
Fix CreateConditional callers in RemoteHost tests
danegsta Mar 7, 2026
6e13b90
Handle conditional ReferenceExpression in Azure publishing paths
danegsta Mar 7, 2026
96eaa09
Resolve static conditionals at publish time, emit ternary only for pa…
danegsta Mar 7, 2026
415b3d3
Add test cases for all conditional expression branches
danegsta Mar 7, 2026
8bada82
Add tests for nested parameters and nested conditionals in Bicep
danegsta Mar 7, 2026
556d29b
Fix Go Handle.ToJSON return type for map compatibility
danegsta Mar 7, 2026
2f3177b
Implement Serialize for Rust ReferenceExpression
danegsta Mar 7, 2026
b45f0c6
Add Deserialize impl for Rust ReferenceExpression
danegsta Mar 7, 2026
e831995
Remove handle mode from ReferenceExpression in Go, Rust, Python, Java
danegsta Mar 7, 2026
081599d
Rename Go factory to NewConditionalReferenceExpression
danegsta Mar 7, 2026
03655e5
Wrap format and args in Option in Rust ReferenceExpression
danegsta Mar 7, 2026
1188db2
Revert ReferenceExpressionTypeId skip blocks from Python and TypeScri…
danegsta Mar 7, 2026
3d867a6
Revert transport.go Handle.ToJSON return type to map[string]string
danegsta Mar 7, 2026
1cc9f31
Remove low-value ConditionalReferenceExpression tests
danegsta Mar 7, 2026
3ad5a25
Fix TypeScript base.ts: import wrapIfHandle for conditional mode
danegsta Mar 7, 2026
bf5a740
Fix incorrect use of wrapIfHandle in ReferenceExpression serialization
danegsta Mar 7, 2026
abbd868
Disable HTTPS certificate for Redis in EndToEnd test AppHost
danegsta Mar 8, 2026
75b7b8c
Add conditional ReferenceExpression support to App Service, Docker Co…
danegsta Mar 8, 2026
09d773b
Use Helm ternary for parameter-based conditionals in Kubernetes publi…
danegsta Mar 8, 2026
7a00975
Fix BuildHelmTernary fallback detection and add HelmExtensions tests
danegsta Mar 8, 2026
0052dbc
Use Helm if/else for conditionals with expression branches
danegsta Mar 8, 2026
1f48c5b
Resolve merge conflict in Java two-pass scanning snapshot
danegsta Mar 9, 2026
434aac0
Populate ValueProviders for conditional ReferenceExpressions
danegsta Mar 9, 2026
d4edea5
Remove Redis connection string caching and fix manifest conditional r…
danegsta Mar 9, 2026
0acd987
Update Go and Rust expression key from $refExpr to $expr
danegsta Mar 9, 2026
51c4b97
Add tests for conditional ReferenceExpression ValueProviders and Refe…
danegsta Mar 9, 2026
3b85a72
Add conditional ReferenceExpression tests to ResourceDependencyTests
danegsta Mar 9, 2026
8c03ad2
Fix ExpressionResolver to handle conditional ReferenceExpressions
danegsta Mar 9, 2026
30cbdc3
Simplify Helm conditional to if/else only with case-insensitive compa…
danegsta Mar 10, 2026
0a0623a
Use toLower for case-insensitive Bicep conditional comparison
danegsta Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal abstract class BaseContainerAppContext(IResource resource, ContainerApp
/// </summary>
public string NormalizedContainerAppName => resource.Name.ToLowerInvariant();

protected record struct EndpointMapping(string Scheme, string Host, int Port, int? TargetPort, bool IsHttpIngress, bool External);
protected record struct EndpointMapping(string Scheme, string Host, int Port, int? TargetPort, bool IsHttpIngress, bool External, bool TlsEnabled);
protected readonly Dictionary<string, EndpointMapping> _endpointMapping = [];

// Resolved environment variables and command line args
Expand Down Expand Up @@ -186,7 +186,7 @@ private void ProcessVolumes()

private BicepValue<string> GetEndpointValue(EndpointMapping mapping, EndpointProperty property)
{
var (scheme, host, port, targetPort, isHttpIngress, external) = mapping;
var (scheme, host, port, targetPort, isHttpIngress, external, tlsEnabled) = mapping;

BicepValue<string> GetHostValue(string? prefix = null, string? suffix = null)
{
Expand All @@ -208,6 +208,7 @@ BicepValue<string> GetHostValue(string? prefix = null, string? suffix = null)
EndpointProperty.HostAndPort => GetHostValue(suffix: $":{port}"),
EndpointProperty.TargetPort => targetPort is null ? AllocateContainerPortParameter() : $"{targetPort}",
EndpointProperty.Scheme => scheme,
EndpointProperty.TlsEnabled => tlsEnabled ? bool.TrueString : bool.FalseString,
_ => throw new NotSupportedException(),
};
}
Expand Down Expand Up @@ -286,6 +287,43 @@ BicepValue<string> GetHostValue(string? prefix = null, string? suffix = null)

if (value is ReferenceExpression expr)
{
// Handle conditional expressions
if (expr.IsConditional)
{
var (conditionVal, _) = ProcessValue(expr.Condition!, secretType, parent: expr);

// If the condition resolves to a static string, evaluate at publish time
string? staticCondition = conditionVal is string str ? str : null;
if (staticCondition is null && conditionVal is BicepValue<string> bv
&& bv.Compile() is StringLiteralExpression sle)
{
staticCondition = sle.Value;
}

if (staticCondition is not null)
{
var branch = string.Equals(staticCondition, expr.MatchValue, StringComparison.OrdinalIgnoreCase)
? expr.WhenTrue!
: expr.WhenFalse!;
return ProcessValue(branch, secretType, parent: parent);
}

// Condition is a Bicep parameter/output — emit a ternary expression
var (whenTrueVal, trueSecret) = ProcessValue(expr.WhenTrue!, secretType, parent: expr);
var (whenFalseVal, falseSecret) = ProcessValue(expr.WhenFalse!, secretType, parent: expr);

var conditional = new ConditionalExpression(
new BinaryExpression(BicepFunction.ToLower(ResolveValue(conditionVal).Compile()).Compile(), BinaryBicepOperator.Equal, new StringLiteralExpression((expr.MatchValue ?? string.Empty).ToLowerInvariant())),
ResolveValue(whenTrueVal).Compile(),
ResolveValue(whenFalseVal).Compile());

var finalSecret = trueSecret != SecretType.None || falseSecret != SecretType.None
? SecretType.Normal
: SecretType.None;

return (new BicepValue<string>(conditional), finalSecret);
}

// Special case simple expressions
if (expr.Format == "{0}" && expr.ValueProviders.Count == 1)
{
Expand Down
24 changes: 12 additions & 12 deletions src/Aspire.Hosting.Azure.AppContainers/ContainerAppContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,13 @@ protected override void ProcessEndpoints()
return;
}

// Only http, https, and tcp are supported
var unsupportedEndpoints = resolvedEndpoints.Where(r => r.Endpoint.UriScheme is not ("tcp" or "http" or "https")).ToArray();
// Validate transport layer: only http-based and tcp transports are supported by Container Apps.
// The URI scheme (e.g. "redis", "rediss", "foo") is independent of transport.
var unsupportedEndpoints = resolvedEndpoints.Where(r => r.Endpoint.Transport is not ("http" or "http2" or "tcp")).ToArray();

if (unsupportedEndpoints.Length > 0)
{
throw new NotSupportedException($"The endpoint(s) {string.Join(", ", unsupportedEndpoints.Select(r => $"'{r.Endpoint.Name}'"))} specify an unsupported scheme. The supported schemes are 'http', 'https', and 'tcp'.");
throw new NotSupportedException($"The endpoint(s) {string.Join(", ", unsupportedEndpoints.Select(r => $"'{r.Endpoint.Name}'"))} specify an unsupported transport. The supported transports are 'http', 'http2', and 'tcp'.");
}

// Group resolved endpoints by target port (aka destinations), this gives us the logical bindings or destinations
Expand All @@ -162,9 +163,9 @@ protected override void ProcessEndpoints()
Port = g.Key,
ResolvedEndpoints = g.Select(x => x.resolved).ToArray(),
External = g.Any(x => x.resolved.Endpoint.IsExternal),
IsHttpOnly = g.All(x => x.resolved.Endpoint.UriScheme is "http" or "https"),
IsHttpOnly = g.All(x => x.resolved.Endpoint.Transport is "http" or "http2"),
AnyH2 = g.Any(x => x.resolved.Endpoint.Transport is "http2"),
UniqueSchemes = g.Select(x => x.resolved.Endpoint.UriScheme).Distinct().ToArray(),
UniqueTransports = g.Select(x => x.resolved.Endpoint.Transport).Distinct().ToArray(),
Index = g.Min(x => x.index)
})
.ToList();
Expand All @@ -183,12 +184,11 @@ protected override void ProcessEndpoints()
throw new NotSupportedException("External non-HTTP(s) endpoints are not supported");
}

// Don't allow mixing http and tcp endpoints
// This means we want to fail if we see a group with http/https and tcp endpoints
static bool Compatible(string[] schemes) =>
schemes.All(s => s is "http" or "https") || schemes.All(s => s is "tcp");
// Don't allow mixing http and tcp transports on the same target port
static bool Compatible(string[] transports) =>
transports.All(t => t is "http" or "http2") || transports.All(t => t is "tcp");

if (endpointsByTargetPort.Any(g => !Compatible(g.UniqueSchemes)))
if (endpointsByTargetPort.Any(g => !Compatible(g.UniqueTransports)))
{
throw new NotSupportedException("HTTP(s) and TCP endpoints cannot be mixed");
}
Expand Down Expand Up @@ -233,7 +233,7 @@ static bool Compatible(string[] schemes) =>
var scheme = preserveHttp ? endpoint.UriScheme : "https";
var port = scheme is "http" ? 80 : 443;

_endpointMapping[endpoint.Name] = new(scheme, NormalizedContainerAppName, port, targetPort, true, httpIngress.External);
_endpointMapping[endpoint.Name] = new(scheme, NormalizedContainerAppName, port, targetPort, true, httpIngress.External, endpoint.TlsEnabled);
}

// Record HTTP endpoints being upgraded (logged once at environment level)
Expand Down Expand Up @@ -265,7 +265,7 @@ static bool Compatible(string[] schemes) =>
foreach (var resolved in g.ResolvedEndpoints)
{
var endpoint = resolved.Endpoint;
_endpointMapping[endpoint.Name] = new(endpoint.UriScheme, NormalizedContainerAppName, resolved.ExposedPort.Value ?? g.Port.Value, g.Port.Value, false, g.External);
_endpointMapping[endpoint.Name] = new(endpoint.UriScheme, NormalizedContainerAppName, resolved.ExposedPort.Value ?? g.Port.Value, g.Port.Value, false, g.External, endpoint.TlsEnabled);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,43 @@ private void ProcessEndpoints()

if (value is ReferenceExpression expr)
{
// Handle conditional expressions
if (expr.IsConditional)
{
var (conditionVal, _) = ProcessValue(expr.Condition!, secretType, parent: expr, isSlot);

// If the condition resolves to a static string, evaluate at publish time
string? staticCondition = conditionVal is string str ? str : null;
if (staticCondition is null && conditionVal is BicepValue<string> bv
&& bv.Compile() is StringLiteralExpression sle)
{
staticCondition = sle.Value;
}

if (staticCondition is not null)
{
var branch = string.Equals(staticCondition, expr.MatchValue, StringComparison.OrdinalIgnoreCase)
? expr.WhenTrue!
: expr.WhenFalse!;
return ProcessValue(branch, secretType, parent: parent, isSlot);
}

// Condition is a Bicep parameter/output — emit a ternary expression
var (whenTrueVal, trueSecret) = ProcessValue(expr.WhenTrue!, secretType, parent: expr, isSlot);
var (whenFalseVal, falseSecret) = ProcessValue(expr.WhenFalse!, secretType, parent: expr, isSlot);

var conditional = new ConditionalExpression(
new BinaryExpression(BicepFunction.ToLower(ResolveValue(conditionVal).Compile()).Compile(), BinaryBicepOperator.Equal, new StringLiteralExpression((expr.MatchValue ?? string.Empty).ToLowerInvariant())),
ResolveValue(whenTrueVal).Compile(),
ResolveValue(whenFalseVal).Compile());

var finalSecret = trueSecret != SecretType.None || falseSecret != SecretType.None
? SecretType.Normal
: SecretType.None;

return (new BicepValue<string>(conditional), finalSecret);
}

if (expr.Format == "{0}" && expr.ValueProviders.Count == 1)
{
var val = ProcessValue(expr.ValueProviders[0], secretType, parent: parent, isSlot);
Expand Down
26 changes: 26 additions & 0 deletions src/Aspire.Hosting.Azure/AzurePublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,38 @@ FormattableString EvalExpr(ReferenceExpression expr)
return FormattableStringFactory.Create(expr.Format, args);
}

object EvalConditionalExpr(ReferenceExpression expr)
{
var conditionVal = Eval(expr.Condition!);

// If the condition resolves to a static string, evaluate at publish time
if (conditionVal is string staticCondition)
{
var branch = string.Equals(staticCondition, expr.MatchValue, StringComparison.OrdinalIgnoreCase)
? expr.WhenTrue!
: expr.WhenFalse!;
return Eval(branch);
}

// Condition is a Bicep parameter/output — emit a ternary expression
var whenTrueVal = ResolveValue(Eval(expr.WhenTrue!));
var whenFalseVal = ResolveValue(Eval(expr.WhenFalse!));

var conditional = new ConditionalExpression(
new BinaryExpression(ResolveValue(conditionVal).Compile(), BinaryBicepOperator.Equal, new StringLiteralExpression(expr.MatchValue!)),
whenTrueVal.Compile(),
whenFalseVal.Compile());

return new BicepValue<string>(conditional);
}

object Eval(object? value) => value switch
{
BicepOutputReference b => GetOutputs(moduleMap[b.Resource], b.Name),
ParameterResource p => ParameterLookup[p],
ConnectionStringReference r => Eval(r.Resource.ConnectionStringExpression),
IResourceWithConnectionString cs => Eval(cs.ConnectionStringExpression),
ReferenceExpression { IsConditional: true } re => EvalConditionalExpr(re),
ReferenceExpression re => EvalExpr(re),
string s => s,
_ => ""
Expand Down
36 changes: 34 additions & 2 deletions src/Aspire.Hosting.CodeGeneration.Go/Resources/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,57 @@ func NewResourceBuilderBase(handle *Handle, client *AspireClient) ResourceBuilde
}

// ReferenceExpression represents a reference expression.
// Supports value mode (Format + Args) and conditional mode (Condition + WhenTrue + WhenFalse).
type ReferenceExpression struct {
Format string
Args []any

// Conditional mode fields
Condition any
WhenTrue *ReferenceExpression
WhenFalse *ReferenceExpression
MatchValue string
isConditional bool
}

// NewReferenceExpression creates a new reference expression.
// NewReferenceExpression creates a new reference expression in value mode.
func NewReferenceExpression(format string, args ...any) *ReferenceExpression {
return &ReferenceExpression{Format: format, Args: args}
}

// NewConditionalReferenceExpression creates a conditional reference expression from its parts.
func NewConditionalReferenceExpression(condition any, matchValue string, whenTrue *ReferenceExpression, whenFalse *ReferenceExpression) *ReferenceExpression {
if matchValue == "" {
matchValue = "True"
}
return &ReferenceExpression{
Condition: condition,
WhenTrue: whenTrue,
WhenFalse: whenFalse,
MatchValue: matchValue,
isConditional: true,
}
}

// RefExpr is a convenience function for creating reference expressions.
func RefExpr(format string, args ...any) *ReferenceExpression {
return NewReferenceExpression(format, args...)
}

// ToJSON returns the reference expression as a JSON-serializable map.
func (r *ReferenceExpression) ToJSON() map[string]any {
if r.isConditional {
return map[string]any{
"$expr": map[string]any{
"condition": SerializeValue(r.Condition),
"whenTrue": r.WhenTrue.ToJSON(),
"whenFalse": r.WhenFalse.ToJSON(),
"matchValue": r.MatchValue,
},
}
}
return map[string]any{
"$refExpr": map[string]any{
"$expr": map[string]any{
"format": r.Format,
"args": r.Args,
},
Expand Down
45 changes: 45 additions & 0 deletions src/Aspire.Hosting.CodeGeneration.Java/Resources/Base.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,40 @@ class ResourceBuilderBase extends HandleWrapperBase {

/**
* ReferenceExpression represents a reference expression.
* Supports value mode (format + args) and conditional mode (condition + whenTrue + whenFalse).
*/
class ReferenceExpression {
// Value mode fields
private final String format;
private final Object[] args;

// Conditional mode fields
private final Object condition;
private final ReferenceExpression whenTrue;
private final ReferenceExpression whenFalse;
private final String matchValue;
private final boolean isConditional;

// Value mode constructor
ReferenceExpression(String format, Object... args) {
this.format = format;
this.args = args;
this.condition = null;
this.whenTrue = null;
this.whenFalse = null;
this.matchValue = null;
this.isConditional = false;
}

// Conditional mode constructor
private ReferenceExpression(Object condition, String matchValue, ReferenceExpression whenTrue, ReferenceExpression whenFalse) {
this.condition = condition;
this.whenTrue = whenTrue;
this.whenFalse = whenFalse;
this.matchValue = matchValue != null ? matchValue : "True";
this.isConditional = true;
this.format = null;
this.args = null;
}

String getFormat() {
Expand All @@ -56,6 +82,18 @@ Object[] getArgs() {
}

Map<String, Object> toJson() {
if (isConditional) {
var condPayload = new java.util.HashMap<String, Object>();
condPayload.put("condition", AspireClient.serializeValue(condition));
condPayload.put("whenTrue", whenTrue.toJson());
condPayload.put("whenFalse", whenFalse.toJson());
condPayload.put("matchValue", matchValue);

var result = new java.util.HashMap<String, Object>();
result.put("$refExpr", condPayload);
return result;
}

Map<String, Object> refExpr = new HashMap<>();
refExpr.put("format", format);
refExpr.put("args", Arrays.asList(args));
Expand All @@ -71,6 +109,13 @@ Map<String, Object> toJson() {
static ReferenceExpression refExpr(String format, Object... args) {
return new ReferenceExpression(format, args);
}

/**
* Creates a conditional reference expression from its parts.
*/
static ReferenceExpression createConditional(Object condition, String matchValue, ReferenceExpression whenTrue, ReferenceExpression whenFalse) {
return new ReferenceExpression(condition, matchValue, whenTrue, whenFalse);
}
}

/**
Expand Down
Loading
Loading