Skip to content

Commit

Permalink
Support callback, custom config key and publishing for parameters. (#…
Browse files Browse the repository at this point in the history
…5586)

* Support callback, custom config key and publishing for parameters.

Fixes #4429

* Test for exception case + simplify error logic

* Rename PublishValue --> PublishDefaultValue

* Dummy change to retrigger build

* Use Theory for tests

* Drive value publishing using flag instead of method

* Delay lambda evaluation

* Rename publishValue to publishValueAsDefault

* Added comment

* Dummy change to retrigger build

* Improve publishValueAsDefault doc

* Dummy change to retrigger build

* Updated the schema and added tests
  • Loading branch information
davidebbo authored Sep 20, 2024
1 parent 3bcf24f commit 4d80745
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 92 deletions.
22 changes: 22 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/ParameterDefault.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,25 @@ static void WriteIntIfNotZero(ManifestPublishingContext context, string property
public override string GetDefaultValue() =>
PasswordGenerator.Generate(MinLength, Lower, Upper, Numeric, Special, MinLower, MinUpper, MinNumeric, MinSpecial);
}

// Simple parameter default that just returns a constant value, at both runtime and publish time.
class ConstantParameterDefault(Func<string> valueGetter) : ParameterDefault
{
private string? _value;
private bool _hasValue;

public override string GetDefaultValue()
{
if (!_hasValue)
{
_value = valueGetter();
_hasValue = true;
}
return _value!;
}

public override void WriteToManifest(ManifestPublishingContext context)
{
context.Writer.WriteString("value", GetDefaultValue());
}
}
58 changes: 49 additions & 9 deletions src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,53 @@ public static IResourceBuilder<ParameterResource> AddParameter(this IDistributed
/// <param name="builder">Distributed application builder</param>
/// <param name="name">Name of parameter resource</param>
/// <param name="value">A string value to use for the paramater</param>
/// <param name="publishValueAsDefault">Indicates whether the value should be published to the manifest. This is not meant for sensitive data.</param>
/// <param name="secret">Optional flag indicating whether the parameter should be regarded as secret.</param>
/// <returns>Resource builder for the parameter.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters",
Justification = "third parameters are mutually exclusive.")]
public static IResourceBuilder<ParameterResource> AddParameter(this IDistributedApplicationBuilder builder, string name, string value, bool secret = false)
public static IResourceBuilder<ParameterResource> AddParameter(this IDistributedApplicationBuilder builder, string name, string value, bool publishValueAsDefault = false, bool secret = false)
{
// An alternate implementation is to use some ConstantParameterDefault implementation that always returns the default value.
// However, doing this causes a "default": {} to be written to the manifest, which is not valid.
// And note that we ignore parameterDefault in the callback, because it can never be non-null, and we want our own value.
return builder.AddParameter(name, parameterDefault => value, secret: secret);
return builder.AddParameter(name, () => value, publishValueAsDefault, secret);
}

/// <summary>
/// Adds a parameter resource to the application with a value coming from a callback function.
/// </summary>
/// <param name="builder">Distributed application builder</param>
/// <param name="name">Name of parameter resource</param>
/// <param name="valueGetter">A callback function that returns the value of the parameter</param>
/// <param name="publishValueAsDefault">Indicates whether the value should be published to the manifest. This is not meant for sensitive data.</param>
/// <param name="secret">Optional flag indicating whether the parameter should be regarded as secret.</param>
/// <returns>Resource builder for the parameter.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters",
Justification = "third parameters are mutually exclusive.")]
public static IResourceBuilder<ParameterResource> AddParameter(this IDistributedApplicationBuilder builder, string name, Func<string> valueGetter, bool publishValueAsDefault = false, bool secret = false)
{
// If publishValueAsDefault is set, we wrap the valueGetter in a ConstantParameterDefault, which gives
// us both the runtime value and the value to publish to the manifest.
// Otherwise, we just use the valueGetter directly, which only gives us the runtime value.
return builder.AddParameter(name,
parameterDefault => parameterDefault != null ? parameterDefault.GetDefaultValue() : valueGetter(),
secret: secret,
parameterDefault: publishValueAsDefault ? new ConstantParameterDefault(valueGetter) : null);
}

/// <summary>
/// Adds a parameter resource to the application, with a value coming from configuration.
/// </summary>
/// <param name="builder">Distributed application builder</param>
/// <param name="name">Name of parameter resource</param>
/// <param name="configurationKey">Configuration key used to get the value of the parameter</param>
/// <param name="secret">Optional flag indicating whether the parameter should be regarded as secret.</param>
/// <returns>Resource builder for the parameter.</returns>
public static IResourceBuilder<ParameterResource> AddParameterFromConfiguration(this IDistributedApplicationBuilder builder, string name, string configurationKey, bool secret = false)
{
return builder.AddParameter(
name,
parameterDefault => GetParameterValue(builder.Configuration, name, parameterDefault, configurationKey),
secret,
configurationKey: configurationKey);
}

/// <summary>
Expand Down Expand Up @@ -74,9 +111,9 @@ public static IResourceBuilder<ParameterResource> AddParameter(this IDistributed
parameterDefault: value);
}

