From 1f51824cbc1873101ed828d34e12b2c94476914d Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 2 Nov 2021 09:38:41 -0700 Subject: [PATCH] Support additional Dapr configuration properties (#1209) * Support more Dapr properties. * Update schema with Dapr extension properties. * Add more Dapr properties. * Fix formatting. --- docs/reference/schema.md | 95 +++++++++++++++ .../Dapr/DaprExtension.cs | 109 +++++++++++++++--- .../Dapr/DaprExtensionConfiguration.cs | 13 +++ .../Dapr/DaprExtensionConfigurationReader.cs | 44 ++++++- 4 files changed, 241 insertions(+), 20 deletions(-) diff --git a/docs/reference/schema.md b/docs/reference/schema.md index 876ffa3f..1c3c148a 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -707,3 +707,98 @@ If both `port` and `protocol` are provided, Tye selects the first biding with th #### `headers` (`(name, value)[]`) Array of headers that are sent as part of the HTTP request that probes the replicas of the service. + +## Extensions + +Each of the Tye extensions have their own custom configuration schema. + +### Dapr + +For the Dapr extension, the available properties closely follow those found on the [`dapr run` command line](https://docs.dapr.io/reference/cli/dapr-run/). The Dapr extension allows many properties to be set and/or overridden on a service-by-service basis using the `services` dictionary. + +```yaml +extensions: +- name: dapr + config: common + enable-profiling: true + services: + frontend: + components-path: "./frontend/components" + backend: + components-path: "./backend/components" + +services: +- name: frontend +- name: backend +``` + +The following properties are annotated as follows: + + - *extension-level*: Can only be set at the root of the extension configuration. + - *service-level*: Can only be set for a specific service within the `services` dictionary. + - *overridable*: Can be set at both the root of the extension configuration or be overridden for a specific service within the `services` dictionary. + +#### `app-id` (`string`) *service-level* + +The ID for your application, used for service discovery. + +#### `app-max-concurrency` (`integer`) *overridable* + +The concurrency level of the +application (otherwise unlimited). + +#### `app-protocol` (`string`) *overridable* + +The protocol (gRPC or HTTP) Dapr uses to talk to the application (with HTTP being the default). + +#### `app-ssl` (`boolean`) *overridable* + +Enable HTTPS when Dapr invokes the application. + +#### `components-path` (`string`) *overridable* + +The path of the components directory. If relative, is relative to the root directory of the application (i.e. of the `tye.yaml`). + +#### `config` (`string`) *overridable* + +The name of the Dapr configuration file (without extension). Assumed to be relative to the `components-path` directory, if specified, else the `components` folder in the root directory of the application (i.e. of the `tye.yaml`). + +#### `enabled` (`boolean`) *service-level* + +Whether a Dapr sidecar is created for the service. If `true`, a sidecar is created even if the default for the service type would be not to create one. If `false`, a sidecar is *not* created even if the default for the service type would be to create one. + +#### `enable-profiling` (`boolean`) *overridable* + +Enable `pprof` profiling via an HTTP endpoint. + +#### `grpc-port` (`integer`) *service-level* + +The gRPC port for Dapr to listen on. + +#### `http-max-request-size` (`integer`) *overridable* + +The maximum size of an HTTP request body in MB. + +#### `http-port` (`integer`) *service-level* + +The HTTP port for Dapr to listen on. + +#### `log-level` (`string`) *overridable* + +The log verbosity. Valid values are: `debug`, `info`, `warn`, `error`, `fatal`, and `panic`. + +#### `metrics-port` (`integer`) *service-level* + +The port used to collect Dapr metrics. + +#### `placement-port` (`integer`) *service-level* + +The port of the Dapr placement service. + +#### `profile-port` (`integer`) *service-level* + +The port for the Dapr profile servicer to listen on. + +#### `services` (`(string, object)[]`) *extension-level* + +The dictionary in which service-level configuration can be set. diff --git a/src/Microsoft.Tye.Extensions/Dapr/DaprExtension.cs b/src/Microsoft.Tye.Extensions/Dapr/DaprExtension.cs index dd0b9972..3e682fb7 100644 --- a/src/Microsoft.Tye.Extensions/Dapr/DaprExtension.cs +++ b/src/Microsoft.Tye.Extensions/Dapr/DaprExtension.cs @@ -19,12 +19,12 @@ namespace Microsoft.Tye.Extensions.Dapr // If we're getting called then the user configured dapr in their tye.yaml. // We don't have any of our own config. + var extensionConfiguration = DaprExtensionConfigurationReader.ReadConfiguration(config.Data); + if (context.Operation == ExtensionContext.OperationKind.LocalRun) { await VerifyDaprInitialized(context); - var extensionConfiguration = DaprExtensionConfigurationReader.ReadConfiguration(config.Data); - // For local run, enumerate all projects, and add services for each dapr proxy. var projects = context.Application.Services.OfType().Cast(); var executables = context.Application.Services.OfType().Cast(); @@ -32,7 +32,9 @@ namespace Microsoft.Tye.Extensions.Dapr foreach (var project in services) { - extensionConfiguration.Services.TryGetValue(project.Name, out DaprExtensionServiceConfiguration? serviceConfiguration); + DaprExtensionServiceConfiguration? serviceConfiguration = null; + + extensionConfiguration?.Services.TryGetValue(project.Name, out serviceConfiguration); if (serviceConfiguration?.Enabled != null && serviceConfiguration.Enabled.Value == false) { @@ -68,13 +70,15 @@ namespace Microsoft.Tye.Extensions.Dapr var daprExecutablePath = GetDaprExecutablePath(); + string appId = serviceConfiguration?.AppId ?? project.Name; + var proxy = new ExecutableServiceBuilder($"{project.Name}-dapr", daprExecutablePath, ServiceSource.Extension) { WorkingDirectory = context.Application.Source.DirectoryName, // These environment variables are replaced with environment variables // defined for this service. - Args = $"run --app-id {project.Name} --dapr-grpc-port %DAPR_GRPC_PORT% --dapr-http-port %DAPR_HTTP_PORT% --metrics-port %METRICS_PORT%", + Args = $"run --app-id {appId} --dapr-grpc-port %DAPR_GRPC_PORT% --dapr-http-port %DAPR_HTTP_PORT% --metrics-port %METRICS_PORT%", }; if (httpBinding != null) @@ -82,7 +86,28 @@ namespace Microsoft.Tye.Extensions.Dapr proxy.Args += $" --app-port %APP_PORT%"; } - var daprPlacementPort = serviceConfiguration?.PlacementPort ?? extensionConfiguration.PlacementPort; + var appMaxConcurrency = serviceConfiguration?.AppMaxConcurrency ?? extensionConfiguration?.AppMaxConcurrency; + + if (appMaxConcurrency != null) + { + proxy.Args += $" --app-max-concurrency {appMaxConcurrency}"; + } + + var appProtocol = serviceConfiguration?.AppProtocol ?? extensionConfiguration?.AppProtocol; + + if (appProtocol != null) + { + proxy.Args += $" --app-protocol {appProtocol}"; + } + + var appSsl = serviceConfiguration?.AppSsl ?? extensionConfiguration?.AppSsl; + + if (appSsl == true) + { + proxy.Args += " --app-ssl"; + } + + var daprPlacementPort = serviceConfiguration?.PlacementPort ?? extensionConfiguration?.PlacementPort; if (daprPlacementPort.HasValue) { @@ -90,11 +115,21 @@ namespace Microsoft.Tye.Extensions.Dapr proxy.Args += $" --placement-host-address localhost:{daprPlacementPort.Value}"; } + string? componentsPath = serviceConfiguration?.ComponentsPath ?? extensionConfiguration?.ComponentsPath; + + if (componentsPath != null) + { + proxy.Args += $" --components-path {componentsPath}"; + } + + string? daprConfig = serviceConfiguration?.Config ?? extensionConfiguration?.Config; + // When running locally `-config` specifies a filename, not a configuration name. By convention // we'll assume the filename and config name are the same. - if (config.Data.TryGetValue("config", out var obj) && obj?.ToString() is string daprConfig) + if (daprConfig != null) { - var configFile = Path.Combine(context.Application.Source.DirectoryName!, "components", $"{daprConfig}.yaml"); + string configDirectory = componentsPath ?? Path.Combine(context.Application.Source.DirectoryName!, "components"); + var configFile = Path.Combine(configDirectory, $"{daprConfig}.yaml"); if (File.Exists(configFile)) { proxy.Args += $" --config \"{configFile}\""; @@ -105,15 +140,25 @@ namespace Microsoft.Tye.Extensions.Dapr } } - if (config.Data.TryGetValue("log-level", out obj) && obj?.ToString() is string logLevel) + int? httpMaxRequestSize = serviceConfiguration?.HttpMaxRequestSize ?? extensionConfiguration?.HttpMaxRequestSize; + + if (httpMaxRequestSize != null) { - proxy.Args += $" --log-level {logLevel}"; + proxy.Args += $" --dapr-http-max-request-size {httpMaxRequestSize}"; } - if (config.Data.TryGetValue("components-path", out obj) && obj?.ToString() is string componentsPath) + if ((serviceConfiguration?.EnableProfiling ?? extensionConfiguration?.EnableProfiling) == true) { - proxy.Args += $" --components-path {componentsPath}"; + proxy.Args += " --enable-profiling"; + } + + string? logLevel = serviceConfiguration?.LogLevel ?? extensionConfiguration?.LogLevel; + + if (logLevel != null) + { + proxy.Args += $" --log-level {logLevel}"; } + // Add dapr proxy as a service available to everyone. proxy.Dependencies.UnionWith(context.Application.Services.Select(s => s.Name)); @@ -129,6 +174,7 @@ namespace Microsoft.Tye.Extensions.Dapr { Name = "grpc", Protocol = "https", + Port = serviceConfiguration?.GrpcPort }; proxy.Bindings.Add(grpc); @@ -137,6 +183,7 @@ namespace Microsoft.Tye.Extensions.Dapr { Name = "http", Protocol = "http", + Port = serviceConfiguration?.HttpPort }; proxy.Bindings.Add(http); @@ -145,6 +192,7 @@ namespace Microsoft.Tye.Extensions.Dapr { Name = "metrics", Protocol = "http", + Port = serviceConfiguration?.MetricsPort }; proxy.Bindings.Add(metrics); @@ -210,6 +258,29 @@ namespace Microsoft.Tye.Extensions.Dapr }, }; proxy.EnvironmentVariables.Add(metricsPort); + + // TODO: Do we add a means of dynamically using the profile port? + if (serviceConfiguration?.ProfilePort != null) + { + proxy.Args += $" --profile-port %PROFILE_PORT%"; + + var profile = new BindingBuilder() + { + Name = "profile", + Protocol = "http", + Port = serviceConfiguration.ProfilePort + }; + proxy.Bindings.Add(profile); + + var profilePort = new EnvironmentVariableBuilder("PROFILE_PORT") + { + Source = new EnvironmentVariableSourceBuilder(proxy.Name, binding: "profile") + { + Kind = EnvironmentVariableSourceBuilder.SourceKind.Port, + }, + }; + proxy.EnvironmentVariables.Add(profilePort); + } } } else @@ -218,6 +289,10 @@ namespace Microsoft.Tye.Extensions.Dapr var projects = context.Application.Services.OfType(); foreach (var project in projects) { + DaprExtensionServiceConfiguration? serviceConfiguration = null; + + extensionConfiguration?.Services.TryGetValue(project.Name, out serviceConfiguration); + // Dapr requires http. If this project isn't listening to HTTP then it's not daprized. var httpBinding = project.Bindings.FirstOrDefault(b => b.Protocol == "http"); if (httpBinding == null) @@ -230,16 +305,22 @@ namespace Microsoft.Tye.Extensions.Dapr continue; } + string appId = serviceConfiguration?.AppId ?? project.Name; + deployment.Annotations.Add("dapr.io/enabled", "true"); - deployment.Annotations.Add("dapr.io/app-id", project.Name); + deployment.Annotations.Add("dapr.io/app-id", appId); deployment.Annotations.Add("dapr.io/app-port", (httpBinding.Port ?? 80).ToString(CultureInfo.InvariantCulture)); - if (config.Data.TryGetValue("config", out var obj) && obj?.ToString() is string daprConfig) + string? daprConfig = serviceConfiguration?.Config ?? extensionConfiguration?.Config; + + if (daprConfig != null) { deployment.Annotations.TryAdd("dapr.io/config", daprConfig); } - if (config.Data.TryGetValue("log-level", out obj) && obj?.ToString() is string logLevel) + string? logLevel = serviceConfiguration?.LogLevel ?? extensionConfiguration?.LogLevel; + + if (logLevel != null) { deployment.Annotations.TryAdd("dapr.io/log-level", logLevel); } diff --git a/src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfiguration.cs b/src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfiguration.cs index 34af1a7a..7a9ff012 100644 --- a/src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfiguration.cs +++ b/src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfiguration.cs @@ -8,12 +8,25 @@ namespace Microsoft.Tye.Extensions.Dapr { internal abstract class DaprExtensionCommonConfiguration { + public int? AppMaxConcurrency { get; set; } + public string? AppProtocol { get; set; } + public bool? AppSsl { get; set; } + public string? ComponentsPath { get; set; } + public string? Config { get; set; } + public bool? EnableProfiling { get; set; } + public int? HttpMaxRequestSize { get; set; } + public string? LogLevel { get; set; } public int? PlacementPort { get; set; } } internal sealed class DaprExtensionServiceConfiguration : DaprExtensionCommonConfiguration { + public string? AppId { get; set; } public bool? Enabled { get; set; } + public int? GrpcPort { get; set; } + public int? HttpPort { get; set; } + public int? MetricsPort { get; set; } + public int? ProfilePort { get; set; } } internal sealed class DaprExtensionConfiguration : DaprExtensionCommonConfiguration diff --git a/src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfigurationReader.cs b/src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfigurationReader.cs index bfec8848..e105da7b 100644 --- a/src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfigurationReader.cs +++ b/src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfigurationReader.cs @@ -41,18 +41,50 @@ namespace Microsoft.Tye.Extensions.Dapr { ReadCommonConfiguration(rawConfiguration, serviceConfiguration); - if (rawConfiguration.TryGetValue("enabled", out var obj) && obj is string && Boolean.TryParse(obj.ToString(), out var enabled)) - { - serviceConfiguration.Enabled = enabled; - } + serviceConfiguration.AppId = TryGetValue(rawConfiguration, "app-id"); + serviceConfiguration.Enabled = TryGetValue(rawConfiguration, "enabled"); + serviceConfiguration.GrpcPort = TryGetValue(rawConfiguration, "grpc-port"); + serviceConfiguration.HttpPort = TryGetValue(rawConfiguration, "http-port"); + serviceConfiguration.MetricsPort = TryGetValue(rawConfiguration, "metrics-port"); + serviceConfiguration.ProfilePort = TryGetValue(rawConfiguration, "profile-port"); } private static void ReadCommonConfiguration(IDictionary rawConfiguration, DaprExtensionCommonConfiguration commonConfiguration) { - if (rawConfiguration.TryGetValue("placement-port", out var obj) && obj?.ToString() is string && int.TryParse(obj.ToString(), out var customPlacementPort)) + commonConfiguration.AppMaxConcurrency = TryGetValue(rawConfiguration, "app-max-concurrency"); + commonConfiguration.AppProtocol = TryGetValue(rawConfiguration, "app-protocol"); + commonConfiguration.AppSsl = TryGetValue(rawConfiguration, "app-ssl"); + commonConfiguration.ComponentsPath = TryGetValue(rawConfiguration, "components-path"); + commonConfiguration.Config = TryGetValue(rawConfiguration, "config"); + commonConfiguration.EnableProfiling = TryGetValue(rawConfiguration, "enable-profiling"); + commonConfiguration.HttpMaxRequestSize = TryGetValue(rawConfiguration, "http-max-request-size"); + commonConfiguration.LogLevel = TryGetValue(rawConfiguration, "log-level"); + commonConfiguration.PlacementPort = TryGetValue(rawConfiguration, "placement-port"); + } + + private static string? TryGetValue(IDictionary rawConfiguration, string name) + { + return rawConfiguration.TryGetValue(name, out var obj) && obj?.ToString() is string + ? obj.ToString() + : null; + } + + private static T? TryGetValue(IDictionary rawConfiguration, string name) + where T : struct + { + if (rawConfiguration.TryGetValue(name, out var obj) && obj?.ToString() is string) { - commonConfiguration.PlacementPort = customPlacementPort; + try + { + return (T?)Convert.ChangeType(obj.ToString(), typeof(T)); + } + catch + { + // No-op. + } } + + return null; } } }