Skip to content

Commit 0a921f4

Browse files
author
MPCoreDeveloper
committed
Finalize multitenancy phase 13 and mark resolved issues
1 parent eb3ff36 commit 0a921f4

17 files changed

+1321
-7
lines changed

.github/issue-drafts/multitenancy-v1.6.0/00-epic-multitenant-saas-hardening-v1.6.0.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
Deliver first-class multi-tenant SaaS support in `SharpCoreDB.Server` on top of current `v1.6.0` capabilities.
33

44
## Status
5-
Implemented in workspace:
5+
**State:** RESOLVED for the current multitenancy draft set in the repository.
6+
7+
Resolved workstreams in repository:
68
- [x] `09-tenant-encryption-key-management.md`
79
- [x] `10-per-tenant-quotas-and-limits.md`
810
- [x] `11-tenant-audit-and-security-events.md`
911
- [x] `12-saas-sample-docs-and-threat-model.md`
12+
- [x] `13-tenant-ops-backup-restore-migrate.md`
1013

11-
Next planned work:
12-
- [ ] `13-tenant-ops-backup-restore-migrate.md`
14+
Roadmap note:
15+
- [x] Multitenancy roadmap implementation complete in the current draft set.
1316

1417
Current baseline in codebase:
1518
- Multi-database hosting in one server instance is available (`DatabaseRegistry`, `ServerConfiguration.Databases`).

.github/issue-drafts/multitenancy-v1.6.0/09-tenant-encryption-key-management.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
Introduce per-tenant encryption key integration and rotation hooks.
33

44
## Status
5-
Completed in workspace.
5+
**State:** RESOLVED
6+
7+
Completed in workspace and committed/pushed to repository.
68

79
## Completed Implementation Notes
810
- Added `ITenantEncryptionKeyProvider` and `ConfigurationTenantEncryptionKeyProvider`.

.github/issue-drafts/multitenancy-v1.6.0/10-per-tenant-quotas-and-limits.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
Add configurable per-tenant quotas and limit enforcement.
33

44
## Status
5-
Completed in workspace.
5+
**State:** RESOLVED
6+
7+
Completed in workspace and committed/pushed to repository.
68

79
## Completed Implementation Notes
810
- Added `TenantQuotaPolicy` model and tenant quota catalog persistence.

.github/issue-drafts/multitenancy-v1.6.0/11-tenant-audit-and-security-events.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
Implement tenant-aware audit trail and security event logging.
33

44
## Status
5-
Completed in workspace.
5+
**State:** RESOLVED
6+
7+
Completed in workspace and committed/pushed to repository.
68

79
## Completed Implementation Notes
810
- Added tenant security audit event model, in-memory store, sink abstraction, and emission service.

.github/issue-drafts/multitenancy-v1.6.0/12-saas-sample-docs-and-threat-model.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
Publish a complete SaaS reference sample and documentation set for multi-tenant deployments.
33

44
## Status
5-
Completed in workspace.
5+
**State:** RESOLVED
6+
7+
Completed in workspace and committed/pushed to repository.
68

79
## Completed Implementation Notes
810
- Added a multi-tenant SaaS reference sample under `Examples/Server/SharpCoreDB.MultiTenantSaaSSample`.

.github/issue-drafts/multitenancy-v1.6.0/13-tenant-ops-backup-restore-migrate.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
## Summary
22
Add per-tenant operational tooling for backup, restore, and migration workflows.
33

