# Surface.Validation Extensibility Guide > **Version:** 1.0 (2025-11-28) > > **Audience:** Scanner Worker/WebService integrators, custom analyzer developers, Zastava contributors ## Overview Surface.Validation provides a pluggable validator framework for ensuring configuration and data preconditions before performing scanner work. This guide covers how to extend the validation system with custom validators, customize reporting, and integrate validation into your components. ## Quick Start ### Basic Registration ```csharp // In Program.cs or your DI configuration builder.Services.AddSurfaceValidation(); ``` This registers the default validators: - `SurfaceEndpointValidator` - Validates Surface.FS endpoint and bucket - `SurfaceCacheValidator` - Validates cache directory writability and quota - `SurfaceSecretsValidator` - Validates secrets provider configuration ### Adding Custom Validators ```csharp builder.Services.AddSurfaceValidation(builder => { builder.AddValidator(); builder.AddValidator(); }); ``` ## Writing Custom Validators ### Validator Interface Implement `ISurfaceValidator` to create a custom validator: ```csharp public interface ISurfaceValidator { ValueTask ValidateAsync( SurfaceValidationContext context, CancellationToken cancellationToken = default); } ``` ### Example: Registry Credentials Validator ```csharp public sealed class RegistryCredentialsValidator : ISurfaceValidator { private readonly IHttpClientFactory _httpClientFactory; public RegistryCredentialsValidator(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async ValueTask ValidateAsync( SurfaceValidationContext context, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(context); var issues = new List(); // Access secrets configuration from context var secrets = context.Environment.Secrets; if (secrets.Provider == "file" && string.IsNullOrEmpty(secrets.Root)) { issues.Add(SurfaceValidationIssue.Error( "REGISTRY_SECRETS_ROOT_MISSING", "Registry secrets root path is not configured.", "Set SCANNER_SURFACE_SECRETS_ROOT to the secrets directory.")); } // Access custom properties passed during validation if (context.Properties.TryGetValue("registryEndpoint", out var endpoint)) { var reachable = await CheckEndpointAsync(endpoint?.ToString(), cancellationToken); if (!reachable) { issues.Add(SurfaceValidationIssue.Warning( "REGISTRY_ENDPOINT_UNREACHABLE", $"Registry endpoint {endpoint} is not reachable.", "Verify network connectivity to the container registry.")); } } return issues.Count == 0 ? SurfaceValidationResult.Success() : SurfaceValidationResult.FromIssues(issues); } private async Task CheckEndpointAsync(string? endpoint, CancellationToken ct) { if (string.IsNullOrEmpty(endpoint)) return true; try { var client = _httpClientFactory.CreateClient(); client.Timeout = TimeSpan.FromMilliseconds(500); // Keep validations fast var response = await client.GetAsync(endpoint, ct); return response.IsSuccessStatusCode; } catch { return false; } } } ``` ### Best Practices for Validators 1. **Keep validations fast** - Target < 500ms per validator to avoid blocking startup 2. **Use appropriate severity levels**: - `Error` - Fatal misconfiguration that prevents operation - `Warning` - Suboptimal configuration that may cause issues - `Info` - Informational notices 3. **Provide actionable hints** - Include remediation steps in the hint parameter 4. **Access services via context** - Use `context.Services.GetService()` for DI 5. **Check cancellation tokens** - Honor cancellation for async operations ## Validation Context ### Creating Context with Properties ```csharp var context = SurfaceValidationContext.Create( serviceProvider, componentName: "Scanner.Worker", environment: surfaceEnvironment, properties: new Dictionary { ["jobId"] = currentJob.Id, ["imageDigest"] = image.Digest, ["configPath"] = "/etc/scanner/config.yaml" }); ``` ### Accessing Context in Validators ```csharp public ValueTask ValidateAsync( SurfaceValidationContext context, CancellationToken cancellationToken = default) { // Access environment settings var endpoint = context.Environment.SurfaceFsEndpoint; var bucket = context.Environment.SurfaceFsBucket; var tenant = context.Environment.Tenant; // Access custom properties if (context.Properties.TryGetValue("imageDigest", out var digest)) { // Validate specific to this image } // Access DI services var logger = context.Services.GetService>(); } ``` ## Running Validators ### Using the Validator Runner ```csharp public class MyService { private readonly ISurfaceValidatorRunner _runner; private readonly ISurfaceEnvironment _environment; public MyService(ISurfaceValidatorRunner runner, ISurfaceEnvironment environment) { _runner = runner; _environment = environment; } public async Task ExecuteAsync(CancellationToken ct) { var context = SurfaceValidationContext.Create( _serviceProvider, "MyService", _environment.Settings); // Option 1: Get results and handle manually var result = await _runner.RunAllAsync(context, ct); if (!result.IsSuccess) { foreach (var issue in result.Issues.Where(i => i.Severity == SurfaceValidationSeverity.Error)) { _logger.LogError("Validation failed: {Code} - {Message}", issue.Code, issue.Message); } return; } // Option 2: Throw on failure (respects options) await _runner.EnsureAsync(context, ct); // Continue with work... } } ``` ## Custom Reporting ### Implementing a Reporter ```csharp public sealed class MetricsSurfaceValidationReporter : ISurfaceValidationReporter { private readonly IMetricsFactory _metrics; public MetricsSurfaceValidationReporter(IMetricsFactory metrics) { _metrics = metrics; } public void Report(SurfaceValidationContext context, SurfaceValidationResult result) { var counter = _metrics.CreateCounter("surface_validation_issues_total"); foreach (var issue in result.Issues) { counter.Add(1, new KeyValuePair[] { new("code", issue.Code), new("severity", issue.Severity.ToString().ToLowerInvariant()), new("component", context.ComponentName) }); } } } ``` ### Registering Custom Reporters ```csharp // Replace default reporter builder.Services.AddSingleton(); // Or add alongside default (using composite pattern) builder.Services.Decorate((inner, sp) => new CompositeSurfaceValidationReporter( inner, sp.GetRequiredService())); ``` ## Configuration Options ### SurfaceValidationOptions | Option | Default | Description | |--------|---------|-------------| | `ThrowOnFailure` | `true` | Whether `EnsureAsync()` throws on validation failure | | `ContinueOnError` | `false` | Whether to continue running validators after first error | Configure via `IConfiguration`: ```json { "Surface": { "Validation": { "ThrowOnFailure": true, "ContinueOnError": false } } } ``` Or programmatically: ```csharp builder.Services.Configure(options => { options.ThrowOnFailure = true; options.ContinueOnError = true; // Useful for diagnostics }); ``` ## Issue Codes ### Standard Codes | Code | Severity | Validator | |------|----------|-----------| | `SURFACE_ENV_MISSING_ENDPOINT` | Error | SurfaceEndpointValidator | | `SURFACE_FS_BUCKET_MISSING` | Error | SurfaceEndpointValidator | | `SURFACE_ENV_CACHE_DIR_UNWRITABLE` | Error | SurfaceCacheValidator | | `SURFACE_ENV_CACHE_QUOTA_INVALID` | Error | SurfaceCacheValidator | | `SURFACE_SECRET_PROVIDER_UNKNOWN` | Error | SurfaceSecretsValidator | | `SURFACE_SECRET_CONFIGURATION_MISSING` | Error | SurfaceSecretsValidator | | `SURFACE_ENV_TENANT_MISSING` | Error | SurfaceSecretsValidator | ### Custom Issue Codes Follow the naming convention: `__` ```csharp public static class MyValidationCodes { public const string RegistrySecretsRootMissing = "REGISTRY_SECRETS_ROOT_MISSING"; public const string RegistryEndpointUnreachable = "REGISTRY_ENDPOINT_UNREACHABLE"; public const string CacheWarmupFailed = "CACHE_WARMUP_FAILED"; } ``` ## Integration Examples ### Scanner Worker Startup ```csharp // In hosted service public async Task StartAsync(CancellationToken ct) { var context = SurfaceValidationContext.Create( _services, "Scanner.Worker", _surfaceEnv.Settings); try { await _validatorRunner.EnsureAsync(context, ct); _logger.LogInformation("Surface validation passed"); } catch (SurfaceValidationException ex) { _logger.LogCritical(ex, "Surface validation failed; worker cannot start"); throw; } } ``` ### Per-Scan Validation ```csharp public async Task ScanImageAsync(ImageReference image, CancellationToken ct) { var context = SurfaceValidationContext.Create( _services, "Scanner.Analyzer", _surfaceEnv.Settings, new Dictionary { ["imageDigest"] = image.Digest, ["imageReference"] = image.Reference }); var result = await _validatorRunner.RunAllAsync(context, ct); if (result.HasErrors) { return ScanResult.Failed(result.Issues.Select(i => i.Message)); } // Proceed with scan... } ``` ### Zastava Webhook Readiness ```csharp app.MapGet("/readyz", async (ISurfaceValidatorRunner runner, ISurfaceEnvironment env) => { var context = SurfaceValidationContext.Create( app.Services, "Zastava.Webhook", env.Settings); var result = await runner.RunAllAsync(context); if (!result.IsSuccess) { return Results.Json(new { status = "unhealthy", issues = result.Issues.Select(i => new { i.Code, i.Message, i.Hint }) }, statusCode: 503); } return Results.Ok(new { status = "healthy" }); }); ``` ## Testing Validators ### Unit Testing ```csharp [Fact] public async Task Validator_MissingEndpoint_ReturnsError() { // Arrange var settings = new SurfaceEnvironmentSettings( SurfaceFsEndpoint: new Uri("https://surface.invalid"), SurfaceFsBucket: "", // ... other settings ); var context = SurfaceValidationContext.Create( new ServiceCollection().BuildServiceProvider(), "Test", settings); var validator = new SurfaceEndpointValidator(); // Act var result = await validator.ValidateAsync(context); // Assert Assert.False(result.IsSuccess); Assert.Contains(result.Issues, i => i.Code == SurfaceValidationIssueCodes.SurfaceEndpointMissing); } ``` ### Integration Testing ```csharp [Fact] public async Task ValidationRunner_AllValidatorsExecute() { // Arrange var services = new ServiceCollection(); services.AddSurfaceValidation(builder => { builder.AddValidator(); builder.AddValidator(); }); var provider = services.BuildServiceProvider(); var runner = provider.GetRequiredService(); var context = SurfaceValidationContext.Create( provider, "IntegrationTest", CreateValidSettings()); // Act var result = await runner.RunAllAsync(context); // Assert Assert.True(result.IsSuccess); } ``` ## References - [Surface.Validation Design](../design/surface-validation.md) - [Surface.Env Design](../design/surface-env.md) - [Surface.Secrets Schema](../design/surface-secrets-schema.md)