Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
DistributedContextPropagator.Current = new AspNetCorePropagator();

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

services
.AddOpenTelemetry()
Expand All @@ -252,22 +253,23 @@
.AddAspNetCoreInstrumentation(opts =>
{
opts.RecordException = true;
opts.Filter = httpContext => !httpContext.Request.Path.StartsWithSegments("/health");
});

if (isTest)
return;

if (!string.IsNullOrWhiteSpace(appInsightsConnectionString))
if (useOpenTelemetryCollector is not true && !string.IsNullOrWhiteSpace(appInsightsConnectionString))
{
builder = builder.AddAzureMonitorTraceExporter(options =>
{
options.ConnectionString = appInsightsConnectionString;
});
}
else
{
builder = builder.AddOtlpExporter();
}

Check notice

Code scanning / CodeQL

Missed ternary opportunity Note

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
})
.WithMetrics(builder =>
{
Expand All @@ -280,17 +282,17 @@
if (isTest)
return;

if (!string.IsNullOrWhiteSpace(appInsightsConnectionString))
if (useOpenTelemetryCollector is not true && !string.IsNullOrWhiteSpace(appInsightsConnectionString))
{
builder = builder.AddAzureMonitorMetricExporter(options =>
{
options.ConnectionString = appInsightsConnectionString;
});
}
else
{
builder = builder.AddOtlpExporter();
}

Check notice

Code scanning / CodeQL

Missed ternary opportunity Note

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
});

services.AddLogging(logging =>
Expand All @@ -302,7 +304,7 @@
if (isTest)
return;

if (!string.IsNullOrWhiteSpace(appInsightsConnectionString))
if (useOpenTelemetryCollector is not true && !string.IsNullOrWhiteSpace(appInsightsConnectionString))
{
options.AddAzureMonitorLogExporter(options =>
{
Expand Down
102 changes: 100 additions & 2 deletions src/Altinn.App.Api/Extensions/WebHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Extensions;
using Altinn.App.Core.Features.Maskinporten.Extensions;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.FileProviders;

namespace Altinn.App.Api.Extensions;

Expand Down Expand Up @@ -29,19 +32,114 @@

configBuilder.AddInMemoryCollection(config);

var runtimeSecretsDirectory = context.Configuration["AppSettings:RuntimeSecretsDirectory"];
if (string.IsNullOrWhiteSpace(runtimeSecretsDirectory))
{
runtimeSecretsDirectory = AppSettings.DefaultRuntimeSecretsDirectory;
}

configBuilder.AddMaskinportenSettingsFile(
context,
"MaskinportenSettingsFilepath",
"/mnt/app-secrets/maskinporten-settings.json"
Path.Join(runtimeSecretsDirectory, "maskinporten-settings.json")
);
configBuilder.AddMaskinportenSettingsFile(
context,
"MaskinportenSettingsInternalFilepath",
"/mnt/app-secrets/maskinporten-settings-internal.json"
Path.Join(runtimeSecretsDirectory, "maskinporten-settings-internal.json")
);

AddRuntimeConfigFiles(configBuilder, context.HostingEnvironment, runtimeSecretsDirectory);
configBuilder.LoadAppConfig(args);
}
);
}

internal static void AddRuntimeConfigFiles(
IConfigurationBuilder configBuilder,
IHostEnvironment hostEnvironment,
string secretsDirectory
)
{
ArgumentNullException.ThrowIfNull(configBuilder);
ArgumentNullException.ThrowIfNull(hostEnvironment);
ArgumentException.ThrowIfNullOrWhiteSpace(secretsDirectory);

if (hostEnvironment.IsDevelopment())
{
return;
}

const string overrideFileNameFragment = "override";
if (!Directory.Exists(secretsDirectory))
{
return;
}

string[] jsonFiles = Directory.GetFiles(secretsDirectory, "*.json", SearchOption.TopDirectoryOnly);
Array.Sort(jsonFiles, StringComparer.OrdinalIgnoreCase);

PhysicalFileProvider? secretsFileProvider = null;
HashSet<string> existingJsonFilePaths = [];

foreach (JsonConfigurationSource source in configBuilder.Sources.OfType<JsonConfigurationSource>())
{
if (source.FileProvider is null || string.IsNullOrWhiteSpace(source.Path))
{
continue;
}

string? existingJsonFilePath = source.FileProvider.GetFileInfo(source.Path).PhysicalPath;
if (string.IsNullOrWhiteSpace(existingJsonFilePath))
{
continue;
}

existingJsonFilePaths.Add(Path.GetFullPath(existingJsonFilePath));
}

foreach (string jsonFile in jsonFiles)
{
string jsonFilePath = Path.GetFullPath(jsonFile);
if (existingJsonFilePaths.Contains(jsonFilePath))
{
continue;
}

string jsonFileName = Path.GetFileName(jsonFile);
if (jsonFileName.Contains(overrideFileNameFragment, StringComparison.OrdinalIgnoreCase))
{
continue;
}

configBuilder.AddJsonFile(
provider: secretsFileProvider ??= new PhysicalFileProvider(secretsDirectory),
path: jsonFileName,
optional: true,
reloadOnChange: true
);
}

foreach (string jsonFile in jsonFiles)
{
string jsonFilePath = Path.GetFullPath(jsonFile);
if (existingJsonFilePaths.Contains(jsonFilePath))
{
continue;
}

string jsonFileName = Path.GetFileName(jsonFile);
if (!jsonFileName.Contains(overrideFileNameFragment, StringComparison.OrdinalIgnoreCase))
{
continue;
}

configBuilder.AddJsonFile(
provider: secretsFileProvider ??= new PhysicalFileProvider(secretsDirectory),
path: jsonFileName,
optional: true,
reloadOnChange: true
);
}
}
}
12 changes: 12 additions & 0 deletions src/Altinn.App.Core/Configuration/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,16 @@ public class AppSettings
/// Improves instrumentation throughout the Altinn app libraries.
/// </summary>
public bool UseOpenTelemetry { get; set; }

