work
This commit is contained in:
@@ -20,6 +20,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\__Libraries\\StellaOps.Scanner.Surface.FS\\StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="..\\__Libraries\\StellaOps.Scanner.Surface.Env\\StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<PackageReference Include="StellaOps.Scanner.Surface.Env" Version="0.1.0-alpha.20251123" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -32,9 +32,8 @@ public sealed class SurfaceManifestStoreOptionsConfigurator : IConfigureOptions<
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.RootDirectory))
|
||||
{
|
||||
options.RootDirectory = Path.Combine(
|
||||
_cacheOptions.Value.ResolveRoot(),
|
||||
"manifests");
|
||||
var cacheRoot = _cacheOptions.Value.RootDirectory ?? Path.Combine(Path.GetTempPath(), "stellaops", "surface-cache");
|
||||
options.RootDirectory = Path.Combine(cacheRoot, "manifests");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,8 +297,20 @@ else
|
||||
});
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Fail fast if surface configuration is invalid at startup.
|
||||
using (var validationScope = app.Services.CreateScope())
|
||||
{
|
||||
var services = validationScope.ServiceProvider;
|
||||
var env = services.GetRequiredService<ISurfaceEnvironment>();
|
||||
var runner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
await runner.EnsureAsync(
|
||||
SurfaceValidationContext.Create(services, "Scanner.WebService.Startup", env.Settings),
|
||||
app.Lifetime.ApplicationStopping)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var resolvedOptions = app.Services.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
|
||||
var authorityConfigured = resolvedOptions.Authority.Enabled;
|
||||
if (authorityConfigured && resolvedOptions.Authority.AllowAnonymousFallback)
|
||||
|
||||
@@ -110,6 +110,22 @@ internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
var results = new List<OSPackageAnalyzerResult>(analyzers.Count);
|
||||
|
||||
var surfaceEnvironment = services.GetRequiredService<ISurfaceEnvironment>();
|
||||
var validatorRunner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
var validationContext = SurfaceValidationContext.Create(
|
||||
services,
|
||||
"Scanner.Worker.OSAnalyzers",
|
||||
surfaceEnvironment.Settings,
|
||||
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["jobId"] = context.JobId,
|
||||
["scanId"] = context.ScanId,
|
||||
["rootfsPath"] = rootfsPath,
|
||||
["analyzerCount"] = analyzers.Count
|
||||
});
|
||||
|
||||
await validatorRunner.EnsureAsync(validationContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var analyzer in analyzers)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -119,6 +119,20 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
return;
|
||||
}
|
||||
|
||||
var validationContext = SurfaceValidationContext.Create(
|
||||
_serviceProvider,
|
||||
"Scanner.Worker.EntryTrace",
|
||||
_surfaceEnvironment.Settings,
|
||||
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["jobId"] = context.JobId,
|
||||
["scanId"] = context.ScanId,
|
||||
["configPath"] = configPath,
|
||||
["rootfs"] = metadata.TryGetValue(_workerOptions.Analyzers.RootFilesystemMetadataKey, out var rootfs) ? rootfs : null
|
||||
});
|
||||
|
||||
await _validatorRunner.EnsureAsync(validationContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var fileSystemHandle = BuildFileSystem(context.JobId, metadata);
|
||||
if (fileSystemHandle is null)
|
||||
{
|
||||
|
||||
@@ -145,6 +145,18 @@ builder.Logging.Configure(options =>
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
// Fail fast if surface configuration is invalid at startup.
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
var env = services.GetRequiredService<ISurfaceEnvironment>();
|
||||
var runner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
await runner.EnsureAsync(
|
||||
SurfaceValidationContext.Create(services, "Scanner.Worker.Startup", env.Settings),
|
||||
host.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await host.RunAsync();
|
||||
|
||||
public partial class Program;
|
||||
@@ -189,9 +201,8 @@ public sealed class SurfaceManifestStoreOptionsConfigurator : IConfigureOptions<
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.RootDirectory))
|
||||
{
|
||||
options.RootDirectory = Path.Combine(
|
||||
_cacheOptions.Value.ResolveRoot(),
|
||||
"manifests");
|
||||
var cacheRoot = _cacheOptions.Value.RootDirectory ?? Path.Combine(Path.GetTempPath(), "stellaops", "surface-cache");
|
||||
options.RootDirectory = Path.Combine(cacheRoot, "manifests");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,38 +29,45 @@ public static class ServiceCollectionExtensions
|
||||
var env = sp.GetRequiredService<ISurfaceEnvironment>();
|
||||
var options = sp.GetRequiredService<IOptions<SurfaceSecretsOptions>>().Value;
|
||||
var logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger("SurfaceSecrets");
|
||||
return CreateProvider(env.Settings.Secrets, logger);
|
||||
return CreateProviderChain(env.Settings.Secrets, logger);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static ISurfaceSecretProvider CreateProvider(SurfaceSecretsConfiguration configuration, ILogger logger)
|
||||
private static ISurfaceSecretProvider CreateProviderChain(SurfaceSecretsConfiguration configuration, ILogger logger)
|
||||
{
|
||||
var providers = new List<ISurfaceSecretProvider>();
|
||||
|
||||
switch (configuration.Provider.ToLowerInvariant())
|
||||
var providers = new List<ISurfaceSecretProvider>
|
||||
{
|
||||
case "kubernetes":
|
||||
providers.Add(new KubernetesSurfaceSecretProvider(configuration, logger));
|
||||
break;
|
||||
case "file":
|
||||
providers.Add(new FileSurfaceSecretProvider(configuration.Root ?? throw new ArgumentException("Secrets root is required for file provider.")));
|
||||
break;
|
||||
case "inline":
|
||||
providers.Add(new InlineSurfaceSecretProvider(configuration));
|
||||
break;
|
||||
default:
|
||||
logger.LogWarning("Unknown surface secret provider '{Provider}'. Falling back to inline provider.", configuration.Provider);
|
||||
providers.Add(new InlineSurfaceSecretProvider(configuration));
|
||||
break;
|
||||
}
|
||||
CreateProvider(configuration.Provider, configuration, logger)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(configuration.FallbackProvider))
|
||||
if (configuration.HasFallback)
|
||||
{
|
||||
providers.Add(new InlineSurfaceSecretProvider(configuration with { Provider = configuration.FallbackProvider }));
|
||||
providers.Add(CreateProvider(configuration.FallbackProvider!, configuration, logger));
|
||||
}
|
||||
|
||||
return providers.Count == 1 ? providers[0] : new CompositeSurfaceSecretProvider(providers);
|
||||
}
|
||||
|
||||
private static ISurfaceSecretProvider CreateProvider(string providerId, SurfaceSecretsConfiguration configuration, ILogger logger)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
{
|
||||
throw new ArgumentException("Provider id is required", nameof(providerId));
|
||||
}
|
||||
|
||||
switch (providerId.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "kubernetes":
|
||||
return new KubernetesSurfaceSecretProvider(configuration, logger);
|
||||
case "file":
|
||||
return new FileSurfaceSecretProvider(configuration.Root ?? throw new ArgumentException("Secrets root is required for file provider."));
|
||||
case "inline":
|
||||
return new InlineSurfaceSecretProvider(configuration);
|
||||
default:
|
||||
logger.LogWarning("Unknown surface secret provider '{Provider}'. Falling back to inline provider if allowed; otherwise requests will fail.", providerId);
|
||||
return new InlineSurfaceSecretProvider(configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public static class SurfaceValidationIssueCodes
|
||||
public const string CacheQuotaInvalid = "SURFACE_ENV_CACHE_QUOTA_INVALID";
|
||||
public const string SecretsProviderUnknown = "SURFACE_SECRET_PROVIDER_UNKNOWN";
|
||||
public const string SecretsConfigurationMissing = "SURFACE_SECRET_CONFIGURATION_MISSING";
|
||||
public const string SecretsConfigurationInvalid = "SURFACE_SECRET_FORMAT_INVALID";
|
||||
public const string TenantMissing = "SURFACE_ENV_TENANT_MISSING";
|
||||
public const string BucketMissing = "SURFACE_FS_BUCKET_MISSING";
|
||||
public const string FeatureUnknown = "SURFACE_FEATURE_UNKNOWN";
|
||||
|
||||
@@ -35,6 +35,14 @@ internal sealed class SurfaceSecretsValidator : ISurfaceValidator
|
||||
"Set SCANNER_SURFACE_SECRETS_PROVIDER to 'kubernetes', 'file', or another supported provider."));
|
||||
}
|
||||
|
||||
if (secrets.HasFallback && !KnownProviders.Contains(secrets.FallbackProvider!))
|
||||
{
|
||||
issues.Add(SurfaceValidationIssue.Error(
|
||||
SurfaceValidationIssueCodes.SecretsProviderUnknown,
|
||||
$"Fallback secrets provider '{secrets.FallbackProvider}' is not recognised.",
|
||||
"Choose a supported fallback provider (kubernetes | file | inline) or clear SCANNER_SURFACE_SECRETS_FALLBACK_PROVIDER."));
|
||||
}
|
||||
|
||||
if (string.Equals(secrets.Provider, "kubernetes", StringComparison.OrdinalIgnoreCase) &&
|
||||
string.IsNullOrWhiteSpace(secrets.Namespace))
|
||||
{
|
||||
@@ -53,6 +61,24 @@ internal sealed class SurfaceSecretsValidator : ISurfaceValidator
|
||||
"Set SCANNER_SURFACE_SECRETS_ROOT to a directory path."));
|
||||
}
|
||||
|
||||
if (string.Equals(secrets.Provider, "file", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrWhiteSpace(secrets.Root) &&
|
||||
!Directory.Exists(secrets.Root))
|
||||
{
|
||||
issues.Add(SurfaceValidationIssue.Error(
|
||||
SurfaceValidationIssueCodes.SecretsConfigurationInvalid,
|
||||
$"File secrets root '{secrets.Root}' does not exist.",
|
||||
"Ensure SCANNER_SURFACE_SECRETS_ROOT points to an existing directory with 0600-style permissions."));
|
||||
}
|
||||
|
||||
if (string.Equals(secrets.Provider, "inline", StringComparison.OrdinalIgnoreCase) && !secrets.AllowInline)
|
||||
{
|
||||
issues.Add(SurfaceValidationIssue.Error(
|
||||
SurfaceValidationIssueCodes.SecretsConfigurationInvalid,
|
||||
"Inline secrets provider is selected but AllowInline=false.",
|
||||
"Either enable SCANNER_SURFACE_SECRETS_ALLOW_INLINE for dev/test or switch provider."));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(secrets.Tenant))
|
||||
{
|
||||
issues.Add(SurfaceValidationIssue.Error(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
@@ -23,6 +24,31 @@ namespace StellaOps.Scanner.Surface.Secrets.Tests
|
||||
Assert.NotNull(secretProvider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSurfaceSecrets_UsesFallbackProvider_WhenPrimaryCannotResolve()
|
||||
{
|
||||
const string key = "SURFACE_SECRET_TENANT_COMPONENT_REGISTRY_DEFAULT";
|
||||
Environment.SetEnvironmentVariable(key, Convert.ToBase64String(new byte[] { 9, 9, 9 }));
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<ISurfaceEnvironment>(_ => new TestSurfaceEnvironmentWithFallback());
|
||||
services.AddLogging(builder => builder.ClearProviders());
|
||||
services.AddSurfaceSecrets();
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var secretProvider = provider.GetRequiredService<ISurfaceSecretProvider>();
|
||||
var handle = await secretProvider.GetAsync(new SurfaceSecretRequest("tenant", "component", "registry"));
|
||||
try
|
||||
{
|
||||
Assert.Equal(new byte[] { 9, 9, 9 }, handle.AsBytes().ToArray());
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Dispose();
|
||||
Environment.SetEnvironmentVariable(key, null);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSurfaceEnvironment : ISurfaceEnvironment
|
||||
{
|
||||
public SurfaceEnvironmentSettings Settings { get; }
|
||||
@@ -48,5 +74,32 @@ namespace StellaOps.Scanner.Surface.Secrets.Tests
|
||||
RawVariables = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSurfaceEnvironmentWithFallback : ISurfaceEnvironment
|
||||
{
|
||||
public SurfaceEnvironmentSettings Settings { get; }
|
||||
public IReadOnlyDictionary<string, string> RawVariables { get; }
|
||||
|
||||
public TestSurfaceEnvironmentWithFallback()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
Settings = new SurfaceEnvironmentSettings(
|
||||
new Uri("https://surface.example"),
|
||||
"surface",
|
||||
null,
|
||||
new DirectoryInfo(Path.GetTempPath()),
|
||||
1024,
|
||||
false,
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("kubernetes", "tenant", Root: root, Namespace: "ns", FallbackProvider: "inline", AllowInline: true),
|
||||
"tenant",
|
||||
new SurfaceTlsConfiguration(null, null, null))
|
||||
{
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
RawVariables = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,63 @@ public sealed class SurfaceValidatorRunnerTests
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAllAsync_Fails_WhenInlineProviderDisallowed()
|
||||
{
|
||||
var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "stellaops-tests", Guid.NewGuid().ToString()));
|
||||
var environment = new SurfaceEnvironmentSettings(
|
||||
new Uri("https://surface.example.com"),
|
||||
"surface-cache",
|
||||
null,
|
||||
directory,
|
||||
1024,
|
||||
false,
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("inline", "tenant-a", Root: null, Namespace: null, FallbackProvider: null, AllowInline: false),
|
||||
"tenant-a",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
|
||||
var services = CreateServices();
|
||||
var runner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
var context = SurfaceValidationContext.Create(services, "TestComponent", environment);
|
||||
|
||||
var result = await runner.RunAllAsync(context);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Contains(result.Issues, i => i.Code == SurfaceValidationIssueCodes.SecretsConfigurationInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAllAsync_Fails_WhenFileRootMissing()
|
||||
{
|
||||
var missingRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", "missing-root", Guid.NewGuid().ToString());
|
||||
var directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "stellaops-tests", Guid.NewGuid().ToString()))
|
||||
{
|
||||
Attributes = FileAttributes.Normal
|
||||
};
|
||||
|
||||
var environment = new SurfaceEnvironmentSettings(
|
||||
new Uri("https://surface.example.com"),
|
||||
"surface-cache",
|
||||
null,
|
||||
directory,
|
||||
1024,
|
||||
false,
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("file", "tenant-a", Root: missingRoot, Namespace: null, FallbackProvider: null, AllowInline: false),
|
||||
"tenant-a",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
|
||||
var services = CreateServices();
|
||||
var runner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
var context = SurfaceValidationContext.Create(services, "TestComponent", environment);
|
||||
|
||||
var result = await runner.RunAllAsync(context);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Contains(result.Issues, i => i.Code == SurfaceValidationIssueCodes.SecretsConfigurationInvalid);
|
||||
}
|
||||
|
||||
private static ServiceProvider CreateServices(Action<IServiceCollection>? configure = null)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Worker;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests;
|
||||
@@ -29,7 +28,7 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
|
||||
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
|
||||
|
||||
var environment = new StubSurfaceEnvironment(settings);
|
||||
var cacheOptions = Options.Create(new SurfaceCacheOptions { RootDirectory = cacheRoot.FullName });
|
||||
var cacheOptions = Microsoft.Extensions.Options.Options.Create(new SurfaceCacheOptions { RootDirectory = cacheRoot.FullName });
|
||||
var configurator = new SurfaceManifestStoreOptionsConfigurator(environment, cacheOptions);
|
||||
var options = new SurfaceManifestStoreOptions();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user