4+
## Status
5+
**State:** RESOLVED
6+
7+
Completed in workspace. Pending git commit/push for the latest phase-13 changes in the current working tree.
8+
9+
## Completed Implementation Notes
10+
- Added tenant-scoped backup and restore operation contracts.
11+
- Implemented `TenantBackupRestoreService` with validation and rollback handling.
12+
- Added `TenantMigrationPlanningService` with migration steps and execution hooks.
13+
- Exposed tenant backup, restore, and migration-plan REST endpoints.
14+
- Published operational guidance in `docs/server/MULTITENANT_BACKUP_RESTORE_MIGRATION_v1.6.0.md`.
15+
16+
## Validation
17+
- Integration tests cover restore correctness and rollback behavior.
18+
- Workspace build passed.
19+
420
## Why
521
SaaS operations need tenant-scoped disaster recovery and move/maintenance workflows.
622

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# SharpCoreDB Server Tenant Backup Restore and Migration Guide v1.6.0
2+
3+
## Overview
4+
This guide documents tenant-scoped backup, restore, and migration procedures for database-per-tenant deployments.
5+
6+
## Goals
7+
- back up a single tenant without a broad outage
8+
- restore a single tenant with validation before activation
9+
- prepare migration cutover plans between server instances
10+
11+
## REST Endpoints
12+
- `POST /api/v1/tenants/{tenantId}/databases/{databaseName}/backup`
13+
- `POST /api/v1/tenants/{tenantId}/databases/{databaseName}/restore`
14+
- `POST /api/v1/tenants/{tenantId}/databases/{databaseName}/migration-plan`
15+
16+
## Backup Flow
17+
1. resolve tenant database mapping from the tenant catalog
18+
2. export the tenant database file to a backup artifact path
19+
3. record lifecycle events for start/completion/failure
20+
4. retain the artifact in durable operator storage
21+
22+
## Restore Flow
23+
1. resolve tenant database mapping
24+
2. create a rollback copy of the active tenant database when present
25+
3. drain and detach only the tenant database being restored
26+
4. copy the backup artifact to the target restore path
27+
5. validate the restored file by opening it before activation
28+
6. re-register the tenant database and update catalog location metadata if needed
29+
7. roll back to the original file if validation or registration fails
30+
31+
## Migration Planning
32+
Migration plans generate:
33+
- source database path and size metadata
34+
- suggested backup artifact path
35+
- ordered cutover steps
36+
- export, restore, and validation execution hooks for operator automation
37+
38+
## Operational Guidance
39+
- back up `master` separately before control-plane changes
40+
- export tenant backup artifacts to durable storage outside the server host
41+
- validate tenant isolation after restore or migration cutover
42+
- keep encryption key references available before restoring encrypted tenant databases

docs/server/MULTITENANT_OPERATIONS_RUNBOOK_v1.6.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## Scope
44
Operational guidance for tenant lifecycle, encryption, quotas, auditing, and incident response.
55

6+
See also:
7+
- `docs/server/MULTITENANT_BACKUP_RESTORE_MIGRATION_v1.6.0.md`
8+
- `docs/server/MULTITENANT_SAAS_REFERENCE_v1.6.0.md`
9+
610
## Daily Operations
711
- review `GET /api/v1/tenant-security/audit`
812
- review `GET /api/v1/tenant-access/audit`

