|
|
|
|
@@ -1,10 +1,23 @@
|
|
|
|
|
# [DEF:backend.src.api.routes.migration:Module]
|
|
|
|
|
# @TIER: STANDARD
|
|
|
|
|
# @SEMANTICS: api, migration, dashboards
|
|
|
|
|
# @PURPOSE: API endpoints for migration operations.
|
|
|
|
|
# @LAYER: API
|
|
|
|
|
# @RELATION: DEPENDS_ON -> backend.src.dependencies
|
|
|
|
|
# @RELATION: DEPENDS_ON -> backend.src.models.dashboard
|
|
|
|
|
# @TIER: CRITICAL
|
|
|
|
|
# @SEMANTICS: api, migration, dashboards, sync, dry-run
|
|
|
|
|
# @PURPOSE: HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints.
|
|
|
|
|
# @LAYER: Infra
|
|
|
|
|
# @RELATION: [DEPENDS_ON] ->[backend.src.dependencies]
|
|
|
|
|
# @RELATION: [DEPENDS_ON] ->[backend.src.core.database]
|
|
|
|
|
# @RELATION: [DEPENDS_ON] ->[backend.src.core.superset_client]
|
|
|
|
|
# @RELATION: [DEPENDS_ON] ->[backend.src.core.migration.dry_run_orchestrator]
|
|
|
|
|
# @RELATION: [DEPENDS_ON] ->[backend.src.core.mapping_service]
|
|
|
|
|
# @RELATION: [DEPENDS_ON] ->[backend.src.models.dashboard]
|
|
|
|
|
# @RELATION: [DEPENDS_ON] ->[backend.src.models.mapping]
|
|
|
|
|
# @INVARIANT: Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.
|
|
|
|
|
# @TEST_CONTRACT: [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]
|
|
|
|
|
# @TEST_SCENARIO: [invalid_environment] -> [HTTP_400_or_404]
|
|
|
|
|
# @TEST_SCENARIO: [valid_execution] -> [success_payload_with_required_fields]
|
|
|
|
|
# @TEST_EDGE: [missing_field] ->[HTTP_400]
|
|
|
|
|
# @TEST_EDGE: [invalid_type] ->[validation_error]
|
|
|
|
|
# @TEST_EDGE: [external_fail] ->[HTTP_500]
|
|
|
|
|
# @TEST_INVARIANT: [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
|
@@ -21,11 +34,11 @@ from ...models.mapping import ResourceMapping
|
|
|
|
|
router = APIRouter(prefix="/api", tags=["migration"])
|
|
|
|
|
|
|
|
|
|
# [DEF:get_dashboards:Function]
|
|
|
|
|
# @PURPOSE: Fetch all dashboards from the specified environment for the grid.
|
|
|
|
|
# @PRE: Environment ID must be valid.
|
|
|
|
|
# @POST: Returns a list of dashboard metadata.
|
|
|
|
|
# @PARAM: env_id (str) - The ID of the environment to fetch from.
|
|
|
|
|
# @RETURN: List[DashboardMetadata]
|
|
|
|
|
# @PURPOSE: Fetch dashboard metadata from a requested environment for migration selection UI.
|
|
|
|
|
# @PRE: env_id is provided and exists in configured environments.
|
|
|
|
|
# @POST: Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.
|
|
|
|
|
# @SIDE_EFFECT: Reads environment configuration and performs remote Superset metadata retrieval over network.
|
|
|
|
|
# @DATA_CONTRACT: Input[str env_id] -> Output[List[DashboardMetadata]]
|
|
|
|
|
@router.get("/environments/{env_id}/dashboards", response_model=List[DashboardMetadata])
|
|
|
|
|
async def get_dashboards(
|
|
|
|
|
env_id: str,
|
|
|
|
|
@@ -44,11 +57,11 @@ async def get_dashboards(
|
|
|
|
|
# [/DEF:get_dashboards:Function]
|
|
|
|
|
|
|
|
|
|
# [DEF:execute_migration:Function]
|
|
|
|
|
# @PURPOSE: Execute the migration of selected dashboards.
|
|
|
|
|
# @PRE: Selection must be valid and environments must exist.
|
|
|
|
|
# @POST: Starts the migration task and returns the task ID.
|
|
|
|
|
# @PARAM: selection (DashboardSelection) - The dashboards to migrate.
|
|
|
|
|
# @RETURN: Dict - {"task_id": str, "message": str}
|
|
|
|
|
# @PURPOSE: Validate migration selection and enqueue asynchronous migration task execution.
|
|
|
|
|
# @PRE: DashboardSelection payload is valid and both source/target environments exist.
|
|
|
|
|
# @POST: Returns {"task_id": str, "message": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.
|
|
|
|
|
# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs.
|
|
|
|
|
# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]]
|
|
|
|
|
@router.post("/migration/execute")
|
|
|
|
|
async def execute_migration(
|
|
|
|
|
selection: DashboardSelection,
|
|
|
|
|
@@ -86,9 +99,11 @@ async def execute_migration(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# [DEF:dry_run_migration:Function]
|
|
|
|
|
# @PURPOSE: Build pre-flight diff and risk summary without applying migration.
|
|
|
|
|
# @PRE: Selection and environments are valid.
|
|
|
|
|
# @POST: Returns deterministic JSON diff and risk scoring.
|
|
|
|
|
# @PURPOSE: Build pre-flight migration diff and risk summary without mutating target systems.
|
|
|
|
|
# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.
|
|
|
|
|
# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.
|
|
|
|
|
# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API.
|
|
|
|
|
# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]]
|
|
|
|
|
@router.post("/migration/dry-run", response_model=Dict[str, Any])
|
|
|
|
|
async def dry_run_migration(
|
|
|
|
|
selection: DashboardSelection,
|
|
|
|
|
@@ -123,7 +138,11 @@ async def dry_run_migration(
|
|
|
|
|
# [/DEF:dry_run_migration:Function]
|
|
|
|
|
|
|
|
|
|
# [DEF:get_migration_settings:Function]
|
|
|
|
|
# @PURPOSE: Get current migration Cron string explicitly.
|
|
|
|
|
# @PURPOSE: Read and return configured migration synchronization cron expression.
|
|
|
|
|
# @PRE: Configuration store is available and requester has READ permission.
|
|
|
|
|
# @POST: Returns {"cron": str} reflecting current persisted settings value.
|
|
|
|
|
# @SIDE_EFFECT: Reads configuration from config manager.
|
|
|
|
|
# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]]
|
|
|
|
|
@router.get("/migration/settings", response_model=Dict[str, str])
|
|
|
|
|
async def get_migration_settings(
|
|
|
|
|
config_manager=Depends(get_config_manager),
|
|
|
|
|
@@ -136,7 +155,11 @@ async def get_migration_settings(
|
|
|
|
|
# [/DEF:get_migration_settings:Function]
|
|
|
|
|
|
|
|
|
|
# [DEF:update_migration_settings:Function]
|
|
|
|
|
# @PURPOSE: Update migration Cron string.
|
|
|
|
|
# @PURPOSE: Validate and persist migration synchronization cron expression update.
|
|
|
|
|
# @PRE: Payload includes "cron" key and requester has WRITE permission.
|
|
|
|
|
# @POST: Returns {"cron": str, "status": "updated"} and persists updated cron value.
|
|
|
|
|
# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager.
|
|
|
|
|
# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]]
|
|
|
|
|
@router.put("/migration/settings", response_model=Dict[str, str])
|
|
|
|
|
async def update_migration_settings(
|
|
|
|
|
payload: Dict[str, str],
|
|
|
|
|
@@ -157,7 +180,11 @@ async def update_migration_settings(
|
|
|
|
|
# [/DEF:update_migration_settings:Function]
|
|
|
|
|
|
|
|
|
|
# [DEF:get_resource_mappings:Function]
|
|
|
|
|
# @PURPOSE: Fetch synchronized object mappings with search, filtering, and pagination.
|
|
|
|
|
# @PURPOSE: Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.
|
|
|
|
|
# @PRE: skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.
|
|
|
|
|
# @POST: Returns {"items": [...], "total": int} where items reflect applied filters and pagination.
|
|
|
|
|
# @SIDE_EFFECT: Executes database read queries against ResourceMapping table.
|
|
|
|
|
# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]]
|
|
|
|
|
@router.get("/migration/mappings-data", response_model=Dict[str, Any])
|
|
|
|
|
async def get_resource_mappings(
|
|
|
|
|
skip: int = Query(0, ge=0),
|
|
|
|
|
@@ -203,9 +230,11 @@ async def get_resource_mappings(
|
|
|
|
|
# [/DEF:get_resource_mappings:Function]
|
|
|
|
|
|
|
|
|
|
# [DEF:trigger_sync_now:Function]
|
|
|
|
|
# @PURPOSE: Triggers an immediate ID synchronization for all environments.
|
|
|
|
|
# @PRE: At least one environment must be configured.
|
|
|
|
|
# @POST: Environment rows are ensured in DB; sync_environment is called for each.
|
|
|
|
|
# @PURPOSE: Trigger immediate ID synchronization for every configured environment.
|
|
|
|
|
# @PRE: At least one environment is configured and requester has EXECUTE permission.
|
|
|
|
|
# @POST: Returns sync summary with synced/failed counts after attempting all environments.
|
|
|
|
|
# @SIDE_EFFECT: Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.
|
|
|
|
|
# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]]
|
|
|
|
|
@router.post("/migration/sync-now", response_model=Dict[str, Any])
|
|
|
|
|
async def trigger_sync_now(
|
|
|
|
|
config_manager=Depends(get_config_manager),
|
|
|
|
|
|