private static string GetParameterValue(IConfiguration configuration, string name, ParameterDefault? parameterDefault)
private static string GetParameterValue(IConfiguration configuration, string name, ParameterDefault? parameterDefault, string? configurationKey = null)
{
var configurationKey = $"Parameters:{name}";
configurationKey ??= $"Parameters:{name}";
return configuration[configurationKey]
?? parameterDefault?.GetDefaultValue()
?? throw new DistributedApplicationException($"Parameter resource could not be used because configuration key '{configurationKey}' is missing and the Parameter has no default value."); ;
Expand All @@ -87,20 +124,23 @@ internal static IResourceBuilder<ParameterResource> AddParameter(this IDistribut
Func<ParameterDefault?, string> callback,
bool secret = false,
bool connectionString = false,
ParameterDefault? parameterDefault = null)
ParameterDefault? parameterDefault = null,
string? configurationKey = null)
{
var resource = new ParameterResource(name, callback, secret);
resource.IsConnectionString = connectionString;
resource.Default = parameterDefault;

configurationKey ??= connectionString ? $"ConnectionStrings:{name}" : $"Parameters:{name}";

var state = new CustomResourceSnapshot()
{
ResourceType = "Parameter",
// hide parameters by default
State = KnownResourceStates.Hidden,
Properties = [
new("parameter.secret", secret.ToString()),
new(CustomResourceKnownProperties.Source, connectionString ? $"ConnectionStrings:{name}" : $"Parameters:{name}")
new(CustomResourceKnownProperties.Source, configurationKey)
]
};

Expand Down
4 changes: 3 additions & 1 deletion src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerName<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithLifetime<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, Aspire.Hosting.ApplicationModel.ContainerLifetime lifetime) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.ParameterDefault! value, bool secret = false, bool persist = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! value, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! value, bool publishValueAsDefault = false, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Func<string!>! valueGetter, bool publishValueAsDefault = false, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameterFromConfiguration(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! configurationKey, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ProjectResourceBuilderExtensions.WithEndpointsInEnvironment(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>! builder, System.Func<Aspire.Hosting.ApplicationModel.EndpointAnnotation!, bool>! filter) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>!
Aspire.Hosting.DistributedApplicationExecutionContext.DistributedApplicationExecutionContext(Aspire.Hosting.DistributedApplicationExecutionContextOptions! options) -> void
Aspire.Hosting.DistributedApplicationExecutionContext.ServiceProvider.get -> System.IServiceProvider!
Expand Down
98 changes: 55 additions & 43 deletions src/Schema/aspire-8.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,53 +228,65 @@
},
"default": {
"type": "object",
"required": ["generate"],
"properties": {
"generate": {
"type": "object",
"required": ["minLength"],
"oneOf": [
{
"required": ["generate"],
"properties": {
"minLength": {
"type": "number",
"description": "The minimum length of the generated value."
"generate": {
"type": "object",
"required": ["minLength"],
"properties": {
"minLength": {
"type": "number",
"description": "The minimum length of the generated value."
},
"lower": {
"type": "boolean",
"description": "Indicates whether lower case characters are allowed in the generated value."
},
"upper": {
"type": "boolean",
"description": "Indicates whether upper case characters are allowed in the generated value."
},
"numeric": {
"type": "boolean",
"description": "Indicates whether numeric characters are allowed in the generated value."
},
"special": {
"type": "boolean",
"description": "Indicates whether special characters are allowed in the generated value."
},
"minLower": {
"type": "number",
"description": "Specifies the minimum number of lower case characters that must appear in the generated value."
},
"minUpper": {
"type": "number",
"description": "Specifies the minimum number of upper case characters that must appear in the generated value."
},
"minNumeric": {
"type": "number",
"description": "Specifies the minimum number of numeric characters that must appear in the generated value."
},
"minSpecial": {
"type": "number",
"description": "Specifies the minimum number of special characters that must appear in the generated value."
}
}
},
"lower": {
"type": "boolean",
"description": "Indicates whether lower case characters are allowed in the generated value."
},
"upper": {
"type": "boolean",
"description": "Indicates whether upper case characters are allowed in the generated value."
},
"numeric": {
"type": "boolean",
"description": "Indicates whether numeric characters are allowed in the generated value."
},
"special": {
"type": "boolean",
"description": "Indicates whether special characters are allowed in the generated value."
},
"minLower": {
"type": "number",
"description": "Specifies the minimum number of lower case characters that must appear in the generated value."
},
"minUpper": {
"type": "number",
"description": "Specifies the minimum number of upper case characters that must appear in the generated value."
},
"minNumeric": {
"type": "number",
"description": "Specifies the minimum number of numeric characters that must appear in the generated value."
},
"minSpecial": {
"type": "number",
"description": "Specifies the minimum number of special characters that must appear in the generated value."
"additionalProperties": false
}
},
{
"required": ["value"],
"properties": {
"value": {
"type": "string",
"description": "The default value to use if the parameter is not provided at deployment time."
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
]
}
},
"additionalProperties": false
Expand Down
Loading

0 comments on commit 4d80745

Please sign in to comment.