src/SharpCoreDB.Server.Core/Api/TenantsController.cs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public sealed class TenantsController(
2424
TenantProvisioningService provisioningService,
2525
TenantEncryptionKeyRotationService keyRotationService,
2626
TenantQuotaEnforcementService quotaEnforcementService,
27+
TenantBackupRestoreService backupRestoreService,
28+
TenantMigrationPlanningService migrationPlanningService,
2729
TenantCatalogRepository catalogRepository,
2830
ILogger<TenantsController> logger) : ControllerBase
2931
{
@@ -454,6 +456,160 @@ public async Task<ActionResult<TenantQuotaApiResponse>> UpsertTenantQuotas(
454456
UpdatedAt = effective.UpdatedAt,
455457
});
456458
}
459+
460+
/// <summary>
461+
/// Creates a tenant-scoped backup export.
462+
/// POST /api/v1/tenants/{tenantId}/databases/{databaseName}/backup
463+
/// </summary>
464+
[HttpPost("{tenantId}/databases/{databaseName}/backup")]
465+
[ProducesResponseType(200)]
466+
[ProducesResponseType(400)]
467+
[ProducesResponseType(404)]
468+
public async Task<ActionResult<TenantBackupApiResponse>> CreateTenantBackup(
469+
string tenantId,
470+
string databaseName,
471+
[FromBody] CreateTenantBackupApiRequest request,
472+
CancellationToken cancellationToken = default)
473+
{
474+
if (string.IsNullOrWhiteSpace(request.BackupDirectory))
475+
{
476+
return BadRequest(new ErrorResponse { Message = "BackupDirectory is required" });
477+
}
478+
479+
var tenant = await catalogRepository.GetTenantByIdAsync(tenantId, cancellationToken);
480+
if (tenant is null)
481+
{
482+
return NotFound(new ErrorResponse { Message = "Tenant not found" });
483+
}
484+
485+
var operation = await backupRestoreService.CreateBackupAsync(
486+
tenantId,
487+
databaseName,
488+
request.BackupDirectory,
489+
request.IdempotencyKey ?? Guid.NewGuid().ToString("N"),
490+
cancellationToken);
491+
492+
if (operation.Status == TenantDataOperationStatus.Failed)
493+
{
494+
return StatusCode(500, new ErrorResponse { Message = operation.ErrorMessage ?? "Backup failed" });
495+
}
496+
497+
return Ok(new TenantBackupApiResponse
498+
{
499+
OperationId = operation.OperationId,
500+
TenantId = operation.TenantId,
501+
DatabaseName = operation.DatabaseName,
502+
BackupPath = operation.BackupPath,
503+
BackupSizeBytes = operation.BackupSizeBytes,
504+
Status = operation.Status.ToString(),
505+
StartedAt = operation.StartedAt,
506+
CompletedAt = operation.CompletedAt,
507+
});
508+
}
509+
510+
/// <summary>
511+
/// Restores a tenant database from a tenant-scoped backup export.
512+
/// POST /api/v1/tenants/{tenantId}/databases/{databaseName}/restore
513+
/// </summary>
514+
[HttpPost("{tenantId}/databases/{databaseName}/restore")]
515+
[ProducesResponseType(200)]
516+
[ProducesResponseType(400)]
517+
[ProducesResponseType(404)]
518+
public async Task<ActionResult<TenantRestoreApiResponse>> RestoreTenantBackup(
519+
string tenantId,
520+
string databaseName,
521+
[FromBody] RestoreTenantBackupApiRequest request,
522+
CancellationToken cancellationToken = default)
523+
{
524+
if (string.IsNullOrWhiteSpace(request.BackupPath))
525+
{
526+
return BadRequest(new ErrorResponse { Message = "BackupPath is required" });
527+
}
528+
529+
var tenant = await catalogRepository.GetTenantByIdAsync(tenantId, cancellationToken);
530+
if (tenant is null)
531+
{
532+
return NotFound(new ErrorResponse { Message = "Tenant not found" });
533+
}
534+
535+
var operation = await backupRestoreService.RestoreBackupAsync(
536+
tenantId,
537+
databaseName,
538+
request.BackupPath,
539+
request.TargetDatabasePath,
540+
request.IdempotencyKey ?? Guid.NewGuid().ToString("N"),
541+
cancellationToken);
542+
543+
if (operation.Status == TenantDataOperationStatus.Failed)
544+
{
545+
return StatusCode(500, new ErrorResponse { Message = operation.ErrorMessage ?? "Restore failed" });
546+
}
547+
548+
return Ok(new TenantRestoreApiResponse
549+
{
550+
OperationId = operation.OperationId,
551+
TenantId = operation.TenantId,
552+
DatabaseName = operation.DatabaseName,
553+
SourceBackupPath = operation.SourceBackupPath,
554+
RestoredDatabasePath = operation.RestoredDatabasePath,
555+
ValidationPassed = operation.ValidationPassed,
556+
RollbackApplied = operation.RollbackApplied,
557+
Status = operation.Status.ToString(),
558+
StartedAt = operation.StartedAt,
559+
CompletedAt = operation.CompletedAt,
560+
});
561+
}
562+
563+
/// <summary>
564+
/// Creates a migration plan for moving a tenant database to another server instance.
565+
/// POST /api/v1/tenants/{tenantId}/databases/{databaseName}/migration-plan
566+
/// </summary>
567+
[HttpPost("{tenantId}/databases/{databaseName}/migration-plan")]
568+
[ProducesResponseType(200)]
569+
[ProducesResponseType(400)]
570+
[ProducesResponseType(404)]
571+
public async Task<ActionResult<TenantMigrationPlanApiResponse>> CreateTenantMigrationPlan(
572+
string tenantId,
573+
string databaseName,
574+
[FromBody] CreateTenantMigrationPlanApiRequest request,
575+
CancellationToken cancellationToken = default)
576+
{
577+
if (string.IsNullOrWhiteSpace(request.TargetServer) || string.IsNullOrWhiteSpace(request.ExportDirectory))
578+
{
579+
return BadRequest(new ErrorResponse { Message = "TargetServer and ExportDirectory are required" });
580+
}
581+
582+
var tenant = await catalogRepository.GetTenantByIdAsync(tenantId, cancellationToken);
583+
if (tenant is null)
584+
{
585+
return NotFound(new ErrorResponse { Message = "Tenant not found" });
586+
}
587+
588+
var plan = await migrationPlanningService.CreateMigrationPlanAsync(
589+
tenantId,
590+
databaseName,
591+
request.TargetServer,
592+
request.ExportDirectory,
593+
cancellationToken);
594+
595+
return Ok(new TenantMigrationPlanApiResponse
596+
{
597+
PlanId = plan.PlanId,
598+
TenantId = plan.TenantId,
599+
DatabaseName = plan.DatabaseName,
600+
SourceDatabasePath = plan.SourceDatabasePath,
601+
TargetServer = plan.TargetServer,
602+
ExportArtifactPath = plan.ExportArtifactPath,
603+
DatabaseSizeBytes = plan.DatabaseSizeBytes,
604+
Steps = plan.Steps,
605+
ExportHook = plan.ExecutionHooks.ExportHook,
606+
RestoreHook = plan.ExecutionHooks.RestoreHook,
607+
ValidationHook = plan.ExecutionHooks.ValidationHook,
608+
CreatedAt = plan.CreatedAt,
609+
});
610+
}
611+
612+
// ...existing code...
457613
}
458614

