up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IPackRunApprovalStore"/>.
|
||||
/// </summary>
|
||||
public sealed class PostgresPackRunApprovalStore : RepositoryBase<TaskRunnerDataSource>, IPackRunApprovalStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private bool _tableInitialized;
|
||||
|
||||
public PostgresPackRunApprovalStore(TaskRunnerDataSource dataSource, ILogger<PostgresPackRunApprovalStore> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task SaveAsync(string runId, IReadOnlyList<PackRunApprovalState> approvals, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
ArgumentNullException.ThrowIfNull(approvals);
|
||||
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Delete existing approvals for this run, then insert all new ones
|
||||
const string deleteSql = "DELETE FROM taskrunner.pack_run_approvals WHERE run_id = @run_id";
|
||||
await using (var deleteCmd = CreateCommand(deleteSql, connection))
|
||||
{
|
||||
AddParameter(deleteCmd, "@run_id", runId);
|
||||
await deleteCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (approvals.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string insertSql = @"
|
||||
INSERT INTO taskrunner.pack_run_approvals (
|
||||
run_id, approval_id, required_grants, step_ids, messages, reason_template,
|
||||
requested_at, status, actor_id, completed_at, summary
|
||||
) VALUES (
|
||||
@run_id, @approval_id, @required_grants, @step_ids, @messages, @reason_template,
|
||||
@requested_at, @status, @actor_id, @completed_at, @summary
|
||||
)";
|
||||
|
||||
foreach (var approval in approvals)
|
||||
{
|
||||
await using var insertCmd = CreateCommand(insertSql, connection);
|
||||
AddParameter(insertCmd, "@run_id", runId);
|
||||
AddParameter(insertCmd, "@approval_id", approval.ApprovalId);
|
||||
AddJsonbParameter(insertCmd, "@required_grants", JsonSerializer.Serialize(approval.RequiredGrants, JsonOptions));
|
||||
AddJsonbParameter(insertCmd, "@step_ids", JsonSerializer.Serialize(approval.StepIds, JsonOptions));
|
||||
AddJsonbParameter(insertCmd, "@messages", JsonSerializer.Serialize(approval.Messages, JsonOptions));
|
||||
AddParameter(insertCmd, "@reason_template", (object?)approval.ReasonTemplate ?? DBNull.Value);
|
||||
AddParameter(insertCmd, "@requested_at", approval.RequestedAt);
|
||||
AddParameter(insertCmd, "@status", approval.Status.ToString());
|
||||
AddParameter(insertCmd, "@actor_id", (object?)approval.ActorId ?? DBNull.Value);
|
||||
AddParameter(insertCmd, "@completed_at", (object?)approval.CompletedAt ?? DBNull.Value);
|
||||
AddParameter(insertCmd, "@summary", (object?)approval.Summary ?? DBNull.Value);
|
||||
|
||||
await insertCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PackRunApprovalState>> GetAsync(string runId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT approval_id, required_grants, step_ids, messages, reason_template,
|
||||
requested_at, status, actor_id, completed_at, summary
|
||||
FROM taskrunner.pack_run_approvals
|
||||
WHERE run_id = @run_id
|
||||
ORDER BY requested_at";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "@run_id", runId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var results = new List<PackRunApprovalState>();
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
results.Add(MapApprovalState(reader));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(string runId, PackRunApprovalState approval, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
ArgumentNullException.ThrowIfNull(approval);
|
||||
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
UPDATE taskrunner.pack_run_approvals
|
||||
SET required_grants = @required_grants,
|
||||
step_ids = @step_ids,
|
||||
messages = @messages,
|
||||
reason_template = @reason_template,
|
||||
requested_at = @requested_at,
|
||||
status = @status,
|
||||
actor_id = @actor_id,
|
||||
completed_at = @completed_at,
|
||||
summary = @summary
|
||||
WHERE run_id = @run_id AND approval_id = @approval_id";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
|
||||
AddParameter(command, "@run_id", runId);
|
||||
AddParameter(command, "@approval_id", approval.ApprovalId);
|
||||
AddJsonbParameter(command, "@required_grants", JsonSerializer.Serialize(approval.RequiredGrants, JsonOptions));
|
||||
AddJsonbParameter(command, "@step_ids", JsonSerializer.Serialize(approval.StepIds, JsonOptions));
|
||||
AddJsonbParameter(command, "@messages", JsonSerializer.Serialize(approval.Messages, JsonOptions));
|
||||
AddParameter(command, "@reason_template", (object?)approval.ReasonTemplate ?? DBNull.Value);
|
||||
AddParameter(command, "@requested_at", approval.RequestedAt);
|
||||
AddParameter(command, "@status", approval.Status.ToString());
|
||||
AddParameter(command, "@actor_id", (object?)approval.ActorId ?? DBNull.Value);
|
||||
AddParameter(command, "@completed_at", (object?)approval.CompletedAt ?? DBNull.Value);
|
||||
AddParameter(command, "@summary", (object?)approval.Summary ?? DBNull.Value);
|
||||
|
||||
var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (rowsAffected == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Approval '{approval.ApprovalId}' not found for run '{runId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static PackRunApprovalState MapApprovalState(NpgsqlDataReader reader)
|
||||
{
|
||||
var approvalId = reader.GetString(0);
|
||||
var requiredGrantsJson = reader.GetString(1);
|
||||
var stepIdsJson = reader.GetString(2);
|
||||
var messagesJson = reader.GetString(3);
|
||||
var reasonTemplate = reader.IsDBNull(4) ? null : reader.GetString(4);
|
||||
var requestedAt = reader.GetFieldValue<DateTimeOffset>(5);
|
||||
var statusString = reader.GetString(6);
|
||||
var actorId = reader.IsDBNull(7) ? null : reader.GetString(7);
|
||||
var completedAt = reader.IsDBNull(8) ? (DateTimeOffset?)null : reader.GetFieldValue<DateTimeOffset>(8);
|
||||
var summary = reader.IsDBNull(9) ? null : reader.GetString(9);
|
||||
|
||||
var requiredGrants = JsonSerializer.Deserialize<List<string>>(requiredGrantsJson, JsonOptions)
|
||||
?? new List<string>();
|
||||
var stepIds = JsonSerializer.Deserialize<List<string>>(stepIdsJson, JsonOptions)
|
||||
?? new List<string>();
|
||||
var messages = JsonSerializer.Deserialize<List<string>>(messagesJson, JsonOptions)
|
||||
?? new List<string>();
|
||||
|
||||
if (!Enum.TryParse<PackRunApprovalStatus>(statusString, ignoreCase: true, out var status))
|
||||
{
|
||||
status = PackRunApprovalStatus.Pending;
|
||||
}
|
||||
|
||||
return new PackRunApprovalState(
|
||||
approvalId,
|
||||
requiredGrants,
|
||||
stepIds,
|
||||
messages,
|
||||
reasonTemplate,
|
||||
requestedAt,
|
||||
status,
|
||||
actorId,
|
||||
completedAt,
|
||||
summary);
|
||||
}
|
||||
|
||||
private async Task EnsureTableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_tableInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string ddl = @"
|
||||
CREATE SCHEMA IF NOT EXISTS taskrunner;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS taskrunner.pack_run_approvals (
|
||||
run_id TEXT NOT NULL,
|
||||
approval_id TEXT NOT NULL,
|
||||
required_grants JSONB NOT NULL,
|
||||
step_ids JSONB NOT NULL,
|
||||
messages JSONB NOT NULL,
|
||||
reason_template TEXT,
|
||||
requested_at TIMESTAMPTZ NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
actor_id TEXT,
|
||||
completed_at TIMESTAMPTZ,
|
||||
summary TEXT,
|
||||
PRIMARY KEY (run_id, approval_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_approvals_status ON taskrunner.pack_run_approvals (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_approvals_requested_at ON taskrunner.pack_run_approvals (requested_at);";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(ddl, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_tableInitialized = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.TaskRunner.Core.Evidence;
|
||||
|
||||
namespace StellaOps.TaskRunner.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IPackRunEvidenceStore"/>.
|
||||
/// </summary>
|
||||
public sealed class PostgresPackRunEvidenceStore : RepositoryBase<TaskRunnerDataSource>, IPackRunEvidenceStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private bool _tableInitialized;
|
||||
|
||||
public PostgresPackRunEvidenceStore(TaskRunnerDataSource dataSource, ILogger<PostgresPackRunEvidenceStore> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task StoreAsync(PackRunEvidenceSnapshot snapshot, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
INSERT INTO taskrunner.pack_run_evidence (
|
||||
snapshot_id, tenant_id, run_id, plan_hash, created_at, kind, materials_json, root_hash, metadata_json
|
||||
) VALUES (
|
||||
@snapshot_id, @tenant_id, @run_id, @plan_hash, @created_at, @kind, @materials_json, @root_hash, @metadata_json
|
||||
)
|
||||
ON CONFLICT (snapshot_id)
|
||||
DO UPDATE SET
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
run_id = EXCLUDED.run_id,
|
||||
plan_hash = EXCLUDED.plan_hash,
|
||||
created_at = EXCLUDED.created_at,
|
||||
kind = EXCLUDED.kind,
|
||||
materials_json = EXCLUDED.materials_json,
|
||||
root_hash = EXCLUDED.root_hash,
|
||||
metadata_json = EXCLUDED.metadata_json";
|
||||
|
||||
var materialsJson = JsonSerializer.Serialize(snapshot.Materials, JsonOptions);
|
||||
var metadataJson = snapshot.Metadata is null
|
||||
? null
|
||||
: JsonSerializer.Serialize(snapshot.Metadata, JsonOptions);
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
|
||||
AddParameter(command, "@snapshot_id", snapshot.SnapshotId);
|
||||
AddParameter(command, "@tenant_id", snapshot.TenantId);
|
||||
AddParameter(command, "@run_id", snapshot.RunId);
|
||||
AddParameter(command, "@plan_hash", snapshot.PlanHash);
|
||||
AddParameter(command, "@created_at", snapshot.CreatedAt);
|
||||
AddParameter(command, "@kind", snapshot.Kind.ToString());
|
||||
AddJsonbParameter(command, "@materials_json", materialsJson);
|
||||
AddParameter(command, "@root_hash", snapshot.RootHash);
|
||||
if (metadataJson is not null)
|
||||
{
|
||||
AddJsonbParameter(command, "@metadata_json", metadataJson);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddParameter(command, "@metadata_json", DBNull.Value);
|
||||
}
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<PackRunEvidenceSnapshot?> GetAsync(Guid snapshotId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT snapshot_id, tenant_id, run_id, plan_hash, created_at, kind, materials_json, root_hash, metadata_json
|
||||
FROM taskrunner.pack_run_evidence
|
||||
WHERE snapshot_id = @snapshot_id";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "@snapshot_id", snapshotId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapSnapshot(reader);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PackRunEvidenceSnapshot>> ListByRunAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT snapshot_id, tenant_id, run_id, plan_hash, created_at, kind, materials_json, root_hash, metadata_json
|
||||
FROM taskrunner.pack_run_evidence
|
||||
WHERE LOWER(tenant_id) = LOWER(@tenant_id) AND run_id = @run_id
|
||||
ORDER BY created_at";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "@tenant_id", tenantId);
|
||||
AddParameter(command, "@run_id", runId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var results = new List<PackRunEvidenceSnapshot>();
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
results.Add(MapSnapshot(reader));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PackRunEvidenceSnapshot>> GetByRunIdAsync(
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT snapshot_id, tenant_id, run_id, plan_hash, created_at, kind, materials_json, root_hash, metadata_json
|
||||
FROM taskrunner.pack_run_evidence
|
||||
WHERE run_id = @run_id
|
||||
ORDER BY created_at";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "@run_id", runId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var results = new List<PackRunEvidenceSnapshot>();
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
results.Add(MapSnapshot(reader));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PackRunEvidenceSnapshot>> ListByKindAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
PackRunEvidenceSnapshotKind kind,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT snapshot_id, tenant_id, run_id, plan_hash, created_at, kind, materials_json, root_hash, metadata_json
|
||||
FROM taskrunner.pack_run_evidence
|
||||
WHERE LOWER(tenant_id) = LOWER(@tenant_id) AND run_id = @run_id AND kind = @kind
|
||||
ORDER BY created_at";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "@tenant_id", tenantId);
|
||||
AddParameter(command, "@run_id", runId);
|
||||
AddParameter(command, "@kind", kind.ToString());
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var results = new List<PackRunEvidenceSnapshot>();
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
results.Add(MapSnapshot(reader));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<PackRunEvidenceVerificationResult> VerifyAsync(
|
||||
Guid snapshotId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var snapshot = await GetAsync(snapshotId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (snapshot is null)
|
||||
{
|
||||
return new PackRunEvidenceVerificationResult(
|
||||
Valid: false,
|
||||
SnapshotId: snapshotId,
|
||||
ExpectedHash: string.Empty,
|
||||
ComputedHash: string.Empty,
|
||||
Error: "Snapshot not found");
|
||||
}
|
||||
|
||||
// Recompute by creating a new snapshot with same materials
|
||||
var recomputed = PackRunEvidenceSnapshot.Create(
|
||||
snapshot.TenantId,
|
||||
snapshot.RunId,
|
||||
snapshot.PlanHash,
|
||||
snapshot.Kind,
|
||||
snapshot.Materials,
|
||||
snapshot.Metadata);
|
||||
|
||||
var valid = string.Equals(snapshot.RootHash, recomputed.RootHash, StringComparison.Ordinal);
|
||||
|
||||
return new PackRunEvidenceVerificationResult(
|
||||
Valid: valid,
|
||||
SnapshotId: snapshotId,
|
||||
ExpectedHash: snapshot.RootHash,
|
||||
ComputedHash: recomputed.RootHash,
|
||||
Error: valid ? null : "Root hash mismatch");
|
||||
}
|
||||
|
||||
private static PackRunEvidenceSnapshot MapSnapshot(NpgsqlDataReader reader)
|
||||
{
|
||||
var snapshotId = reader.GetGuid(0);
|
||||
var tenantId = reader.GetString(1);
|
||||
var runId = reader.GetString(2);
|
||||
var planHash = reader.GetString(3);
|
||||
var createdAt = reader.GetFieldValue<DateTimeOffset>(4);
|
||||
var kindString = reader.GetString(5);
|
||||
var materialsJson = reader.GetString(6);
|
||||
var rootHash = reader.GetString(7);
|
||||
var metadataJson = reader.IsDBNull(8) ? null : reader.GetString(8);
|
||||
|
||||
if (!Enum.TryParse<PackRunEvidenceSnapshotKind>(kindString, ignoreCase: true, out var kind))
|
||||
{
|
||||
kind = PackRunEvidenceSnapshotKind.RunCompletion;
|
||||
}
|
||||
|
||||
var materials = JsonSerializer.Deserialize<List<PackRunEvidenceMaterial>>(materialsJson, JsonOptions)
|
||||
?? new List<PackRunEvidenceMaterial>();
|
||||
|
||||
IReadOnlyDictionary<string, string>? metadata = null;
|
||||
if (metadataJson is not null)
|
||||
{
|
||||
metadata = JsonSerializer.Deserialize<Dictionary<string, string>>(metadataJson, JsonOptions);
|
||||
}
|
||||
|
||||
return new PackRunEvidenceSnapshot(
|
||||
snapshotId,
|
||||
tenantId,
|
||||
runId,
|
||||
planHash,
|
||||
createdAt,
|
||||
kind,
|
||||
materials,
|
||||
rootHash,
|
||||
metadata);
|
||||
}
|
||||
|
||||
private async Task EnsureTableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_tableInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string ddl = @"
|
||||
CREATE SCHEMA IF NOT EXISTS taskrunner;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS taskrunner.pack_run_evidence (
|
||||
snapshot_id UUID PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
plan_hash TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
materials_json JSONB NOT NULL,
|
||||
root_hash TEXT NOT NULL,
|
||||
metadata_json JSONB
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_evidence_run_id ON taskrunner.pack_run_evidence (run_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_evidence_tenant_run ON taskrunner.pack_run_evidence (tenant_id, run_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_evidence_kind ON taskrunner.pack_run_evidence (tenant_id, run_id, kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_evidence_created_at ON taskrunner.pack_run_evidence (created_at);";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(ddl, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_tableInitialized = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IPackRunLogStore"/>.
|
||||
/// </summary>
|
||||
public sealed class PostgresPackRunLogStore : RepositoryBase<TaskRunnerDataSource>, IPackRunLogStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private bool _tableInitialized;
|
||||
|
||||
public PostgresPackRunLogStore(TaskRunnerDataSource dataSource, ILogger<PostgresPackRunLogStore> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task AppendAsync(string runId, PackRunLogEntry entry, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
INSERT INTO taskrunner.pack_run_logs (run_id, timestamp, level, event_type, message, step_id, metadata)
|
||||
VALUES (@run_id, @timestamp, @level, @event_type, @message, @step_id, @metadata)";
|
||||
|
||||
var metadataJson = entry.Metadata is null
|
||||
? null
|
||||
: JsonSerializer.Serialize(entry.Metadata, JsonOptions);
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
|
||||
AddParameter(command, "@run_id", runId);
|
||||
AddParameter(command, "@timestamp", entry.Timestamp);
|
||||
AddParameter(command, "@level", entry.Level);
|
||||
AddParameter(command, "@event_type", entry.EventType);
|
||||
AddParameter(command, "@message", entry.Message);
|
||||
AddParameter(command, "@step_id", (object?)entry.StepId ?? DBNull.Value);
|
||||
if (metadataJson is not null)
|
||||
{
|
||||
AddJsonbParameter(command, "@metadata", metadataJson);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddParameter(command, "@metadata", DBNull.Value);
|
||||
}
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<PackRunLogEntry> ReadAsync(
|
||||
string runId,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT timestamp, level, event_type, message, step_id, metadata
|
||||
FROM taskrunner.pack_run_logs
|
||||
WHERE run_id = @run_id
|
||||
ORDER BY timestamp, id";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "@run_id", runId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return MapLogEntry(reader);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string runId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT EXISTS(SELECT 1 FROM taskrunner.pack_run_logs WHERE run_id = @run_id)";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "@run_id", runId);
|
||||
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
return result is true;
|
||||
}
|
||||
|
||||
private static PackRunLogEntry MapLogEntry(NpgsqlDataReader reader)
|
||||
{
|
||||
var timestamp = reader.GetFieldValue<DateTimeOffset>(0);
|
||||
var level = reader.GetString(1);
|
||||
var eventType = reader.GetString(2);
|
||||
var message = reader.GetString(3);
|
||||
var stepId = reader.IsDBNull(4) ? null : reader.GetString(4);
|
||||
var metadataJson = reader.IsDBNull(5) ? null : reader.GetString(5);
|
||||
|
||||
IReadOnlyDictionary<string, string>? metadata = null;
|
||||
if (metadataJson is not null)
|
||||
{
|
||||
metadata = JsonSerializer.Deserialize<Dictionary<string, string>>(metadataJson, JsonOptions);
|
||||
}
|
||||
|
||||
return new PackRunLogEntry(timestamp, level, eventType, message, stepId, metadata);
|
||||
}
|
||||
|
||||
private async Task EnsureTableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_tableInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string ddl = @"
|
||||
CREATE SCHEMA IF NOT EXISTS taskrunner;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS taskrunner.pack_run_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
timestamp TIMESTAMPTZ NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
step_id TEXT,
|
||||
metadata JSONB
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_logs_run_id ON taskrunner.pack_run_logs (run_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_logs_timestamp ON taskrunner.pack_run_logs (timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_logs_run_timestamp ON taskrunner.pack_run_logs (run_id, timestamp, id);";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(ddl, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_tableInitialized = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
|
||||
namespace StellaOps.TaskRunner.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IPackRunStateStore"/>.
|
||||
/// </summary>
|
||||
public sealed class PostgresPackRunStateStore : RepositoryBase<TaskRunnerDataSource>, IPackRunStateStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private bool _tableInitialized;
|
||||
|
||||
public PostgresPackRunStateStore(TaskRunnerDataSource dataSource, ILogger<PostgresPackRunStateStore> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<PackRunState?> GetAsync(string runId, CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT run_id, plan_hash, plan_json, failure_policy_json, requested_at, created_at, updated_at, steps_json, tenant_id
|
||||
FROM taskrunner.pack_run_state
|
||||
WHERE run_id = @run_id";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "@run_id", runId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapPackRunState(reader);
|
||||
}
|
||||
|
||||
public async Task SaveAsync(PackRunState state, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
INSERT INTO taskrunner.pack_run_state (run_id, plan_hash, plan_json, failure_policy_json, requested_at, created_at, updated_at, steps_json, tenant_id)
|
||||
VALUES (@run_id, @plan_hash, @plan_json, @failure_policy_json, @requested_at, @created_at, @updated_at, @steps_json, @tenant_id)
|
||||
ON CONFLICT (run_id)
|
||||
DO UPDATE SET
|
||||
plan_hash = EXCLUDED.plan_hash,
|
||||
plan_json = EXCLUDED.plan_json,
|
||||
failure_policy_json = EXCLUDED.failure_policy_json,
|
||||
requested_at = EXCLUDED.requested_at,
|
||||
updated_at = EXCLUDED.updated_at,
|
||||
steps_json = EXCLUDED.steps_json,
|
||||
tenant_id = EXCLUDED.tenant_id";
|
||||
|
||||
var planJson = JsonSerializer.Serialize(state.Plan, JsonOptions);
|
||||
var failurePolicyJson = JsonSerializer.Serialize(state.FailurePolicy, JsonOptions);
|
||||
var stepsJson = JsonSerializer.Serialize(state.Steps, JsonOptions);
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
|
||||
AddParameter(command, "@run_id", state.RunId);
|
||||
AddParameter(command, "@plan_hash", state.PlanHash);
|
||||
AddJsonbParameter(command, "@plan_json", planJson);
|
||||
AddJsonbParameter(command, "@failure_policy_json", failurePolicyJson);
|
||||
AddParameter(command, "@requested_at", state.RequestedAt);
|
||||
AddParameter(command, "@created_at", state.CreatedAt);
|
||||
AddParameter(command, "@updated_at", state.UpdatedAt);
|
||||
AddJsonbParameter(command, "@steps_json", stepsJson);
|
||||
AddParameter(command, "@tenant_id", (object?)state.TenantId ?? DBNull.Value);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PackRunState>> ListAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = @"
|
||||
SELECT run_id, plan_hash, plan_json, failure_policy_json, requested_at, created_at, updated_at, steps_json, tenant_id
|
||||
FROM taskrunner.pack_run_state
|
||||
ORDER BY created_at DESC";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var results = new List<PackRunState>();
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
results.Add(MapPackRunState(reader));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static PackRunState MapPackRunState(NpgsqlDataReader reader)
|
||||
{
|
||||
var runId = reader.GetString(0);
|
||||
var planHash = reader.GetString(1);
|
||||
var planJson = reader.GetString(2);
|
||||
var failurePolicyJson = reader.GetString(3);
|
||||
var requestedAt = reader.GetFieldValue<DateTimeOffset>(4);
|
||||
var createdAt = reader.GetFieldValue<DateTimeOffset>(5);
|
||||
var updatedAt = reader.GetFieldValue<DateTimeOffset>(6);
|
||||
var stepsJson = reader.GetString(7);
|
||||
var tenantId = reader.IsDBNull(8) ? null : reader.GetString(8);
|
||||
|
||||
var plan = JsonSerializer.Deserialize<TaskPackPlan>(planJson, JsonOptions)
|
||||
?? throw new InvalidOperationException($"Failed to deserialize plan for run '{runId}'");
|
||||
var failurePolicy = JsonSerializer.Deserialize<TaskPackPlanFailurePolicy>(failurePolicyJson, JsonOptions)
|
||||
?? throw new InvalidOperationException($"Failed to deserialize failure policy for run '{runId}'");
|
||||
var steps = JsonSerializer.Deserialize<Dictionary<string, PackRunStepStateRecord>>(stepsJson, JsonOptions)
|
||||
?? new Dictionary<string, PackRunStepStateRecord>(StringComparer.Ordinal);
|
||||
|
||||
return new PackRunState(
|
||||
runId,
|
||||
planHash,
|
||||
plan,
|
||||
failurePolicy,
|
||||
requestedAt,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
steps,
|
||||
tenantId);
|
||||
}
|
||||
|
||||
private async Task EnsureTableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_tableInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string ddl = @"
|
||||
CREATE SCHEMA IF NOT EXISTS taskrunner;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS taskrunner.pack_run_state (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
plan_hash TEXT NOT NULL,
|
||||
plan_json JSONB NOT NULL,
|
||||
failure_policy_json JSONB NOT NULL,
|
||||
requested_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
steps_json JSONB NOT NULL,
|
||||
tenant_id TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_state_tenant_id ON taskrunner.pack_run_state (tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pack_run_state_created_at ON taskrunner.pack_run_state (created_at DESC);";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(ddl, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_tableInitialized = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using StellaOps.TaskRunner.Core.Evidence;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Storage.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.TaskRunner.Storage.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring TaskRunner PostgreSQL storage services.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds TaskRunner PostgreSQL storage services.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <param name="configuration">Configuration root.</param>
|
||||
/// <param name="sectionName">Configuration section name for PostgreSQL options.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddTaskRunnerPostgresStorage(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = "Postgres:TaskRunner")
|
||||
{
|
||||
services.Configure<PostgresOptions>(sectionName, configuration.GetSection(sectionName));
|
||||
services.AddSingleton<TaskRunnerDataSource>();
|
||||
|
||||
// Register repositories as scoped (per-request lifetime)
|
||||
services.AddScoped<IPackRunStateStore, PostgresPackRunStateStore>();
|
||||
services.AddScoped<IPackRunApprovalStore, PostgresPackRunApprovalStore>();
|
||||
services.AddScoped<IPackRunLogStore, PostgresPackRunLogStore>();
|
||||
services.AddScoped<IPackRunEvidenceStore, PostgresPackRunEvidenceStore>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds TaskRunner PostgreSQL storage services with explicit options.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <param name="configureOptions">Options configuration action.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddTaskRunnerPostgresStorage(
|
||||
this IServiceCollection services,
|
||||
Action<PostgresOptions> configureOptions)
|
||||
{
|
||||
services.Configure(configureOptions);
|
||||
services.AddSingleton<TaskRunnerDataSource>();
|
||||
|
||||
// Register repositories as scoped (per-request lifetime)
|
||||
services.AddScoped<IPackRunStateStore, PostgresPackRunStateStore>();
|
||||
services.AddScoped<IPackRunApprovalStore, PostgresPackRunApprovalStore>();
|
||||
services.AddScoped<IPackRunLogStore, PostgresPackRunLogStore>();
|
||||
services.AddScoped<IPackRunEvidenceStore, PostgresPackRunEvidenceStore>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>StellaOps.TaskRunner.Storage.Postgres</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.TaskRunner/StellaOps.TaskRunner.Core/StellaOps.TaskRunner.Core.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
|
||||
namespace StellaOps.TaskRunner.Storage.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL data source for TaskRunner module.
|
||||
/// </summary>
|
||||
public sealed class TaskRunnerDataSource : DataSourceBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Default schema name for TaskRunner tables.
|
||||
/// </summary>
|
||||
public const string DefaultSchemaName = "taskrunner";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new TaskRunner data source.
|
||||
/// </summary>
|
||||
public TaskRunnerDataSource(IOptions<PostgresOptions> options, ILogger<TaskRunnerDataSource> logger)
|
||||
: base(CreateOptions(options.Value), logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ModuleName => "TaskRunner";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureDataSourceBuilder(NpgsqlDataSourceBuilder builder)
|
||||
{
|
||||
base.ConfigureDataSourceBuilder(builder);
|
||||
}
|
||||
|
||||
private static PostgresOptions CreateOptions(PostgresOptions baseOptions)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(baseOptions.SchemaName))
|
||||
{
|
||||
baseOptions.SchemaName = DefaultSchemaName;
|
||||
}
|
||||
return baseOptions;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user