diff --git a/src/ApplicationInsights.Kubernetes/ContainerIdProviders/ContainerIdNormalizer.cs b/src/ApplicationInsights.Kubernetes/ContainerIdProviders/ContainerIdNormalizer.cs
new file mode 100644
index 00000000..3da7689a
--- /dev/null
+++ b/src/ApplicationInsights.Kubernetes/ContainerIdProviders/ContainerIdNormalizer.cs
@@ -0,0 +1,57 @@
+#nullable enable
+
+using System;
+using System.Text.RegularExpressions;
+using Microsoft.ApplicationInsights.Kubernetes.Debugging;
+
+namespace Microsoft.ApplicationInsights.Kubernetes.ContainerIdProviders;
+
+///
+/// A simple container id normalizer that picks out 64 digits of GUID/UUID from a container id with prefix / suffix.
+/// For example:
+/// cri-containerd-5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06.scope will be normalized to
+/// 5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06
+///
+internal class ContainerIdNormalizer : IContainerIdNormalizer
+{
+ // Simple rule: First 64-characters GUID/UUID.
+ private const string ContainerIdIdentifierPattern = @"([a-f\d]{64})";
+ private readonly Regex _matcher = new Regex(ContainerIdIdentifierPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, matchTimeout: TimeSpan.FromSeconds(1));
+ private readonly ApplicationInsightsKubernetesDiagnosticSource _logger = ApplicationInsightsKubernetesDiagnosticSource.Instance;
+
+ ///
+ /// Gets normalized container id.
+ ///
+ /// The original container id. String.Empty yields string.Empty with true. Null is not accepted.
+ /// The normalized container id.
+ /// True when the normalized succeeded. False otherwise.
+ public bool TryNormalize(string input, out string? normalized)
+ {
+ // Should not happen. Put here just in case.
+ if (input is null)
+ {
+ throw new ArgumentNullException(nameof(input));
+ }
+
+ // Special case: string.Empty in, string.Empty out.
+ if (input == string.Empty)
+ {
+ normalized = string.Empty;
+ return true;
+ }
+
+ _logger.LogDebug($"Normalize container id: {input}");
+
+ Match match = _matcher.Match(input);
+ if (!match.Success)
+ {
+ _logger.LogDebug($"Failed match any container id by pattern: {ContainerIdIdentifierPattern}.");
+ normalized = null;
+ return false;
+ }
+ normalized = match.Groups[1].Value;
+ _logger.LogTrace($"Container id normalized to: {normalized}");
+ return true;
+ }
+}
+
diff --git a/src/ApplicationInsights.Kubernetes/ContainerIdProviders/IContainerIdNormalizer.cs b/src/ApplicationInsights.Kubernetes/ContainerIdProviders/IContainerIdNormalizer.cs
new file mode 100644
index 00000000..fc56de7a
--- /dev/null
+++ b/src/ApplicationInsights.Kubernetes/ContainerIdProviders/IContainerIdNormalizer.cs
@@ -0,0 +1,17 @@
+#nullable enable
+
+namespace Microsoft.ApplicationInsights.Kubernetes.ContainerIdProviders;
+
+///
+/// A service to normalize container id.
+///
+internal interface IContainerIdNormalizer
+{
+ ///
+ /// Tries to normalize container id.
+ ///
+ /// The container id.
+ /// The normalized container id.
+ /// True when normalized. False otherwise.
+ bool TryNormalize(string input, out string? normalized);
+}
diff --git a/src/ApplicationInsights.Kubernetes/Extensions/KubernetesServiceCollectionBuilder.cs b/src/ApplicationInsights.Kubernetes/Extensions/KubernetesServiceCollectionBuilder.cs
index 86ca5b76..a39c43e9 100644
--- a/src/ApplicationInsights.Kubernetes/Extensions/KubernetesServiceCollectionBuilder.cs
+++ b/src/ApplicationInsights.Kubernetes/Extensions/KubernetesServiceCollectionBuilder.cs
@@ -106,6 +106,8 @@ protected virtual void RegisterSettingsProvider(IServiceCollection serviceCollec
_logger.LogError("Unsupported OS.");
}
+ serviceCollection.TryAddSingleton();
+
// Notes: pay attention to the order. Injecting uses the order of registering in this case.
// For backward compatibility, $APPINSIGHTS_KUBERNETES_POD_NAME has been agreed upon to allow customize pod name with downward API.
serviceCollection.TryAddEnumerable(ServiceDescriptor.Singleton());
diff --git a/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpClientSettingsBase.cs b/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpClientSettingsBase.cs
index d947a66f..cebbc90b 100644
--- a/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpClientSettingsBase.cs
+++ b/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpClientSettingsBase.cs
@@ -15,12 +15,14 @@ namespace Microsoft.ApplicationInsights.Kubernetes
internal abstract class KubeHttpClientSettingsBase : IKubeHttpClientSettingsProvider
{
private readonly IEnumerable _containerIdProviders;
+ private readonly IContainerIdNormalizer _containerIdNormalizer;
protected readonly ApplicationInsightsKubernetesDiagnosticSource _logger = ApplicationInsightsKubernetesDiagnosticSource.Instance;
public KubeHttpClientSettingsBase(
string? kubernetesServiceHost,
string? kubernetesServicePort,
- IEnumerable containerIdProviders)
+ IEnumerable containerIdProviders,
+ IContainerIdNormalizer containerIdNormalizer)
{
kubernetesServiceHost = kubernetesServiceHost ?? Environment.GetEnvironmentVariable(@"KUBERNETES_SERVICE_HOST");
if (string.IsNullOrEmpty(kubernetesServiceHost))
@@ -38,7 +40,7 @@ public KubeHttpClientSettingsBase(
_logger.LogDebug("Kubernetes base address: {0}", baseAddress);
ServiceBaseAddress = new Uri(baseAddress, UriKind.Absolute);
_containerIdProviders = containerIdProviders ?? throw new ArgumentNullException(nameof(containerIdProviders));
-
+ _containerIdNormalizer = containerIdNormalizer ?? throw new ArgumentNullException(nameof(containerIdNormalizer));
ContainerId = GetContainerIdOrThrow();
}
@@ -162,7 +164,12 @@ private string GetContainerIdOrThrow()
{
throw new InvalidOperationException("Valid containerId can't be null.");
}
- return containerId;
+
+ if (!_containerIdNormalizer.TryNormalize(containerId, out string? normalized))
+ {
+ throw new InvalidOperationException($"Container id format can't be recognized: {containerId}");
+ }
+ return normalized!;
}
}
throw new InvalidOperationException("Failed fetching container id.");
diff --git a/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpClientSettingsProvider.cs b/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpClientSettingsProvider.cs
index 05f77cc0..54c85052 100644
--- a/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpClientSettingsProvider.cs
+++ b/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpClientSettingsProvider.cs
@@ -15,19 +15,20 @@ internal class KubeHttpClientSettingsProvider : KubeHttpClientSettingsBase, IKub
private readonly string _certFilePath;
private readonly string _tokenFilePath;
- public KubeHttpClientSettingsProvider(IEnumerable containerIdProviders)
- : this(containerIdProviders, kubernetesServiceHost: null)
+ public KubeHttpClientSettingsProvider(IEnumerable containerIdProviders, IContainerIdNormalizer containerIdNormalizer)
+ : this(containerIdProviders, containerIdNormalizer, kubernetesServiceHost: null)
{
}
public KubeHttpClientSettingsProvider(
IEnumerable containerIdProviders,
+ IContainerIdNormalizer containerIdNormalizer,
string pathToToken = @"/var/run/secrets/kubernetes.io/serviceaccount/token",
string pathToCert = @"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
string pathToNamespace = @"/var/run/secrets/kubernetes.io/serviceaccount/namespace",
string? kubernetesServiceHost = null,
string? kubernetesServicePort = null)
- : base(kubernetesServiceHost, kubernetesServicePort, containerIdProviders)
+ : base(kubernetesServiceHost, kubernetesServicePort, containerIdProviders, containerIdNormalizer)
{
_tokenFilePath = Arguments.IsNotNullOrEmpty(pathToToken, nameof(pathToToken));
_certFilePath = Arguments.IsNotNullOrEmpty(pathToCert, nameof(pathToCert));
diff --git a/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpSettingsWinContainerProvider.cs b/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpSettingsWinContainerProvider.cs
index a3391905..91444017 100644
--- a/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpSettingsWinContainerProvider.cs
+++ b/src/ApplicationInsights.Kubernetes/K8sHttpClient/KubeHttpSettingsWinContainerProvider.cs
@@ -13,13 +13,14 @@ internal class KubeHttpSettingsWinContainerProvider : KubeHttpClientSettingsBase
public KubeHttpSettingsWinContainerProvider(
IEnumerable containerIdProviders,
+ IContainerIdNormalizer containerIdNormalizer,
string serviceAccountFolder = @"C:\var\run\secrets\kubernetes.io\serviceaccount",
string tokenFileName = "token",
string certFileName = "ca.crt",
string namespaceFileName = "namespace",
string kubernetesServiceHost = null,
string kubernetesServicePort = null)
- : base(kubernetesServiceHost, kubernetesServicePort, containerIdProviders)
+ : base(kubernetesServiceHost, kubernetesServicePort, containerIdProviders, containerIdNormalizer)
{
// Container id won't be fetched for windows container.
DirectoryInfo serviceAccountDirectory =
diff --git a/tests/UnitTests/ContainerIdNormalizerTests.cs b/tests/UnitTests/ContainerIdNormalizerTests.cs
new file mode 100644
index 00000000..aee11073
--- /dev/null
+++ b/tests/UnitTests/ContainerIdNormalizerTests.cs
@@ -0,0 +1,65 @@
+using System;
+using Xunit;
+
+namespace Microsoft.ApplicationInsights.Kubernetes.ContainerIdProviders.Tests;
+
+public class ContainerIdNormalizerTests
+{
+ [Theory]
+ // With prefix and suffix:
+ [InlineData(@"cri-containerd-5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06.scope", "5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06")]
+ // With prefix only:
+ [InlineData(@"docker://5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06", "5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06")]
+ // With suffix only:
+ [InlineData(@"5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06-scope", "5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06")]
+ // Same as normalized:
+ [InlineData(@"5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06", "5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06")]
+ // Longer than 64 digits - notes: so that the match regex is simplified.
+ [InlineData(@"5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06a", "5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f06")]
+ public void TryGetNormalizedShouldNormalizeContainerIds(string input, string expected)
+ {
+ ContainerIdNormalizer target = new ContainerIdNormalizer();
+ bool result = target.TryNormalize(input, out string actual);
+
+ Assert.True(result);
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ // Input has no container id
+ [InlineData("Input has no container id")]
+ // Short guid is not accepted
+ [InlineData("f78375b1c487")]
+ // Shorter than 64 digits
+ [InlineData("5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f0")]
+ // Not a valid guid with character of z
+ [InlineData("5146b2bcd77ab4f2624bc1fbd98cf9751741344a80b043dbd77a4e847bff4f0z")]
+ public void TryGetNormalizedShouldNotAcceptInvalidContainerIds(string input)
+ {
+ ContainerIdNormalizer target = new ContainerIdNormalizer();
+ bool result = target.TryNormalize(input, out string actual);
+
+ Assert.False(result);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void TryGetNormalizedShouldHandleStringEmpty()
+ {
+ ContainerIdNormalizer target = new ContainerIdNormalizer();
+ bool result = target.TryNormalize(string.Empty, out string actual);
+
+ Assert.True(result);
+ Assert.Equal(string.Empty, actual);
+ }
+
+ [Fact]
+ public void TryGetNormalizedShouldDoesNotAcceptNull()
+ {
+ ContainerIdNormalizer target = new ContainerIdNormalizer();
+ Assert.Throws(() =>
+ {
+ _ = target.TryNormalize(null, out string actual);
+ });
+ }
+}
diff --git a/tests/UnitTests/KuteHttpClientSettingsProviderTests.cs b/tests/UnitTests/KuteHttpClientSettingsProviderTests.cs
index f43e17e3..9706a264 100644
--- a/tests/UnitTests/KuteHttpClientSettingsProviderTests.cs
+++ b/tests/UnitTests/KuteHttpClientSettingsProviderTests.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using Microsoft.ApplicationInsights.Kubernetes.ContainerIdProviders;
+using Moq;
using Xunit;
namespace Microsoft.ApplicationInsights.Kubernetes
@@ -11,8 +12,10 @@ public class KuteHttpClientSettingsProviderTests
[Fact(DisplayName = "Base address is formed by constructor")]
public void BaseAddressShouldBeFormed()
{
+ IContainerIdNormalizer containerIdNormalizer = new ContainerIdNormalizer();
IKubeHttpClientSettingsProvider target = new KubeHttpClientSettingsProvider(
GetConatinerIdProviders(),
+ containerIdNormalizer,
pathToNamespace: "namespace",
kubernetesServiceHost: "127.0.0.1",
kubernetesServicePort: "8001");
@@ -25,8 +28,10 @@ public void BaseAddressShouldBeFormed()
[Fact(DisplayName = "Base address is formed by constructor of windows kube settings provider")]
public void BaseAddressShouldBeFormedWin()
{
+ IContainerIdNormalizer containerIdNormalizer = new ContainerIdNormalizer();
IKubeHttpClientSettingsProvider target = new KubeHttpSettingsWinContainerProvider(
GetConatinerIdProviders(),
+ containerIdNormalizer,
serviceAccountFolder: ".",
namespaceFileName: "namespace",
kubernetesServiceHost: "127.0.0.1",
@@ -39,8 +44,10 @@ public void BaseAddressShouldBeFormedWin()
[Fact(DisplayName = "Container id is set to string.Empty for windows container settings")]
public void ContainerIdIsAlwaysNullForWinSettings()
{
+ IContainerIdNormalizer containerIdNormalizer = new ContainerIdNormalizer();
IKubeHttpClientSettingsProvider target = new KubeHttpSettingsWinContainerProvider(
GetConatinerIdProviders(),
+ containerIdNormalizer,
serviceAccountFolder: ".",
namespaceFileName: "namespace",
kubernetesServiceHost: "127.0.0.1",
@@ -52,8 +59,10 @@ public void ContainerIdIsAlwaysNullForWinSettings()
public void TokenShoudBeFetched()
{
+ IContainerIdNormalizer containerIdNormalizer = new ContainerIdNormalizer();
IKubeHttpClientSettingsProvider target = new KubeHttpClientSettingsProvider(
GetConatinerIdProviders(),
+ containerIdNormalizer,
pathToNamespace: "namespace",
pathToToken: "token",
kubernetesServiceHost: "127.0.0.1",
@@ -64,8 +73,10 @@ public void TokenShoudBeFetched()
[Fact(DisplayName = "Token can be fetched by windows settings provider")]
public void TokenShouldBeFetchedForWin()
{
+ IContainerIdNormalizer containerIdNormalizer = new ContainerIdNormalizer();
IKubeHttpClientSettingsProvider target = new KubeHttpSettingsWinContainerProvider(
GetConatinerIdProviders(),
+ containerIdNormalizer,
serviceAccountFolder: ".",
namespaceFileName: "namespace",
tokenFileName:"token",
@@ -78,8 +89,10 @@ public void TokenShouldBeFetchedForWin()
[Fact(DisplayName = "Return true when certificate chain is valid")]
public void TrueWhenValidCertificate()
{
+ IContainerIdNormalizer containerIdNormalizer = new ContainerIdNormalizer();
KubeHttpClientSettingsProvider target = new KubeHttpClientSettingsProvider(
GetConatinerIdProviders(),
+ containerIdNormalizer,
pathToNamespace: "namespace",
kubernetesServiceHost: "127.0.0.1",
kubernetesServicePort: "8001");
@@ -95,8 +108,10 @@ public void TrueWhenValidCertificate()
[Fact(DisplayName = "Return false when certificate chain is invalid")]
public void FalseWhenInvalidCertificate()
{
+ IContainerIdNormalizer containerIdNormalizer = new ContainerIdNormalizer();
KubeHttpClientSettingsProvider target = new KubeHttpClientSettingsProvider(
GetConatinerIdProviders(),
+ containerIdNormalizer,
pathToNamespace: "namespace",
kubernetesServiceHost: "127.0.0.1",
kubernetesServicePort: "8001");
@@ -112,8 +127,10 @@ public void FalseWhenInvalidCertificate()
[Fact(DisplayName = "Return false when certificate out of date")]
public void FalseWhenOutOfDateCertificate()
{
+ IContainerIdNormalizer containerIdNormalizer = new ContainerIdNormalizer();
KubeHttpClientSettingsProvider target = new KubeHttpClientSettingsProvider(
GetConatinerIdProviders(),
+ containerIdNormalizer,
pathToNamespace: "namespace",
kubernetesServiceHost: "127.0.0.1",
kubernetesServicePort: "8001");