459615
// API Request/Response DTOs
@@ -629,3 +785,82 @@ public sealed class TenantQuotaApiResponse
629785
public required int MaxBatchSize { get; init; }
630786
public required DateTime? UpdatedAt { get; init; }
631787
}
788+
789+
/// <summary>
790+
/// Request to create a tenant backup.
791+
/// </summary>
792+
public sealed class CreateTenantBackupApiRequest
793+
{
794+
public required string BackupDirectory { get; init; }
795+
public string? IdempotencyKey { get; init; }
796+
}
797+
798+
/// <summary>
799+
/// Response from tenant backup request.
800+
/// </summary>
801+
public sealed class TenantBackupApiResponse
802+
{
803+
public required string OperationId { get; init; }
804+
public required string TenantId { get; init; }
805+
public required string DatabaseName { get; init; }
806+
public required string BackupPath { get; init; }
807+
public required long BackupSizeBytes { get; init; }
808+
public required string Status { get; init; }
809+
public required DateTime StartedAt { get; init; }
810+
public required DateTime? CompletedAt { get; init; }
811+
}
812+
813+
/// <summary>
814+
/// Request to restore a tenant backup.
815+
/// </summary>
816+
public sealed class RestoreTenantBackupApiRequest
817+
{
818+
public required string BackupPath { get; init; }
819+
public string? TargetDatabasePath { get; init; }
820+
public string? IdempotencyKey { get; init; }
821+
}
822+
823+
/// <summary>
824+
/// Response from tenant restore request.
825+
/// </summary>
826+
public sealed class TenantRestoreApiResponse
827+
{
828+
public required string OperationId { get; init; }
829+
public required string TenantId { get; init; }
830+
public required string DatabaseName { get; init; }
831+
public required string SourceBackupPath { get; init; }
832+
public required string RestoredDatabasePath { get; init; }
833+
public required bool ValidationPassed { get; init; }
834+
public required bool RollbackApplied { get; init; }
835+
public required string Status { get; init; }
836+
public required DateTime StartedAt { get; init; }
837+
public required DateTime? CompletedAt { get; init; }
838+
}
839+
840+
/// <summary>
841+
/// Request to create a tenant migration plan.
842+
/// </summary>
843+
public sealed class CreateTenantMigrationPlanApiRequest
844+
{
845+
public required string TargetServer { get; init; }
846+
public required string ExportDirectory { get; init; }
847+
}
848+
849+
/// <summary>
850+
/// Response from tenant migration plan request.
851+
/// </summary>
852+
public sealed class TenantMigrationPlanApiResponse
853+
{
854+
public required string PlanId { get; init; }
855+
public required string TenantId { get; init; }
856+
public required string DatabaseName { get; init; }
857+
public required string SourceDatabasePath { get; init; }
858+
public required string TargetServer { get; init; }
859+
public required string ExportArtifactPath { get; init; }
860+
public required long DatabaseSizeBytes { get; init; }
861+
public required string[] Steps { get; init; }
862+
public required string ExportHook { get; init; }
863+
public required string RestoreHook { get; init; }
864+
public required string ValidationHook { get; init; }
865+
public required DateTime CreatedAt { get; init; }
866+
}

0 commit comments

Comments
 (0)