packages feed

clickhouse-migrate-cli-0.1.0.0: src/ClickHouseMigrate/CLI/Handler.hs

-- | Dispatch with application-owned configuration, sinks and command policy.
module ClickHouseMigrate.CLI.Handler (commandEnvironment, handleCommand, planReport, quiescenceForCommand) where

import ClickHouseMigrate
import ClickHouseMigrate.Adoption qualified as A
import ClickHouseMigrate.CLI.AdoptionFile
import ClickHouseMigrate.CLI.Parser (commandName, isMutation)
import ClickHouseMigrate.CLI.Types
import ClickHouseMigrate.Event qualified as E
import ClickHouseMigrate.ExclusiveAccess (ExclusiveAccess)
import ClickHouseMigrate.Inspection qualified as I
import ClickHouseMigrate.Ledger qualified as L
import ClickHouseMigrate.Prelude
import ClickHouseMigrate.Repair qualified as Repair
import ClickHouseMigrate.Report qualified as R
import ClickHouseMigrate.Runner qualified as Run
import Data.List.NonEmpty qualified as NE
import Data.Set qualified as Set

commandEnvironment :: Maybe TargetConfig -> Maybe ExclusiveAccess -> E.EventSink -> E.DiagnosticPolicy -> CommandPolicy -> CommandEnvironment
commandEnvironment = CommandEnvironment

planReport :: MigrationPlan -> [PlanComponent]
planReport plan =
  [ PlanComponent
      (componentNameText (componentViewName component))
      (map componentNameText (Set.toAscList (componentDependencies component)))
      [ PlanMigration
          (migrationNameText (migrationIdName (migrationId migration)))
          (migrationDescription migration)
          (migrationChecksumHex (migrationChecksum migration))
          [PlanStep (stepNameText (stepViewName step)) (stepChecksumHex (stepChecksum step)) | step <- NE.toList (migrationSteps migration)]
      | migration <- NE.toList (componentMigrations component)
      ]
  | component <- NE.toList (inspectPlan plan)
  ]

makeAudit :: AuditInput -> Either CommandError L.AuditContext
makeAudit (AuditInput actor reason refs) = either (const (Left InvalidAuditInput)) Right (L.auditContext actor reason refs)

makeQuiet :: TargetKey -> AuditInput -> Bool -> Either CommandError (Maybe L.QuiescenceEvidence)
makeQuiet _ _ False = Right Nothing
makeQuiet key input@(AuditInput _ _ refs) True = do
  unless (not (null refs)) (Left RecoveryEvidenceRequired)
  audit <- makeAudit input
  Just <$> either (const (Left InvalidAuditInput)) Right (L.attestQuiescence key audit)

-- Applications may use this to construct a conservative development provider.
-- It does not prove exclusion/drain; the assertion remains the caller's duty.
quiescenceForCommand :: TargetKey -> Command -> Either CommandError (Maybe L.QuiescenceEvidence)
quiescenceForCommand actual = \case
  UpCommand (UpInput expected audit _ quiet) -> match expected >> makeQuiet actual audit quiet
  RepairCommand _ (RepairInput expected _ _ _ audit quiet _) -> match expected >> required audit quiet
  AdoptCommand _ (AdoptionInput expected _ _ audit quiet _ _) -> match expected >> required audit quiet
  _ -> Right Nothing
  where
    match expected = unless (actual == expected) (Left ExpectedTargetMismatch)
    required audit quiet = do
      unless quiet (Left QuiescenceRequired)
      makeQuiet actual audit quiet

data Ready = Ready !TargetConfig !(Maybe ExclusiveAccess) !E.EventSink !E.DiagnosticPolicy

prepare :: CommandEnvironment -> Command -> Either CommandError Ready
prepare (CommandEnvironment configured provider sink diagnostics policy) command = do
  when (isMutation command && policy /= AdministrativeCommands) (Left CommandNotAllowed)
  config <- maybe (Left MissingTargetConfiguration) Right configured
  when (isMutation command && isNothing provider) (Left MissingExclusiveAccess)
  let key = configuredTargetKey config
  _ <- quiescenceForCommand key command
  case command of
    UpCommand (UpInput _ audit _ _) -> void (makeAudit audit)
    RepairCommand _ (RepairInput _ _ _ _ audit _ _) -> void (makeAudit audit)
    AdoptCommand _ (AdoptionInput _ _ _ audit _ _ file) -> do
      void (makeAudit audit)
      when (null file || '\0' `elem` file) (Left InvalidAdoptionInput)
    _ -> pure ()
  pure (Ready config provider sink diagnostics)

inspection :: Maybe ExclusiveAccess -> Bool -> Either CommandError I.InspectionOptions
inspection _ False = Right I.Observe
inspection provider True = maybe (Left MissingExclusiveAccess) (Right . I.Guarded) provider

