glean/tools/gleancli/GleanCLI.hs (401 lines of code) (raw):

{- Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. -} {-# LANGUAGE CPP, ApplicativeDo, TypeApplications, AllowAmbiguousTypes #-} module GleanCLI (main) where import Control.Exception import Control.Monad import qualified Data.Bifunctor import qualified Data.ByteString as B import Data.Default import Data.Foldable import qualified Data.HashMap.Strict as HashMap import qualified Data.Map.Strict as Map import Data.List (isInfixOf) import Data.List.Split import Data.Proxy import Data.Text (Text) import qualified Data.Text as Text import Options.Applicative import System.IO import System.Environment import Util.Control.Exception import Util.EventBase import Util.IO import Util.OptParse import System.Exit (exitWith, ExitCode(..)) import qualified Glean hiding (options) import Glean.Init import qualified Glean.LocalOrRemote as Glean import qualified Glean.Database.Work as Database import Glean.Schema.Util import Glean.Types as Thrift hiding (ValidateSchema) import qualified Glean.Types as Thrift import Glean.Impl.ConfigProvider import Glean.Util.ConfigProvider import Glean.Util.ShellPrint import Glean.Shell import GleanCLI.Common import GleanCLI.Complete import GleanCLI.Derive import GleanCLI.Restore import GleanCLI.Query import GleanCLI.Types import GleanCLI.Write #if FACEBOOK import GleanCLI.Facebook #endif data Config = Config { cfgService :: Glean.Service , cfgCommand :: PluginCommand } data PluginType where PluginType :: forall c . Plugin c => Proxy c -> PluginType plugin :: forall c . Plugin c => PluginType plugin = PluginType (Proxy @c) data PluginCommand where PluginCommand :: forall c . Plugin c => c -> PluginCommand plugins :: [PluginType] plugins = [ plugin @WriteCommand , plugin @FinishCommand , plugin @UnfinishCommand , plugin @ListCommand , plugin @StatusCommand , plugin @DumpCommand , plugin @DeleteCommand , plugin @DeriveCommand , plugin @QueryCommand , plugin @RestoreCommand , plugin @ValidateCommand , plugin @ValidateSchemaCommand , plugin @StatsCommand , plugin @PropertiesCommand , plugin @OwnershipCommand , plugin @SetPropertyCommand , plugin @WriteSerializedInventoryCommand , plugin @ShellCommand , plugin @CompleteCommand #if FACEBOOK , plugin @FacebookPlugin #endif ] options :: ParserInfo (Config, ConfigOptions ConfigAPI) options = info (((,) <$> parser <*> configOptions) <**> helper <**> helpAll) (fullDesc <> progDesc "Create, manipulate and query Glean databases") where #if MIN_VERSION_optparse_applicative(0,16,0) parseError = ShowHelpText Nothing #else parseError = ShowHelpText #endif helpAll :: Parser (a -> a) helpAll = abortOption parseError $ mconcat [ long "help-all" , help "Show all possible options." , hidden ] parser :: Parser Config parser = do cfgService <- Glean.options cfgCommand <- asum [ PluginCommand <$> parseCommand @c | PluginType (Proxy :: Proxy c) <- plugins ] return Config{..} main :: IO () main = do ((Config{..}, cfgOpts), leftOverArgs) <- do args <- getArgs r <- tryAll $ partialParse (prefs subparserInline) options args case r of Left e -> withGflags ["--help" | "--help-all" <- args] $ throwIO e Right r -> return r case cfgCommand of PluginCommand c -> withGflags (argTransform c leftOverArgs) $ withEventBaseDataplane $ \evb -> withConfigProvider cfgOpts $ \cfgAPI -> withService evb cfgAPI cfgService c -- ----------------------------------------------------------------------------- -- Commands -- A few small commands that don't deserve their own modules. data UnfinishCommand = Unfinish { repo :: Repo , handle :: Text } instance Plugin UnfinishCommand where parseCommand = commandParser "unfinish" (progDesc $ "Unfinish a local database "<> "(turn it from complete to incomplete state)") $ do repo <- repoOpts handle <- handleOpt return Unfinish{..} runCommand _ _ backend Unfinish{..} = do case Glean.backendKind backend of Glean.BackendEnv env -> do Database.unfinishDatabase env repo handle _ -> die 5 "It is NOT possible to unfinish a remote database" data ListCommand = List { listDbNames :: [String] , listFormat :: Maybe ShellPrintFormat } instance Plugin ListCommand where parseCommand = commandParser "list" (progDesc "List databases which match REPONAME") $ do listDbNames <- many $ strArgument (metavar "REPONAME") listFormat <- shellFormatOpt return List{..} runCommand _ _ backend List{..} = do r <- Glean.listDatabases backend def let repoFilter db str = str `isInfixOf` Glean.showRepo repo where repo = Thrift.database_repo db xs = Thrift.listDatabasesResult_databases r dbs = filter f xs f db = null listDbNames || any (repoFilter db) listDbNames putShellPrintLn listFormat $ dbs `withFormatOpts` DbSummarise data StatusCommand = Status { statusRepo :: Repo , statusFormat :: Maybe ShellPrintFormat , statusSetExitCode :: Bool } instance Plugin StatusCommand where parseCommand = commandParser "status" (progDesc "Get the status of a db") $ do statusRepo <- repoOpts statusFormat <- shellFormatOpt statusSetExitCode <- switch ( short 'e' <> help "Set an error status code if the db is not complete" ) return Status{..} runCommand _ _ backend Status{..} = do db <- Thrift.getDatabaseResult_database <$> Glean.getDatabase backend statusRepo putShellPrintLn statusFormat $ db `withFormatOpts` DbSummarise when statusSetExitCode $ case exitCode db of ExitFailure code -> exitWith $ ExitFailure code _ -> return () where exitCode db = case Thrift.database_status db of Thrift.DatabaseStatus_Complete -> ExitSuccess Thrift.DatabaseStatus_Incomplete -> ExitFailure 101 Thrift.DatabaseStatus_Restoring -> ExitFailure 102 Thrift.DatabaseStatus_Broken -> ExitFailure 103 Thrift.DatabaseStatus_Restorable -> ExitFailure 104 Thrift.DatabaseStatus_Finalizing -> ExitFailure 105 Thrift.DatabaseStatus_Missing -> ExitFailure 106 data PropertiesCommand = Properties { propertiesRepo :: Repo , propertiesFormat :: Maybe ShellPrintFormat } instance Plugin PropertiesCommand where parseCommand = commandParser "properties" (progDesc "Get the properties of a db") $ do propertiesRepo <- repoOpts propertiesFormat <- shellFormatOpt return Properties{..} runCommand _ _ backend Properties{..} = do db <- Thrift.getDatabaseResult_database <$> Glean.getDatabase backend propertiesRepo putShellPrintLn propertiesFormat $ Thrift.database_properties db data DumpCommand = Dump { dumpRepo :: Repo , dumpFile :: FilePath } instance Plugin DumpCommand where parseCommand = commandParser "dump" (progDesc "Dump the contents of the specified database into a file") $ do dumpRepo <- repoOpts dumpFile <- strArgument ( metavar "FILE" <> help "Destination file path" ) return Dump{..} runCommand _ _ backend Dump{..} = Glean.dumpJsonToFile backend dumpRepo dumpFile data DeleteCommand = Delete { deleteRepo :: Repo } instance Plugin DeleteCommand where parseCommand = commandParser "delete" (progDesc "Delete a database") $ do Delete <$> repoOpts runCommand _ _ backend Delete{..} = void $ Glean.deleteDatabase backend deleteRepo data ValidateCommand = Validate { validateRepo :: Repo , validate :: Glean.Validate } instance Plugin ValidateCommand where parseCommand = commandParser "validate" (progDesc "Validate a local database") $ do repo <- repoOpts no_typecheck <- switch ( long "no-typecheck" <> help "don't typecheck facts" ) no_keys <- switch ( long "no-keys" <> help "don't verify key uniqueness" ) limit <- optional $ option auto ( long "limit" <> metavar "N" <> help "only validate the first N facts" ) return Validate { validateRepo = repo , validate = def { Glean.validateTypecheck = not no_typecheck , Glean.validateKeys = not no_keys , Glean.validateLimit = limit } } runCommand _ _ backend Validate{..} = case Glean.backendKind backend of Glean.BackendEnv env -> Glean.validate env validateRepo validate _ -> die 2 "Can't validate a remote database" data ValidateSchemaCommand = ValidateSchema { file :: FilePath } instance Plugin ValidateSchemaCommand where parseCommand = commandParser "validate-schema" (progDesc "Validate a schema") $ do file <- strArgument ( metavar "FILE" <> help "Name of schema file" ) return (ValidateSchema file) runCommand _ _ backend ValidateSchema{..} = do str <- B.readFile file Glean.validateSchema backend (Thrift.ValidateSchema str) data StatsCommand = Stats { statsRepo :: Repo , perPredicate :: Bool , excludeBase :: Bool , statsPredicates :: [Text] , statsFormat :: Maybe ShellPrintFormat , statsSetExitCode :: Bool } instance Plugin StatsCommand where parseCommand = commandParser "stats" (progDesc "Get fact counts and sizes") $ do statsRepo <- repoOpts perPredicate <- switch ( long "per-predicate" ) excludeBase <- switch ( long "exclude-base" ) statsPredicates <- many $ strArgument (metavar "PREDICATE") statsFormat <- shellFormatOpt statsSetExitCode <- switch ( short 'e' <> help "Set an error status code if there are no facts for the predicate(s)" ) return Stats{..} runCommand _ _ backend Stats{..} = do xs <- Map.toList <$> Glean.predicateStats backend statsRepo (if excludeBase then Glean.ExcludeBase else Glean.IncludeBase) schemaInfo <- Glean.getSchemaInfo backend statsRepo let matchRefsArgs = parseRef <$> statsPredicates preds = map (Data.Bifunctor.first (lookupPid schemaInfo)) xs filterPred :: Either Thrift.Id PredicateRef -> Bool filterPred ref = (perPredicate && null matchRefsArgs) -- argument given and || any (refMatches ref) matchRefsArgs -- reference matches argument refMatches (Right lhs) rhs = predicateMatches lhs rhs refMatches (Left _) _ = False statsFormatOpts = StatsFormatOpts { showTotal = True, sortBySize = False } refIsPresent ref = any ((`refMatches` ref) . fst) preds matchRefsArgsNotPresent = filter (not . refIsPresent) matchRefsArgs putShellPrintLn statsFormat $ (filterPred, preds) `withFormatOpts` statsFormatOpts when (not $ null matchRefsArgsNotPresent) $ do forM_ matchRefsArgsNotPresent $ \ref -> do hPutStrLn stderr $ Text.unpack $ "No facts found for: " <> showSourceRef ref when statsSetExitCode $ exitWith $ ExitFailure 100 where lookupPid SchemaInfo{..} pid = maybe (Left pid) Right $ Map.lookup pid schemaInfo_predicateIds predicateMatches PredicateRef{..} SourceRef{..} = predicateRef_name == sourceRefName && maybe True (== predicateRef_version) sourceRefVersion data OwnershipCommand = Ownership { ownershipRepo :: Repo } instance Plugin OwnershipCommand where parseCommand = commandParser "ownership" (progDesc "") $ do ownershipRepo <- repoOpts return Ownership{..} runCommand _ _ backend Ownership{..} = case Glean.backendKind backend of Glean.BackendEnv env -> Glean.computeOwnership env ownershipRepo _ -> die 2 "Need local database to compute ownership" data SetPropertyCommand = SetProperty { setPropRepo :: Repo , properties :: [(Text,Text)] } instance Plugin SetPropertyCommand where parseCommand = commandParser "set-property" (progDesc "") $ do setPropRepo <- repoOpts properties <- many $ argument readOption (metavar "NAME=VALUE") return SetProperty{..} where readOption = maybeReader $ \s -> case splitOn "=" s of [name, value] -> Just (Text.pack name, Text.pack value) _ -> Nothing runCommand _ _ backend SetProperty{..} = void $ Glean.updateProperties backend setPropRepo (HashMap.fromList properties) [] data WriteSerializedInventoryCommand = WriteSerializedInventory { writeSerializedInventoryRepo :: Repo , outputFile :: FilePath } instance Plugin WriteSerializedInventoryCommand where parseCommand = commandParser "write-serialized-inventory" (progDesc "") $ do writeSerializedInventoryRepo <- repoOpts outputFile <- strArgument (metavar "OUTPUT_FILE") return WriteSerializedInventory{..} runCommand _ _ backend WriteSerializedInventory{..} = do inventory <- Glean.serializeInventory backend writeSerializedInventoryRepo withFile outputFile WriteMode $ \hdl -> do B.hPutStr hdl inventory