feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled

- Added ConsoleExportClient for managing export requests and responses.
- Introduced ConsoleExportRequest and ConsoleExportResponse models.
- Implemented methods for creating and retrieving exports with appropriate headers.

feat(crypto): Add Software SM2/SM3 Cryptography Provider

- Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography.
- Added support for signing and verification using SM2 algorithm.
- Included hashing functionality with SM3 algorithm.
- Configured options for loading keys from files and environment gate checks.

test(crypto): Add unit tests for SmSoftCryptoProvider

- Created comprehensive tests for signing, verifying, and hashing functionalities.
- Ensured correct behavior for key management and error handling.

feat(api): Enhance Console Export Models

- Expanded ConsoleExport models to include detailed status and event types.
- Added support for various export formats and notification options.

test(time): Implement TimeAnchorPolicyService tests

- Developed tests for TimeAnchorPolicyService to validate time anchors.
- Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
StellaOps Bot
2025-12-07 00:27:33 +02:00
parent 9bd6a73926
commit 0de92144d2
229 changed files with 32351 additions and 1481 deletions

View File

@@ -85,6 +85,11 @@ if (storageSection.Exists())
builder.Services.AddSchedulerPostgresStorage(storageSection);
builder.Services.AddScoped<IGraphJobRepository, GraphJobRepository>();
builder.Services.AddSingleton<IGraphJobStore, PostgresGraphJobStore>();
builder.Services.AddScoped<IScheduleRepository, ScheduleRepository>();
builder.Services.AddScoped<IRunRepository, RunRepository>();
builder.Services.AddSingleton<IRunSummaryService, RunSummaryService>();
builder.Services.AddScoped<IImpactSnapshotRepository, ImpactSnapshotRepository>();
builder.Services.AddScoped<IPolicyRunJobRepository, PolicyRunJobRepository>();
builder.Services.AddSingleton<IPolicyRunService, PolicyRunService>();
builder.Services.AddSingleton<IPolicySimulationMetricsProvider, PolicySimulationMetricsProvider>();
builder.Services.AddSingleton<IPolicySimulationMetricsRecorder>(static sp => (IPolicySimulationMetricsRecorder)sp.GetRequiredService<IPolicySimulationMetricsProvider>());

View File

