Skip to content

Commit 9a6d1e1

Browse files
committed
chore: runtime configuration improvements
1 parent c491ecc commit 9a6d1e1

5 files changed

Lines changed: 259 additions & 5 deletions

File tree

src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ private static void AddOpenTelemetry(IServiceCollection services, IConfiguration
235235
DistributedContextPropagator.Current = new AspNetCorePropagator();
236236

237237
var appInsightsConnectionString = GetAppInsightsConnectionStringForOtel(config, env);
238+
var useOpenTelemetryCollector = config.GetValue<bool?>("AppSettings:UseOpenTelemetryCollector");
238239

239240
services
240241
.AddOpenTelemetry()
@@ -252,12 +253,13 @@ private static void AddOpenTelemetry(IServiceCollection services, IConfiguration
252253
.AddAspNetCoreInstrumentation(opts =>
253254
{
254255
opts.RecordException = true;
256+
opts.Filter = httpContext => !httpContext.Request.Path.StartsWithSegments("/health");
255257
});
256258

257259
if (isTest)
258260
return;
259261

260-
if (!string.IsNullOrWhiteSpace(appInsightsConnectionString))
262+
if (useOpenTelemetryCollector is not true && !string.IsNullOrWhiteSpace(appInsightsConnectionString))
261263
{
262264
builder = builder.AddAzureMonitorTraceExporter(options =>
263265
{
@@ -280,7 +282,7 @@ private static void AddOpenTelemetry(IServiceCollection services, IConfiguration
280282
if (isTest)
281283
return;
282284

283-
if (!string.IsNullOrWhiteSpace(appInsightsConnectionString))
285+
if (useOpenTelemetryCollector is not true && !string.IsNullOrWhiteSpace(appInsightsConnectionString))
284286
{
285287
builder = builder.AddAzureMonitorMetricExporter(options =>
286288
{
@@ -302,7 +304,7 @@ private static void AddOpenTelemetry(IServiceCollection services, IConfiguration
302304
if (isTest)
303305
return;
304306

305-
if (!string.IsNullOrWhiteSpace(appInsightsConnectionString))
307+
if (useOpenTelemetryCollector is not true && !string.IsNullOrWhiteSpace(appInsightsConnectionString))
306308
{
307309
options.AddAzureMonitorLogExporter(options =>
308310
{

src/Altinn.App.Api/Extensions/WebHostBuilderExtensions.cs

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
using Altinn.App.Core.Configuration;
12
using Altinn.App.Core.Extensions;
23
using Altinn.App.Core.Features.Maskinporten.Extensions;
4+
using Microsoft.Extensions.Configuration.Json;
5+
using Microsoft.Extensions.FileProviders;
36

47
namespace Altinn.App.Api.Extensions;
58

@@ -29,19 +32,114 @@ public static void ConfigureAppWebHost(this IWebHostBuilder builder, string[] ar
2932

3033
configBuilder.AddInMemoryCollection(config);
3134

35+
var runtimeSecretsDirectory = context.Configuration["AppSettings:RuntimeSecretsDirectory"];
36+
if (string.IsNullOrWhiteSpace(runtimeSecretsDirectory))
37+
{
38+
runtimeSecretsDirectory = AppSettings.DefaultRuntimeSecretsDirectory;
39+
}
40+
3241
configBuilder.AddMaskinportenSettingsFile(
3342
context,
3443
"MaskinportenSettingsFilepath",
35-
"/mnt/app-secrets/maskinporten-settings.json"
44+
Path.Join(runtimeSecretsDirectory, "maskinporten-settings.json")
3645
);
3746
configBuilder.AddMaskinportenSettingsFile(
3847
context,
3948
"MaskinportenSettingsInternalFilepath",
40-
"/mnt/app-secrets/maskinporten-settings-internal.json"
49+
Path.Join(runtimeSecretsDirectory, "maskinporten-settings-internal.json")
4150
);
4251

52+
AddRuntimeConfigFiles(configBuilder, context.HostingEnvironment, runtimeSecretsDirectory);
4353
configBuilder.LoadAppConfig(args);
4454
}
4555
);
4656
}
57+
58+
internal static void AddRuntimeConfigFiles(
59+
IConfigurationBuilder configBuilder,
60+
IHostEnvironment hostEnvironment,
61+
string secretsDirectory
62+
)
63+
{
64+
ArgumentNullException.ThrowIfNull(configBuilder);
65+
ArgumentNullException.ThrowIfNull(hostEnvironment);
66+
ArgumentException.ThrowIfNullOrWhiteSpace(secretsDirectory);
67+
68+
if (hostEnvironment.IsDevelopment())
69+
{
70+
return;
71+
}
72+
73+
const string overrideFileNameFragment = "override";
74+
if (!Directory.Exists(secretsDirectory))
75+
{
76+
return;
77+
}
78+
79+
string[] jsonFiles = Directory.GetFiles(secretsDirectory, "*.json", SearchOption.TopDirectoryOnly);
80+
Array.Sort(jsonFiles, StringComparer.OrdinalIgnoreCase);
81+
82+
PhysicalFileProvider? secretsFileProvider = null;
83+
HashSet<string> existingJsonFilePaths = [];
84+
85+
foreach (JsonConfigurationSource source in configBuilder.Sources.OfType<JsonConfigurationSource>())
86+
{
87+
if (source.FileProvider is null || string.IsNullOrWhiteSpace(source.Path))
88+
{
89+
continue;
90+
}
91+
92+
string? existingJsonFilePath = source.FileProvider.GetFileInfo(source.Path).PhysicalPath;
93+
if (string.IsNullOrWhiteSpace(existingJsonFilePath))
94+
{
95+
continue;
96+
}
97+
98+
existingJsonFilePaths.Add(Path.GetFullPath(existingJsonFilePath));
99+
}
100+
101+
foreach (string jsonFile in jsonFiles)
102+
{
103+
string jsonFilePath = Path.GetFullPath(jsonFile);
104+
if (existingJsonFilePaths.Contains(jsonFilePath))
105+
{
106+
continue;
107+
}
108+
109+
string jsonFileName = Path.GetFileName(jsonFile);
110+
if (jsonFileName.Contains(overrideFileNameFragment, StringComparison.OrdinalIgnoreCase))
111+
{
112+
continue;
113+
}
114+
115+
configBuilder.AddJsonFile(
116+
provider: secretsFileProvider ??= new PhysicalFileProvider(secretsDirectory),
117+
path: jsonFileName,
118+
optional: true,
119+
reloadOnChange: true
120+
);
121+
}
122+
123+
foreach (string jsonFile in jsonFiles)
124+
{
125+
string jsonFilePath = Path.GetFullPath(jsonFile);
126+
if (existingJsonFilePaths.Contains(jsonFilePath))
127+
{
128+
continue;
129+
}
130+
131+
string jsonFileName = Path.GetFileName(jsonFile);
132+
if (!jsonFileName.Contains(overrideFileNameFragment, StringComparison.OrdinalIgnoreCase))
133+
{
134+
continue;
135+
}
136+
137+
configBuilder.AddJsonFile(
138+
provider: secretsFileProvider ??= new PhysicalFileProvider(secretsDirectory),
139+
path: jsonFileName,
140+
optional: true,
141+
reloadOnChange: true
142+
);
143+
}
144+
}
47145
}

src/Altinn.App.Core/Configuration/AppSettings.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,16 @@ public class AppSettings
220220
/// Improves instrumentation throughout the Altinn app libraries.
221221
/// </summary>
222222
public bool UseOpenTelemetry { get; set; }
223+
224+
/// <summary>
225+
/// Use OpenTelemetry collector via OTLP exporter instead of Azure Monitor exporters.
226+
/// </summary>
227+
public bool UseOpenTelemetryCollector { get; set; }
228+
229+
internal const string DefaultRuntimeSecretsDirectory = "/mnt/app-secrets";
230+
231+
/// <summary>
232+
/// Directory containing runtime secrets JSON files.
233+
/// </summary>
234+
public string RuntimeSecretsDirectory { get; set; } = DefaultRuntimeSecretsDirectory;
223235
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System.IO;
2+
using Altinn.App.Api.Extensions;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.Configuration.Json;
5+
using Microsoft.Extensions.FileProviders;
6+
using Microsoft.Extensions.Hosting;
7+
using Xunit.Abstractions;
8+
9+
namespace Altinn.App.Api.Tests.Extensions;
10+
11+
public sealed class WebHostBuilderExtensionsTests
12+
{
13+
private readonly ITestOutputHelper _outputHelper;
14+
15+
public WebHostBuilderExtensionsTests(ITestOutputHelper outputHelper) => _outputHelper = outputHelper;
16+
17+
[Fact]
18+
public void AddRuntimeConfigFiles_Development_DoesNotAddRuntimeFiles()
19+
{
20+
using var tempDirectory = new TempDirectory(_outputHelper);
21+
File.WriteAllText(Path.Join(tempDirectory.Path, "appsettings.json"), "{}");
22+
IConfigurationBuilder configBuilder = new ConfigurationBuilder();
23+
24+
WebHostBuilderExtensions.AddRuntimeConfigFiles(
25+
configBuilder,
26+
new TestHostEnvironment(Environments.Development),
27+
tempDirectory.Path
28+
);
29+
30+
Assert.Empty(configBuilder.Sources.OfType<JsonConfigurationSource>());
31+
}
32+
33+
[Fact]
34+
public void AddRuntimeConfigFiles_Production_AddsNonOverrideBeforeOverride()
35+
{
36+
using var tempDirectory = new TempDirectory(_outputHelper);
37+
File.WriteAllText(Path.Join(tempDirectory.Path, "30-config.json"), "{}");
38+
File.WriteAllText(Path.Join(tempDirectory.Path, "10-settings.json"), "{}");
39+
File.WriteAllText(Path.Join(tempDirectory.Path, "20-OVERRIDE.json"), "{}");
40+
File.WriteAllText(Path.Join(tempDirectory.Path, "40-settings.override.json"), "{}");
41+
IConfigurationBuilder configBuilder = new ConfigurationBuilder();
42+
43+
WebHostBuilderExtensions.AddRuntimeConfigFiles(
44+
configBuilder,
45+
new TestHostEnvironment(Environments.Production),
46+
tempDirectory.Path
47+
);
48+
49+
string[] jsonSourcePaths = configBuilder
50+
.Sources.OfType<JsonConfigurationSource>()
51+
.Select(source => source.Path ?? string.Empty)
52+
.ToArray();
53+
54+
Assert.Equal(
55+
new[] { "10-settings.json", "30-config.json", "20-OVERRIDE.json", "40-settings.override.json" },
56+
jsonSourcePaths
57+
);
58+
}
59+
60+
[Fact]
61+
public void AddRuntimeConfigFiles_Production_SkipsFilesAlreadyInConfigurationSources()
62+
{
63+
using var tempDirectory = new TempDirectory(_outputHelper);
64+
File.WriteAllText(Path.Join(tempDirectory.Path, "maskinporten-settings.json"), "{}");
65+
File.WriteAllText(Path.Join(tempDirectory.Path, "appsettings.json"), "{}");
66+
File.WriteAllText(Path.Join(tempDirectory.Path, "appsettings.override.json"), "{}");
67+
68+
IConfigurationBuilder configBuilder = new ConfigurationBuilder();
69+
var fileProvider = new PhysicalFileProvider(tempDirectory.Path);
70+
configBuilder.AddJsonFile(
71+
provider: fileProvider,
72+
path: "maskinporten-settings.json",
73+
optional: true,
74+
reloadOnChange: false
75+
);
76+
77+
WebHostBuilderExtensions.AddRuntimeConfigFiles(
78+
configBuilder,
79+
new TestHostEnvironment(Environments.Production),
80+
tempDirectory.Path
81+
);
82+
83+
string[] jsonSourcePaths = configBuilder
84+
.Sources.OfType<JsonConfigurationSource>()
85+
.Select(source => source.Path ?? string.Empty)
86+
.ToArray();
87+
88+
Assert.Equal(
89+
1,
90+
jsonSourcePaths.Count(path => string.Equals(path, "maskinporten-settings.json", StringComparison.Ordinal))
91+
);
92+
Assert.Contains("appsettings.json", jsonSourcePaths);
93+
Assert.Contains("appsettings.override.json", jsonSourcePaths);
94+
Assert.True(
95+
Array.IndexOf(jsonSourcePaths, "appsettings.override.json")
96+
> Array.IndexOf(jsonSourcePaths, "appsettings.json")
97+
);
98+
}
99+
100+
private sealed class TestHostEnvironment(string environmentName) : IHostEnvironment
101+
{
102+
public string EnvironmentName { get; set; } = environmentName;
103+
104+
public string ApplicationName { get; set; } = nameof(WebHostBuilderExtensionsTests);
105+
106+
public string ContentRootPath { get; set; } = AppContext.BaseDirectory;
107+
108+
public IFileProvider ContentRootFileProvider { get; set; } = new PhysicalFileProvider(AppContext.BaseDirectory);
109+
}
110+
111+
private readonly struct TempDirectory : IDisposable
112+
{
113+
private readonly ITestOutputHelper _outputHelper;
114+
115+
public TempDirectory(ITestOutputHelper outputHelper)
116+
{
117+
_outputHelper = outputHelper;
118+
Path = Directory.CreateTempSubdirectory().FullName;
119+
}
120+
121+
public string Path { get; }
122+
123+
public void Dispose()
124+
{
125+
if (!Directory.Exists(Path))
126+
return;
127+
128+
try
129+
{
130+
Directory.Delete(Path, recursive: true);
131+
}
132+
catch (Exception ex)
133+
{
134+
_outputHelper.WriteLine(
135+
$"WARNING: Failed to clean up temp directory '{Path}': {ex.GetType().Name}: {ex.Message}"
136+
);
137+
}
138+
}
139+
}
140+
}

test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ namespace Altinn.App.Core.Configuration
4040
public string RuntimeAppFileName { get; set; }
4141
public string RuntimeCookieName { get; set; }
4242
public string RuntimeCssFileName { get; set; }
43+
public string RuntimeSecretsDirectory { get; set; }
4344
public string ServiceStylesConfigFileName { get; set; }
4445
public string TextFolder { get; set; }
4546
public string UiFolder { get; set; }
4647
public bool UseOpenTelemetry { get; set; }
48+
public bool UseOpenTelemetryCollector { get; set; }
4749
public string ValidationConfigurationFileName { get; set; }
4850
}
4951
public class CacheSettings

0 commit comments

Comments
 (0)