diff --git a/src/Frontend/src/components/serviceControlClient.ts b/src/Frontend/src/components/serviceControlClient.ts
index a5d025b9dd..73e3c0c9e0 100644
--- a/src/Frontend/src/components/serviceControlClient.ts
+++ b/src/Frontend/src/components/serviceControlClient.ts
@@ -96,6 +96,10 @@ class ServiceControlClient {
}
private getUrl() {
+ if (window.defaultConfig?.isIntegrated && window.defaultConfig.service_control_url?.length) {
+ return window.defaultConfig.service_control_url;
+ }
+
const searchParams = new URLSearchParams(window.location.search);
const scu = searchParams.get("scu");
const existingScu = window.localStorage.getItem("scu");
diff --git a/src/Frontend/test/specs/monitoring/sorting-endpoints.spec.ts b/src/Frontend/test/specs/monitoring/sorting-endpoints.spec.ts
index 4a636d9231..68841efc34 100644
--- a/src/Frontend/test/specs/monitoring/sorting-endpoints.spec.ts
+++ b/src/Frontend/test/specs/monitoring/sorting-endpoints.spec.ts
@@ -1,5 +1,6 @@
import { expect } from "vitest";
import { test, describe } from "../../drivers/vitest/driver";
+import { waitFor } from "@testing-library/vue";
import { groupEndpointsBy } from "./actions/groupEndpointsBy";
import { endpointGroupNames } from "./questions/endpointGroupNames";
import { endpointGroup } from "./questions/endpointGroup";
@@ -68,10 +69,12 @@ describe("FEATURE: Endpoint sorting", () => {
await sortEndpointsBy({ column: columnName.ENDPOINTNAME });
//Assert
- expect(endpointGroupNames()).toEqual(["Universe.Solarsystem.Venus", "Universe.Solarsystem.Mercury", "Universe.Solarsystem.Earth"]);
- expect(endpointGroup("Universe.Solarsystem.Venus").Endpoints).toEqual(["Endpoint4", "Endpoint3"]);
- expect(endpointGroup("Universe.Solarsystem.Mercury").Endpoints).toEqual(["Endpoint2", "Endpoint1"]);
- expect(endpointGroup("Universe.Solarsystem.Earth").Endpoints).toEqual(["Endpoint6", "Endpoint5"]);
+ await waitFor(() => {
+ expect(endpointGroupNames()).toEqual(["Universe.Solarsystem.Venus", "Universe.Solarsystem.Mercury", "Universe.Solarsystem.Earth"]);
+ expect(endpointGroup("Universe.Solarsystem.Venus").Endpoints).toEqual(["Endpoint4", "Endpoint3"]);
+ expect(endpointGroup("Universe.Solarsystem.Mercury").Endpoints).toEqual(["Endpoint2", "Endpoint1"]);
+ expect(endpointGroup("Universe.Solarsystem.Earth").Endpoints).toEqual(["Endpoint6", "Endpoint5"]);
+ });
});
test("EXAMPLE: Endpoints inside of the groups and group names should be sorted in ascending order when clicking twice on the endpoint name column title", async ({ driver }) => {
@@ -95,10 +98,12 @@ describe("FEATURE: Endpoint sorting", () => {
await sortEndpointsBy({ column: columnName.ENDPOINTNAME }); //Click the column title again for ascending
//Assert
- expect(endpointGroupNames()).toEqual(["Universe.Solarsystem.Earth", "Universe.Solarsystem.Mercury", "Universe.Solarsystem.Venus"]);
- expect(endpointGroup("Universe.Solarsystem.Earth").Endpoints).toEqual(["Endpoint5", "Endpoint6"]);
- expect(endpointGroup("Universe.Solarsystem.Mercury").Endpoints).toEqual(["Endpoint1", "Endpoint2"]);
- expect(endpointGroup("Universe.Solarsystem.Venus").Endpoints).toEqual(["Endpoint3", "Endpoint4"]);
+ await waitFor(() => {
+ expect(endpointGroupNames()).toEqual(["Universe.Solarsystem.Earth", "Universe.Solarsystem.Mercury", "Universe.Solarsystem.Venus"]);
+ expect(endpointGroup("Universe.Solarsystem.Earth").Endpoints).toEqual(["Endpoint5", "Endpoint6"]);
+ expect(endpointGroup("Universe.Solarsystem.Mercury").Endpoints).toEqual(["Endpoint1", "Endpoint2"]);
+ expect(endpointGroup("Universe.Solarsystem.Venus").Endpoints).toEqual(["Endpoint3", "Endpoint4"]);
+ });
});
});
diff --git a/src/Particular.PlatformSample.ServicePulse/Particular.PlatformSample.ServicePulse.csproj b/src/Particular.PlatformSample.ServicePulse/Particular.PlatformSample.ServicePulse.csproj
index ce2d8ddf1c..2ada072576 100644
--- a/src/Particular.PlatformSample.ServicePulse/Particular.PlatformSample.ServicePulse.csproj
+++ b/src/Particular.PlatformSample.ServicePulse/Particular.PlatformSample.ServicePulse.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
false
true
Particular ServicePulse binaries for use by Particular.PlatformSample. Not intended for use outside of Particular.PlatformSample.
diff --git a/src/Particular.ServicePulse.Core/ConstantsFile.cs b/src/Particular.ServicePulse.Core/ConstantsFile.cs
new file mode 100644
index 0000000000..d790f0a6de
--- /dev/null
+++ b/src/Particular.ServicePulse.Core/ConstantsFile.cs
@@ -0,0 +1,30 @@
+namespace ServicePulse;
+
+using System.Reflection;
+
+class ConstantsFile
+{
+ public static string GetContent(ServicePulseSettings settings)
+ {
+ var version = GetVersionInformation();
+
+ var constantsFile = $$"""
+window.defaultConfig = {
+ default_route: '{{settings.DefaultRoute}}',
+ version: '{{version}}',
+ service_control_url: '{{settings.ServiceControlUrl}}',
+ monitoring_urls: ['{{settings.MonitoringUrl ?? "!"}}'],
+ showPendingRetry: {{settings.ShowPendingRetry.ToString().ToLower()}},
+ isIntegrated: {{settings.IsIntegrated.ToString().ToLower()}}
+}
+""";
+
+ return constantsFile;
+ }
+
+ static string GetVersionInformation()
+ => typeof(ConstantsFile).Assembly
+ .GetCustomAttributes()
+ .SingleOrDefault(attribute => attribute.Key == "MajorMinorPatch")
+ ?.Value ?? "0.0.0";
+}
diff --git a/src/Particular.ServicePulse.Core/Particular.ServicePulse.Core.csproj b/src/Particular.ServicePulse.Core/Particular.ServicePulse.Core.csproj
new file mode 100644
index 0000000000..8aaa0d525d
--- /dev/null
+++ b/src/Particular.ServicePulse.Core/Particular.ServicePulse.Core.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net10.0
+ enable
+ enable
+ Particular ServicePulse binaries for use by integrated ServicePulse. Not intended for use outside of ServiceControl.
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Particular.ServicePulse.Core/ServicePulseHostingExtensions.cs b/src/Particular.ServicePulse.Core/ServicePulseHostingExtensions.cs
new file mode 100644
index 0000000000..3e65383cdc
--- /dev/null
+++ b/src/Particular.ServicePulse.Core/ServicePulseHostingExtensions.cs
@@ -0,0 +1,37 @@
+namespace ServicePulse;
+
+using System.Net.Mime;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+
+///
+/// Extensions for hosting ServicePulse within a WebApplication.
+///
+public static class ServicePulseHostingExtensions
+{
+ ///
+ /// Adds ServicePulse static file serving and configuration endpoint to the WebApplication.
+ ///
+ public static void UseServicePulse(this WebApplication app, ServicePulseSettings settings, IFileProvider? overrideFileProvider = null)
+ {
+ var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(ServicePulseHostingExtensions).Assembly, "wwwroot");
+ IFileProvider fileProvider = overrideFileProvider is null
+ ? manifestEmbeddedFileProvider
+ : new CompositeFileProvider(overrideFileProvider, manifestEmbeddedFileProvider);
+
+ var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider };
+ app.UseDefaultFiles(defaultFilesOptions);
+
+ var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider };
+ app.UseStaticFiles(staticFileOptions);
+
+ var constantsFile = ConstantsFile.GetContent(settings);
+
+ app.MapGet("/js/app.constants.js", (HttpContext context) =>
+ {
+ context.Response.ContentType = MediaTypeNames.Text.JavaScript;
+ return constantsFile;
+ }).AllowAnonymous();
+ }
+}
diff --git a/src/Particular.ServicePulse.Core/ServicePulseSettings.cs b/src/Particular.ServicePulse.Core/ServicePulseSettings.cs
new file mode 100644
index 0000000000..368c0aa701
--- /dev/null
+++ b/src/Particular.ServicePulse.Core/ServicePulseSettings.cs
@@ -0,0 +1,112 @@
+namespace ServicePulse;
+
+using System.Text.Json;
+
+///
+/// Application Settings for ServicePulse.
+///
+public record ServicePulseSettings
+{
+ ///
+ /// The location of the ServiceControl API.
+ ///
+ public required string ServiceControlUrl { get; init; }
+
+ ///
+ /// The location of the ServiceControl Monitoring API.
+ ///
+ public required string? MonitoringUrl { get; init; }
+
+ ///
+ /// The default route to navigate to from the root.
+ ///
+ public required string DefaultRoute { get; init; }
+
+ ///
+ /// Flag to enable the pending retry feature.
+ ///
+ public required bool ShowPendingRetry { get; init; }
+
+ ///
+ /// Flag to indicate if ServicePulse is running in integrated mode.
+ ///
+ public required bool IsIntegrated { get; init; }
+
+ ///
+ /// Loads the settings from environment variables.
+ ///
+ public static ServicePulseSettings GetFromEnvironmentVariables()
+ {
+ var serviceControlUrl = Environment.GetEnvironmentVariable("SERVICECONTROL_URL") ?? "http://localhost:33333/api/";
+
+ if (!serviceControlUrl.EndsWith("/", StringComparison.Ordinal))
+ {
+ serviceControlUrl += "/";
+ }
+
+ if (!serviceControlUrl.EndsWith("api/", StringComparison.Ordinal))
+ {
+ serviceControlUrl += "api/";
+ }
+
+ var serviceControlUri = new Uri(serviceControlUrl);
+
+ var monitoringUrls = ParseLegacyMonitoringValue(Environment.GetEnvironmentVariable("MONITORING_URLS"));
+ var monitoringUrl = Environment.GetEnvironmentVariable("MONITORING_URL");
+
+ monitoringUrl ??= monitoringUrls;
+ monitoringUrl ??= "http://localhost:33633/";
+
+ var monitoringUri = monitoringUrl == "!" ? null : new Uri(monitoringUrl);
+
+ var defaultRoute = Environment.GetEnvironmentVariable("DEFAULT_ROUTE") ?? "/dashboard";
+
+ var showPendingRetryValue = Environment.GetEnvironmentVariable("SHOW_PENDING_RETRY");
+ bool.TryParse(showPendingRetryValue, out var showPendingRetry);
+
+ return new ServicePulseSettings
+ {
+ ServiceControlUrl = serviceControlUri.ToString(),
+ MonitoringUrl = monitoringUri?.ToString(),
+ DefaultRoute = defaultRoute,
+ ShowPendingRetry = showPendingRetry,
+ IsIntegrated = false
+ };
+ }
+
+ static string? ParseLegacyMonitoringValue(string? value)
+ {
+ if (value is null)
+ {
+ return null;
+ }
+
+ var cleanedValue = value.Replace('\'', '"');
+ var json = $$"""{"Addresses":{{cleanedValue}}}""";
+
+ MonitoringUrls? result;
+
+ try
+ {
+ result = JsonSerializer.Deserialize(json);
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+
+ var addresses = result?.Addresses;
+
+ if (addresses is not null && addresses.Length > 0)
+ {
+ return addresses[0];
+ }
+
+ return null;
+ }
+
+ class MonitoringUrls
+ {
+ public string[] Addresses { get; set; } = [];
+ }
+}
diff --git a/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs b/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs
index f6036cf31e..a29503dfba 100644
--- a/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs
+++ b/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs
@@ -129,7 +129,7 @@ public async Task Should_find_prefer_constants_file_on_disk_over_embedded_if_bot
}
};
await middleware.Invoke(context);
- const long sizeOfFileOnDisk = 215; // this is the /app/js/app.constants.js file
+ const long sizeOfFileOnDisk = 239; // this is the /app/js/app.constants.js file
Assert.That(context.Response.ContentLength, Is.EqualTo(sizeOfFileOnDisk));
Assert.That(context.Response.ContentType, Is.EqualTo("application/javascript"));
}
diff --git a/src/ServicePulse.Host/Hosting/HostArguments.cs b/src/ServicePulse.Host/Hosting/HostArguments.cs
index 0a7dc0d000..76f65ac939 100644
--- a/src/ServicePulse.Host/Hosting/HostArguments.cs
+++ b/src/ServicePulse.Host/Hosting/HostArguments.cs
@@ -2,9 +2,6 @@ namespace ServicePulse.Host.Hosting
{
using System;
using System.Collections.Generic;
-#if DEBUG
- using System.Diagnostics;
-#endif
using System.IO;
using System.Linq;
using System.Reflection;
@@ -355,8 +352,7 @@ void ValidateArgs()
goto case ExecutionMode.Run;
case ExecutionMode.Run:
- Uri spUri;
- if (!Uri.TryCreate(Url, UriKind.Absolute, out spUri) || (!validProtocols.Contains(spUri.Scheme, StringComparer.OrdinalIgnoreCase)))
+ if (!Uri.TryCreate(Url, UriKind.Absolute, out Uri spUri) || (!validProtocols.Contains(spUri.Scheme, StringComparer.OrdinalIgnoreCase)))
{
throw new Exception("The value specified for 'url' is not a valid URL");
}
diff --git a/src/ServicePulse.Tests/ServicePulse.Tests.csproj b/src/ServicePulse.Tests/ServicePulse.Tests.csproj
index a4dfa7b200..f012869f33 100644
--- a/src/ServicePulse.Tests/ServicePulse.Tests.csproj
+++ b/src/ServicePulse.Tests/ServicePulse.Tests.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
enable
enable
false
@@ -13,8 +13,8 @@
-
-
+
+
@@ -23,4 +23,4 @@
-
+
\ No newline at end of file
diff --git a/src/ServicePulse.sln b/src/ServicePulse.sln
index 1114063682..10e720dc9d 100644
--- a/src/ServicePulse.sln
+++ b/src/ServicePulse.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.1.32319.34
+# Visual Studio Version 18
+VisualStudioVersion = 18.1.11312.151
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServicePulse.Host", "ServicePulse.Host\ServicePulse.Host.csproj", "{D120B791-BD1B-4E06-B4E1-69801A73209B}"
EndProject
@@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePulse", "ServicePuls
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePulse.Tests", "ServicePulse.Tests\ServicePulse.Tests.csproj", "{9B75F526-937E-4B25-A9F6-2862129993EB}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Particular.ServicePulse.Core", "Particular.ServicePulse.Core\Particular.ServicePulse.Core.csproj", "{8FCA3827-719C-49B7-A143-E48461D60F3C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -94,6 +96,10 @@ Global
{9B75F526-937E-4B25-A9F6-2862129993EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B75F526-937E-4B25-A9F6-2862129993EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B75F526-937E-4B25-A9F6-2862129993EB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8FCA3827-719C-49B7-A143-E48461D60F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8FCA3827-719C-49B7-A143-E48461D60F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8FCA3827-719C-49B7-A143-E48461D60F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8FCA3827-719C-49B7-A143-E48461D60F3C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/ServicePulse/ConstantsFile.cs b/src/ServicePulse/ConstantsFile.cs
deleted file mode 100644
index fc76fb95da..0000000000
--- a/src/ServicePulse/ConstantsFile.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-namespace ServicePulse;
-
-using System.Reflection;
-
-class ConstantsFile
-{
- public static string GetContent(Settings settings)
- {
- var version = GetVersionInformation();
-
- string serviceControlUrl;
- string monitoringUrl;
-
- if (settings.EnableReverseProxy)
- {
- serviceControlUrl = "/api/";
- monitoringUrl = settings.MonitoringUri == null ? "!" : "/monitoring-api/";
- }
- else
- {
- // When HTTPS is enabled, upgrade backend URLs to HTTPS
- var scUri = settings.HttpsEnabled
- ? UpgradeToHttps(settings.ServiceControlUri)
- : settings.ServiceControlUri;
- serviceControlUrl = scUri.ToString();
-
- if (settings.MonitoringUri != null)
- {
- var mUri = settings.HttpsEnabled
- ? UpgradeToHttps(settings.MonitoringUri)
- : settings.MonitoringUri;
- monitoringUrl = mUri.ToString();
- }
- else
- {
- monitoringUrl = "!";
- }
- }
-
- var constantsFile = $$"""
-window.defaultConfig = {
- default_route: '{{settings.DefaultRoute}}',
- version: '{{version}}',
- service_control_url: '{{serviceControlUrl}}',
- monitoring_urls: ['{{monitoringUrl}}'],
- showPendingRetry: {{(settings.ShowPendingRetry ? "true" : "false")}},
-}
-""";
-
- return constantsFile;
- }
-
- static string GetVersionInformation()
- {
- var majorMinorPatch = "0.0.0";
-
- var attributes = Assembly.GetExecutingAssembly().GetCustomAttributes
();
-
- foreach (var attribute in attributes)
- {
- if (attribute.Key == "MajorMinorPatch")
- {
- majorMinorPatch = attribute.Value ?? "0.0.0";
- }
- }
-
- return majorMinorPatch;
- }
-
- static Uri UpgradeToHttps(Uri uri)
- {
- if (uri.Scheme == Uri.UriSchemeHttps)
- {
- return uri;
- }
-
- var builder = new UriBuilder(uri)
- {
- Scheme = Uri.UriSchemeHttps,
- Port = uri.IsDefaultPort ? -1 : uri.Port
- };
-
- return builder.Uri;
- }
-}
diff --git a/src/ServicePulse/Dockerfile b/src/ServicePulse/Dockerfile
index baf02139c1..61574cc16c 100644
--- a/src/ServicePulse/Dockerfile
+++ b/src/ServicePulse/Dockerfile
@@ -7,7 +7,7 @@ RUN npm install
RUN npm run build
# Host build image
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG TARGETARCH
WORKDIR /
ENV CI=true
@@ -15,7 +15,7 @@ COPY --from=frontend . .
RUN dotnet publish src/ServicePulse/ServicePulse.csproj -a $TARGETARCH -o /app
# Host runtime image
-FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled-composite
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-composite
WORKDIR /app
ENV ASPNETCORE_HTTP_PORTS=9090
diff --git a/src/ServicePulse/Program.cs b/src/ServicePulse/Program.cs
index 9b4ca85024..69aea49208 100644
--- a/src/ServicePulse/Program.cs
+++ b/src/ServicePulse/Program.cs
@@ -1,49 +1,35 @@
-using System.Net.Mime;
-using Microsoft.Extensions.FileProviders;
using ServicePulse;
var builder = WebApplication.CreateBuilder(args);
-var settings = Settings.GetFromEnvironmentVariables();
+var settings = ServicePulseSettings.GetFromEnvironmentVariables();
+var hostSettings = ServicePulseHostSettings.GetFromEnvironmentVariables();
+
+hostSettings.UpdateApplicationSettings(ref settings);
// Configure Kestrel for HTTPS if enabled
-builder.ConfigureHttps(settings);
+builder.ConfigureHttps(hostSettings);
-if (settings.EnableReverseProxy)
+if (hostSettings.EnableReverseProxy)
{
- var (routes, clusters) = ReverseProxy.GetConfiguration(settings);
+ var (routes, clusters) = ReverseProxy.GetConfiguration(ref settings);
builder.Services.AddReverseProxy().LoadFromMemory(routes, clusters);
}
var app = builder.Build();
// Forwarded headers must be first in the pipeline for correct scheme/host detection
-app.UseForwardedHeaders(settings);
+app.UseForwardedHeaders(hostSettings);
// HTTPS middleware (HSTS and redirect)
-app.UseHttpsConfiguration(settings);
-
-var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly, "wwwroot");
-var fileProvider = new CompositeFileProvider(builder.Environment.WebRootFileProvider, manifestEmbeddedFileProvider);
-
-var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider };
-app.UseDefaultFiles(defaultFilesOptions);
+app.UseHttpsConfiguration(hostSettings);
-var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider };
-app.UseStaticFiles(staticFileOptions);
-
-if (settings.EnableReverseProxy)
+if (hostSettings.EnableReverseProxy)
{
app.MapReverseProxy();
}
-var constantsFile = ConstantsFile.GetContent(settings);
-
-app.MapGet("/js/app.constants.js", (HttpContext context) =>
-{
- context.Response.ContentType = MediaTypeNames.Text.JavaScript;
- return constantsFile;
-});
+app.UseServicePulse(settings, builder.Environment.WebRootFileProvider);
app.Run();
diff --git a/src/ServicePulse/ReverseProxy.cs b/src/ServicePulse/ReverseProxy.cs
index 61e11c833f..6714265951 100644
--- a/src/ServicePulse/ReverseProxy.cs
+++ b/src/ServicePulse/ReverseProxy.cs
@@ -5,22 +5,18 @@
static class ReverseProxy
{
- public static (List routes, List clusters) GetConfiguration(Settings settings)
+ public static (List routes, List clusters) GetConfiguration(ref ServicePulseSettings settings)
{
var routes = new List();
var clusters = new List();
- // When HTTPS is enabled on ServicePulse, assume ServiceControl also uses HTTPS
- var serviceControlUri = settings.HttpsEnabled
- ? UpgradeToHttps(settings.ServiceControlUri)
- : settings.ServiceControlUri;
var serviceControlInstance = new ClusterConfig
{
ClusterId = "serviceControlInstance",
Destinations = new Dictionary
{
- { "instance", new DestinationConfig { Address = serviceControlUri.ToString() } }
+ { "instance", new DestinationConfig { Address = settings.ServiceControlUrl } }
}
};
var serviceControlRoute = new RouteConfig
@@ -35,20 +31,16 @@ public static (List routes, List clusters) GetConfig
clusters.Add(serviceControlInstance);
routes.Add(serviceControlRoute);
+ settings = settings with { ServiceControlUrl = "/api/" };
- if (settings.MonitoringUri != null)
+ if (settings.MonitoringUrl != null)
{
- // When HTTPS is enabled on ServicePulse, assume Monitoring also uses HTTPS
- var monitoringUri = settings.HttpsEnabled
- ? UpgradeToHttps(settings.MonitoringUri)
- : settings.MonitoringUri;
-
var monitoringInstance = new ClusterConfig
{
ClusterId = "monitoringInstance",
Destinations = new Dictionary
{
- { "instance", new DestinationConfig { Address = monitoringUri.ToString() } }
+ { "instance", new DestinationConfig { Address = settings.MonitoringUrl } }
}
};
@@ -61,24 +53,9 @@ public static (List routes, List clusters) GetConfig
clusters.Add(monitoringInstance);
routes.Add(monitoringRoute);
+ settings = settings with { MonitoringUrl = "/monitoring-api/" };
}
return (routes, clusters);
}
-
- static Uri UpgradeToHttps(Uri uri)
- {
- if (uri.Scheme == Uri.UriSchemeHttps)
- {
- return uri;
- }
-
- var builder = new UriBuilder(uri)
- {
- Scheme = Uri.UriSchemeHttps,
- Port = uri.IsDefaultPort ? -1 : uri.Port
- };
-
- return builder.Uri;
- }
}
diff --git a/src/ServicePulse/ServicePulse.csproj b/src/ServicePulse/ServicePulse.csproj
index 4230dd8456..6d03f196c9 100644
--- a/src/ServicePulse/ServicePulse.csproj
+++ b/src/ServicePulse/ServicePulse.csproj
@@ -1,10 +1,9 @@
-
+
- net8.0
+ net10.0
enable
enable
- true
false
@@ -13,13 +12,12 @@
-
-
+
-
+
\ No newline at end of file
diff --git a/src/ServicePulse/Settings.cs b/src/ServicePulse/ServicePulseHostSettings.cs
similarity index 84%
rename from src/ServicePulse/Settings.cs
rename to src/ServicePulse/ServicePulseHostSettings.cs
index 494136b38e..87b1705d37 100644
--- a/src/ServicePulse/Settings.cs
+++ b/src/ServicePulse/ServicePulseHostSettings.cs
@@ -1,21 +1,11 @@
namespace ServicePulse;
using System.Net;
-using System.Text.Json;
using Microsoft.Extensions.Logging;
-using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
-class Settings
+class ServicePulseHostSettings
{
- static readonly ILogger logger = LoggerUtil.CreateStaticLogger();
-
- public required Uri ServiceControlUri { get; init; }
-
- public required Uri? MonitoringUri { get; init; }
-
- public required string DefaultRoute { get; init; }
-
- public required bool ShowPendingRetry { get; init; }
+ static readonly ILogger logger = LoggerUtil.CreateStaticLogger();
public required bool EnableReverseProxy { get; init; }
@@ -79,35 +69,8 @@ class Settings
///
public required bool HttpsHstsIncludeSubDomains { get; init; }
- public static Settings GetFromEnvironmentVariables()
+ public static ServicePulseHostSettings GetFromEnvironmentVariables()
{
- var serviceControlUrl = Environment.GetEnvironmentVariable("SERVICECONTROL_URL") ?? "http://localhost:33333/api/";
-
- if (!serviceControlUrl.EndsWith("/", StringComparison.Ordinal))
- {
- serviceControlUrl += "/";
- }
-
- if (!serviceControlUrl.EndsWith("api/", StringComparison.Ordinal))
- {
- serviceControlUrl += "api/";
- }
-
- var serviceControlUri = new Uri(serviceControlUrl);
-
- var monitoringUrls = ParseLegacyMonitoringValue(Environment.GetEnvironmentVariable("MONITORING_URLS"));
- var monitoringUrl = Environment.GetEnvironmentVariable("MONITORING_URL");
-
- monitoringUrl ??= monitoringUrls;
- monitoringUrl ??= "http://localhost:33633/";
-
- var monitoringUri = monitoringUrl == "!" ? null : new Uri(monitoringUrl);
-
- var defaultRoute = Environment.GetEnvironmentVariable("DEFAULT_ROUTE") ?? "/dashboard";
-
- var showPendingRetryValue = Environment.GetEnvironmentVariable("SHOW_PENDING_RETRY");
- bool.TryParse(showPendingRetryValue, out var showPendingRetry);
-
var enableReverseProxyValue = Environment.GetEnvironmentVariable("ENABLE_REVERSE_PROXY");
if (!bool.TryParse(enableReverseProxyValue, out var enableReverseProxy))
@@ -162,12 +125,8 @@ public static Settings GetFromEnvironmentVariables()
Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_HSTSINCLUDESUBDOMAINS"),
defaultValue: false);
- var settings = new Settings
+ var settings = new ServicePulseHostSettings
{
- ServiceControlUri = serviceControlUri,
- MonitoringUri = monitoringUri,
- DefaultRoute = defaultRoute,
- ShowPendingRetry = showPendingRetry,
EnableReverseProxy = enableReverseProxy,
ForwardedHeadersEnabled = forwardedHeadersEnabled,
ForwardedHeadersTrustAllProxies = forwardedHeadersTrustAllProxies,
@@ -187,6 +146,7 @@ public static Settings GetFromEnvironmentVariables()
settings.ValidateHttpsCertificateConfiguration();
settings.LogHttpsConfiguration();
+
return settings;
}
@@ -259,42 +219,6 @@ static IReadOnlyList ParseNetworks(string? value)
return networks;
}
- static string? ParseLegacyMonitoringValue(string? value)
- {
- if (value is null)
- {
- return null;
- }
-
- var cleanedValue = value.Replace('\'', '"');
- var json = $$"""{"Addresses":{{cleanedValue}}}""";
-
- MonitoringUrls? result;
-
- try
- {
- result = JsonSerializer.Deserialize(json);
- }
- catch (JsonException)
- {
- return null;
- }
-
- var addresses = result?.Addresses;
-
- if (addresses is not null && addresses.Length > 0)
- {
- return addresses[0];
- }
-
- return null;
- }
-
- class MonitoringUrls
- {
- public string[] Addresses { get; set; } = [];
- }
-
///
/// Logs the forwarded headers configuration and warns about potential misconfigurations.
///
@@ -420,4 +344,37 @@ void LogHttpsConfiguration()
logger.LogInformation("SERVICEPULSE_HTTPS_PORT is not configured. HTTPS redirect will be ignored");
}
}
+
+ public void UpdateApplicationSettings(ref ServicePulseSettings settings)
+ {
+ // When HTTPS is enabled on ServicePulse, assume ServiceControl (and Monitoring, if configured) also uses HTTPS
+ if (HttpsEnabled)
+ {
+ settings = settings with
+ {
+ ServiceControlUrl = UpgradeToHttps(settings.ServiceControlUrl),
+ MonitoringUrl = settings.MonitoringUrl is not null
+ ? UpgradeToHttps(settings.MonitoringUrl)
+ : null
+ };
+ }
+ }
+
+ static string UpgradeToHttps(string url)
+ {
+ var uri = new Uri(url);
+
+ if (uri.Scheme == Uri.UriSchemeHttps)
+ {
+ return url;
+ }
+
+ var builder = new UriBuilder(uri)
+ {
+ Scheme = Uri.UriSchemeHttps,
+ Port = uri.IsDefaultPort ? -1 : uri.Port
+ };
+
+ return builder.Uri.ToString();
+ }
}
diff --git a/src/ServicePulse/WebApplicationBuilderExtensions.cs b/src/ServicePulse/WebApplicationBuilderExtensions.cs
index 1645af9da8..ecbc9491f3 100644
--- a/src/ServicePulse/WebApplicationBuilderExtensions.cs
+++ b/src/ServicePulse/WebApplicationBuilderExtensions.cs
@@ -4,7 +4,7 @@ namespace ServicePulse;
static class WebApplicationBuilderExtensions
{
- public static void ConfigureHttps(this WebApplicationBuilder builder, Settings settings)
+ public static void ConfigureHttps(this WebApplicationBuilder builder, ServicePulseHostSettings settings)
{
// EnableHsts is disabled by default
// Hsts is automatically disabled in Development environments
@@ -51,7 +51,7 @@ public static void ConfigureHttps(this WebApplicationBuilder builder, Settings s
}
}
- static X509Certificate2 LoadCertificate(Settings settings)
+ static X509Certificate2 LoadCertificate(ServicePulseHostSettings settings)
{
var certPath = settings.HttpsCertificatePath
?? throw new InvalidOperationException("HTTPS is enabled but HTTPS_CERTIFICATEPATH is not set.");
@@ -62,7 +62,7 @@ static X509Certificate2 LoadCertificate(Settings settings)
}
return string.IsNullOrEmpty(settings.HttpsCertificatePassword)
- ? new X509Certificate2(certPath)
- : new X509Certificate2(certPath, settings.HttpsCertificatePassword);
+ ? X509CertificateLoader.LoadPkcs12FromFile(certPath, null)
+ : X509CertificateLoader.LoadPkcs12FromFile(certPath, settings.HttpsCertificatePassword);
}
}
diff --git a/src/ServicePulse/WebApplicationExtensions.cs b/src/ServicePulse/WebApplicationExtensions.cs
index 3f2f76da01..319a992719 100644
--- a/src/ServicePulse/WebApplicationExtensions.cs
+++ b/src/ServicePulse/WebApplicationExtensions.cs
@@ -4,7 +4,7 @@ namespace ServicePulse;
static class WebApplicationExtensions
{
- public static void UseForwardedHeaders(this WebApplication app, Settings settings)
+ public static void UseForwardedHeaders(this WebApplication app, ServicePulseHostSettings settings)
{
// Register debug endpoint first (before early return) so it's always available in Development
if (app.Environment.IsDevelopment())
@@ -27,7 +27,7 @@ public static void UseForwardedHeaders(this WebApplication app, Settings setting
// Configuration
var knownProxies = settings.ForwardedHeadersKnownProxies.Select(p => p.ToString()).ToArray();
- var knownNetworks = settings.ForwardedHeadersKnownNetworks.Select(n => $"{n.Prefix}/{n.PrefixLength}").ToArray();
+ var knownNetworks = settings.ForwardedHeadersKnownNetworks.Select(n => n.ToString()).ToArray();
return new
{
@@ -58,7 +58,7 @@ public static void UseForwardedHeaders(this WebApplication app, Settings setting
// Clear default loopback-only restrictions
options.KnownProxies.Clear();
- options.KnownNetworks.Clear();
+ options.KnownIPNetworks.Clear();
// Enabled by default
if (settings.ForwardedHeadersTrustAllProxies)
@@ -76,14 +76,14 @@ public static void UseForwardedHeaders(this WebApplication app, Settings setting
foreach (var network in settings.ForwardedHeadersKnownNetworks)
{
- options.KnownNetworks.Add(network);
+ options.KnownIPNetworks.Add(network);
}
}
app.UseForwardedHeaders(options);
}
- public static void UseHttpsConfiguration(this WebApplication app, Settings settings)
+ public static void UseHttpsConfiguration(this WebApplication app, ServicePulseHostSettings settings)
{
// EnableHsts is disabled by default
// Hsts is automatically disabled in Development environments