@@ -0,0 +1,8 @@
namespace StellaOps.Scheduler.Models;
public interface IRunSummaryService
{
Task<RunSummaryProjection> ProjectAsync(Run run, CancellationToken cancellationToken = default);
Task<RunSummaryProjection?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<RunSummaryProjection>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,5 @@
// Temporary compatibility stub to allow transition away from MongoDB driver.
namespace MongoDB.Driver
{
public interface IClientSessionHandle { }
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Immutable;
namespace StellaOps.Scheduler.Models;
public sealed record RunSummarySnapshot(string RunId, DateTimeOffset CompletedAt, RunState State, int Deltas);
public sealed record RunSummaryCounters(
int Total,
int Planning,
int Queued,
int Running,
int Completed,
int Error,
int Cancelled,
int TotalDeltas,
int TotalNewCriticals,
int TotalNewHigh,
int TotalNewMedium,
int TotalNewLow);
public sealed record RunSummaryProjection(
string TenantId,
string ScheduleId,
DateTimeOffset UpdatedAt,
string? LastRunId,
ImmutableArray<RunSummarySnapshot> RecentRuns,
RunSummaryCounters Counters);

View File

@@ -0,0 +1,19 @@
using System.Text.Json;
namespace StellaOps.Scheduler.Storage.Postgres;
internal static class CanonicalJsonSerializer
{
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
public static string Serialize<T>(T value) => JsonSerializer.Serialize(value, Options);
public static T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, Options);
public static JsonSerializerOptions Settings => Options;
}

View File

@@ -0,0 +1,75 @@
-- Scheduler Schema Migration 003: Runs, Impact Snapshots, Policy Run Jobs
DO $$ BEGIN
CREATE TYPE scheduler.run_state AS ENUM ('planning','queued','running','completed','error','cancelled');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE scheduler.policy_run_status AS ENUM ('pending','submitted','retrying','failed','completed','cancelled');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
CREATE TABLE IF NOT EXISTS scheduler.runs (
id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
schedule_id TEXT,
trigger JSONB NOT NULL,
state scheduler.run_state NOT NULL,
stats JSONB NOT NULL,
reason JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
error TEXT,
deltas JSONB NOT NULL,
retry_of TEXT,
schema_version TEXT,
PRIMARY KEY (tenant_id, id)
);
CREATE INDEX IF NOT EXISTS idx_runs_state ON scheduler.runs(state);
CREATE INDEX IF NOT EXISTS idx_runs_schedule ON scheduler.runs(tenant_id, schedule_id);
CREATE INDEX IF NOT EXISTS idx_runs_created ON scheduler.runs(created_at);
CREATE TABLE IF NOT EXISTS scheduler.impact_snapshots (
snapshot_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
run_id TEXT,
impact JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_impact_snapshots_run ON scheduler.impact_snapshots(run_id);
CREATE TABLE IF NOT EXISTS scheduler.policy_run_jobs (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
policy_id TEXT NOT NULL,
policy_version INT,
mode TEXT NOT NULL,
priority INT NOT NULL,
priority_rank INT NOT NULL,
run_id TEXT,
requested_by TEXT,
correlation_id TEXT,
metadata JSONB,
inputs JSONB NOT NULL,
queued_at TIMESTAMPTZ,
status scheduler.policy_run_status NOT NULL,
attempt_count INT NOT NULL,
last_attempt_at TIMESTAMPTZ,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
available_at TIMESTAMPTZ NOT NULL,
submitted_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
lease_owner TEXT,
lease_expires_at TIMESTAMPTZ,
cancellation_requested BOOLEAN NOT NULL DEFAULT FALSE,
cancellation_requested_at TIMESTAMPTZ,
cancellation_reason TEXT,
cancelled_at TIMESTAMPTZ,
schema_version TEXT
);
CREATE INDEX IF NOT EXISTS idx_policy_run_jobs_tenant ON scheduler.policy_run_jobs(tenant_id);
CREATE INDEX IF NOT EXISTS idx_policy_run_jobs_status ON scheduler.policy_run_jobs(status);
CREATE INDEX IF NOT EXISTS idx_policy_run_jobs_run ON scheduler.policy_run_jobs(run_id);
CREATE INDEX IF NOT EXISTS idx_policy_run_jobs_policy ON scheduler.policy_run_jobs(tenant_id, policy_id);

View File

@@ -88,7 +88,10 @@ public sealed class GraphJobRepository : IGraphJobRepository
Status = (short?)status,
Limit = limit
});
return rows.Select(r => CanonicalJsonSerializer.Deserialize<GraphBuildJob>(r)).ToArray();
return rows
.Select(r => CanonicalJsonSerializer.Deserialize<GraphBuildJob>(r))
.Where(r => r is not null)!
.ToArray()!;
}
public async ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken)
@@ -108,7 +111,10 @@ public sealed class GraphJobRepository : IGraphJobRepository
Status = (short?)status,
Limit = limit
});
return rows.Select(r => CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(r)).ToArray();
return rows
.Select(r => CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(r))
.Where(r => r is not null)!
.ToArray()!;
}
public ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken)

View File