-- Offline plan deliberately does not force the environment argument.
handleCommand :: CommandEnvironment -> MigrationPlan -> Command -> IO CommandResult
handleCommand _ plan PlanCommand = pure (CommandResult "plan" (Right (PlanReport (planReport plan) [(componentNameText (migrationIdComponent identity), migrationNameText (migrationIdName identity)) | view <- NE.toList (inspectSchedule plan), let identity = migrationId view])))
handleCommand environment plan command =
  let name = commandName command
      reject = pure . CommandResult name . Left . CommandRejected
      finish = pure . CommandResult name
   in case prepare environment command of
        Left failure -> reject failure
        Right (Ready config provider sink diagnostics) ->
          let readWith guarded action = case inspection provider guarded of Left failure -> reject failure; Right options -> action options >>= finish
              mutate action = case provider of Nothing -> reject MissingExclusiveAccess; Just access -> action (Run.mutationContext config access sink diagnostics)
           in case command of
                StatusCommand guarded -> readWith guarded (\options -> fmap (either (Left . InspectionFailed) (Right . StatusReport)) (I.status options config plan))
                HistoryCommand guarded -> readWith guarded (\options -> fmap (either (Left . InspectionFailed) (Right . HistoryReport)) (I.history options config))
                VerifyCommand current guarded -> readWith guarded (\options -> fmap (either (Left . InspectionFailed) (Right . VerificationReport)) (I.verify options config (I.verifyOptions current) plan))
                UpCommand (UpInput target input initialization _) -> case makeAudit input of
                  Left failure -> reject failure
                  Right audit -> mutate $ \context -> Run.up context (Run.upOptions audit (fmap (\ledger -> L.authorizeInitialization target ledger audit) initialization)) plan >>= finish . either (Left . RunFailed) (Right . RunReport)
                RepairCommand operation (RepairInput _ expected attempt selected input quiet receipts) -> case recovery config input quiet receipts of
                  Left failure -> reject failure
                  Right evidence -> case buildRepair operation expected attempt selected evidence of
                    Left failure -> reject failure
                    Right request -> mutate $ \context -> Repair.repair context plan request >>= finish . either (Left . RunFailed) (Right . RunReport)
                AdoptCommand operation (AdoptionInput target expected initialization input quiet guarded file) -> case makeAdoption target expected initialization input quiet of
                  Left failure -> reject failure
                  Right (state, authorization, established, audit) -> do
                    loaded <- readAdoptionInput file
                    case loaded >>= decodeAdoptionInput target state authorization established audit of
                      Left failure -> reject failure
                      Right request -> case inspection provider guarded of
                        Left failure -> reject failure
                        Right options -> do
                          observed <- I.status options config plan
                          case observed of
                            Left failure -> finish (Left (InspectionFailed failure))
                            Right snapshot -> case A.planAdoption plan snapshot request of
                              Left failure -> let R.InspectionReport observedHistory _ _ = snapshot in finish (Left (AdoptionRejected failure observedHistory))
                              Right adoption -> case operation of
                                PlanAdoption -> let R.InspectionReport observedHistory _ _ = snapshot in finish (Right (AdoptionPlanReport observedHistory (A.inspectAdoption adoption) (A.adoptionEntries adoption)))
                                ApplyAdoption -> mutate $ \context -> A.applyAdoption context plan adoption >>= finish . either (Left . RunFailed) (Right . RunReport)
                                ResumeAdoption -> mutate $ \context -> A.resumeAdoption context plan adoption >>= finish . either (Left . RunFailed) (Right . RunReport)

recovery :: TargetConfig -> AuditInput -> Bool -> [L.AppendReceipt] -> Either CommandError Repair.RecoveryEvidence
recovery config input quiet receipts = do
  established <- makeQuiet (configuredTargetKey config) input quiet >>= maybe (Left QuiescenceRequired) Right
  audit <- makeAudit input
  either (const (Left InvalidRepairInput)) Right (Repair.recoveryEvidence audit established receipts)

buildRepair :: RepairOperation -> L.LedgerPosition -> L.AttemptId -> Maybe R.StepAttempt -> Repair.RecoveryEvidence -> Either CommandError Repair.RepairRequest
buildRepair operation expected attempt selected evidence = either (const (Left InvalidRepairInput)) Right $ case (operation, selected) of
  (Resume, Nothing) -> Repair.resumeRequest expected attempt evidence
  (Finalize, Nothing) -> Repair.finalizeRequest expected attempt evidence
  (RetryStep, Just step) -> Repair.retryStepRequest expected attempt step evidence
  (AcceptStep, Just step) -> Repair.acceptStepRequest expected attempt step evidence
  _ -> Left Repair.InvalidStepAttempt

makeAdoption :: TargetKey -> ExpectedInput -> Maybe LedgerId -> AuditInput -> Bool -> Either CommandError (L.ExpectedLedgerState, Maybe L.InitializationAuthorization, L.QuiescenceEvidence, L.AuditContext)
makeAdoption target expected initialization input quiet = do
  established <- makeQuiet target input quiet >>= maybe (Left QuiescenceRequired) Right
  audit <- makeAudit input
  let state = case expected of AbsentInput -> L.ExpectAbsent target; PositionInput position -> L.ExpectPosition position
      authorization = fmap (\ledger -> L.authorizeInitialization target ledger audit) initialization
  when (case expected of AbsentInput -> isNothing authorization; _ -> False) (Left InitializationRequired)
  pure (state, authorization, established, audit)