FUll implementation plan (first draft)
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Authentication;
|
||||
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration;
|
||||
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery;
|
||||
using System.Formats.Tar;
|
||||
|
||||
namespace StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch;
|
||||
|
||||
public sealed class OciAttestationFetcher
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<OciAttestationFetcher> _logger;
|
||||
|
||||
public OciAttestationFetcher(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IFileSystem fileSystem,
|
||||
ILogger<OciAttestationFetcher> logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<OciAttestationDocument> FetchAsync(
|
||||
OciAttestationDiscoveryResult discovery,
|
||||
OciOpenVexAttestationConnectorOptions options,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(discovery);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
foreach (var target in discovery.Targets)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
bool yieldedOffline = false;
|
||||
if (target.OfflineBundle is not null && target.OfflineBundle.Exists)
|
||||
{
|
||||
await foreach (var offlineDocument in ReadOfflineAsync(target, cancellationToken))
|
||||
{
|
||||
yieldedOffline = true;
|
||||
yield return offlineDocument;
|
||||
}
|
||||
|
||||
if (!discovery.AllowNetworkFallback)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (discovery.PreferOffline && yieldedOffline && !discovery.AllowNetworkFallback)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!discovery.PreferOffline || discovery.AllowNetworkFallback || !yieldedOffline)
|
||||
{
|
||||
await foreach (var registryDocument in FetchFromRegistryAsync(discovery, options, target, cancellationToken))
|
||||
{
|
||||
yield return registryDocument;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<OciAttestationDocument> ReadOfflineAsync(
|
||||
OciAttestationTarget target,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var offline = target.OfflineBundle!;
|
||||
var path = _fileSystem.Path.GetFullPath(offline.Path);
|
||||
|
||||
if (!_fileSystem.File.Exists(path))
|
||||
{
|
||||
if (offline.Exists)
|
||||
{
|
||||
_logger.LogWarning("Offline bundle {Path} disappeared before processing.", path);
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
var extension = _fileSystem.Path.GetExtension(path).ToLowerInvariant();
|
||||
var subjectDigest = target.Image.Digest ?? target.ExpectedSubjectDigest;
|
||||
|
||||
if (string.Equals(extension, ".json", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(extension, ".dsse", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var bytes = await _fileSystem.File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
var metadata = BuildOfflineMetadata(target, path, entryName: null, subjectDigest);
|
||||
yield return new OciAttestationDocument(
|
||||
new Uri(path, UriKind.Absolute),
|
||||
bytes,
|
||||
metadata,
|
||||
subjectDigest,
|
||||
null,
|
||||
null,
|
||||
"offline");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (string.Equals(extension, ".tgz", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(extension, ".gz", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(extension, ".tar", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await foreach (var document in ReadTarArchiveAsync(target, path, subjectDigest, cancellationToken))
|
||||
{
|
||||
yield return document;
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Default: treat as binary blob.
|
||||
var fallbackBytes = await _fileSystem.File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
var fallbackMetadata = BuildOfflineMetadata(target, path, entryName: null, subjectDigest);
|
||||
yield return new OciAttestationDocument(
|
||||
new Uri(path, UriKind.Absolute),
|
||||
fallbackBytes,
|
||||
fallbackMetadata,
|
||||
subjectDigest,
|
||||
null,
|
||||
null,
|
||||
"offline");
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<OciAttestationDocument> ReadTarArchiveAsync(
|
||||
OciAttestationTarget target,
|
||||
string path,
|
||||
string? subjectDigest,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await using var fileStream = _fileSystem.File.OpenRead(path);
|
||||
Stream archiveStream = fileStream;
|
||||
|
||||
if (path.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
archiveStream = new GZipStream(fileStream, CompressionMode.Decompress, leaveOpen: false);
|
||||
}
|
||||
|
||||
using var tarReader = new TarReader(archiveStream, leaveOpen: false);
|
||||
TarEntry? entry;
|
||||
|
||||
while ((entry = await tarReader.GetNextEntryAsync(copyData: false, cancellationToken).ConfigureAwait(false)) is not null)
|
||||
{
|
||||
if (entry.EntryType is not TarEntryType.RegularFile || entry.DataStream is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await using var entryStream = entry.DataStream;
|
||||
using var buffer = new MemoryStream();
|
||||
await entryStream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var metadata = BuildOfflineMetadata(target, path, entry.Name, subjectDigest);
|
||||
var sourceUri = new Uri($"{_fileSystem.Path.GetFullPath(path)}#{entry.Name}", UriKind.Absolute);
|
||||
yield return new OciAttestationDocument(
|
||||
sourceUri,
|
||||
buffer.ToArray(),
|
||||
metadata,
|
||||
subjectDigest,
|
||||
null,
|
||||
null,
|
||||
"offline");
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<OciAttestationDocument> FetchFromRegistryAsync(
|
||||
OciAttestationDiscoveryResult discovery,
|
||||
OciOpenVexAttestationConnectorOptions options,
|
||||
OciAttestationTarget target,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var registryClient = new OciRegistryClient(
|
||||
_httpClientFactory,
|
||||
_logger,
|
||||
discovery.RegistryAuthorization,
|
||||
options);
|
||||
|
||||
var subjectDigest = target.Image.Digest ?? target.ExpectedSubjectDigest;
|
||||
if (string.IsNullOrWhiteSpace(subjectDigest))
|
||||
{
|
||||
subjectDigest = await registryClient.ResolveDigestAsync(target.Image, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subjectDigest))
|
||||
{
|
||||
_logger.LogWarning("Unable to resolve subject digest for {Reference}; skipping registry fetch.", target.Image.Canonical);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(target.ExpectedSubjectDigest) &&
|
||||
!string.Equals(target.ExpectedSubjectDigest, subjectDigest, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Resolved digest {Resolved} does not match expected digest {Expected} for {Reference}.",
|
||||
subjectDigest,
|
||||
target.ExpectedSubjectDigest,
|
||||
target.Image.Canonical);
|
||||
}
|
||||
|
||||
var descriptors = await registryClient.ListReferrersAsync(target.Image, subjectDigest, cancellationToken).ConfigureAwait(false);
|
||||
if (descriptors.Count == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var document = await registryClient.DownloadAttestationAsync(target.Image, descriptor, subjectDigest, cancellationToken).ConfigureAwait(false);
|
||||
if (document is not null)
|
||||
{
|
||||
yield return document;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<string, string> BuildOfflineMetadata(
|
||||
OciAttestationTarget target,
|
||||
string bundlePath,
|
||||
string? entryName,
|
||||
string? subjectDigest)
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
builder["oci.image.registry"] = target.Image.Registry;
|
||||
builder["oci.image.repository"] = target.Image.Repository;
|
||||
builder["oci.image.reference"] = target.Image.Canonical;
|
||||
if (!string.IsNullOrWhiteSpace(subjectDigest))
|
||||
{
|
||||
builder["oci.image.subjectDigest"] = subjectDigest;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(target.ExpectedSubjectDigest))
|
||||
{
|
||||
builder["oci.image.expectedSubjectDigest"] = target.ExpectedSubjectDigest!;
|
||||
}
|
||||
|
||||
builder["oci.attestation.sourceKind"] = "offline";
|
||||
builder["oci.attestation.source"] = bundlePath;
|
||||
if (!string.IsNullOrWhiteSpace(entryName))
|
||||
{
|
||||
builder["oci.attestation.bundleEntry"] = entryName!;
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user