@@ -0,0 +1,9 @@
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public interface IImpactSnapshotRepository
{
Task UpsertAsync(ImpactSet snapshot, CancellationToken cancellationToken = default);
Task<ImpactSet?> GetBySnapshotIdAsync(string snapshotId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,14 @@
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public interface IPolicyRunJobRepository
{
Task<PolicyRunJob?> GetAsync(string tenantId, string jobId, CancellationToken cancellationToken = default);
Task<PolicyRunJob?> GetByRunIdAsync(string tenantId, string runId, CancellationToken cancellationToken = default);
Task InsertAsync(PolicyRunJob job, CancellationToken cancellationToken = default);
Task<long> CountAsync(string tenantId, PolicyRunMode mode, IReadOnlyCollection<PolicyRunJobStatus> statuses, CancellationToken cancellationToken = default);
Task<PolicyRunJob?> LeaseAsync(string leaseOwner, DateTimeOffset now, TimeSpan leaseDuration, int maxAttempts, CancellationToken cancellationToken = default);
Task<bool> ReplaceAsync(PolicyRunJob job, string? expectedLeaseOwner = null, CancellationToken cancellationToken = default);
Task<IReadOnlyList<PolicyRunJob>> ListAsync(string tenantId, string? policyId = null, PolicyRunMode? mode = null, IReadOnlyCollection<PolicyRunJobStatus>? statuses = null, DateTimeOffset? queuedAfter = null, int limit = 50, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,12 @@
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public interface IRunRepository
{
Task InsertAsync(Run run, CancellationToken cancellationToken = default);
Task<bool> UpdateAsync(Run run, CancellationToken cancellationToken = default);
Task<Run?> GetAsync(string tenantId, string runId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Run>> ListAsync(string tenantId, RunQueryOptions? options = null, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Run>> ListByStateAsync(RunState state, int limit = 50, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,11 @@
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public interface IScheduleRepository
{
Task UpsertAsync(Schedule schedule, CancellationToken cancellationToken = default);
Task<Schedule?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Schedule>> ListAsync(string tenantId, ScheduleQueryOptions? options = null, CancellationToken cancellationToken = default);
Task<bool> SoftDeleteAsync(string tenantId, string scheduleId, string deletedBy, DateTimeOffset deletedAt, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,53 @@
using System.Text.Json;
using Dapper;
using StellaOps.Scheduler.Models;
using StellaOps.Infrastructure.Postgres.Connections;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class ImpactSnapshotRepository : IImpactSnapshotRepository
{
private readonly SchedulerDataSource _dataSource;
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
public ImpactSnapshotRepository(SchedulerDataSource dataSource)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
}
public async Task UpsertAsync(ImpactSet snapshot, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(snapshot);
var tenantId = snapshot.Selector?.TenantId ?? string.Empty;
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken);
const string sql = """
INSERT INTO scheduler.impact_snapshots (snapshot_id, tenant_id, impact, created_at)
VALUES (@SnapshotId, @TenantId, @Impact, NOW())
ON CONFLICT (snapshot_id) DO UPDATE SET impact = EXCLUDED.impact;
""";
await conn.ExecuteAsync(sql, new
{
SnapshotId = snapshot.SnapshotId ?? $"impact::{Guid.NewGuid():N}",
TenantId = tenantId,
Impact = JsonSerializer.Serialize(snapshot, _serializer)
});
}
public async Task<ImpactSet?> GetBySnapshotIdAsync(string snapshotId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(snapshotId);
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
const string sql = """
SELECT impact
FROM scheduler.impact_snapshots
WHERE snapshot_id = @SnapshotId
LIMIT 1;
""";
var json = await conn.ExecuteScalarAsync<string?>(sql, new { SnapshotId = snapshotId });
return json is null ? null : JsonSerializer.Deserialize<ImpactSet>(json, _serializer);
}
}

View File

@@ -0,0 +1,258 @@
using System.Collections.Immutable;
using System.Text.Json;
using Dapper;
using StellaOps.Scheduler.Models;
using StellaOps.Infrastructure.Postgres.Connections;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class PolicyRunJobRepository : IPolicyRunJobRepository
{
private readonly SchedulerDataSource _dataSource;
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
public PolicyRunJobRepository(SchedulerDataSource dataSource)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
}
public async Task<PolicyRunJob?> GetAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(jobId);
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
const string sql = "SELECT * FROM scheduler.policy_run_jobs WHERE tenant_id = @TenantId AND id = @Id LIMIT 1;";
var row = await conn.QuerySingleOrDefaultAsync(sql, new { TenantId = tenantId, Id = jobId });
return row is null ? null : Map(row);
}
public async Task<PolicyRunJob?> GetByRunIdAsync(string tenantId, string runId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
const string sql = "SELECT * FROM scheduler.policy_run_jobs WHERE tenant_id = @TenantId AND run_id = @RunId LIMIT 1;";
var row = await conn.QuerySingleOrDefaultAsync(sql, new { TenantId = tenantId, RunId = runId });
return row is null ? null : Map(row);
}
public async Task InsertAsync(PolicyRunJob job, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);
await using var conn = await _dataSource.OpenConnectionAsync(job.TenantId, "writer", cancellationToken);
const string sql = """
INSERT INTO scheduler.policy_run_jobs (
id, tenant_id, policy_id, policy_version, mode, priority, priority_rank, run_id, requested_by, correlation_id,
metadata, inputs, queued_at, status, attempt_count, last_attempt_at, last_error,
created_at, updated_at, available_at, submitted_at, completed_at, lease_owner, lease_expires_at,
cancellation_requested, cancellation_requested_at, cancellation_reason, cancelled_at, schema_version)
VALUES (
@Id, @TenantId, @PolicyId, @PolicyVersion, @Mode, @Priority, @PriorityRank, @RunId, @RequestedBy, @CorrelationId,
@Metadata, @Inputs, @QueuedAt, @Status, @AttemptCount, @LastAttemptAt, @LastError,
@CreatedAt, @UpdatedAt, @AvailableAt, @SubmittedAt, @CompletedAt, @LeaseOwner, @LeaseExpiresAt,
@CancellationRequested, @CancellationRequestedAt, @CancellationReason, @CancelledAt, @SchemaVersion)
ON CONFLICT (id) DO NOTHING;
""";
await conn.ExecuteAsync(sql, MapParams(job));
}
public async Task<long> CountAsync(string tenantId, PolicyRunMode mode, IReadOnlyCollection<PolicyRunJobStatus> statuses, CancellationToken cancellationToken = default)
{
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
const string sql = """
SELECT COUNT(*) FROM scheduler.policy_run_jobs
WHERE tenant_id = @TenantId AND mode = @Mode AND status = ANY(@Statuses);
""";
return await conn.ExecuteScalarAsync<long>(sql, new
{
TenantId = tenantId,
Mode = mode.ToString().ToLowerInvariant(),
Statuses = statuses.Select(s => s.ToString().ToLowerInvariant()).ToArray()
});
}
public async Task<PolicyRunJob?> LeaseAsync(string leaseOwner, DateTimeOffset now, TimeSpan leaseDuration, int maxAttempts, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(leaseOwner);
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
const string sql = """
WITH candidate AS (
SELECT *
FROM scheduler.policy_run_jobs
WHERE status IN ('pending','retrying')
ORDER BY available_at ASC, priority_rank DESC, created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE scheduler.policy_run_jobs j
SET lease_owner = @LeaseOwner,
lease_expires_at = @LeaseExpires,
attempt_count = j.attempt_count + 1,
last_attempt_at = @Now,
status = CASE WHEN j.status = 'pending' THEN 'submitted' ELSE 'retrying' END,
updated_at = @Now
FROM candidate c
WHERE j.id = c.id
AND j.attempt_count < @MaxAttempts
RETURNING j.*;
""";
var row = await conn.QuerySingleOrDefaultAsync(sql, new
{
LeaseOwner = leaseOwner,
LeaseExpires = now.Add(leaseDuration),
Now = now,
MaxAttempts = maxAttempts
});
return row is null ? null : Map(row);
}
public async Task<bool> ReplaceAsync(PolicyRunJob job, string? expectedLeaseOwner = null, CancellationToken cancellationToken = default)
{
await using var conn = await _dataSource.OpenConnectionAsync(job.TenantId, "writer", cancellationToken);
var matchLease = string.IsNullOrWhiteSpace(expectedLeaseOwner)
? ""
: "AND lease_owner = @ExpectedLeaseOwner";
var sql = $"""
UPDATE scheduler.policy_run_jobs
SET policy_version = @PolicyVersion,
status = @Status,
attempt_count = @AttemptCount,
last_attempt_at = @LastAttemptAt,
last_error = @LastError,
available_at = @AvailableAt,
submitted_at = @SubmittedAt,
completed_at = @CompletedAt,
lease_owner = @LeaseOwner,
lease_expires_at = @LeaseExpiresAt,
cancellation_requested = @CancellationRequested,
cancellation_requested_at = @CancellationRequestedAt,
cancellation_reason = @CancellationReason,
cancelled_at = @CancelledAt,
updated_at = @UpdatedAt,
run_id = @RunId
WHERE id = @Id {matchLease};
""";
var affected = await conn.ExecuteAsync(sql, MapParams(job, expectedLeaseOwner));
return affected > 0;
}
public async Task<IReadOnlyList<PolicyRunJob>> ListAsync(
string tenantId,
string? policyId = null,
PolicyRunMode? mode = null,
IReadOnlyCollection<PolicyRunJobStatus>? statuses = null,
DateTimeOffset? queuedAfter = null,
int limit = 50,
CancellationToken cancellationToken = default)
{
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
var filters = new List<string> { "tenant_id = @TenantId" };
if (!string.IsNullOrWhiteSpace(policyId)) filters.Add("policy_id = @PolicyId");
if (mode is not null) filters.Add("mode = @Mode");
if (statuses is not null && statuses.Count > 0) filters.Add("status = ANY(@Statuses)");
if (queuedAfter is not null) filters.Add("queued_at > @QueuedAfter");
var sql = $"""
SELECT *
FROM scheduler.policy_run_jobs
WHERE {string.Join(" AND ", filters)}
ORDER BY created_at DESC
LIMIT @Limit;
""";
var rows = await conn.QueryAsync(sql, new
{
TenantId = tenantId,
PolicyId = policyId,
Mode = mode?.ToString().ToLowerInvariant(),
Statuses = statuses?.Select(s => s.ToString().ToLowerInvariant()).ToArray(),
QueuedAfter = queuedAfter,
Limit = limit
});
return rows.Select(Map).ToList();
}
private object MapParams(PolicyRunJob job, string? expectedLeaseOwner = null) => new
{
job.Id,
job.TenantId,
job.PolicyId,
job.PolicyVersion,
Mode = job.Mode.ToString().ToLowerInvariant(),
Priority = (int)job.Priority,
job.PriorityRank,
job.RunId,
job.RequestedBy,
job.CorrelationId,
Metadata = job.Metadata is null ? null : JsonSerializer.Serialize(job.Metadata, _serializer),
Inputs = JsonSerializer.Serialize(job.Inputs, _serializer),
job.QueuedAt,
Status = job.Status.ToString().ToLowerInvariant(),
job.AttemptCount,
job.LastAttemptAt,
job.LastError,
job.CreatedAt,
job.UpdatedAt,
job.AvailableAt,
job.SubmittedAt,
job.CompletedAt,
job.LeaseOwner,
job.LeaseExpiresAt,
job.CancellationRequested,
job.CancellationRequestedAt,
job.CancellationReason,
job.CancelledAt,
job.SchemaVersion,
ExpectedLeaseOwner = expectedLeaseOwner
};
private PolicyRunJob Map(dynamic row)
{
var metadata = row.metadata is null
? null
: JsonSerializer.Deserialize<ImmutableSortedDictionary<string, string>>((string)row.metadata, _serializer);
var inputs = JsonSerializer.Deserialize<PolicyRunInputs>((string)row.inputs, _serializer)!;
return new PolicyRunJob(
(string?)row.schema_version ?? SchedulerSchemaVersions.PolicyRunJob,
(string)row.id,
(string)row.tenant_id,
(string)row.policy_id,
(int?)row.policy_version,
Enum.Parse<PolicyRunMode>((string)row.mode, true),
(PolicyRunPriority)row.priority,
(int)row.priority_rank,
(string?)row.run_id,
(string?)row.requested_by,
(string?)row.correlation_id,
metadata,
inputs,
row.queued_at is null ? null : DateTime.SpecifyKind(row.queued_at, DateTimeKind.Utc),
Enum.Parse<PolicyRunJobStatus>((string)row.status, true),
(int)row.attempt_count,
row.last_attempt_at is null ? null : DateTime.SpecifyKind(row.last_attempt_at, DateTimeKind.Utc),
(string?)row.last_error,
DateTime.SpecifyKind(row.created_at, DateTimeKind.Utc),
DateTime.SpecifyKind(row.updated_at, DateTimeKind.Utc),
DateTime.SpecifyKind(row.available_at, DateTimeKind.Utc),
row.submitted_at is null ? null : DateTime.SpecifyKind(row.submitted_at, DateTimeKind.Utc),
row.completed_at is null ? null : DateTime.SpecifyKind(row.completed_at, DateTimeKind.Utc),
(string?)row.lease_owner,
row.lease_expires_at is null ? null : DateTime.SpecifyKind(row.lease_expires_at, DateTimeKind.Utc),
(bool)row.cancellation_requested,
row.cancellation_requested_at is null ? null : DateTime.SpecifyKind(row.cancellation_requested_at, DateTimeKind.Utc),
(string?)row.cancellation_reason,
row.cancelled_at is null ? null : DateTime.SpecifyKind(row.cancelled_at, DateTimeKind.Utc));
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Immutable;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class RunQueryOptions
{
public string? ScheduleId { get; init; }
public ImmutableArray<RunState> States { get; init; } = ImmutableArray<RunState>.Empty;
public DateTimeOffset? CreatedAfter { get; init; }
public bool SortAscending { get; init; } = false;
public int? Limit { get; init; }
}

View File

@@ -0,0 +1,190 @@
using System.Data;
using System.Text.Json;
using Dapper;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Infrastructure.Postgres.Connections;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class RunRepository : IRunRepository
{
private readonly SchedulerDataSource _dataSource;
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
public RunRepository(SchedulerDataSource dataSource)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
}
public async Task InsertAsync(Run run, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(run);
await using var conn = await _dataSource.OpenConnectionAsync(run.TenantId, "writer", cancellationToken);
const string sql = """
INSERT INTO scheduler.runs (
id, tenant_id, schedule_id, trigger, state, stats, reason, created_at, started_at, finished_at,
error, deltas, retry_of, schema_version)
VALUES (@Id, @TenantId, @ScheduleId, @Trigger, @State, @Stats, @Reason, @CreatedAt, @StartedAt, @FinishedAt,
@Error, @Deltas, @RetryOf, @SchemaVersion)
ON CONFLICT (tenant_id, id) DO NOTHING;
""";
var payload = MapParams(run);
await conn.ExecuteAsync(new CommandDefinition(sql, payload, cancellationToken: cancellationToken));
}
public async Task<bool> UpdateAsync(Run run, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(run);
await using var conn = await _dataSource.OpenConnectionAsync(run.TenantId, "writer", cancellationToken);
const string sql = """
UPDATE scheduler.runs
SET state = @State,
stats = @Stats,
reason = @Reason,
started_at = @StartedAt,
finished_at = @FinishedAt,
error = @Error,
deltas = @Deltas,
retry_of = @RetryOf,
schema_version = @SchemaVersion
WHERE tenant_id = @TenantId AND id = @Id;
""";
var payload = MapParams(run);
var affected = await conn.ExecuteAsync(new CommandDefinition(sql, payload, cancellationToken: cancellationToken));
return affected > 0;
}
public async Task<Run?> GetAsync(string tenantId, string runId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
const string sql = """
SELECT *
FROM scheduler.runs
WHERE tenant_id = @TenantId AND id = @RunId
LIMIT 1;
""";
var row = await conn.QuerySingleOrDefaultAsync(sql, new { TenantId = tenantId, RunId = runId });
return row is null ? null : MapRun(row);
}
public async Task<IReadOnlyList<Run>> ListAsync(string tenantId, RunQueryOptions? options = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
options ??= new RunQueryOptions();
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
var filters = new List<string> { "tenant_id = @TenantId" };
if (!string.IsNullOrWhiteSpace(options.ScheduleId))
{
filters.Add("schedule_id = @ScheduleId");
}
if (!options.States.IsDefaultOrEmpty)
{
filters.Add("state = ANY(@States)");
}
if (options.CreatedAfter is { } after)
{
filters.Add("created_at > @CreatedAfter");
}
var order = options.SortAscending ? "created_at ASC, id ASC" : "created_at DESC, id DESC";
var limit = options.Limit.GetValueOrDefault(50);
var sql = $"""
SELECT *
FROM scheduler.runs
WHERE {string.Join(" AND ", filters)}
ORDER BY {order}
LIMIT @Limit;
""";
var rows = await conn.QueryAsync(sql, new
{
TenantId = tenantId,
ScheduleId = options.ScheduleId,
States = options.States.Select(s => s.ToString().ToLowerInvariant()).ToArray(),
CreatedAfter = options.CreatedAfter?.UtcDateTime,
Limit = limit
});
return rows.Select(MapRun).ToList();
}
public async Task<IReadOnlyList<Run>> ListByStateAsync(RunState state, int limit = 50, CancellationToken cancellationToken = default)
{
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
const string sql = """
SELECT *
FROM scheduler.runs
WHERE state = @State
ORDER BY created_at ASC
LIMIT @Limit;
""";
var rows = await conn.QueryAsync(sql, new { State = state.ToString().ToLowerInvariant(), Limit = limit });
return rows.Select(MapRun).ToList();
}
private object MapParams(Run run) => new
{
run.Id,
run.TenantId,
run.ScheduleId,
Trigger = Serialize(run.Trigger),
State = run.State.ToString().ToLowerInvariant(),
Stats = Serialize(run.Stats),
Reason = Serialize(run.Reason),
run.CreatedAt,
run.StartedAt,
run.FinishedAt,
run.Error,
Deltas = Serialize(run.Deltas),
run.RetryOf,
run.SchemaVersion
};
private Run MapRun(dynamic row)
{
var trigger = Deserialize<RunTrigger>(row.trigger);
var state = Enum.Parse<RunState>(row.state, true);
var stats = Deserialize<RunStats>(row.stats);
var reason = Deserialize<RunReason>(row.reason);
var deltas = Deserialize<IEnumerable<DeltaSummary>>(row.deltas) ?? Enumerable.Empty<DeltaSummary>();
return new Run(
(string)row.id,
(string)row.tenant_id,
trigger,
state,
stats,
reason,
(string?)row.schedule_id,
DateTime.SpecifyKind(row.created_at, DateTimeKind.Utc),
row.started_at is null ? null : DateTime.SpecifyKind(row.started_at, DateTimeKind.Utc),
row.finished_at is null ? null : DateTime.SpecifyKind(row.finished_at, DateTimeKind.Utc),
(string?)row.error,
deltas,
(string?)row.retry_of,
(string?)row.schema_version);
}
private string Serialize<T>(T value) =>
JsonSerializer.Serialize(value, _serializer);
private T? Deserialize<T>(string json) =>
JsonSerializer.Deserialize<T>(json, _serializer);
}

View File

@@ -0,0 +1,56 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using StellaOps.Scheduler.Models;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class RunSummaryService : IRunSummaryService
{
private readonly ConcurrentDictionary<(string TenantId, string ScheduleId), RunSummaryProjection> _projections = new();
public Task<RunSummaryProjection> ProjectAsync(Run run, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(run);
var scheduleId = run.ScheduleId ?? string.Empty;
var updatedAt = run.FinishedAt ?? run.StartedAt ?? run.CreatedAt;
var counters = new RunSummaryCounters(
Total: 1,
Planning: run.State == RunState.Planning ? 1 : 0,
Queued: run.State == RunState.Queued ? 1 : 0,
Running: run.State == RunState.Running ? 1 : 0,
Completed: run.State == RunState.Completed ? 1 : 0,
Error: run.State == RunState.Error ? 1 : 0,
Cancelled: run.State == RunState.Cancelled ? 1 : 0,
TotalDeltas: run.Stats.Deltas,
TotalNewCriticals: run.Stats.NewCriticals,
TotalNewHigh: run.Stats.NewHigh,
TotalNewMedium: run.Stats.NewMedium,
TotalNewLow: run.Stats.NewLow);
var projection = new RunSummaryProjection(
run.TenantId,
scheduleId,
updatedAt,
run.Id,
ImmutableArray<RunSummarySnapshot>.Empty,
counters);
_projections[(run.TenantId, scheduleId)] = projection;
return Task.FromResult(projection);
}
public Task<RunSummaryProjection?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default)
{
_projections.TryGetValue((tenantId, scheduleId), out var projection);
return Task.FromResult<RunSummaryProjection?>(projection);
}
public Task<IReadOnlyList<RunSummaryProjection>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
{
var results = _projections.Values
.Where(p => string.Equals(p.TenantId, tenantId, StringComparison.Ordinal))
.ToList();
return Task.FromResult<IReadOnlyList<RunSummaryProjection>>(results);
}
}

View File

@@ -0,0 +1,7 @@
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class ScheduleQueryOptions
{
public bool IncludeDisabled { get; init; } = false;
public int? Limit { get; init; }
}

View File

@@ -0,0 +1,155 @@
using System.Text.Json;
using Dapper;
using StellaOps.Scheduler.Models;
using StellaOps.Infrastructure.Postgres.Connections;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class ScheduleRepository : IScheduleRepository
{
private readonly SchedulerDataSource _dataSource;
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
public ScheduleRepository(SchedulerDataSource dataSource)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
}
public async Task UpsertAsync(Schedule schedule, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(schedule);
await using var conn = await _dataSource.OpenConnectionAsync(schedule.TenantId, "writer", cancellationToken);
const string sql = """
INSERT INTO scheduler.schedules (
id, tenant_id, name, description, enabled, cron_expression, timezone, mode,
selection, only_if, notify, limits, subscribers, created_at, created_by,
updated_at, updated_by, deleted_at, deleted_by, schema_version)
VALUES (
@Id, @TenantId, @Name, @Description, @Enabled, @CronExpression, @Timezone, @Mode,
@Selection, @OnlyIf, @Notify, @Limits, @Subscribers, @CreatedAt, @CreatedBy,
@UpdatedAt, @UpdatedBy, NULL, NULL, @SchemaVersion)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
enabled = EXCLUDED.enabled,
cron_expression = EXCLUDED.cron_expression,
timezone = EXCLUDED.timezone,
mode = EXCLUDED.mode,
selection = EXCLUDED.selection,
only_if = EXCLUDED.only_if,
notify = EXCLUDED.notify,
limits = EXCLUDED.limits,
subscribers = EXCLUDED.subscribers,
updated_at = EXCLUDED.updated_at,
updated_by = EXCLUDED.updated_by,
schema_version = EXCLUDED.schema_version,
deleted_at = NULL,
deleted_by = NULL;
""";
await conn.ExecuteAsync(sql, new
{
schedule.Id,
schedule.TenantId,
schedule.Name,
Description = (string?)null,
schedule.Enabled,
schedule.CronExpression,
schedule.Timezone,
Mode = schedule.Mode.ToString().ToLowerInvariant(),
Selection = JsonSerializer.Serialize(schedule.Selection, _serializer),
OnlyIf = JsonSerializer.Serialize(schedule.OnlyIf, _serializer),
Notify = JsonSerializer.Serialize(schedule.Notify, _serializer),
Limits = JsonSerializer.Serialize(schedule.Limits, _serializer),
Subscribers = schedule.Subscribers.ToArray(),
schedule.CreatedAt,
schedule.CreatedBy,
schedule.UpdatedAt,
schedule.UpdatedBy,
schedule.SchemaVersion
});
}
public async Task<Schedule?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(scheduleId);
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
const string sql = """
SELECT *
FROM scheduler.schedules
WHERE tenant_id = @TenantId AND id = @ScheduleId AND deleted_at IS NULL
LIMIT 1;
""";
var row = await conn.QuerySingleOrDefaultAsync(sql, new { TenantId = tenantId, ScheduleId = scheduleId });
return row is null ? null : Map(row);
}
public async Task<IReadOnlyList<Schedule>> ListAsync(string tenantId, ScheduleQueryOptions? options = null, CancellationToken cancellationToken = default)
{
options ??= new ScheduleQueryOptions();
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
var where = options.IncludeDisabled
? "tenant_id = @TenantId AND deleted_at IS NULL"
: "tenant_id = @TenantId AND deleted_at IS NULL AND enabled = TRUE";
var limit = options.Limit.GetValueOrDefault(200);
var sql = $"""
SELECT *
FROM scheduler.schedules
WHERE {where}
ORDER BY name ASC
LIMIT @Limit;
""";
var rows = await conn.QueryAsync(sql, new { TenantId = tenantId, Limit = limit });
return rows.Select(Map).ToList();
}
public async Task<bool> SoftDeleteAsync(string tenantId, string scheduleId, string deletedBy, DateTimeOffset deletedAt, CancellationToken cancellationToken = default)
{
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken);
const string sql = """
UPDATE scheduler.schedules
SET deleted_at = @DeletedAt, deleted_by = @DeletedBy
WHERE tenant_id = @TenantId AND id = @ScheduleId AND deleted_at IS NULL;
""";
var affected = await conn.ExecuteAsync(sql, new
{
TenantId = tenantId,
ScheduleId = scheduleId,
DeletedBy = deletedBy,
DeletedAt = deletedAt
});
return affected > 0;
}
private Schedule Map(dynamic row)
{
return new Schedule(
(string)row.id,
(string)row.tenant_id,
(string)row.name,
(bool)row.enabled,
(string)row.cron_expression,
(string)row.timezone,
Enum.Parse<ScheduleMode>((string)row.mode, true),
JsonSerializer.Deserialize<Selector>((string)row.selection, _serializer)!,
JsonSerializer.Deserialize<ScheduleOnlyIf>((string)row.only_if, _serializer)!,
JsonSerializer.Deserialize<ScheduleNotify>((string)row.notify, _serializer)!,
JsonSerializer.Deserialize<ScheduleLimits>((string)row.limits, _serializer)!,
JsonSerializer.Deserialize<System.Collections.Immutable.ImmutableArray<string>>((string)row.subscribers, _serializer),
DateTime.SpecifyKind(row.created_at, DateTimeKind.Utc),
(string)row.created_by,
DateTime.SpecifyKind(row.updated_at, DateTimeKind.Utc),
(string)row.updated_by,
(string?)row.schema_version);
}
}

View File

@@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Infrastructure.Postgres;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
namespace StellaOps.Scheduler.Storage.Postgres;
@@ -34,6 +35,11 @@ public static class ServiceCollectionExtensions
services.AddScoped<IJobHistoryRepository, JobHistoryRepository>();
services.AddScoped<IMetricsRepository, MetricsRepository>();
services.AddScoped<IGraphJobRepository, GraphJobRepository>();
services.AddScoped<IRunRepository, RunRepository>();
services.AddScoped<IScheduleRepository, ScheduleRepository>();
services.AddScoped<IImpactSnapshotRepository, ImpactSnapshotRepository>();
services.AddScoped<IPolicyRunJobRepository, PolicyRunJobRepository>();
services.AddSingleton<IRunSummaryService, RunSummaryService>();
return services;
}

View File

@@ -6,7 +6,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
using StellaOps.Scheduler.Worker.Events;
using StellaOps.Scheduler.Worker.Observability;

View File

@@ -5,7 +5,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Options;
namespace StellaOps.Scheduler.Worker.Graph;

View File

@@ -4,7 +4,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;
using StellaOps.Scheduler.Worker.Options;

View File

@@ -5,7 +5,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Options;
namespace StellaOps.Scheduler.Worker.Graph;

View File

@@ -4,7 +4,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;
using StellaOps.Scheduler.Worker.Options;

View File

@@ -1,7 +1,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Options;
namespace StellaOps.Scheduler.Worker.Planning;

View File

@@ -2,7 +2,7 @@ using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Observability;

View File

@@ -6,7 +6,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Options;
namespace StellaOps.Scheduler.Worker.Policy;

View File

@@ -4,7 +4,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Observability;
using StellaOps.Scheduler.Worker.Options;

View File

@@ -1,7 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.WebService.GraphJobs;
using Xunit;

View File

@@ -1,7 +1,7 @@
using System;
using System.Linq;
using System.Threading;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Repositories;

View File

@@ -1,6 +1,6 @@
using System;
using System.Threading;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Repositories;

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Repositories;

View File

@@ -1,6 +1,6 @@
using System;
using System.Threading;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Repositories;

View File

@@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Services;

View File

@@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Services;

View File

@@ -8,7 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.WebService.PolicySimulations;
using Xunit;

View File

@@ -10,7 +10,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
namespace StellaOps.Scheduler.WebService.Tests;

View File

@@ -5,7 +5,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Graph;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;

View File

@@ -5,7 +5,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Graph;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;

View File

@@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Driver;
using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Observability;

View File

@@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Planning;

View File

@@ -6,7 +6,7 @@ using MongoDB.Driver;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Policy;
using StellaOps.Scheduler.Worker.Observability;

View File

@@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Observability;
using StellaOps.Scheduler.Worker.Policy;

View File

@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Driver;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Queue;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
using StellaOps.Scheduler.Worker.Events;