diff --git a/all.sln b/all.sln index 2ebeb4c..12a7fb3 100644 --- a/all.sln +++ b/all.sln @@ -101,6 +101,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PlacementSample", "Placemen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlacementSample", "samples\AspNetCore\PlacementSample\PlacementSample\PlacementSample.csproj", "{2FC86574-6A81-4E2B-A0D4-78D46528A917}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SchedulerSample", "SchedulerSample", "{8FD8AFF0-A56A-4BDC-B40E-F498AA147790}" + ProjectSection(SolutionItems) = preProject + samples\AspNetCore\SchedulerSample\README.md = samples\AspNetCore\SchedulerSample\README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SchedulerSample", "samples\AspNetCore\SchedulerSample\SchedulerSample\SchedulerSample.csproj", "{C0B2943F-3EA8-43A9-8714-7D6B84AD788E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -315,6 +322,18 @@ Global {2FC86574-6A81-4E2B-A0D4-78D46528A917}.Release|x64.Build.0 = Release|Any CPU {2FC86574-6A81-4E2B-A0D4-78D46528A917}.Release|x86.ActiveCfg = Release|Any CPU {2FC86574-6A81-4E2B-A0D4-78D46528A917}.Release|x86.Build.0 = Release|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Debug|x64.Build.0 = Debug|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Debug|x86.Build.0 = Debug|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Release|Any CPU.Build.0 = Release|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Release|x64.ActiveCfg = Release|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Release|x64.Build.0 = Release|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Release|x86.ActiveCfg = Release|Any CPU + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -348,6 +367,8 @@ Global {9F4DA8E9-F253-4312-A0BB-E2873A21C41A} = {7F6A5D8C-9780-4824-8284-CC3149683C70} {AE430C04-78BD-4CAE-86D7-EBC599774D9C} = {DA3D8137-F2DD-465D-81AA-3CA5C75087D2} {2FC86574-6A81-4E2B-A0D4-78D46528A917} = {AE430C04-78BD-4CAE-86D7-EBC599774D9C} + {8FD8AFF0-A56A-4BDC-B40E-F498AA147790} = {DA3D8137-F2DD-465D-81AA-3CA5C75087D2} + {C0B2943F-3EA8-43A9-8714-7D6B84AD788E} = {8FD8AFF0-A56A-4BDC-B40E-F498AA147790} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E906E97D-7D56-4E02-A13F-1C48AEB47A88} diff --git a/all.v3.ncrunchsolution b/all.v3.ncrunchsolution index 94494bd..bc095f7 100644 --- a/all.v3.ncrunchsolution +++ b/all.v3.ncrunchsolution @@ -7,6 +7,7 @@ True Run Solution Unit Tests automatically [SnapshotShared] Optimised + True True diff --git a/samples/AspNetCore/ControllerSample/ControllerSample/appsettings.json b/samples/AspNetCore/ControllerSample/ControllerSample/appsettings.json index d9d9a9b..37d58f2 100644 --- a/samples/AspNetCore/ControllerSample/ControllerSample/appsettings.json +++ b/samples/AspNetCore/ControllerSample/ControllerSample/appsettings.json @@ -1,10 +1,13 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } + "Serilog": { + "MinimumLevel": "Debug" }, - "AllowedHosts": "*" -} + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" + } diff --git a/samples/AspNetCore/PlacementSample/README.md b/samples/AspNetCore/PlacementSample/README.md index ac72ff5..d7aea36 100644 --- a/samples/AspNetCore/PlacementSample/README.md +++ b/samples/AspNetCore/PlacementSample/README.md @@ -14,7 +14,7 @@ builder.Services.AddDaprSidekick(builder.Configuration) .AddPlacement(); ``` -Typically when installing Dapr in self-hosted mode, a Placement service container is added to Docker exposing the default port 6500. If this samle is run +Typically when installing Dapr in self-hosted mode, a Placement service container is added to Docker exposing the default port 6050. If this same is run while that container is up it will be unable to start due to a port conflict. Instead a different port 6501 is assigned to Placement in configuration: ```json5 @@ -30,7 +30,7 @@ unless a specific remote address is defined. For example the following specifies ```json5 "Sidecar": { - "PlacementHostAddress": "remote-host-1:6050,remote-host-2:6050,remote-host-3:6050" + "PlacementHostAddress": "remote-host-1:50005,remote-host-2:50005,remote-host-3:50005" } ``` diff --git a/samples/AspNetCore/SchedulerSample/README.md b/samples/AspNetCore/SchedulerSample/README.md new file mode 100644 index 0000000..213ab32 --- /dev/null +++ b/samples/AspNetCore/SchedulerSample/README.md @@ -0,0 +1,42 @@ +# Scheduler example + +This sample shows how Dapr Sidekick can be used to host the Dapr Scheduler service alongside a Dapr Sidecar instance. Sidekick will launch the Scheduler service and wait for it +to enter a healthy running state, then will launch the Sidecar alongside. The Sidecar will be configured to use the launched Scheduler service by default. +As with the Sidecar, the Scheduler service will be continually monitored and maintained for the lifetime of the application. + +## How Dapr Sidekick was added + +This is an ASP.NET Core minimal Web API project. Dapr Sidekick was added to the [Program.cs](SchedulerSample\Program.cs) file as follows: + +```csharp +// Add Dapr Sidekick with Scheduler +builder.Services.AddDaprSidekick(builder.Configuration) + .AddScheduler(); +``` + +Typically when installing Dapr in self-hosted mode, a Scheduler service container is added to Docker exposing the default port 6060. If this same is run +while that container is up it will be unable to start due to a port conflict. Instead a different port 6061 is assigned to Scheduler in configuration: + +```json5 +"Scheduler": { + "RuntimeDirectory": "scheduler", + "Id": "dapr-scheduler-server-0", // Optional unique identifier when used in a cluster + "Port": 6061 // To avoid conflicts with local Dapr Scheduler container. Sidecar will use this automatically as well. +} +``` + +By default the Sidecar that is launched alongside the Scheduler service will look for the Scheduler service locally on this custom port, +unless a specific remote address is defined. For example the following specifies a three-host remote Scheduler cluster: + +```json5 +"Sidecar": { + "SchedulerHostAddress": "remote-host-1:50006,remote-host-2:50006,remote-host-3:50006" +} +``` + +## Running the sample + + To run the sample simply set `SchedulerSample` as the startup project and run it in Visual Studio, + it will launch first the Scheduler service then the Dapr sidecar, then open a browser and display + the configured launch options for both. + diff --git a/samples/AspNetCore/SchedulerSample/SchedulerSample/Program.cs b/samples/AspNetCore/SchedulerSample/SchedulerSample/Program.cs new file mode 100644 index 0000000..c21ef7c --- /dev/null +++ b/samples/AspNetCore/SchedulerSample/SchedulerSample/Program.cs @@ -0,0 +1,39 @@ +using Man.Dapr.Sidekick; +using Serilog; + +// Add Serilog for enhanced console logging. +Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); + +// Add Dapr Sidekick with Scheduler +builder.Services.AddDaprSidekick(builder.Configuration) + .AddScheduler(); + +builder.Host.UseSerilog(); + +var app = builder.Build(); + +app.MapGet("/status", (IDaprSidecarHost sidecarHost, IDaprSchedulerHost schedulerHost) => Results.Ok(new +{ + sidecar = new + { + process = sidecarHost.GetProcessInfo(), // Information about the sidecar process such as if it is running + options = sidecarHost.GetProcessOptions() // The sidecar options if running, including ports and locations + }, + scheduler = new + { + process = schedulerHost.GetProcessInfo(), // Information about the sentry process such as if it is running + options = schedulerHost.GetProcessOptions() // The sentry options if running, including ports and locations + }, +})); + +// For Dapr +app.MapHealthChecks("/health"); + +app.Run(); diff --git a/samples/AspNetCore/SchedulerSample/SchedulerSample/Properties/launchSettings.json b/samples/AspNetCore/SchedulerSample/SchedulerSample/Properties/launchSettings.json new file mode 100644 index 0000000..45c3fef --- /dev/null +++ b/samples/AspNetCore/SchedulerSample/SchedulerSample/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "SchedulerSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "status", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/samples/AspNetCore/SchedulerSample/SchedulerSample/SchedulerSample.csproj b/samples/AspNetCore/SchedulerSample/SchedulerSample/SchedulerSample.csproj new file mode 100644 index 0000000..ef1bd3f --- /dev/null +++ b/samples/AspNetCore/SchedulerSample/SchedulerSample/SchedulerSample.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/samples/AspNetCore/SchedulerSample/SchedulerSample/appsettings.Development.json b/samples/AspNetCore/SchedulerSample/SchedulerSample/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/AspNetCore/SchedulerSample/SchedulerSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/AspNetCore/SchedulerSample/SchedulerSample/appsettings.json b/samples/AspNetCore/SchedulerSample/SchedulerSample/appsettings.json new file mode 100644 index 0000000..d2f67e0 --- /dev/null +++ b/samples/AspNetCore/SchedulerSample/SchedulerSample/appsettings.json @@ -0,0 +1,19 @@ +{ + "DaprSidekick": { + "Sidecar": { + "RuntimeDirectory": "dapr" + }, + "Scheduler": { + "RuntimeDirectory": "scheduler", + "Id": "dapr-scheduler-server-0", // Optional unique identifier when used in a cluster + "Port": 6061 // To avoid conflicts with local Dapr Scheduler container. Sidecar will use this automatically as well. + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/AspNetCore/SchedulerSample/SchedulerSample/dapr/config.yaml b/samples/AspNetCore/SchedulerSample/SchedulerSample/dapr/config.yaml new file mode 100644 index 0000000..4b11e3a --- /dev/null +++ b/samples/AspNetCore/SchedulerSample/SchedulerSample/dapr/config.yaml @@ -0,0 +1,8 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: daprsystem + namespace: default +spec: + mtls: + enabled: true \ No newline at end of file diff --git a/samples/AspNetCore/SchedulerSample/SchedulerSample/scheduler/config.yaml b/samples/AspNetCore/SchedulerSample/SchedulerSample/scheduler/config.yaml new file mode 100644 index 0000000..60702f9 --- /dev/null +++ b/samples/AspNetCore/SchedulerSample/SchedulerSample/scheduler/config.yaml @@ -0,0 +1,6 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: daprsystem + namespace: default +spec: diff --git a/src/Man.Dapr.Sidekick.AspNetCore/DaprSidekickBuilder.cs b/src/Man.Dapr.Sidekick.AspNetCore/DaprSidekickBuilder.cs index e15ac3e..48ba9cf 100644 --- a/src/Man.Dapr.Sidekick.AspNetCore/DaprSidekickBuilder.cs +++ b/src/Man.Dapr.Sidekick.AspNetCore/DaprSidekickBuilder.cs @@ -1,6 +1,7 @@ using System.Linq; using Man.Dapr.Sidekick.AspNetCore.Metrics; using Man.Dapr.Sidekick.AspNetCore.Placement; +using Man.Dapr.Sidekick.AspNetCore.Scheduler; using Man.Dapr.Sidekick.AspNetCore.Sentry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -33,6 +34,22 @@ public IDaprSidekickBuilder AddPlacement() return this; } + public IDaprSidekickBuilder AddScheduler() + { + // Add the scheduler host + _services.TryAddSingleton(); + _services.TryAddHostedService(); + + // Add the health checks and metrics + _services.AddHealthChecks().AddDaprScheduler(); + _services.AddSingleton(); + + // Override the default sidecar hosted service, to one that only starts when the Scheduler service is available. + ReplaceSidecarHostedService(); + + return this; + } + public IDaprSidekickBuilder AddSentry() { // Add the Sentry host diff --git a/src/Man.Dapr.Sidekick.AspNetCore/IDaprSidekickBuilder.cs b/src/Man.Dapr.Sidekick.AspNetCore/IDaprSidekickBuilder.cs index 1f3c5db..bf107ff 100644 --- a/src/Man.Dapr.Sidekick.AspNetCore/IDaprSidekickBuilder.cs +++ b/src/Man.Dapr.Sidekick.AspNetCore/IDaprSidekickBuilder.cs @@ -11,6 +11,12 @@ public interface IDaprSidekickBuilder /// This instance to allow calls to be chained. public IDaprSidekickBuilder AddPlacement(); + /// + /// Adds the Dapr Scheduler service. + /// + /// This instance to allow calls to be chained. + public IDaprSidekickBuilder AddScheduler(); + /// /// Adds the Dapr Sentry service. /// diff --git a/src/Man.Dapr.Sidekick.AspNetCore/Metrics/DaprMetricsConstants.cs b/src/Man.Dapr.Sidekick.AspNetCore/Metrics/DaprMetricsConstants.cs index 09e44f3..d09363d 100644 --- a/src/Man.Dapr.Sidekick.AspNetCore/Metrics/DaprMetricsConstants.cs +++ b/src/Man.Dapr.Sidekick.AspNetCore/Metrics/DaprMetricsConstants.cs @@ -13,6 +13,7 @@ public static class DaprMetricsConstants public static readonly string DaprSidecarLabel = "dapr-sidecar"; public static readonly string DaprPlacementLabel = "dapr-placement"; + public static readonly string DaprSchedulerLabel = "dapr-scheduler"; public static readonly string DaprSentryLabel = "dapr-sentry"; public static readonly string ExporterContentType = "text/plain; charset=utf-8"; diff --git a/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHealthCheck.cs b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHealthCheck.cs new file mode 100644 index 0000000..0601428 --- /dev/null +++ b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHealthCheck.cs @@ -0,0 +1,11 @@ +namespace Man.Dapr.Sidekick.AspNetCore.Scheduler +{ + public class DaprSchedulerHealthCheck : DaprProcessHealthCheck + { + public DaprSchedulerHealthCheck( + IDaprSchedulerHost daprSchedulerHost) + : base(daprSchedulerHost) + { + } + } +} diff --git a/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHealthCheckBuilderExtensions.cs b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHealthCheckBuilderExtensions.cs new file mode 100644 index 0000000..77174d7 --- /dev/null +++ b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHealthCheckBuilderExtensions.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Man.Dapr.Sidekick.AspNetCore.Metrics; +using Man.Dapr.Sidekick.AspNetCore.Scheduler; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class DaprSchedulerHealthCheckBuilderExtensions + { + /// + /// Add a health check for the Dapr Scheduler. + /// + /// The . + /// The health check name. Optional. If null the type name 'dapr_Scheduler' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// The to allow calls to be chained. + public static IHealthChecksBuilder AddDaprScheduler( + this IHealthChecksBuilder builder, + string name = default, + HealthStatus? failureStatus = default, + IEnumerable tags = default) + { + builder.AddCheck(name ?? DaprMetricsConstants.DaprSchedulerLabel, failureStatus, tags); + return builder; + } + } +} diff --git a/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHostedService.cs b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHostedService.cs new file mode 100644 index 0000000..0800f5a --- /dev/null +++ b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerHostedService.cs @@ -0,0 +1,25 @@ +using System.Threading; +using Man.Dapr.Sidekick.AspNetCore.Metrics; +using Microsoft.Extensions.Options; + +namespace Man.Dapr.Sidekick.AspNetCore.Scheduler +{ + public class DaprSchedulerHostedService : DaprHostedService + { + public DaprSchedulerHostedService( + IDaprSchedulerHost daprSchedulerHost, + IOptionsMonitor optionsAccessor) + : base(daprSchedulerHost, optionsAccessor) + { + } + + protected override void OnStarting(DaprOptions options, CancellationToken cancellationToken) + { + // Assign metrics + options.Scheduler ??= new DaprSchedulerOptions(); + options.Scheduler.Metrics ??= new DaprMetricsOptions(); + options.Scheduler.Metrics.SetLabel(DaprMetricsConstants.ServiceLabelName, options.Sidecar?.AppId); + options.Scheduler.Metrics.SetLabel(DaprMetricsConstants.AppLabelName, DaprMetricsConstants.DaprSchedulerLabel); + } + } +} diff --git a/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerMetricsCollector.cs b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerMetricsCollector.cs new file mode 100644 index 0000000..95759a4 --- /dev/null +++ b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerMetricsCollector.cs @@ -0,0 +1,12 @@ +using Man.Dapr.Sidekick.AspNetCore.Metrics; + +namespace Man.Dapr.Sidekick.AspNetCore.Scheduler +{ + public class DaprSchedulerMetricsCollector : DaprProcessHostPrometheusCollector + { + public DaprSchedulerMetricsCollector(IDaprSchedulerHost daprSchedulerHost) + : base(daprSchedulerHost) + { + } + } +} diff --git a/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerSidecarHostedService.cs b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerSidecarHostedService.cs new file mode 100644 index 0000000..c74a0b9 --- /dev/null +++ b/src/Man.Dapr.Sidekick.AspNetCore/Scheduler/DaprSchedulerSidecarHostedService.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Man.Dapr.Sidekick.AspNetCore.Sidecar; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Man.Dapr.Sidekick.AspNetCore.Scheduler +{ + /// + /// A hosted service for managing the sidecar lifetime. + /// Specifically waits for the Scheduler service to start successfully and allocate ports/environment variables + /// before starting itself. + /// + public class DaprSchedulerSidecarHostedService : DaprSidecarHostedService + { + private readonly IDaprSchedulerHost _daprSchedulerHost; + private readonly ILogger _logger; + + public DaprSchedulerSidecarHostedService( + IDaprSidecarHost daprSidecarHost, + IDaprSchedulerHost daprSchedulerHost, + IOptionsMonitor optionsAccessor, + ILogger logger, + IServiceProvider serviceProvider = null) + : base(daprSidecarHost, optionsAccessor, serviceProvider) + { + _daprSchedulerHost = daprSchedulerHost; + _logger = logger; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await Task.Run( + async () => + { + var processInfo = _daprSchedulerHost.GetProcessInfo(); + while (!processInfo.IsRunning && processInfo.Status != Process.DaprProcessStatus.Disabled) + { + _logger.LogInformation("Dapr Sidecar process is waiting for the Dapr Scheduler process to finish starting up..."); + await Task.Delay(250); + if (cancellationToken.IsCancellationRequested) + { + break; + } + + processInfo = _daprSchedulerHost.GetProcessInfo(); + } + }, + cancellationToken) + .ContinueWith(_ => base.StartAsync(cancellationToken)); + } + } +} diff --git a/src/Man.Dapr.Sidekick/DaprConstants.cs b/src/Man.Dapr.Sidekick/DaprConstants.cs index 523280a..0cb5b4c 100644 --- a/src/Man.Dapr.Sidekick/DaprConstants.cs +++ b/src/Man.Dapr.Sidekick/DaprConstants.cs @@ -31,6 +31,9 @@ public class DaprConstants public const string DaprPlacementMetricsPortEnvironmentVariable = "DAPR_PLACEMENT_METRICS_PORT"; public const string DaprPlacementPortEnvironmentVariable = "DAPR_PLACEMENT_PORT"; public const string DaprProfilePortEnvironmentVariable = "DAPR_PROFILE_PORT"; + public const string DaprSchedulerHealthPortEnvironmentVariable = "DAPR_SCHEDULER_HEALTH_PORT"; + public const string DaprSchedulerMetricsPortEnvironmentVariable = "DAPR_SCHEDULER_METRICS_PORT"; + public const string DaprSchedulerPortEnvironmentVariable = "DAPR_SCHEDULER_PORT"; public const string DaprSentryMetricsPortEnvironmentVariable = "DAPR_SENTRY_METRICS_PORT"; public const string DaprTrustAnchorsEnvironmentVariable = "DAPR_TRUST_ANCHORS"; public const string NamespaceEnvironmentVariable = "NAMESPACE"; @@ -40,6 +43,7 @@ public class DaprConstants // Processes public const string DaprSidecarProcessName = "daprd"; + public const string DaprSchedulerProcessName = "scheduler"; public const string DaprSentryProcessName = "sentry"; public const string DaprPlacementProcessName = "placement"; diff --git a/src/Man.Dapr.Sidekick/DaprSchedulerHost.cs b/src/Man.Dapr.Sidekick/DaprSchedulerHost.cs new file mode 100644 index 0000000..0383163 --- /dev/null +++ b/src/Man.Dapr.Sidekick/DaprSchedulerHost.cs @@ -0,0 +1,14 @@ +using Man.Dapr.Sidekick.Http; +using Man.Dapr.Sidekick.Logging; +using Man.Dapr.Sidekick.Process; + +namespace Man.Dapr.Sidekick +{ + public sealed class DaprSchedulerHost : DaprProcessHost, IDaprSchedulerHost + { + public DaprSchedulerHost(IDaprProcessFactory daprProcessFactory, IDaprProcessHttpClientFactory daprHttpClientFactory, IDaprLoggerFactory loggerFactory) + : base(() => daprProcessFactory.CreateDaprSchedulerProcess(), daprHttpClientFactory, new DaprLogger(loggerFactory)) + { + } + } +} diff --git a/src/Man.Dapr.Sidekick/IDaprSchedulerHost.cs b/src/Man.Dapr.Sidekick/IDaprSchedulerHost.cs new file mode 100644 index 0000000..b111a87 --- /dev/null +++ b/src/Man.Dapr.Sidekick/IDaprSchedulerHost.cs @@ -0,0 +1,8 @@ +using Man.Dapr.Sidekick.Process; + +namespace Man.Dapr.Sidekick +{ + public interface IDaprSchedulerHost : IDaprProcessHost + { + } +} diff --git a/src/Man.Dapr.Sidekick/Options/DaprOptions.cs b/src/Man.Dapr.Sidekick/Options/DaprOptions.cs index 2eb522d..b951dc8 100644 --- a/src/Man.Dapr.Sidekick/Options/DaprOptions.cs +++ b/src/Man.Dapr.Sidekick/Options/DaprOptions.cs @@ -15,6 +15,8 @@ public DaprOptions() public DaprPlacementOptions Placement { get; set; } + public DaprSchedulerOptions Scheduler { get; set; } + public DaprSentryOptions Sentry { get; set; } /// @@ -26,6 +28,7 @@ public DaprOptions() var clone = (DaprOptions)base.Clone(); clone.Sidecar = Sidecar?.Clone(); clone.Placement = Placement?.Clone(); + clone.Scheduler = Scheduler?.Clone(); clone.Sentry = Sentry?.Clone(); return clone; } diff --git a/src/Man.Dapr.Sidekick/Options/DaprPlacementOptions.cs b/src/Man.Dapr.Sidekick/Options/DaprPlacementOptions.cs index da8729f..fdace79 100644 --- a/src/Man.Dapr.Sidekick/Options/DaprPlacementOptions.cs +++ b/src/Man.Dapr.Sidekick/Options/DaprPlacementOptions.cs @@ -21,6 +21,11 @@ public class DaprPlacementOptions : Options.DaprProcessOptions /// public bool? EnableMetrics { get; set; } + /// + /// Gets or sets the listening address for the healthz server. + /// + public string HealthListenAddress { get; set; } + /// /// Gets or sets the HTTP port for the health server (default 8081). /// @@ -41,11 +46,31 @@ public class DaprPlacementOptions : Options.DaprProcessOptions /// public bool? InmemStoreEnabled { get; set; } + /// + /// Gets or sets the address for the placenment server to listen on. + /// + public string ListenAddress { get; set; } + + /// + /// Gets or sets a value that determines if the placement tables are exposed on the healthz server (default false). + /// + public bool? MetadataEnabled { get; set; } + + /// + /// Gets or sets the address for the metrics server (default "0.0.0.0"). + /// + public string MetricsListenAddress { get; set; } + /// /// Gets or sets the port for the metrics server (default 9091). /// public int? MetricsPort { get; set; } + /// + /// Gets or sets the runtime mode for the placement service (default "standalone"). + /// + public string Mode { get; set; } + /// /// Gets or sets a value that determines if TLS should be enabled for the placement gRPC server. /// @@ -66,6 +91,11 @@ public class DaprPlacementOptions : Options.DaprProcessOptions /// public int? ReplicationFactor { get; set; } + /// + /// Gets or sets a value that determines if TLS should be enabled for the placement gRPC server. + /// + public bool? TlsEnabled { get; set; } + /// /// Gets or sets the filepath to the trust anchors for the Dapr control plane (default "/var/run/secrets/dapr.io/tls/ca.crt"). /// Available since 1.12.0. diff --git a/src/Man.Dapr.Sidekick/Options/DaprSchedulerOptions.cs b/src/Man.Dapr.Sidekick/Options/DaprSchedulerOptions.cs new file mode 100644 index 0000000..9112bbb --- /dev/null +++ b/src/Man.Dapr.Sidekick/Options/DaprSchedulerOptions.cs @@ -0,0 +1,149 @@ +using System; + +namespace Man.Dapr.Sidekick +{ + public class DaprSchedulerOptions : Options.DaprProcessOptions + { + /// + /// Gets or sets the path to the credentials directory holding the issuer data. + /// If not specified this will default to a directory called "certs" under the runtime folder. + /// Provided for backwards compatibility logic only. + /// + public string CertsDirectory { get; set; } + + /// + /// Gets or sets custom arguments for the process. These will be appended "as-is" after all other arguments specified through these options. + /// + public string CustomArguments { get; set; } + + /// + /// Gets or sets a value that determines if Prometheus metrics are enabled in the scheduler service (default true). + /// + public bool? EnableMetrics { get; set; } + + /// + /// Gets or sets the ports for etcd client http communication. + /// + public string EtcdClientHttpPorts { get; set; } + + /// + /// Gets or sets the ports for etcd client communication (default [dapr-scheduler-server-0=2379]). + /// + public string EtcdClientPorts { get; set; } + + /// + /// Gets or sets the compaction mode for etcd. Can be 'periodic' or 'revision' (default "periodic"). + /// + public string EtcdCompactionMode { get; set; } + + /// + /// Gets or sets the compaction retention for etcd. + /// Can express time or number of revisions, depending on the value of (default "24h"). + /// + public string EtcdCompactionRetention { get; set; } + + /// + /// Gets or sets the directory to store scheduler etcd data (default "./data"). + /// + public string EtcdDataDir { get; set; } + + /// + /// Gets or sets the space quota for etcd (default 2147483648). + /// + public int? EtcdSpaceQuota { get; set; } + + /// + /// Gets or sets the listening address for the healthz server. + /// + public string HealthListenAddress { get; set; } + + /// + /// Gets or sets the HTTP port for the health server (default 8082). + /// + public int? HealthPort { get; set; } + + /// + /// Gets or sets the scheduler server ID (default "dapr-scheduler-server-0"). + /// + public string Id { get; set; } + + /// + /// Gets or sets the raft cluster peers (default "dapr-scheduler-server-0=http://localhost:2380"). + /// + public string InitialCluster { get; set; } + + /// + /// Gets or sets the address for the scheduler server to listen on. + /// + public string ListenAddress { get; set; } + + /// + /// Gets or sets the address for the metrics server (default "0.0.0.0"). + /// + public string MetricsListenAddress { get; set; } + + /// + /// Gets or sets the port for the metrics server (default 9093). + /// + public int? MetricsPort { get; set; } + + /// + /// Gets or sets the runtime mode for the scheduler service (default "standalone"). + /// + public string Mode { get; set; } + + /// + /// Gets or sets the gRPC port for the scheduler service (defaults to 6060 on Windows and 50006 on other platforms). + /// + public int? Port { get; set; } + + /// + /// Gets or sets the total number of scheduler replicas in the cluster (default 1). + /// + public uint? ReplicaCount { get; set; } + + /// + /// Gets or sets a value that determines if TLS should be enabled for the scheduler gRPC server. + /// + public bool? TlsEnabled { get; set; } + + /// + /// Gets or sets the filepath to the trust anchors for the Dapr control plane (default "/var/run/secrets/dapr.io/tls/ca.crt"). + /// + public string TrustAnchorsFile { get; set; } + + /// + /// Gets or sets the trust domain for the Dapr control plane (default "localhost"). + /// + public string TrustDomain { get; set; } + + /// + /// Creates a deep clone of this instance. + /// + /// A deep clone of this insteance. + public new DaprSchedulerOptions Clone() => (DaprSchedulerOptions)base.Clone(); + + protected override bool AddHealthUri(UriBuilder builder) + { + if (!HealthPort.HasValue) + { + return false; + } + + builder.Port = HealthPort.Value; + builder.Path = "healthz"; + return true; + } + + protected override bool AddMetricsUri(UriBuilder builder) + { + if (!MetricsPort.HasValue || EnableMetrics == false) + { + return false; + } + + builder.Port = MetricsPort.Value; + return true; + } + } +} diff --git a/src/Man.Dapr.Sidekick/Options/DaprSidecarOptions.cs b/src/Man.Dapr.Sidekick/Options/DaprSidecarOptions.cs index 5a979a9..396a09e 100644 --- a/src/Man.Dapr.Sidekick/Options/DaprSidecarOptions.cs +++ b/src/Man.Dapr.Sidekick/Options/DaprSidecarOptions.cs @@ -153,6 +153,11 @@ public DaprSidecarOptions() /// public bool? Profiling { get; set; } + /// + /// Gets or sets the scheduler service host address. This is typically a comma-separated list of host:port endpoints. + /// + public string SchedulerHostAddress { get; set; } + /// /// Gets or sets the address for the Sentry CA service. /// @@ -173,6 +178,11 @@ public DaprSidecarOptions() /// public bool? UseDefaultPlacementHostAddress { get; set; } + /// + /// Gets or sets a value that determines if the default Scheduler Host address is used when is not specified. Defaults to true. + /// + public bool? UseDefaultSchedulerHostAddress { get; set; } + /// /// Gets the address of the metdata endpoint, such as http://127.0.0.1:3500/v1.0/metadata. /// diff --git a/src/Man.Dapr.Sidekick/Process/DaprPlacementProcess.cs b/src/Man.Dapr.Sidekick/Process/DaprPlacementProcess.cs index 64fafcc..107cdc8 100644 --- a/src/Man.Dapr.Sidekick/Process/DaprPlacementProcess.cs +++ b/src/Man.Dapr.Sidekick/Process/DaprPlacementProcess.cs @@ -7,13 +7,18 @@ internal class DaprPlacementProcess : DaprProcess, IDaprPl { private const string CertChainArgument = "certchain"; private const string EnableMetricsArgument = "enable-metrics"; + private const string HealthzListenAddressArgument = "healthz-listen-address"; private const string HealthzPortArgument = "healthz-port"; private const string IdArgument = "id"; private const string InitialClusterArgument = "initial-cluster"; private const string InmemStoreEnabledArgument = "inmem-store-enabled"; + private const string ListenAddressArgument = "listen-address"; private const string LogAsJsonArgument = "log-as-json"; private const string LogLevelArgument = "log-level"; + private const string MetadataEnabledArgument = "metadata-enabled"; + private const string MetricsListenAddressArgument = "metrics-listen-address"; private const string MetricsPortArgument = "metrics-port"; + private const string ModeArgument = "mode"; private const string PortArgument = "port"; private const string RaftLogstorePathArgument = "raft-logstore-path"; private const string ReplicationFactorArgument = "replicationfactor"; @@ -71,17 +76,22 @@ protected override void AssignLocations(DaprPlacementOptions options, string dap protected override void AddCommandLineArguments(DaprPlacementOptions source, CommandLineArgumentBuilder builder) => builder .Add(CertChainArgument, source.CertsDirectory) .Add(EnableMetricsArgument, source.EnableMetrics) + .Add(HealthzListenAddressArgument, source.HealthListenAddress) .Add(HealthzPortArgument, source.HealthPort) .Add(IdArgument, source.Id) .Add(InitialClusterArgument, source.InitialCluster) .Add(InmemStoreEnabledArgument, source.InmemStoreEnabled) + .Add(ListenAddressArgument, source.ListenAddress) .Add(LogAsJsonArgument, true) // All logging must be JSON .Add(LogLevelArgument, source.LogLevel) + .Add(MetadataEnabledArgument, source.MetadataEnabled) + .Add(MetricsListenAddressArgument, source.MetricsListenAddress) .Add(MetricsPortArgument, source.MetricsPort) + .Add(ModeArgument, source.Mode) .Add(PortArgument, source.Port) .Add(RaftLogstorePathArgument, source.RaftLogstorePath) .Add(ReplicationFactorArgument, source.ReplicationFactor) - .Add(TlsEnabledArgument, source.Mtls) + .Add(TlsEnabledArgument, source.TlsEnabled ?? source.Mtls) // Check Mtls for backwards comaptibility .Add(TrustAnchorsFileArgument, source.TrustAnchorsFile) .Add(TrustDomainArgument, source.TrustDomain) .Add(source.CustomArguments, requiresValue: false); @@ -103,6 +113,10 @@ protected override void ParseCommandLineArgument(DaprPlacementOptions target, st target.EnableMetrics = !bool.TryParse(value, out var enableMetrics) || enableMetrics; break; + case HealthzListenAddressArgument: + target.HealthListenAddress = value; + break; + case HealthzPortArgument: target.HealthPort = int.TryParse(value, out var healthzPort) ? (int?)healthzPort : null; break; @@ -119,14 +133,30 @@ protected override void ParseCommandLineArgument(DaprPlacementOptions target, st target.InmemStoreEnabled = !bool.TryParse(value, out var inmemStoreEnabled) || inmemStoreEnabled; break; + case ListenAddressArgument: + target.ListenAddress = value; + break; + case LogLevelArgument: target.LogLevel = value; break; + case MetadataEnabledArgument: + target.MetadataEnabled = !bool.TryParse(value, out var metadataEnabled) || metadataEnabled; + break; + + case MetricsListenAddressArgument: + target.MetricsListenAddress = value; + break; + case MetricsPortArgument: target.MetricsPort = int.TryParse(value, out var metricsPort) ? (int?)metricsPort : null; break; + case ModeArgument: + target.Mode = value; + break; + case PortArgument: target.Port = int.TryParse(value, out var port) ? (int?)port : null; break; @@ -140,7 +170,15 @@ protected override void ParseCommandLineArgument(DaprPlacementOptions target, st break; case TlsEnabledArgument: - target.Mtls = !bool.TryParse(value, out var tlsEnabled) || tlsEnabled; + target.TlsEnabled = !bool.TryParse(value, out var tlsEnabled) || tlsEnabled; + break; + + case TrustAnchorsFileArgument: + target.TrustAnchorsFile = value; + break; + + case TrustDomainArgument: + target.TrustDomain = value; break; } } diff --git a/src/Man.Dapr.Sidekick/Process/DaprProcessFactory.cs b/src/Man.Dapr.Sidekick/Process/DaprProcessFactory.cs index 1e1004c..dfb2c6a 100644 --- a/src/Man.Dapr.Sidekick/Process/DaprProcessFactory.cs +++ b/src/Man.Dapr.Sidekick/Process/DaprProcessFactory.cs @@ -6,6 +6,8 @@ public class DaprProcessFactory : IDaprProcessFactory public IDaprPlacementProcess CreateDaprPlacementProcess() => new DaprPlacementProcess(); + public IDaprSchedulerProcess CreateDaprSchedulerProcess() => new DaprSchedulerProcess(); + public IDaprSentryProcess CreateDaprSentryProcess() => new DaprSentryProcess(); } } diff --git a/src/Man.Dapr.Sidekick/Process/DaprSchedulerProcess.cs b/src/Man.Dapr.Sidekick/Process/DaprSchedulerProcess.cs new file mode 100644 index 0000000..6ea69aa --- /dev/null +++ b/src/Man.Dapr.Sidekick/Process/DaprSchedulerProcess.cs @@ -0,0 +1,215 @@ +using System; +using System.IO; + +namespace Man.Dapr.Sidekick.Process +{ + internal class DaprSchedulerProcess : DaprProcess, IDaprSchedulerProcess + { + private const string EnableMetricsArgument = "enable-metrics"; + private const string EtcdClientHttpPortsArgument = "etcd-client-http-ports"; + private const string EtcdClientPortsArgument = "etcd-client-ports"; + private const string EtcdCompactionModeArgument = "etcd-compaction-mode"; + private const string EtcdCompactionRetentionArgument = "etcd-compaction-retention"; + private const string EtcdDataDirArgument = "etcd-data-dir"; + private const string EtcdSpaceQuotaArgument = "etcd-space-quota"; + private const string HealthzListenAddressArgument = "healthz-listen-address"; + private const string HealthzPortArgument = "healthz-port"; + private const string IdArgument = "id"; + private const string InitialClusterArgument = "initial-cluster"; + private const string ListenAddressArgument = "listen-address"; + private const string LogAsJsonArgument = "log-as-json"; + private const string LogLevelArgument = "log-level"; + private const string MetricsListenAddressArgument = "metrics-listen-address"; + private const string MetricsPortArgument = "metrics-port"; + private const string ModeArgument = "mode"; + private const string PortArgument = "port"; + private const string ReplicaCountArgument = "replica-count"; + private const string TlsEnabledArgument = "tls-enabled"; + private const string TrustAnchorsFileArgument = "trust-anchors-file"; + private const string TrustDomainArgument = "trust-domain"; + + public DaprSchedulerProcess() + : base(DaprConstants.DaprSchedulerProcessName) + { + } + + protected override DaprSchedulerOptions GetProcessOptions(DaprOptions daprOptions) + { + // Get a clone of the current options as we will be modifying it. + var options = daprOptions.Scheduler?.Clone() ?? new DaprSchedulerOptions(); + options.EnrichFrom(daprOptions); + + // The scheduler service needs to maintain the same ports on restarts to ensure if either + // it or the sidecar are restarted out of sequence the port allocations remain the same. + // This should be fine in general as the scheduler service normally runs on a dedicated host. + options.RetainPortsOnRestart = true; + + return options; + } + + protected override void AssignPorts(PortAssignmentBuilder builder) => + builder + .Add(x => x.HealthPort, 8082) + .Add(x => x.MetricsPort, 9093) + .Add(x => x.Port, DaprConstants.IsWindows ? 6060 : 50006); + + protected override void AssignLocations(DaprSchedulerOptions options, string daprFolder) + { + // Certs directory - defaults to \certs + var certsDirectory = string.IsNullOrEmpty(options.CertsDirectory) ? + Path.Combine(daprFolder, DaprConstants.DaprCertsDirectory) : + Path.GetFullPath(options.CertsDirectory); + + // Write any defined certificate options + WriteDefaultCertificates(certsDirectory, options); + + // Pass the trust anchors file to scheduler + options.TrustAnchorsFile ??= Path.Combine(certsDirectory, DaprConstants.TrustAnchorsCertificateFilename); + } + + protected override void AddCommandLineArguments(DaprSchedulerOptions source, CommandLineArgumentBuilder builder) => builder + .Add(EnableMetricsArgument, source.EnableMetrics) + .Add(EtcdClientHttpPortsArgument, source.EtcdClientHttpPorts) + .Add(EtcdClientPortsArgument, source.EtcdClientPorts) + .Add(EtcdCompactionModeArgument, source.EtcdCompactionMode) + .Add(EtcdCompactionRetentionArgument, source.EtcdCompactionRetention) + .Add(EtcdDataDirArgument, source.EtcdDataDir) + .Add(EtcdSpaceQuotaArgument, source.EtcdSpaceQuota) + .Add(HealthzListenAddressArgument, source.HealthListenAddress) + .Add(HealthzPortArgument, source.HealthPort) + .Add(IdArgument, source.Id) + .Add(InitialClusterArgument, source.InitialCluster) + .Add(ListenAddressArgument, source.ListenAddress) + .Add(LogAsJsonArgument, true) // All logging must be JSON + .Add(LogLevelArgument, source.LogLevel) + .Add(MetricsListenAddressArgument, source.MetricsListenAddress) + .Add(MetricsPortArgument, source.MetricsPort) + .Add(ModeArgument, source.Mode) + .Add(PortArgument, source.Port) + .Add(ReplicaCountArgument, source.ReplicaCount) + .Add(TlsEnabledArgument, source.TlsEnabled) + .Add(TrustAnchorsFileArgument, source.TrustAnchorsFile) + .Add(TrustDomainArgument, source.TrustDomain) + .Add(source.CustomArguments, requiresValue: false); + + protected override void AddEnvironmentVariables(DaprSchedulerOptions source, EnvironmentVariableBuilder builder) => builder + .Add(DaprConstants.DaprSchedulerPortEnvironmentVariable, source.Port) + .Add(DaprConstants.DaprSchedulerHealthPortEnvironmentVariable, source.HealthPort) + .Add(DaprConstants.DaprSchedulerMetricsPortEnvironmentVariable, source.MetricsPort, () => source.EnableMetrics != false); + + protected override void ParseCommandLineArgument(DaprSchedulerOptions target, string name, string value) + { + switch (name.ToLower()) + { + case EnableMetricsArgument: + target.EnableMetrics = !bool.TryParse(value, out var enableMetrics) || enableMetrics; + break; + + case EtcdClientHttpPortsArgument: + target.EtcdClientHttpPorts = value; + break; + + case EtcdClientPortsArgument: + target.EtcdClientPorts = value; + break; + + case EtcdCompactionModeArgument: + target.EtcdCompactionMode = value; + break; + + case EtcdCompactionRetentionArgument: + target.EtcdCompactionRetention = value; + break; + + case EtcdDataDirArgument: + target.EtcdDataDir = value; + break; + + case EtcdSpaceQuotaArgument: + target.EtcdSpaceQuota = int.TryParse(value, out var etcdSpaceQuota) ? (int?)etcdSpaceQuota : null; + break; + + case HealthzListenAddressArgument: + target.HealthListenAddress = value; + break; + + case HealthzPortArgument: + target.HealthPort = int.TryParse(value, out var healthzPort) ? (int?)healthzPort : null; + break; + + case IdArgument: + target.Id = value; + break; + + case InitialClusterArgument: + target.InitialCluster = value; + break; + + case ListenAddressArgument: + target.ListenAddress = value; + break; + + case LogLevelArgument: + target.LogLevel = value; + break; + + case MetricsListenAddressArgument: + target.MetricsListenAddress = value; + break; + + case MetricsPortArgument: + target.MetricsPort = int.TryParse(value, out var metricsPort) ? (int?)metricsPort : null; + break; + + case ModeArgument: + target.Mode = value; + break; + + case PortArgument: + target.Port = int.TryParse(value, out var port) ? (int?)port : null; + break; + + case ReplicaCountArgument: + target.ReplicaCount = uint.TryParse(value, out var replicaCount) ? (uint?)replicaCount : null; + break; + + case TlsEnabledArgument: + target.TlsEnabled = !bool.TryParse(value, out var tlsEnabled) || tlsEnabled; + break; + + case TrustAnchorsFileArgument: + target.TrustAnchorsFile = value; + break; + + case TrustDomainArgument: + target.TrustDomain = value; + break; + } + } + + protected override ProcessComparison CompareProcessOptions(DaprSchedulerOptions proposedProcessOptions, DaprSchedulerOptions existingProcessOptions, IProcess existingProcess) + { + if (!string.Equals(proposedProcessOptions.Id, existingProcessOptions.Id, StringComparison.InvariantCultureIgnoreCase)) + { + // Processes are for different app-ids + return ProcessComparison.None; + } + else if (proposedProcessOptions.Port.HasValue && existingProcessOptions.Port.HasValue && proposedProcessOptions.Port == existingProcessOptions.Port) + { + // Same app-id, same app port. Attachable. + Logger?.LogDebug( + "Found attachable process {DaprProcessName} PID:{DaprProcessId} with proposed id {DaprSchedulerId} and port {DaprSchedulerPort}", + existingProcess.Name, + existingProcess.Id, + proposedProcessOptions.Id, + proposedProcessOptions.Port); + return ProcessComparison.Attachable; + } + else + { + // Duplicate + return ProcessComparison.Duplicate; + } + } + } +} diff --git a/src/Man.Dapr.Sidekick/Process/DaprSidecarProcess.cs b/src/Man.Dapr.Sidekick/Process/DaprSidecarProcess.cs index 13f260b..2176ba5 100644 --- a/src/Man.Dapr.Sidekick/Process/DaprSidecarProcess.cs +++ b/src/Man.Dapr.Sidekick/Process/DaprSidecarProcess.cs @@ -29,6 +29,7 @@ internal class DaprSidecarProcess : DaprProcess, IDaprSideca private const string ModeArgument = "mode"; private const string PlacementHostAddressArgument = "placement-host-address"; private const string ProfilePortArgument = "profile-port"; + private const string SchedulerHostAddressArgument = "scheduler-host-address"; private const string SentryAddressArgument = "sentry-address"; public DaprSidecarProcess() @@ -61,6 +62,16 @@ protected override DaprSidecarOptions GetProcessOptions(DaprOptions daprOptions) options.PlacementHostAddress = $"{DaprConstants.LocalhostAddress}:{port}"; } + // Set local scheduler information + if (string.IsNullOrEmpty(options.SchedulerHostAddress) && options.UseDefaultSchedulerHostAddress != false) + { + // If we have a local enabled scheduler running in this solution, then use that port + // else use the defaults from the Dapr CLI - 6060 (Windows) or 50006 (Non-Windows) + var port = daprOptions.Scheduler?.Enabled != false && daprOptions.Scheduler?.Port != null ? daprOptions.Scheduler?.Port.Value : + DaprConstants.IsWindows ? 6060 : 50006; + options.SchedulerHostAddress = $"{DaprConstants.LocalhostAddress}:{port}"; + } + // Make sure we have a namespace if (string.IsNullOrEmpty(options.Namespace) && options.Mtls == true) { @@ -124,6 +135,7 @@ protected override void AddCommandLineArguments(DaprSidecarOptions source, Comma .Add(ModeArgument, source.Mode) .Add(PlacementHostAddressArgument, source.PlacementHostAddress) .Add(ProfilePortArgument, source.ProfilePort, predicate: () => source.Profiling == true) + .Add(SchedulerHostAddressArgument, source.SchedulerHostAddress) .Add(SentryAddressArgument, source.SentryAddress, predicate: () => !source.SentryAddress.IsNullOrWhiteSpaceEx()) .Add(ConfigFileArgument, source.ConfigFile, predicate: () => File.Exists(source.ConfigFile)) .Add(ResourcesPathArgument, source.ResourcesDirectory, predicate: () => Directory.Exists(source.ResourcesDirectory)) @@ -229,6 +241,10 @@ protected override void ParseCommandLineArgument(DaprSidecarOptions target, stri target.ProfilePort = int.TryParse(value, out var profilePort) ? (int?)profilePort : null; break; + case SchedulerHostAddressArgument: + target.SchedulerHostAddress = value; + break; + case SentryAddressArgument: target.SentryAddress = value; break; diff --git a/src/Man.Dapr.Sidekick/Process/IDaprProcessFactory.cs b/src/Man.Dapr.Sidekick/Process/IDaprProcessFactory.cs index c18df3a..b7e07d2 100644 --- a/src/Man.Dapr.Sidekick/Process/IDaprProcessFactory.cs +++ b/src/Man.Dapr.Sidekick/Process/IDaprProcessFactory.cs @@ -6,6 +6,8 @@ public interface IDaprProcessFactory IDaprPlacementProcess CreateDaprPlacementProcess(); + IDaprSchedulerProcess CreateDaprSchedulerProcess(); + IDaprSentryProcess CreateDaprSentryProcess(); } } diff --git a/src/Man.Dapr.Sidekick/Process/IDaprSchedulerProcess.cs b/src/Man.Dapr.Sidekick/Process/IDaprSchedulerProcess.cs new file mode 100644 index 0000000..48af783 --- /dev/null +++ b/src/Man.Dapr.Sidekick/Process/IDaprSchedulerProcess.cs @@ -0,0 +1,6 @@ +namespace Man.Dapr.Sidekick.Process +{ + public interface IDaprSchedulerProcess : IDaprProcess + { + } +} diff --git a/tests/Man.Dapr.Sidekick.Tests/Options/DaprSidecarOptionsTests.cs b/tests/Man.Dapr.Sidekick.Tests/Options/DaprSidecarOptionsTests.cs index 293b522..e0512ea 100644 --- a/tests/Man.Dapr.Sidekick.Tests/Options/DaprSidecarOptionsTests.cs +++ b/tests/Man.Dapr.Sidekick.Tests/Options/DaprSidecarOptionsTests.cs @@ -147,6 +147,7 @@ private static void Compare(DaprSidecarOptions source, DaprSidecarOptions target Assert.That(target.PlacementHostAddress, Is.EqualTo(source.PlacementHostAddress)); Assert.That(target.ProfilePort, Is.EqualTo(source.ProfilePort)); Assert.That(target.Profiling, Is.EqualTo(source.Profiling)); + Assert.That(target.SchedulerHostAddress, Is.EqualTo(source.SchedulerHostAddress)); Assert.That(target.SentryAddress, Is.EqualTo(source.SentryAddress)); Assert.That(target.UseDefaultAppApiToken, Is.EqualTo(source.UseDefaultAppApiToken)); Assert.That(target.UseDefaultDaprApiToken, Is.EqualTo(source.UseDefaultDaprApiToken)); @@ -179,6 +180,7 @@ private static void Compare(DaprSidecarOptions source, DaprSidecarOptions target PlacementHostAddress = "PlacementHostAddress", ProfilePort = 800, Profiling = true, + SchedulerHostAddress = "SchedulerHostAddress", SentryAddress = "SentryAddress", UseDefaultAppApiToken = true, UseDefaultDaprApiToken = true diff --git a/tests/Man.Dapr.Sidekick.Tests/Process/DaprPlacementProcessTests.cs b/tests/Man.Dapr.Sidekick.Tests/Process/DaprPlacementProcessTests.cs index ed5fba1..21cf5db 100644 --- a/tests/Man.Dapr.Sidekick.Tests/Process/DaprPlacementProcessTests.cs +++ b/tests/Man.Dapr.Sidekick.Tests/Process/DaprPlacementProcessTests.cs @@ -124,16 +124,22 @@ public void Should_add_all_arguments() { CertsDirectory = "CertsDirectory", EnableMetrics = true, + HealthListenAddress = "HealthzListenAddress", HealthPort = 1234, Id = "Id", InitialCluster = "InitialCluster", InmemStoreEnabled = true, LogLevel = "LogLevel", + MetadataEnabled = true, + MetricsListenAddress = "MetricsListenAddress", MetricsPort = 2345, + Mode = "Mode", Port = 3456, RaftLogstorePath = "RaftLogstorePath", ReplicationFactor = 100, - Mtls = true, + TlsEnabled = true, + TrustAnchorsFile = "TrustAnchorsFile", + TrustDomain = "TrustDomain", CustomArguments = "arg1 val1" }; @@ -142,17 +148,23 @@ public void Should_add_all_arguments() Assert.That(builder.ToString(), Is.EqualTo( "--certchain CertsDirectory " + "--enable-metrics " + + "--healthz-listen-address HealthzListenAddress " + "--healthz-port 1234 " + "--id Id " + "--initial-cluster InitialCluster " + "--inmem-store-enabled " + "--log-as-json " + "--log-level LogLevel " + + "--metadata-enabled " + + "--metrics-listen-address MetricsListenAddress " + "--metrics-port 2345 " + + "--mode Mode " + "--port 3456 " + "--raft-logstore-path RaftLogstorePath " + "--replicationfactor 100 " + "--tls-enabled " + + "--trust-anchors-file TrustAnchorsFile " + + "--trust-domain TrustDomain " + "--arg1 val1")); } } @@ -190,29 +202,44 @@ public void Should_parse_all_arguments() p.ParseCommandLineArgument(options, "certchain", "CertsDirectory"); p.ParseCommandLineArgument(options, "enable-metrics", null); + p.ParseCommandLineArgument(options, "healthz-listen-address", "HealthzListenAddress"); p.ParseCommandLineArgument(options, "healthz-port", "1234"); p.ParseCommandLineArgument(options, "id", "Id"); p.ParseCommandLineArgument(options, "initial-cluster", "InitialCluster"); p.ParseCommandLineArgument(options, "inmem-store-enabled", null); + p.ParseCommandLineArgument(options, "listen-address", "ListenAddress"); p.ParseCommandLineArgument(options, "log-level", "LogLevel"); + p.ParseCommandLineArgument(options, "metadata-enabled", null); + p.ParseCommandLineArgument(options, "metrics-listen-address", "MetricsListenAddress"); p.ParseCommandLineArgument(options, "metrics-port", "2345"); + p.ParseCommandLineArgument(options, "mode", "Mode"); p.ParseCommandLineArgument(options, "port", "3456"); p.ParseCommandLineArgument(options, "raft-logstore-path", "RaftLogstorePath"); p.ParseCommandLineArgument(options, "replicationfactor", "100"); p.ParseCommandLineArgument(options, "tls-enabled", null); + p.ParseCommandLineArgument(options, "trust-anchors-file", "TrustAnchorsFile"); + p.ParseCommandLineArgument(options, "trust-domain", "TrustDomain"); Assert.That(options.CertsDirectory, Is.EqualTo("CertsDirectory")); Assert.That(options.EnableMetrics, Is.True); + Assert.That(options.HealthListenAddress, Is.EqualTo("HealthzListenAddress")); Assert.That(options.HealthPort, Is.EqualTo(1234)); Assert.That(options.Id, Is.EqualTo("Id")); Assert.That(options.InitialCluster, Is.EqualTo("InitialCluster")); Assert.That(options.InmemStoreEnabled, Is.True); + Assert.That(options.ListenAddress, Is.EqualTo("ListenAddress")); Assert.That(options.LogLevel, Is.EqualTo("LogLevel")); + Assert.That(options.MetadataEnabled, Is.True); + Assert.That(options.MetricsListenAddress, Is.EqualTo("MetricsListenAddress")); Assert.That(options.MetricsPort, Is.EqualTo(2345)); + Assert.That(options.Mtls, Is.Null); + Assert.That(options.Mode, Is.EqualTo("Mode")); Assert.That(options.Port, Is.EqualTo(3456)); Assert.That(options.RaftLogstorePath, Is.EqualTo("RaftLogstorePath")); Assert.That(options.ReplicationFactor, Is.EqualTo(100)); - Assert.That(options.Mtls, Is.True); + Assert.That(options.TlsEnabled, Is.True); + Assert.That(options.TrustAnchorsFile, Is.EqualTo("TrustAnchorsFile")); + Assert.That(options.TrustDomain, Is.EqualTo("TrustDomain")); } } diff --git a/tests/Man.Dapr.Sidekick.Tests/Process/DaprSchedulerProcessTests.cs b/tests/Man.Dapr.Sidekick.Tests/Process/DaprSchedulerProcessTests.cs new file mode 100644 index 0000000..66453e6 --- /dev/null +++ b/tests/Man.Dapr.Sidekick.Tests/Process/DaprSchedulerProcessTests.cs @@ -0,0 +1,323 @@ +using System.IO; +using Man.Dapr.Sidekick.Logging; +using NSubstitute; +using NUnit.Framework; + +namespace Man.Dapr.Sidekick.Process +{ + public class DaprSchedulerProcessTests + { + public class GetProcessOptions + { + [Test] + public void Should_create_scheduler_section_if_null() + { + var p = new MockDaprSchedulerProcess(); + var options = new DaprOptions(); + + Assert.That(options.Scheduler, Is.Null); + + var newOptions = p.GetProcessOptions(options); + Assert.That(newOptions, Is.Not.Null); + } + + [Test] + public void Should_use_existing_scheduler_section() + { + var p = new MockDaprSchedulerProcess(); + var options = new DaprOptions + { + Scheduler = new DaprSchedulerOptions + { + Id = "TEST", + RetainPortsOnRestart = false + } + }; + + var newOptions = p.GetProcessOptions(options); + Assert.That(newOptions, Is.Not.Null); + Assert.That(newOptions.Id, Is.EqualTo("TEST")); + Assert.That(newOptions.RetainPortsOnRestart, Is.True); + } + } + + public class AssignPorts + { + [Test] + public void Should_assign_expected_values() + { + var p = new MockDaprSchedulerProcess(); + var builder = new PortAssignmentBuilder(new MockPortAvailabilityChecker()); + var options = new DaprSchedulerOptions(); + var logger = Substitute.For(); + + p.AssignPorts(builder); + builder.Build(options, new DaprSchedulerOptions(), logger); + Assert.That(options.HealthPort, Is.EqualTo(8082)); + Assert.That(options.MetricsPort, Is.EqualTo(9093)); + Assert.That(options.Port, Is.EqualTo(DaprConstants.IsWindows ? 6060 : 50006)); + } + } + + public class AssignLocations + { + [Test] + public void Should_assign_default_paths() + { + var p = new MockDaprSchedulerProcess(); + var folder = Path.GetTempPath(); + var options = new DaprSchedulerOptions(); + + p.AssignLocations(options, folder); + Assert.That(options.CertsDirectory, Is.Null); + Assert.That(options.TrustAnchorsFile, Is.EqualTo(Path.Combine(folder, Path.Combine(DaprConstants.DaprCertsDirectory, DaprConstants.TrustAnchorsCertificateFilename)))); + } + + [Test] + public void Should_assign_specified_paths() + { + var p = new MockDaprSchedulerProcess(); + var folder = Path.GetTempPath(); + var options = new DaprSchedulerOptions + { + CertsDirectory = folder + @"relative\path\cert.txt" + }; + + p.AssignLocations(options, folder); + + Assert.That(options.CertsDirectory, Is.EqualTo(Path.Combine(folder, @"relative\path\cert.txt"))); + } + } + + public class AddCommandLineArguments + { + [Test] + public void Should_add_default_arguments() + { + var p = new MockDaprSchedulerProcess(); + var builder = new CommandLineArgumentBuilder(); + var options = new DaprSchedulerOptions(); + + p.AddCommandLineArguments(options, builder); + + Assert.That(builder.ToString(), Is.EqualTo("--log-as-json")); + } + + [Test] + public void Should_add_all_arguments() + { + var p = new MockDaprSchedulerProcess(); + var builder = new CommandLineArgumentBuilder(); + var options = new DaprSchedulerOptions + { + CertsDirectory = "CertsDirectory", + EnableMetrics = true, + EtcdClientHttpPorts = "EtcdClientHttpPorts", + EtcdClientPorts = "EtcdClientPorts", + EtcdCompactionMode = "EtcdCompactionMode", + EtcdCompactionRetention = "EtcdCompactionRetention", + EtcdDataDir = "EtcdDataDir", + EtcdSpaceQuota = 9876, + HealthListenAddress = "HealthzListenAddress", + HealthPort = 1234, + Id = "Id", + InitialCluster = "InitialCluster", + ListenAddress = "ListenAddress", + LogLevel = "LogLevel", + MetricsListenAddress = "MetricsListenAddress", + MetricsPort = 2345, + Mode = "Mode", + Port = 3456, + ReplicaCount = 5, + TlsEnabled = true, + TrustAnchorsFile = "TrustAnchorsFile", + TrustDomain = "TrustDomain", + CustomArguments = "arg1 val1" + }; + + p.AddCommandLineArguments(options, builder); + + Assert.That(builder.ToString(), Is.EqualTo( + "--enable-metrics " + + "--etcd-client-http-ports EtcdClientHttpPorts " + + "--etcd-client-ports EtcdClientPorts " + + "--etcd-compaction-mode EtcdCompactionMode " + + "--etcd-compaction-retention EtcdCompactionRetention " + + "--etcd-data-dir EtcdDataDir " + + "--etcd-space-quota 9876 " + + "--healthz-listen-address HealthzListenAddress " + + "--healthz-port 1234 " + + "--id Id " + + "--initial-cluster InitialCluster " + + "--listen-address ListenAddress " + + "--log-as-json " + + "--log-level LogLevel " + + "--metrics-listen-address MetricsListenAddress " + + "--metrics-port 2345 " + + "--mode Mode " + + "--port 3456 " + + "--replica-count 5 " + + "--tls-enabled " + + "--trust-anchors-file TrustAnchorsFile " + + "--trust-domain TrustDomain " + + "--arg1 val1")); + } + } + + public class AddEnvironmentVariables + { + [Test] + public void Should_add_expected_values() + { + var p = new MockDaprSchedulerProcess(); + var builder = new EnvironmentVariableBuilder(); + var options = new DaprSchedulerOptions + { + HealthPort = 1234, + MetricsPort = 2345, + Port = 3456 + }; + + p.AddEnvironmentVariables(options, builder); + + var values = builder.ToDictionary(); + Assert.That(values["DAPR_SCHEDULER_HEALTH_PORT"], Is.EqualTo(1234)); + Assert.That(values["DAPR_SCHEDULER_METRICS_PORT"], Is.EqualTo(2345)); + Assert.That(values["DAPR_SCHEDULER_PORT"], Is.EqualTo(3456)); + } + } + + public class ParseCommandLineArgument + { + [Test] + public void Should_parse_all_arguments() + { + var p = new MockDaprSchedulerProcess(); + var options = new DaprSchedulerOptions(); + + p.ParseCommandLineArgument(options, "enable-metrics", null); + p.ParseCommandLineArgument(options, "etcd-client-http-ports", "EtcdClientHttpPorts"); + p.ParseCommandLineArgument(options, "etcd-client-ports", "EtcdClientPorts"); + p.ParseCommandLineArgument(options, "etcd-compaction-mode", "EtcdCompactionMode"); + p.ParseCommandLineArgument(options, "etcd-compaction-retention", "EtcdCompactionRetention"); + p.ParseCommandLineArgument(options, "etcd-data-dir", "EtcdDataDir"); + p.ParseCommandLineArgument(options, "etcd-space-quota", "9876"); + p.ParseCommandLineArgument(options, "healthz-listen-address", "HealthzListenAddress"); + p.ParseCommandLineArgument(options, "healthz-port", "1234"); + p.ParseCommandLineArgument(options, "id", "Id"); + p.ParseCommandLineArgument(options, "initial-cluster", "InitialCluster"); + p.ParseCommandLineArgument(options, "listen-address", "ListenAddress"); + p.ParseCommandLineArgument(options, "log-level", "LogLevel"); + p.ParseCommandLineArgument(options, "metrics-listen-address", "MetricsListenAddress"); + p.ParseCommandLineArgument(options, "metrics-port", "2345"); + p.ParseCommandLineArgument(options, "mode", "Mode"); + p.ParseCommandLineArgument(options, "port", "3456"); + p.ParseCommandLineArgument(options, "port", "3456"); + p.ParseCommandLineArgument(options, "replica-count", "5"); + p.ParseCommandLineArgument(options, "tls-enabled", null); + p.ParseCommandLineArgument(options, "trust-anchors-file", "TrustAnchorsFile"); + p.ParseCommandLineArgument(options, "trust-domain", "TrustDomain"); + + Assert.That(options.EnableMetrics, Is.True); + Assert.That(options.EtcdClientHttpPorts, Is.EqualTo("EtcdClientHttpPorts")); + Assert.That(options.EtcdClientPorts, Is.EqualTo("EtcdClientPorts")); + Assert.That(options.EtcdCompactionMode, Is.EqualTo("EtcdCompactionMode")); + Assert.That(options.EtcdCompactionRetention, Is.EqualTo("EtcdCompactionRetention")); + Assert.That(options.EtcdDataDir, Is.EqualTo("EtcdDataDir")); + Assert.That(options.EtcdSpaceQuota, Is.EqualTo(9876)); + Assert.That(options.HealthListenAddress, Is.EqualTo("HealthzListenAddress")); + Assert.That(options.HealthPort, Is.EqualTo(1234)); + Assert.That(options.Id, Is.EqualTo("Id")); + Assert.That(options.InitialCluster, Is.EqualTo("InitialCluster")); + Assert.That(options.ListenAddress, Is.EqualTo("ListenAddress")); + Assert.That(options.LogLevel, Is.EqualTo("LogLevel")); + Assert.That(options.MetricsListenAddress, Is.EqualTo("MetricsListenAddress")); + Assert.That(options.MetricsPort, Is.EqualTo(2345)); + Assert.That(options.Mode, Is.EqualTo("Mode")); + Assert.That(options.Port, Is.EqualTo(3456)); + Assert.That(options.ReplicaCount, Is.EqualTo(5)); + Assert.That(options.TlsEnabled, Is.True); + Assert.That(options.TrustAnchorsFile, Is.EqualTo("TrustAnchorsFile")); + Assert.That(options.TrustDomain, Is.EqualTo("TrustDomain")); + } + } + + public class CompareProcessOptions + { + [Test] + public void Should_return_none() + { + var p = new MockDaprSchedulerProcess(); + var process = Substitute.For(); + var o1 = new DaprSchedulerOptions + { + Id = "P1" + }; + + var o2 = new DaprSchedulerOptions + { + Id = "P2" + }; + + Assert.That(p.CompareProcessOptions(o1, o2, process), Is.EqualTo(ProcessComparison.None)); + } + + [Test] + public void Should_return_duplicate() + { + var p = new MockDaprSchedulerProcess(); + var process = Substitute.For(); + var o1 = new DaprSchedulerOptions + { + Id = "P1", + Port = 1234 + }; + + var o2 = new DaprSchedulerOptions + { + Id = "P1", + Port = 2345 + }; + + Assert.That(p.CompareProcessOptions(o1, o2, process), Is.EqualTo(ProcessComparison.Duplicate)); + } + + [Test] + public void Should_return_attachable() + { + var p = new MockDaprSchedulerProcess(); + var process = Substitute.For(); + var o1 = new DaprSchedulerOptions + { + Id = "P1", + Port = 1234 + }; + + var o2 = new DaprSchedulerOptions + { + Id = "P1", + Port = 1234 + }; + + Assert.That(p.CompareProcessOptions(o1, o2, process), Is.EqualTo(ProcessComparison.Attachable)); + } + } + + private class MockDaprSchedulerProcess : DaprSchedulerProcess + { + public new DaprSchedulerOptions GetProcessOptions(DaprOptions daprOptions) => base.GetProcessOptions(daprOptions); + + public new void AssignPorts(PortAssignmentBuilder builder) => base.AssignPorts(builder); + + public new void AssignLocations(DaprSchedulerOptions options, string daprFolder) => base.AssignLocations(options, daprFolder); + + public new void AddCommandLineArguments(DaprSchedulerOptions source, CommandLineArgumentBuilder builder) => base.AddCommandLineArguments(source, builder); + + public new void AddEnvironmentVariables(DaprSchedulerOptions source, EnvironmentVariableBuilder builder) => base.AddEnvironmentVariables(source, builder); + + public new void ParseCommandLineArgument(DaprSchedulerOptions target, string name, string value) => base.ParseCommandLineArgument(target, name, value); + + public new ProcessComparison CompareProcessOptions(DaprSchedulerOptions proposedProcessOptions, DaprSchedulerOptions existingProcessOptions, IProcess existingProcess) => base.CompareProcessOptions(proposedProcessOptions, existingProcessOptions, existingProcess); + } + } +} diff --git a/tests/Man.Dapr.Sidekick.Tests/Process/DaprSidecarProcessTests.cs b/tests/Man.Dapr.Sidekick.Tests/Process/DaprSidecarProcessTests.cs index ecbfbd3..6916250 100644 --- a/tests/Man.Dapr.Sidekick.Tests/Process/DaprSidecarProcessTests.cs +++ b/tests/Man.Dapr.Sidekick.Tests/Process/DaprSidecarProcessTests.cs @@ -52,6 +52,15 @@ public void Should_use_default_placementhost_address() Assert.That(newOptions.PlacementHostAddress, Is.EqualTo("127.0.0.1:6050")); } + [Test] + public void Should_use_default_schedulerhost_address() + { + var p = new MockDaprSidecarProcess(); + var options = new DaprOptions(); + var newOptions = p.GetProcessOptions(options); + Assert.That(newOptions.SchedulerHostAddress, Is.EqualTo("127.0.0.1:6060")); + } + [Test] public void Should_not_use_default_placementhost_address() { @@ -68,6 +77,22 @@ public void Should_not_use_default_placementhost_address() Assert.That(newOptions.PlacementHostAddress, Is.Null); } + [Test] + public void Should_not_use_default_schedulerhost_address() + { + var p = new MockDaprSidecarProcess(); + var options = new DaprOptions + { + Sidecar = new DaprSidecarOptions + { + UseDefaultSchedulerHostAddress = false + } + }; + + var newOptions = p.GetProcessOptions(options); + Assert.That(newOptions.SchedulerHostAddress, Is.Null); + } + [Test] public void Should_use_default_namespace() { @@ -201,6 +226,7 @@ public void Should_add_all_arguments() Mode = "Mode", PlacementHostAddress = "PlacementHostAddress", ProfilePort = 6789, + SchedulerHostAddress = "SchedulerHostAddress", SentryAddress = "SentryAddress", CustomArguments = "arg1 val1" }; @@ -228,6 +254,7 @@ public void Should_add_all_arguments() "--mode Mode " + "--placement-host-address PlacementHostAddress " + "--profile-port 6789 " + + "--scheduler-host-address SchedulerHostAddress " + "--sentry-address SentryAddress " + "--config " + configFile + " " + "--resources-path " + componentsPath + " " + @@ -319,6 +346,7 @@ public void Should_parse_all_arguments() p.ParseCommandLineArgument(options, "mode", "Mode"); p.ParseCommandLineArgument(options, "placement-host-address", "PlacementHostAddress"); p.ParseCommandLineArgument(options, "profile-port", "6789"); + p.ParseCommandLineArgument(options, "scheduler-host-address", "SchedulerHostAddress"); p.ParseCommandLineArgument(options, "sentry-address", "SentryAddress"); Assert.That(options.AllowedOrigins, Is.EqualTo("AllowedOrigins")); @@ -342,6 +370,7 @@ public void Should_parse_all_arguments() Assert.That(options.Mode, Is.EqualTo("Mode")); Assert.That(options.PlacementHostAddress, Is.EqualTo("PlacementHostAddress")); Assert.That(options.ProfilePort, Is.EqualTo(6789)); + Assert.That(options.SchedulerHostAddress, Is.EqualTo("SchedulerHostAddress")); Assert.That(options.SentryAddress, Is.EqualTo("SentryAddress")); } }