Skip to content

Commit

Permalink
Use endpoint references for all allocated endpoint resolution (#2596)
Browse files Browse the repository at this point in the history
* Support live references in the application model
- This change removes setting manifest expressions strings in publish mode (except inputs) and pushes object references into the model directly.
- This change also introduces IManifestExpressionProvider and IValueProvider as way for external code to represent manifest and values without code taking a strong dependendency on it. These are implemented on ParameterResource, IResourceWithConnectionString, EndpointReference and ConnectionStringReference in the core.
- Introduce ConnectionStringReference which stores the underlying resource and an option bool (preserving the syntax of the call itself).
- Added methods on EndpointReference to allow getting various parts of the URL
- BicepOutputReference and BicepSecretOutputReference also implement the new interfaces.
- Use endpoint references for all allocated endpoint resolution
- Don't use lower level AllocatedEndpointAnnotation directly, instead use EndpointReference to represent the primary endpoint for container resources.
- Pick a primary named endpoint
  • Loading branch information
davidfowl committed Mar 4, 2024
1 parent f6a2f9f commit 990f158
Show file tree
Hide file tree
Showing 46 changed files with 464 additions and 642 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public override async Task<bool> ConfigureResourceAsync(IConfiguration configura
return false;
}

var currentCheckSum = GetCurrentChecksum(resource, section);
var currentCheckSum = await GetCurrentChecksumAsync(resource, section, cancellationToken).ConfigureAwait(false);
var configCheckSum = section["CheckSum"];

if (currentCheckSum != configCheckSum)
Expand Down Expand Up @@ -203,7 +203,7 @@ await notificationService.PublishUpdateAsync(resource, state => state with

// Convert the parameters to a JSON object
var parameters = new JsonObject();
SetParameters(parameters, resource);
await SetParametersAsync(parameters, resource, cancellationToken: cancellationToken).ConfigureAwait(false);

var sw = Stopwatch.StartNew();

Expand Down Expand Up @@ -408,7 +408,7 @@ internal static string GetChecksum(AzureBicepResource resource, JsonObject param
return Convert.ToHexString(hashedContents).ToLowerInvariant();
}

internal static string? GetCurrentChecksum(AzureBicepResource resource, IConfiguration section)
internal static async ValueTask<string?> GetCurrentChecksumAsync(AzureBicepResource resource, IConfiguration section, CancellationToken cancellationToken = default)
{
// Fill in parameters from configuration
if (section["Parameters"] is not string jsonString)
Expand All @@ -427,7 +427,7 @@ internal static string GetChecksum(AzureBicepResource resource, JsonObject param

// Now overwite with live object values skipping known values.
// This is important because the provisioner will fill in the known values
SetParameters(parameters, resource, skipKnownValues: true);
await SetParametersAsync(parameters, resource, skipKnownValues: true, cancellationToken: cancellationToken).ConfigureAwait(false);

// Get the checksum of the new values
return GetChecksum(resource, parameters);
Expand All @@ -451,7 +451,7 @@ internal static string GetChecksum(AzureBicepResource resource, JsonObject param
];
// Converts the parameters to a JSON object compatible with the ARM template
internal static void SetParameters(JsonObject parameters, AzureBicepResource resource, bool skipKnownValues = false)
internal static async Task SetParametersAsync(JsonObject parameters, AzureBicepResource resource, bool skipKnownValues = false, CancellationToken cancellationToken = default)
{
// Convert the parameters to a JSON object
foreach (var parameter in resource.Parameters)
Expand All @@ -473,12 +473,12 @@ internal static void SetParameters(JsonObject parameters, AzureBicepResource res
int i => i,
bool b => b,
JsonNode node => node,
IResourceBuilder<IResourceWithConnectionString> c => c.Resource.GetConnectionString(),
IResourceBuilder<ParameterResource> p => p.Resource.Value,
// TODO: Support this
AzureBicepResource bicepResource => throw new NotSupportedException("Referencing bicep resources is not supported"),
BicepOutputReference reference => throw new NotSupportedException("Referencing bicep outputs is not supported"),
object o => o.ToString()!,
IValueProvider v => await v.GetValueAsync(cancellationToken).ConfigureAwait(false),
null => null,
_ => throw new NotSupportedException($"The parameter value type {parameterValue.GetType()} is not supported.")
}
};
}
Expand Down
8 changes: 3 additions & 5 deletions src/Aspire.Hosting.Azure/AzureBicepResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ public virtual void WriteToManifest(ManifestPublishingContext context)

var value = input.Value switch
{
IResourceBuilder<ParameterResource> p => p.Resource.ValueExpression,
IResourceBuilder<IResourceWithConnectionString> p => p.Resource.ConnectionStringReferenceExpression,
BicepOutputReference output => output.ValueExpression,
IManifestExpressionProvider output => output.ValueExpression,
object obj => obj.ToString(),
null => ""
};
Expand Down Expand Up @@ -241,7 +239,7 @@ public void Dispose()
/// </summary>
/// <param name="name">The name of the secret output.</param>
/// <param name="resource">The <see cref="AzureBicepResource"/>.</param>
public class BicepSecretOutputReference(string name, AzureBicepResource resource)
public class BicepSecretOutputReference(string name, AzureBicepResource resource) : IManifestExpressionProvider, IValueProvider
{
/// <summary>
/// Name of the output.
Expand Down Expand Up @@ -293,7 +291,7 @@ public string? Value
/// </summary>
/// <param name="name">The name of the output</param>
/// <param name="resource">The <see cref="AzureBicepResource"/>.</param>
public class BicepOutputReference(string name, AzureBicepResource resource)
public class BicepOutputReference(string name, AzureBicepResource resource) : IManifestExpressionProvider, IValueProvider
{
/// <summary>
/// Name of the output.
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.Azure/AzureConstructResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static void AssignParameter<T>(this Resource<T> resource, Expression<Func
throw new ArgumentException("Cannot bind Aspire parameter resource to this construct.", nameof(resource));
}

construct.Resource.Parameters[parameterName] = parameterResourceBuilder;
construct.Resource.Parameters[parameterName] = parameterResourceBuilder.Resource;

if (resource.Scope.GetParameters().Any(p => p.Name == parameterName))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -75,17 +74,9 @@ public static BicepSecretOutputReference GetSecretOutput(this IResourceBuilder<A
public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> builder, string name, BicepOutputReference bicepOutputReference)
where T : IResourceWithEnvironment
{
return builder.WithEnvironment(async ctx =>
return builder.WithEnvironment(ctx =>
{
if (ctx.ExecutionContext.IsPublishMode)
{
ctx.EnvironmentVariables[name] = bicepOutputReference.ValueExpression;
return;
}
ctx.Logger?.LogInformation("Getting bicep output {Name} from resource {ResourceName}", bicepOutputReference.Name, bicepOutputReference.Resource.Name);
ctx.EnvironmentVariables[name] = await bicepOutputReference.GetValueAsync(ctx.CancellationToken).ConfigureAwait(false) ?? "";
ctx.EnvironmentVariables[name] = bicepOutputReference;
});
}

Expand All @@ -100,17 +91,9 @@ public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> bu
public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> builder, string name, BicepSecretOutputReference bicepOutputReference)
where T : IResourceWithEnvironment
{
return builder.WithEnvironment(async ctx =>
return builder.WithEnvironment(ctx =>
{
if (ctx.ExecutionContext.IsPublishMode)
{
ctx.EnvironmentVariables[name] = bicepOutputReference.ValueExpression;
return;
}
ctx.Logger?.LogInformation("Getting bicep secret output {Name} from resource {ResourceName}", bicepOutputReference.Name, bicepOutputReference.Resource.Name);
ctx.EnvironmentVariables[name] = await bicepOutputReference.GetValueAsync(ctx.CancellationToken).ConfigureAwait(false) ?? "";
ctx.EnvironmentVariables[name] = bicepOutputReference;
});
}

Expand Down Expand Up @@ -199,7 +182,7 @@ public static IResourceBuilder<T> WithParameter<T>(this IResourceBuilder<T> buil
public static IResourceBuilder<T> WithParameter<T>(this IResourceBuilder<T> builder, string name, IResourceBuilder<ParameterResource> value)
where T : AzureBicepResource
{
builder.Resource.Parameters[name] = value;
builder.Resource.Parameters[name] = value.Resource;
return builder;
}

Expand All @@ -214,7 +197,7 @@ public static IResourceBuilder<T> WithParameter<T>(this IResourceBuilder<T> buil
public static IResourceBuilder<T> WithParameter<T>(this IResourceBuilder<T> builder, string name, IResourceBuilder<IResourceWithConnectionString> value)
where T : AzureBicepResource
{
builder.Resource.Parameters[name] = value;
builder.Resource.Parameters[name] = value.Resource;
return builder;
}

Expand Down
33 changes: 33 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents a reference to a connection string.
/// </summary>
public class ConnectionStringReference(IResourceWithConnectionString resource, bool optional) : IManifestExpressionProvider, IValueProvider
{
/// <summary>
/// The resource that the connection string is referencing.
/// </summary>
public IResourceWithConnectionString Resource { get; } = resource;

/// <summary>
/// A flag indicating whether the connection string is optional.
/// </summary>
public bool Optional { get; } = optional;

string IManifestExpressionProvider.ValueExpression => Resource.ValueExpression;

async ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken)
{
var value = await Resource.GetValueAsync(cancellationToken).ConfigureAwait(false);

if (value is null && !Optional)
{
throw new DistributedApplicationException($"The connection string for the resource `{Resource.Name}` is not available.");
}

return value;
}
}
81 changes: 63 additions & 18 deletions src/Aspire.Hosting/ApplicationModel/EndpointReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// </summary>
/// <param name="owner">The resource with endpoints that owns the endpoint reference.</param>
/// <param name="endpointName">The name of the endpoint.</param>
public sealed class EndpointReference(IResourceWithEndpoints owner, string endpointName)
public sealed class EndpointReference(IResourceWithEndpoints owner, string endpointName) : IManifestExpressionProvider, IValueProvider
{
/// <summary>
/// Gets the owner of the endpoint reference.
Expand All @@ -21,34 +21,79 @@ public sealed class EndpointReference(IResourceWithEndpoints owner, string endpo
public string EndpointName { get; } = endpointName;

/// <summary>
/// Gets the expression used in the manifest to reference the value of the endpoint.
/// Gets a value indicating whether the endpoint is allocated.
/// </summary>
public string ValueExpression => $"{{{Owner.Name}.bindings.{EndpointName}.url}}";
public bool IsAllocated => Owner.Annotations.OfType<AllocatedEndpointAnnotation>().Any(a => a.Name == EndpointName);

string IManifestExpressionProvider.ValueExpression => GetExpression();

ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken) => new(Url);

/// <summary>
/// Gets the URI string for the endpoint reference.
/// Gets the specified property expression of the endpoint. Defaults to the URL if no property is specified.
/// </summary>
public string Value
public string GetExpression(EndpointProperty property = EndpointProperty.Url)
{
get
var prop = property switch
{
var allocatedEndpoint = Owner.Annotations.OfType<AllocatedEndpointAnnotation>().SingleOrDefault(a => a.Name == EndpointName);
EndpointProperty.Url => "url",
EndpointProperty.Host => "host",
EndpointProperty.Port => "port",
EndpointProperty.Scheme => "scheme",
_ => throw new InvalidOperationException($"The property `{property}` is not supported for the endpoint `{EndpointName}`.")
};

return allocatedEndpoint?.UriString ??
throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Owner.Name}`.");
}
return $"{{{Owner.Name}.bindings.{EndpointName}.{prop}}}";
}

/// <summary>
/// Gets the URI string for the endpoint reference.
/// Gets the port for this endpoint.
/// </summary>
public int Port => GetAllocatedEndpoint().Port;

/// <summary>
/// Gets the host for this endpoint.
/// </summary>
public string Host => GetAllocatedEndpoint().Address ?? "localhost";

/// <summary>
/// Gets the scheme for this endpoint.
/// </summary>
public string Scheme => GetAllocatedEndpoint().UriScheme;

/// <summary>
/// Gets the URL for this endpoint.
/// </summary>
[Obsolete("Use Value instead.")]
public string UriString
public string Url => GetAllocatedEndpoint().UriString;

private AllocatedEndpointAnnotation GetAllocatedEndpoint()
{
get
{
var allocatedEndpoint = Owner.Annotations.OfType<AllocatedEndpointAnnotation>().SingleOrDefault(a => a.Name == EndpointName);
return allocatedEndpoint?.UriString ?? $"{{{Owner.Name}.bindings.{EndpointName}.url}}";
}
var allocatedEndpoint = Owner.Annotations.OfType<AllocatedEndpointAnnotation>().SingleOrDefault(a => a.Name == EndpointName) ??
throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Owner.Name}`.");

return allocatedEndpoint;
}
}

/// <summary>
/// Represents the properties of an endpoint that can be referenced.
/// </summary>
public enum EndpointProperty
{
/// <summary>
/// The entire URL of the endpoint.
/// </summary>
Url,
/// <summary>
/// The host of the endpoint.
/// </summary>
Host,
/// <summary>
/// The port of the endpoint.
/// </summary>
Port,
/// <summary>
/// The scheme of the endpoint.
/// </summary>
Scheme
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public EnvironmentCallbackAnnotation(string name, Func<string> callback)
/// Initializes a new instance of the <see cref="EnvironmentCallbackAnnotation"/> class with the specified callback action.
/// </summary>
/// <param name="callback">The callback action to be executed.</param>
public EnvironmentCallbackAnnotation(Action<Dictionary<string, string>> callback)
public EnvironmentCallbackAnnotation(Action<Dictionary<string, object>> callback)
{
Callback = (c) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// <param name="executionContext">The execution context for this invocation of the AppHost.</param>
/// <param name="environmentVariables">The environment variables associated with this execution.</param>
/// <param name="cancellationToken">The cancellation token associated with this execution.</param>
public class EnvironmentCallbackContext(DistributedApplicationExecutionContext executionContext, Dictionary<string, string>? environmentVariables = null, CancellationToken cancellationToken = default)
public class EnvironmentCallbackContext(DistributedApplicationExecutionContext executionContext, Dictionary<string, object>? environmentVariables = null, CancellationToken cancellationToken = default)
{
/// <summary>
/// Obsolete. Use ExecutionContext instead. Will be removed in next preview.
Expand All @@ -22,7 +22,7 @@ public class EnvironmentCallbackContext(DistributedApplicationExecutionContext e
/// <summary>
/// Gets the environment variables associated with the callback context.
/// </summary>
public Dictionary<string, string> EnvironmentVariables { get; } = environmentVariables ?? new();
public Dictionary<string, object> EnvironmentVariables { get; } = environmentVariables ?? new();

/// <summary>
/// Gets the CancellationToken associated with the callback context.
Expand Down
15 changes: 15 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/IManifestExpressionProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// An interface that allows an object to express how it should be represented in a manifest.
/// </summary>
public interface IManifestExpressionProvider
{
/// <summary>
/// Gets the expression that represents a value in manifest.
/// </summary>
string ValueExpression { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// <summary>
/// Represents a resource that has a connection string associated with it.
/// </summary>
public interface IResourceWithConnectionString : IResource
public interface IResourceWithConnectionString : IResource, IManifestExpressionProvider, IValueProvider
{
/// <summary>
/// Gets the connection string associated with the resource.
Expand All @@ -21,6 +21,10 @@ public interface IResourceWithConnectionString : IResource
/// <returns>The connection string associated with the resource, when one is available.</returns>
public ValueTask<string?> GetConnectionStringAsync(CancellationToken cancellationToken = default) => new(GetConnectionString());

string IManifestExpressionProvider.ValueExpression => ConnectionStringReferenceExpression;

ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken) => GetConnectionStringAsync(cancellationToken);

/// <summary>
/// Describes the connection string format string used for this resource in the manifest.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/IValueProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// A interface that allows the value to be provided for an environment variable.
/// </summary>
public interface IValueProvider
{
/// <summary>
/// Gets the value for use as an environment variable.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public ValueTask<string?> GetValueAsync(CancellationToken cancellationToken = default);
}
Loading

0 comments on commit 990f158

Please sign in to comment.