up
This commit is contained in:
@@ -3,9 +3,11 @@ using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Zastava.Core.Configuration;
|
||||
using StellaOps.Zastava.Observer.Backend;
|
||||
using StellaOps.Zastava.Observer.Configuration;
|
||||
@@ -27,6 +29,7 @@ public static class ObserverServiceCollectionExtensions
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
services.AddZastavaRuntimeCore(configuration, componentName: "observer");
|
||||
services.AddStellaOpsCrypto();
|
||||
|
||||
services.AddOptions<ZastavaObserverOptions>()
|
||||
.Bind(configuration.GetSection(ZastavaObserverOptions.SectionName))
|
||||
@@ -117,11 +120,12 @@ public static class ObserverServiceCollectionExtensions
|
||||
options.RequiredSecretTypes.Add("attestation");
|
||||
});
|
||||
|
||||
// Surface validation for preflight checks
|
||||
services.AddSurfaceValidation();
|
||||
|
||||
services.TryAddSingleton(sp => sp.GetRequiredService<ISurfaceEnvironment>().Settings);
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceCacheOptions>>(sp =>
|
||||
new SurfaceCacheOptionsConfigurator(sp.GetRequiredService<SurfaceEnvironmentSettings>())));
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceManifestStoreOptions>>(sp =>
|
||||
new SurfaceManifestStoreOptionsConfigurator(sp.GetRequiredService<SurfaceEnvironmentSettings>())));
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceCacheOptions>, SurfaceCacheOptionsConfigurator>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceManifestStoreOptions>, SurfaceManifestStoreOptionsConfigurator>());
|
||||
|
||||
services.TryAddSingleton<IObserverSurfaceSecrets, ObserverSurfaceSecrets>();
|
||||
services.TryAddSingleton<IRuntimeSurfaceFsClient, RuntimeSurfaceFsClient>();
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Zastava.Core/StellaOps.Zastava.Core.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/StellaOps.Scanner.Surface.Secrets.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Zastava.Core/StellaOps.Zastava.Core.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/StellaOps.Scanner.Surface.Secrets.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Validation/StellaOps.Scanner.Surface.Validation.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos/runtime/v1/runtime.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2,9 +2,11 @@ using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Zastava.Core.Configuration;
|
||||
using StellaOps.Zastava.Webhook.Admission;
|
||||
using StellaOps.Zastava.Webhook.Authority;
|
||||
@@ -17,12 +19,13 @@ using StellaOps.Zastava.Webhook.Secrets;
|
||||
using StellaOps.Zastava.Webhook.Surface;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddZastavaWebhook(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddZastavaWebhook(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddZastavaRuntimeCore(configuration, "webhook");
|
||||
services.AddStellaOpsCrypto();
|
||||
|
||||
services.AddOptions<ZastavaWebhookOptions>()
|
||||
.Bind(configuration.GetSection(ZastavaWebhookOptions.SectionName))
|
||||
@@ -54,11 +57,12 @@ public static class ServiceCollectionExtensions
|
||||
options.RequiredSecretTypes.Add("attestation");
|
||||
});
|
||||
|
||||
// Surface validation for preflight checks
|
||||
services.AddSurfaceValidation();
|
||||
|
||||
services.TryAddSingleton(sp => sp.GetRequiredService<ISurfaceEnvironment>().Settings);
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceCacheOptions>>(sp =>
|
||||
new SurfaceCacheOptionsConfigurator(sp.GetRequiredService<SurfaceEnvironmentSettings>())));
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceManifestStoreOptions>>(sp =>
|
||||
new SurfaceManifestStoreOptionsConfigurator(sp.GetRequiredService<SurfaceEnvironmentSettings>())));
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceCacheOptions>, SurfaceCacheOptionsConfigurator>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SurfaceManifestStoreOptions>, SurfaceManifestStoreOptionsConfigurator>());
|
||||
|
||||
services.TryAddSingleton<AdmissionReviewParser>();
|
||||
services.TryAddSingleton<AdmissionResponseBuilder>();
|
||||
|
||||
@@ -19,5 +19,7 @@
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/StellaOps.Scanner.Surface.Secrets.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Validation/StellaOps.Scanner.Surface.Validation.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -39,6 +39,7 @@ public sealed class SurfaceEnvironmentRegistrationTests
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddZastavaObserver(configuration);
|
||||
|
||||
|
||||
@@ -11,75 +11,139 @@ namespace StellaOps.Zastava.Observer.Tests.Secrets;
|
||||
|
||||
public sealed class ObserverSurfaceSecretsTests
|
||||
{
|
||||
// All environment variables that might affect these tests (to avoid pollution from parallel tests)
|
||||
private static readonly string[] AllRelevantEnvVars =
|
||||
[
|
||||
"ZASTAVA_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"ZASTAVA_OBSERVER_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_OBSERVER_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_OBSERVER_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_OBSERVER_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"SURFACE_SECRET_DEFAULT_ZASTAVA.OBSERVER_CAS-ACCESS_CAS-ACCESS",
|
||||
"SURFACE_SECRET_DEFAULT_ZASTAVA.OBSERVER_ATTESTATION_ATTESTATION"
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public async Task GetCasAccessAsync_ResolvesInlineSecret()
|
||||
{
|
||||
var env = new Dictionary<string, string?>
|
||||
var envVars = new Dictionary<string, string?>
|
||||
{
|
||||
["ZASTAVA_SURFACE_FS_ENDPOINT"] = "https://surface.example",
|
||||
["ZASTAVA_SURFACE_SECRETS_PROVIDER"] = "inline",
|
||||
["ZASTAVA_SURFACE_SECRETS_ALLOW_INLINE"] = "true",
|
||||
["SURFACE_SECRET_DEFAULT_ZASTAVA.OBSERVER_CAS-ACCESS_PRIMARY"] = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{\"driver\":\"s3\",\"accessKeyId\":\"ak\",\"secretAccessKey\":\"sk\"}"))
|
||||
["SURFACE_SECRET_DEFAULT_ZASTAVA.OBSERVER_CAS-ACCESS_CAS-ACCESS"] = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{\"driver\":\"s3\",\"accessKeyId\":\"ak\",\"secretAccessKey\":\"sk\"}"))
|
||||
};
|
||||
|
||||
var services = BuildServices(env);
|
||||
var originals = CaptureEnvironment(AllRelevantEnvVars);
|
||||
try
|
||||
{
|
||||
ClearEnvironment(AllRelevantEnvVars);
|
||||
foreach (var pair in envVars)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
var secrets = services.GetRequiredService<IObserverSurfaceSecrets>();
|
||||
var result = await secrets.GetCasAccessAsync(name: null);
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaObserverOptions.SectionName}:runtimes:0:engine"] = "Containerd",
|
||||
[$"{ZastavaObserverOptions.SectionName}:backend:baseAddress"] = "https://scanner.internal"
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Equal("s3", result.Driver);
|
||||
Assert.Equal("ak", result.AccessKeyId);
|
||||
Assert.Equal("sk", result.SecretAccessKey);
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddZastavaObserver(configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var secrets = provider.GetRequiredService<IObserverSurfaceSecrets>();
|
||||
var result = await secrets.GetCasAccessAsync(name: null);
|
||||
|
||||
Assert.Equal("s3", result.Driver);
|
||||
Assert.Equal("ak", result.AccessKeyId);
|
||||
Assert.Equal("sk", result.SecretAccessKey);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestoreEnvironment(originals);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAttestationAsync_ResolvesInlineSecret()
|
||||
{
|
||||
var payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{\"keyPem\":\"KEY\",\"rekorToken\":\"token123\"}"));
|
||||
var env = new Dictionary<string, string?>
|
||||
var envVars = new Dictionary<string, string?>
|
||||
{
|
||||
["ZASTAVA_SURFACE_FS_ENDPOINT"] = "https://surface.example",
|
||||
["ZASTAVA_SURFACE_SECRETS_PROVIDER"] = "inline",
|
||||
["ZASTAVA_SURFACE_SECRETS_ALLOW_INLINE"] = "true",
|
||||
["SURFACE_SECRET_DEFAULT_ZASTAVA.OBSERVER_ATTESTATION_SIGNING"] = payload
|
||||
["SURFACE_SECRET_DEFAULT_ZASTAVA.OBSERVER_ATTESTATION_ATTESTATION"] = payload
|
||||
};
|
||||
|
||||
var services = BuildServices(env);
|
||||
var originals = CaptureEnvironment(AllRelevantEnvVars);
|
||||
try
|
||||
{
|
||||
ClearEnvironment(AllRelevantEnvVars);
|
||||
foreach (var pair in envVars)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
var secrets = services.GetRequiredService<IObserverSurfaceSecrets>();
|
||||
var result = await secrets.GetAttestationAsync(name: null);
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaObserverOptions.SectionName}:runtimes:0:engine"] = "Containerd",
|
||||
[$"{ZastavaObserverOptions.SectionName}:backend:baseAddress"] = "https://scanner.internal"
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Equal("KEY", result.KeyPem);
|
||||
Assert.Equal("token123", result.RekorApiToken);
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddZastavaObserver(configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var secrets = provider.GetRequiredService<IObserverSurfaceSecrets>();
|
||||
var result = await secrets.GetAttestationAsync(name: null);
|
||||
|
||||
Assert.Equal("KEY", result.KeyPem);
|
||||
Assert.Equal("token123", result.RekorApiToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestoreEnvironment(originals);
|
||||
}
|
||||
}
|
||||
|
||||
private static ServiceProvider BuildServices(Dictionary<string, string?> env)
|
||||
private static IReadOnlyDictionary<string, string?> CaptureEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
var originals = new Dictionary<string, string?>();
|
||||
foreach (var pair in env)
|
||||
var snapshot = new Dictionary<string, string?>();
|
||||
foreach (var name in names)
|
||||
{
|
||||
originals[pair.Key] = Environment.GetEnvironmentVariable(pair.Key);
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
snapshot[name] = Environment.GetEnvironmentVariable(name);
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaObserverOptions.SectionName}:runtimes:0:engine"] = "Containerd"
|
||||
})
|
||||
.Build();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddZastavaObserver(configuration);
|
||||
private static void ClearEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, null);
|
||||
}
|
||||
}
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
foreach (var pair in originals)
|
||||
private static void RestoreEnvironment(IReadOnlyDictionary<string, string?> snapshot)
|
||||
{
|
||||
foreach (var pair in snapshot)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Zastava.Observer/StellaOps.Zastava.Observer.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -11,48 +11,114 @@ namespace StellaOps.Zastava.Observer.Tests.Surface;
|
||||
|
||||
public sealed class RuntimeSurfaceFsClientTests
|
||||
{
|
||||
// All environment variables that might affect these tests (to avoid pollution from parallel tests)
|
||||
private static readonly string[] AllRelevantEnvVars =
|
||||
[
|
||||
"ZASTAVA_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"ZASTAVA_SURFACE_SECRETS_NAMESPACE",
|
||||
"ZASTAVA_SURFACE_FEATURES",
|
||||
"ZASTAVA_SURFACE_TENANT",
|
||||
"ZASTAVA_OBSERVER_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_OBSERVER_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_OBSERVER_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_OBSERVER_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"ZASTAVA_OBSERVER_SURFACE_SECRETS_NAMESPACE",
|
||||
"ZASTAVA_OBSERVER_SURFACE_FEATURES",
|
||||
"ZASTAVA_OBSERVER_SURFACE_TENANT"
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetManifestAsync_ReturnsPublishedManifest()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaObserverOptions.SectionName}:runtimes:0:engine"] = "Containerd",
|
||||
[$"{ZastavaObserverOptions.SectionName}:backend:baseAddress"] = "https://scanner.internal"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddZastavaObserver(configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var manifestWriter = provider.GetRequiredService<ISurfaceManifestWriter>();
|
||||
var client = provider.GetRequiredService<IRuntimeSurfaceFsClient>();
|
||||
|
||||
var document = new SurfaceManifestDocument
|
||||
var envVars = new Dictionary<string, string?>
|
||||
{
|
||||
Tenant = "default",
|
||||
ImageDigest = "sha256:deadbeef",
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
Artifacts = new[]
|
||||
{
|
||||
new SurfaceManifestArtifact
|
||||
{
|
||||
Kind = "entry-trace",
|
||||
Uri = "cas://surface-cache/manifest/entry-trace",
|
||||
Digest = "sha256:abc123",
|
||||
MediaType = "application/json",
|
||||
Format = "ndsjon"
|
||||
}
|
||||
}
|
||||
["ZASTAVA_SURFACE_FS_ENDPOINT"] = "https://surface.example"
|
||||
};
|
||||
|
||||
var published = await manifestWriter.PublishAsync(document, default);
|
||||
var fetched = await client.TryGetManifestAsync(published.ManifestDigest, default);
|
||||
var originals = CaptureEnvironment(AllRelevantEnvVars);
|
||||
try
|
||||
{
|
||||
ClearEnvironment(AllRelevantEnvVars);
|
||||
foreach (var pair in envVars)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
Assert.NotNull(fetched);
|
||||
Assert.Equal(document.Tenant, fetched!.Tenant);
|
||||
Assert.Equal(document.ImageDigest, fetched.ImageDigest);
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaObserverOptions.SectionName}:runtimes:0:engine"] = "Containerd",
|
||||
[$"{ZastavaObserverOptions.SectionName}:backend:baseAddress"] = "https://scanner.internal"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddZastavaObserver(configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var manifestWriter = provider.GetRequiredService<ISurfaceManifestWriter>();
|
||||
var client = provider.GetRequiredService<IRuntimeSurfaceFsClient>();
|
||||
|
||||
var document = new SurfaceManifestDocument
|
||||
{
|
||||
Tenant = "default",
|
||||
ImageDigest = "sha256:deadbeef",
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
Artifacts = new[]
|
||||
{
|
||||
new SurfaceManifestArtifact
|
||||
{
|
||||
Kind = "entry-trace",
|
||||
Uri = "cas://surface-cache/manifest/entry-trace",
|
||||
Digest = "sha256:abc123",
|
||||
MediaType = "application/json",
|
||||
Format = "ndjson"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var published = await manifestWriter.PublishAsync(document, default);
|
||||
var fetched = await client.TryGetManifestAsync(published.ManifestDigest, default);
|
||||
|
||||
Assert.NotNull(fetched);
|
||||
Assert.Equal(document.Tenant, fetched!.Tenant);
|
||||
Assert.Equal(document.ImageDigest, fetched.ImageDigest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestoreEnvironment(originals);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string?> CaptureEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
var snapshot = new Dictionary<string, string?>();
|
||||
foreach (var name in names)
|
||||
{
|
||||
snapshot[name] = Environment.GetEnvironmentVariable(name);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static void ClearEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestoreEnvironment(IReadOnlyDictionary<string, string?> snapshot)
|
||||
{
|
||||
foreach (var pair in snapshot)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"parallelizeTestCollections": false,
|
||||
"parallelizeAssembly": false
|
||||
}
|
||||
@@ -10,6 +10,25 @@ namespace StellaOps.Zastava.Webhook.Tests.DependencyInjection;
|
||||
|
||||
public sealed class SurfaceEnvironmentRegistrationTests
|
||||
{
|
||||
// All environment variables that might affect these tests (to avoid pollution from parallel tests)
|
||||
private static readonly string[] AllRelevantEnvVars =
|
||||
[
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_NAMESPACE",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FEATURES",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_TENANT",
|
||||
"ZASTAVA_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"ZASTAVA_SURFACE_SECRETS_NAMESPACE",
|
||||
"ZASTAVA_SURFACE_FEATURES",
|
||||
"ZASTAVA_SURFACE_TENANT"
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void AddZastavaWebhook_RegistersSurfaceEnvironmentWithZastavaPrefixes()
|
||||
{
|
||||
@@ -23,9 +42,10 @@ public sealed class SurfaceEnvironmentRegistrationTests
|
||||
["ZASTAVA_WEBHOOK_SURFACE_TENANT"] = "tenant-w"
|
||||
};
|
||||
|
||||
var originals = CaptureEnvironment(env.Keys);
|
||||
var originals = CaptureEnvironment(AllRelevantEnvVars);
|
||||
try
|
||||
{
|
||||
ClearEnvironment(AllRelevantEnvVars);
|
||||
foreach (var pair in env)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
@@ -39,6 +59,7 @@ public sealed class SurfaceEnvironmentRegistrationTests
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddZastavaWebhook(configuration);
|
||||
|
||||
@@ -73,6 +94,14 @@ public sealed class SurfaceEnvironmentRegistrationTests
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static void ClearEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestoreEnvironment(IReadOnlyDictionary<string, string?> snapshot)
|
||||
{
|
||||
foreach (var pair in snapshot)
|
||||
|
||||
@@ -11,54 +11,93 @@ namespace StellaOps.Zastava.Webhook.Tests.DependencyInjection;
|
||||
|
||||
public sealed class SurfaceSecretsRegistrationTests
|
||||
{
|
||||
// All environment variables that might affect these tests (to avoid pollution from parallel tests)
|
||||
private static readonly string[] AllRelevantEnvVars =
|
||||
[
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_NAMESPACE",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FEATURES",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_TENANT",
|
||||
"ZASTAVA_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"SURFACE_SECRET_DEFAULT_ZASTAVA.WEBHOOK_ATTESTATION_ATTESTATION"
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public async Task AddZastavaWebhook_ResolvesInlineAttestationSecret()
|
||||
{
|
||||
var payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{\"keyPem\":\"KEY\",\"rekorToken\":\"rekor\"}"));
|
||||
var env = new Dictionary<string, string?>
|
||||
var envVars = new Dictionary<string, string?>
|
||||
{
|
||||
["ZASTAVA_WEBHOOK_SURFACE_FS_ENDPOINT"] = "https://surface.example",
|
||||
["ZASTAVA_WEBHOOK_SURFACE_SECRETS_PROVIDER"] = "inline",
|
||||
["ZASTAVA_WEBHOOK_SURFACE_SECRETS_ALLOW_INLINE"] = "true",
|
||||
["SURFACE_SECRET_DEFAULT_ZASTAVA.WEBHOOK_ATTESTATION_VERIFICATION"] = payload
|
||||
["SURFACE_SECRET_DEFAULT_ZASTAVA.WEBHOOK_ATTESTATION_ATTESTATION"] = payload
|
||||
};
|
||||
|
||||
var services = BuildServices(env);
|
||||
var originals = CaptureEnvironment(AllRelevantEnvVars);
|
||||
try
|
||||
{
|
||||
ClearEnvironment(AllRelevantEnvVars);
|
||||
foreach (var pair in envVars)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
var secrets = services.GetRequiredService<IWebhookSurfaceSecrets>();
|
||||
var result = await secrets.GetAttestationAsync(name: null);
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaWebhookOptions.SectionName}:tls:mode"] = "Secret"
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Equal("KEY", result.KeyPem);
|
||||
Assert.Equal("rekor", result.RekorApiToken);
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddZastavaWebhook(configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var secrets = provider.GetRequiredService<IWebhookSurfaceSecrets>();
|
||||
var result = await secrets.GetAttestationAsync(name: null);
|
||||
|
||||
Assert.Equal("KEY", result.KeyPem);
|
||||
Assert.Equal("rekor", result.RekorApiToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestoreEnvironment(originals);
|
||||
}
|
||||
}
|
||||
|
||||
private static ServiceProvider BuildServices(Dictionary<string, string?> env)
|
||||
private static IReadOnlyDictionary<string, string?> CaptureEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
var originals = new Dictionary<string, string?>();
|
||||
foreach (var pair in env)
|
||||
var snapshot = new Dictionary<string, string?>();
|
||||
foreach (var name in names)
|
||||
{
|
||||
originals[pair.Key] = Environment.GetEnvironmentVariable(pair.Key);
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
snapshot[name] = Environment.GetEnvironmentVariable(name);
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaWebhookOptions.SectionName}:tls:mode"] = "Secret"
|
||||
})
|
||||
.Build();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddZastavaWebhook(configuration);
|
||||
private static void ClearEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, null);
|
||||
}
|
||||
}
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
foreach (var pair in originals)
|
||||
private static void RestoreEnvironment(IReadOnlyDictionary<string, string?> snapshot)
|
||||
{
|
||||
foreach (var pair in snapshot)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Zastava.Webhook/StellaOps.Zastava.Webhook.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -11,47 +11,114 @@ namespace StellaOps.Zastava.Webhook.Tests.Surface;
|
||||
|
||||
public sealed class WebhookSurfaceFsClientTests
|
||||
{
|
||||
// All environment variables that might affect these tests (to avoid pollution from parallel tests)
|
||||
private static readonly string[] AllRelevantEnvVars =
|
||||
[
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_SECRETS_NAMESPACE",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_FEATURES",
|
||||
"ZASTAVA_WEBHOOK_SURFACE_TENANT",
|
||||
"ZASTAVA_SURFACE_FS_ENDPOINT",
|
||||
"ZASTAVA_SURFACE_FS_BUCKET",
|
||||
"ZASTAVA_SURFACE_SECRETS_PROVIDER",
|
||||
"ZASTAVA_SURFACE_SECRETS_ALLOW_INLINE",
|
||||
"ZASTAVA_SURFACE_SECRETS_NAMESPACE",
|
||||
"ZASTAVA_SURFACE_FEATURES",
|
||||
"ZASTAVA_SURFACE_TENANT"
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetManifestAsync_ReturnsPointerWhenPresent()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaWebhookOptions.SectionName}:tls:mode"] = "Secret"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddZastavaWebhook(configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var writer = provider.GetRequiredService<ISurfaceManifestWriter>();
|
||||
var client = provider.GetRequiredService<IWebhookSurfaceFsClient>();
|
||||
|
||||
var doc = new SurfaceManifestDocument
|
||||
var envVars = new Dictionary<string, string?>
|
||||
{
|
||||
Tenant = "default",
|
||||
ImageDigest = "sha256:deadbeef",
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
Artifacts = new[]
|
||||
{
|
||||
new SurfaceManifestArtifact
|
||||
{
|
||||
Kind = "entry-trace",
|
||||
Uri = "cas://surface-cache/manifest/entry-trace",
|
||||
Digest = "sha256:abc123",
|
||||
MediaType = "application/json",
|
||||
Format = "ndjson"
|
||||
}
|
||||
}
|
||||
["ZASTAVA_WEBHOOK_SURFACE_FS_ENDPOINT"] = "https://surface.example"
|
||||
};
|
||||
|
||||
var published = await writer.PublishAsync(doc, default);
|
||||
var (found, pointer) = await client.TryGetManifestAsync(published.ManifestDigest, default);
|
||||
var originals = CaptureEnvironment(AllRelevantEnvVars);
|
||||
try
|
||||
{
|
||||
ClearEnvironment(AllRelevantEnvVars);
|
||||
foreach (var pair in envVars)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
Assert.True(found);
|
||||
Assert.NotNull(pointer);
|
||||
Assert.Contains("surface-cache", pointer); // matches default bucket
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"{ZastavaWebhookOptions.SectionName}:tls:mode"] = "Secret"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddZastavaWebhook(configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var writer = provider.GetRequiredService<ISurfaceManifestWriter>();
|
||||
var client = provider.GetRequiredService<IWebhookSurfaceFsClient>();
|
||||
|
||||
var doc = new SurfaceManifestDocument
|
||||
{
|
||||
Tenant = "default",
|
||||
ImageDigest = "sha256:deadbeef",
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
Artifacts = new[]
|
||||
{
|
||||
new SurfaceManifestArtifact
|
||||
{
|
||||
Kind = "entry-trace",
|
||||
Uri = "cas://surface-cache/manifest/entry-trace",
|
||||
Digest = "sha256:abc123",
|
||||
MediaType = "application/json",
|
||||
Format = "ndjson"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var published = await writer.PublishAsync(doc, default);
|
||||
var (found, pointer) = await client.TryGetManifestAsync(published.ManifestDigest, default);
|
||||
|
||||
Assert.True(found);
|
||||
Assert.NotNull(pointer);
|
||||
// Bucket defaults to "surface-cache" when not overridden
|
||||
Assert.Contains("surface-cache", pointer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestoreEnvironment(originals);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string?> CaptureEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
var snapshot = new Dictionary<string, string?>();
|
||||
foreach (var name in names)
|
||||
{
|
||||
snapshot[name] = Environment.GetEnvironmentVariable(name);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static void ClearEnvironment(IEnumerable<string> names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestoreEnvironment(IReadOnlyDictionary<string, string?> snapshot)
|
||||
{
|
||||
foreach (var pair in snapshot)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"parallelizeTestCollections": false,
|
||||
"parallelizeAssembly": false
|
||||
}
|
||||
Reference in New Issue
Block a user