/// <summary>
/// Use OpenTelemetry collector via OTLP exporter instead of Azure Monitor exporters.
/// </summary>
public bool UseOpenTelemetryCollector { get; set; }

internal const string DefaultRuntimeSecretsDirectory = "/mnt/app-secrets";

/// <summary>
/// Directory containing runtime secrets JSON files.
/// </summary>
public string RuntimeSecretsDirectory { get; set; } = DefaultRuntimeSecretsDirectory;
}
140 changes: 140 additions & 0 deletions test/Altinn.App.Api.Tests/Extensions/WebHostBuilderExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System.IO;
using Altinn.App.Api.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Xunit.Abstractions;

namespace Altinn.App.Api.Tests.Extensions;

public sealed class WebHostBuilderExtensionsTests
{
private readonly ITestOutputHelper _outputHelper;

public WebHostBuilderExtensionsTests(ITestOutputHelper outputHelper) => _outputHelper = outputHelper;

[Fact]
public void AddRuntimeConfigFiles_Development_DoesNotAddRuntimeFiles()
{
using var tempDirectory = new TempDirectory(_outputHelper);
File.WriteAllText(Path.Join(tempDirectory.Path, "appsettings.json"), "{}");
IConfigurationBuilder configBuilder = new ConfigurationBuilder();

WebHostBuilderExtensions.AddRuntimeConfigFiles(
configBuilder,
new TestHostEnvironment(Environments.Development),
tempDirectory.Path
);

Assert.Empty(configBuilder.Sources.OfType<JsonConfigurationSource>());
}

[Fact]
public void AddRuntimeConfigFiles_Production_AddsNonOverrideBeforeOverride()
{
using var tempDirectory = new TempDirectory(_outputHelper);
File.WriteAllText(Path.Join(tempDirectory.Path, "30-config.json"), "{}");
File.WriteAllText(Path.Join(tempDirectory.Path, "10-settings.json"), "{}");
File.WriteAllText(Path.Join(tempDirectory.Path, "20-OVERRIDE.json"), "{}");
File.WriteAllText(Path.Join(tempDirectory.Path, "40-settings.override.json"), "{}");
IConfigurationBuilder configBuilder = new ConfigurationBuilder();

WebHostBuilderExtensions.AddRuntimeConfigFiles(
configBuilder,
new TestHostEnvironment(Environments.Production),
tempDirectory.Path
);

string[] jsonSourcePaths = configBuilder
.Sources.OfType<JsonConfigurationSource>()
.Select(source => source.Path ?? string.Empty)
.ToArray();

Assert.Equal(
new[] { "10-settings.json", "30-config.json", "20-OVERRIDE.json", "40-settings.override.json" },
jsonSourcePaths
);
}

[Fact]
public void AddRuntimeConfigFiles_Production_SkipsFilesAlreadyInConfigurationSources()
{
using var tempDirectory = new TempDirectory(_outputHelper);
File.WriteAllText(Path.Join(tempDirectory.Path, "maskinporten-settings.json"), "{}");
File.WriteAllText(Path.Join(tempDirectory.Path, "appsettings.json"), "{}");
File.WriteAllText(Path.Join(tempDirectory.Path, "appsettings.override.json"), "{}");

IConfigurationBuilder configBuilder = new ConfigurationBuilder();
var fileProvider = new PhysicalFileProvider(tempDirectory.Path);
configBuilder.AddJsonFile(
provider: fileProvider,
path: "maskinporten-settings.json",
optional: true,
reloadOnChange: false
);

WebHostBuilderExtensions.AddRuntimeConfigFiles(
configBuilder,
new TestHostEnvironment(Environments.Production),
tempDirectory.Path
);

string[] jsonSourcePaths = configBuilder
.Sources.OfType<JsonConfigurationSource>()
.Select(source => source.Path ?? string.Empty)
.ToArray();

Assert.Equal(
1,
jsonSourcePaths.Count(path => string.Equals(path, "maskinporten-settings.json", StringComparison.Ordinal))
);
Assert.Contains("appsettings.json", jsonSourcePaths);
Assert.Contains("appsettings.override.json", jsonSourcePaths);
Assert.True(
Array.IndexOf(jsonSourcePaths, "appsettings.override.json")
> Array.IndexOf(jsonSourcePaths, "appsettings.json")
);
}

private sealed class TestHostEnvironment(string environmentName) : IHostEnvironment
{
public string EnvironmentName { get; set; } = environmentName;

public string ApplicationName { get; set; } = nameof(WebHostBuilderExtensionsTests);

public string ContentRootPath { get; set; } = AppContext.BaseDirectory;

public IFileProvider ContentRootFileProvider { get; set; } = new PhysicalFileProvider(AppContext.BaseDirectory);
}

private readonly struct TempDirectory : IDisposable
{
private readonly ITestOutputHelper _outputHelper;

public TempDirectory(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
Path = Directory.CreateTempSubdirectory().FullName;
}

public string Path { get; }

public void Dispose()
{
if (!Directory.Exists(Path))
return;

try
{
Directory.Delete(Path, recursive: true);
}
catch (Exception ex)
{
_outputHelper.WriteLine(
$"WARNING: Failed to clean up temp directory '{Path}': {ex.GetType().Name}: {ex.Message}"
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ namespace Altinn.App.Core.Configuration
public string RuntimeAppFileName { get; set; }
public string RuntimeCookieName { get; set; }
public string RuntimeCssFileName { get; set; }
public string RuntimeSecretsDirectory { get; set; }
public string ServiceStylesConfigFileName { get; set; }
public string TextFolder { get; set; }
public string UiFolder { get; set; }
public bool UseOpenTelemetry { get; set; }
public bool UseOpenTelemetryCollector { get; set; }
public string ValidationConfigurationFileName { get; set; }
}
public class CacheSettings
Expand Down
Loading