adds category managment to embassy-publish and admin API

This commit is contained in:
Keagan McClelland
2022-06-06 17:02:23 -06:00
parent f3b9e78fca
commit a2f7b97942
6 changed files with 170 additions and 76 deletions

View File

@@ -20,4 +20,6 @@
-- ADMIN API V0 -- ADMIN API V0
/admin/v0/upload PkgUploadR POST !admin /admin/v0/upload PkgUploadR POST !admin
/admin/v0/index PkgIndexR POST !admin /admin/v0/index PkgIndexR POST !admin
/admin/v0/deindex PkgDeindexR GET POST !admin /admin/v0/deindex PkgDeindexR GET POST !admin
/admin/v0/category/#Text CategoryR POST DELETE !admin
/admin/v0/categorize/#Text/#PkgId PkgCategorizeR POST DELETE !admin

View File

@@ -151,29 +151,16 @@ import qualified Database.Persist.Migration.Postgres
import Database.Persist.Sql ( SqlBackend ) import Database.Persist.Sql ( SqlBackend )
import Foundation ( Handler import Foundation ( Handler
, RegistryCtx(..) , RegistryCtx(..)
, Route , Route(..)
( AppManifestR
, AppR
, EosR
, EosVersionR
, ErrorLogsR
, IconsR
, InfoR
, InstructionsR
, LicenseR
, PackageListR
, PkgDeindexR
, PkgIndexR
, PkgUploadR
, PkgVersionR
, ReleaseNotesR
, VersionLatestR
)
, resourcesRegistryCtx , resourcesRegistryCtx
, setWebProcessThreadId , setWebProcessThreadId
, unsafeHandler , unsafeHandler
) )
import Handler.Admin ( getPkgDeindexR import Handler.Admin ( deleteCategoryR
, deletePkgCategorizeR
, getPkgDeindexR
, postCategoryR
, postPkgCategorizeR
, postPkgDeindexR , postPkgDeindexR
, postPkgIndexR , postPkgIndexR
, postPkgUploadR , postPkgUploadR

View File

@@ -1,11 +1,12 @@
{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-orphans #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Cli.Cli module Cli.Cli
( cliMain ( cliMain
@@ -25,7 +26,9 @@ import Control.Monad.Logger ( LogLevel(..)
import Crypto.Hash ( SHA256(SHA256) import Crypto.Hash ( SHA256(SHA256)
, hashWith , hashWith
) )
import Data.Aeson ( eitherDecodeStrict ) import Data.Aeson ( ToJSON
, eitherDecodeStrict
)
import Data.ByteArray.Encoding ( Base(..) import Data.ByteArray.Encoding ( Base(..)
, convertToBase , convertToBase
) )
@@ -41,6 +44,8 @@ import Data.HashMap.Internal.Strict ( HashMap
, lookup , lookup
, traverseWithKey , traverseWithKey
) )
import Data.String.Interpolate.IsString
( i )
import Data.Text ( toLower ) import Data.Text ( toLower )
import Dhall ( Encoder(embed) import Dhall ( Encoder(embed)
, FromDhall(..) , FromDhall(..)
@@ -51,7 +56,8 @@ import Dhall ( Encoder(embed)
, inputFile , inputFile
) )
import Dhall.Core ( pretty ) import Dhall.Core ( pretty )
import Handler.Admin ( IndexPkgReq(IndexPkgReq) import Handler.Admin ( AddCategoryReq(AddCategoryReq)
, IndexPkgReq(IndexPkgReq)
, PackageList(..) , PackageList(..)
) )
import Lib.External.AppMgr ( sourceManifest ) import Lib.External.AppMgr ( sourceManifest )
@@ -88,6 +94,7 @@ import Options.Applicative ( (<$>)
, Applicative((<*>), liftA2, pure) , Applicative((<*>), liftA2, pure)
, Parser , Parser
, ParserInfo , ParserInfo
, auto
, command , command
, execParser , execParser
, fullDesc , fullDesc
@@ -98,6 +105,7 @@ import Options.Applicative ( (<$>)
, long , long
, mappend , mappend
, metavar , metavar
, option
, optional , optional
, progDesc , progDesc
, short , short
@@ -131,6 +139,7 @@ import Startlude ( ($)
, ExitCode(..) , ExitCode(..)
, FilePath , FilePath
, IO , IO
, Int
, IsString(..) , IsString(..)
, Maybe(..) , Maybe(..)
, Monad((>>=)) , Monad((>>=))
@@ -151,6 +160,7 @@ import Startlude ( ($)
, fromMaybe , fromMaybe
, fst , fst
, headMay , headMay
, not
, panic , panic
, show , show
, snd , snd
@@ -224,6 +234,10 @@ data Command
| CmdUpload !Upload | CmdUpload !Upload
| CmdIndex !String !String !Version !Bool | CmdIndex !String !String !Version !Bool
| CmdListUnindexed !String | CmdListUnindexed !String
| CmdCatAdd !String !String !(Maybe String) !(Maybe Int)
| CmdCatDel !String !String
| CmdPkgCatAdd !String !PkgId !String
| CmdPkgCatDel !String !PkgId !String
deriving Show deriving Show
cfgLocation :: IO FilePath cfgLocation :: IO FilePath
@@ -314,23 +328,62 @@ parseCommand =
<|> parseIndex <|> parseIndex
<|> parseDeindex <|> parseDeindex
<|> (CmdListUnindexed <$> parseListUnindexed) <|> (CmdListUnindexed <$> parseListUnindexed)
<|> parseCat
<|> parsePkgCat
where reg = parseRepoAdd <|> (CmdRegDel <$> parseRepoDel) <|> (parseRepoList $> CmdRegList) where reg = parseRepoAdd <|> (CmdRegDel <$> parseRepoDel) <|> (parseRepoList $> CmdRegList)
parseCat :: Parser Command
parseCat = subparser $ command "category" (info (add <|> del) $ progDesc "Manage categories")
where
add = subparser $ command
"add"
( info
( CmdCatAdd
<$> strOption (short 't' <> long "target" <> metavar "REGISTRY_NAME")
<*> strArgument (metavar "CATEGORY")
<*> optional (strOption (short 'd' <> long "description" <> metavar "DESCRIPTION"))
<*> optional
(option Options.Applicative.auto (short 'p' <> long "priority" <> metavar "PRIORITY"))
)
$ progDesc "Adds category to registry"
)
del = subparser $ command
"rm"
( info
(CmdCatDel <$> strOption (short 't' <> long "target" <> metavar "REGISTRY_NAME") <*> strArgument
(metavar "CATEGORY")
)
$ progDesc "Removes category from registry"
)
parsePkgCat :: Parser Command
parsePkgCat = subparser $ command "categorize" (info cat $ progDesc "Add or remove package from category")
where
cat :: Parser Command
cat =
let cmd rm = if not rm then CmdPkgCatAdd else CmdPkgCatDel
in cmd
<$> switch (long "remove")
<*> strOption (short 't' <> long "target" <> metavar "REGISTRY_NAME")
<*> strArgument (metavar "PACKAGE_ID")
<*> strArgument (metavar "CATEGORY")
opts :: ParserInfo Command opts :: ParserInfo Command
opts = info (parseCommand <**> helper) (fullDesc <> progDesc "Publish tool for Embassy Packages") opts = info (parseCommand <**> helper) (fullDesc <> progDesc "Publish tool for Embassy Packages")
cliMain :: IO () cliMain :: IO ()
cliMain = cliMain = execParser opts >>= \case
execParser opts CmdInit sh -> init sh
>>= (\case CmdRegAdd s pcr -> regAdd s pcr
CmdInit sh -> init sh CmdRegDel s -> regRm s
CmdRegAdd s pcr -> regAdd s pcr CmdRegList -> regLs
CmdRegDel s -> regRm s CmdUpload up -> upload up
CmdRegList -> regLs CmdIndex name pkg v shouldIndex -> if shouldIndex then index name pkg v else deindex name pkg v
CmdUpload up -> upload up CmdListUnindexed name -> listUnindexed name
CmdIndex name pkg v shouldIndex -> if shouldIndex then index name pkg v else deindex name pkg v CmdCatAdd target cat desc pri -> catAdd target cat desc pri
CmdListUnindexed name -> listUnindexed name CmdCatDel target cat -> catDel target cat
) CmdPkgCatAdd target pkg cat -> pkgCatAdd target pkg cat
CmdPkgCatDel target pkg cat -> pkgCatDel target pkg cat
init :: Maybe Shell -> IO () init :: Maybe Shell -> IO ()
init sh = do init sh = do
@@ -356,7 +409,7 @@ init sh = do
regAdd :: String -> PublishCfgRepo -> IO () regAdd :: String -> PublishCfgRepo -> IO ()
regAdd name val = do regAdd name val = do
loc <- cfgLocation loc <- cfgLocation
PublishCfg cfg <- inputFile auto loc PublishCfg cfg <- inputFile Dhall.auto loc
let cfg' = insert name val cfg let cfg' = insert name val cfg
writeFile loc (pretty $ embed inject $ PublishCfg cfg') writeFile loc (pretty $ embed inject $ PublishCfg cfg')
putChunkLn $ "Below is the hash to provide to the server operator for your admin credentials" & fore yellow putChunkLn $ "Below is the hash to provide to the server operator for your admin credentials" & fore yellow
@@ -373,14 +426,14 @@ regAdd name val = do
regRm :: String -> IO () regRm :: String -> IO ()
regRm name = do regRm name = do
loc <- cfgLocation loc <- cfgLocation
PublishCfg cfg <- inputFile auto loc PublishCfg cfg <- inputFile Dhall.auto loc
let cfg' = delete name cfg let cfg' = delete name cfg
writeFile loc (pretty $ embed inject $ PublishCfg cfg') writeFile loc (pretty $ embed inject $ PublishCfg cfg')
regLs :: IO () regLs :: IO ()
regLs = do regLs = do
loc <- cfgLocation loc <- cfgLocation
PublishCfg cfg <- inputFile auto loc PublishCfg cfg <- inputFile Dhall.auto loc
void $ traverseWithKey f cfg void $ traverseWithKey f cfg
where where
f k v = do f k v = do
@@ -439,39 +492,10 @@ upload (Upload name mpkg shouldIndex) = do
sfs2prog StreamFileStatus {..} = Progress (fromIntegral readSoFar) (fromIntegral fileSize) () sfs2prog StreamFileStatus {..} = Progress (fromIntegral readSoFar) (fromIntegral fileSize) ()
index :: String -> String -> Version -> IO () index :: String -> String -> Version -> IO ()
index name pkg v = do index name pkg v = performHttp name "POST" [i|/admin/v0/index|] (IndexPkgReq (PkgId $ toS pkg) v)
PublishCfgRepo {..} <- findNameInCfg name
noBody <-
parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/index")
<&> setRequestHeaders [("accept", "text/plain")]
<&> applyBasicAuth (B8.pack publishCfgRepoUser) (B8.pack publishCfgRepoPass)
let withBody = setRequestBodyJSON (IndexPkgReq (PkgId $ toS pkg) v) noBody
res <- httpLBS withBody
-- no output is successful
if getResponseStatus res == status200
then pure ()
else do
$logError (decodeUtf8 . LB.toStrict $ getResponseBody res)
exitWith (ExitFailure 1)
deindex :: String -> String -> Version -> IO () deindex :: String -> String -> Version -> IO ()
deindex name pkg v = do deindex name pkg v = performHttp name "POST" [i|/admin/v0/deindex|] (IndexPkgReq (PkgId $ toS pkg) v)
PublishCfgRepo {..} <- findNameInCfg name
noBody <-
parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/deindex")
<&> setRequestHeaders [("accept", "text/plain")]
<&> applyBasicAuth (B8.pack publishCfgRepoUser) (B8.pack publishCfgRepoPass)
let withBody = setRequestBodyJSON (IndexPkgReq (PkgId $ toS pkg) v) noBody
res <- httpLBS withBody
-- no output is successful
if getResponseStatus res == status200
then pure ()
else do
$logError (decodeUtf8 . LB.toStrict $ getResponseBody res)
exitWith (ExitFailure 1)
listUnindexed :: String -> IO () listUnindexed :: String -> IO ()
listUnindexed name = do listUnindexed name = do
@@ -485,16 +509,45 @@ listUnindexed name = do
putChunk (chunk (unPkgId k <> ": ") & fore blue) putChunk (chunk (unPkgId k <> ": ") & fore blue)
putChunkLn $ chunk (show v) & fore yellow putChunkLn $ chunk (show v) & fore yellow
catAdd :: String -> String -> Maybe String -> Maybe Int -> IO ()
catAdd target name desc pri =
performHttp target "POST" [i|/admin/v0/category/#{name}|] (AddCategoryReq (toS <$> desc) pri)
catDel :: String -> String -> IO ()
catDel target name = performHttp target "DELETE" [i|/admin/v0/category/#{name}|] ()
pkgCatAdd :: String -> PkgId -> String -> IO ()
pkgCatAdd target pkg cat = performHttp target "POST" [i|/admin/v0/categorize/#{cat}/#{pkg}|] ()
pkgCatDel :: String -> PkgId -> String -> IO ()
pkgCatDel target pkg cat = performHttp target "DELETE" [i|/admin/v0/categorize/#{cat}/#{pkg}|] ()
findNameInCfg :: String -> IO PublishCfgRepo findNameInCfg :: String -> IO PublishCfgRepo
findNameInCfg name = do findNameInCfg name = do
loc <- cfgLocation loc <- cfgLocation
PublishCfg cfg <- inputFile auto loc PublishCfg cfg <- inputFile Dhall.auto loc
case lookup name cfg of case lookup name cfg of
Nothing -> do Nothing -> do
$logError "Registry name not found!" $logError "Registry name not found!"
exitWith $ ExitFailure 1 exitWith $ ExitFailure 1
Just pcr -> pure pcr Just pcr -> pure pcr
performHttp :: ToJSON a => String -> String -> String -> a -> IO ()
performHttp target method route body = do
PublishCfgRepo {..} <- findNameInCfg target
noBody <-
parseRequest (method <> " " <> show publishCfgRepoLocation <> route)
<&> setRequestHeaders [("accept", "text/plain")]
<&> applyBasicAuth (B8.pack publishCfgRepoUser) (B8.pack publishCfgRepoPass)
let withBody = setRequestBodyJSON body noBody
res <- httpLBS withBody
if getResponseStatus res == status200
then pure ()
else do
$logError (decodeUtf8 . LB.toStrict $ getResponseBody res)
exitWith (ExitFailure 1)
instance MonadLogger IO where instance MonadLogger IO where
monadLoggerLog _ _ LevelDebug = putChunkLn . colorLog white monadLoggerLog _ _ LevelDebug = putChunkLn . colorLog white
monadLoggerLog _ _ LevelInfo = putChunkLn . colorLog blue monadLoggerLog _ _ LevelInfo = putChunkLn . colorLog blue

View File

@@ -12,6 +12,7 @@ import Control.Exception ( ErrorCall(ErrorCall) )
import Control.Monad.Reader.Has ( ask ) import Control.Monad.Reader.Has ( ask )
import Control.Monad.Trans.Maybe ( MaybeT(..) ) import Control.Monad.Trans.Maybe ( MaybeT(..) )
import Data.Aeson ( (.:) import Data.Aeson ( (.:)
, (.:?)
, (.=) , (.=)
, FromJSON(parseJSON) , FromJSON(parseJSON)
, ToJSON , ToJSON
@@ -29,7 +30,11 @@ import Data.List ( (\\)
) )
import Data.String.Interpolate.IsString import Data.String.Interpolate.IsString
( i ) ( i )
import Database.Persist ( entityVal import Database.Persist ( Entity(entityKey)
, PersistStoreRead(get)
, PersistUniqueRead(getBy)
, PersistUniqueWrite(deleteBy, insertUnique, upsert)
, entityVal
, insert_ , insert_
, selectList , selectList
) )
@@ -48,12 +53,16 @@ import Lib.Types.AppIndex ( PackageManifest(..)
, PkgId(unPkgId) , PkgId(unPkgId)
) )
import Lib.Types.Emver ( Version(..) ) import Lib.Types.Emver ( Version(..) )
import Model ( Key(AdminKey, PkgRecordKey, VersionRecordKey) import Model ( Category(..)
, Key(AdminKey, PkgRecordKey, VersionRecordKey)
, PkgCategory(PkgCategory)
, Unique(UniqueName, UniquePkgCategory)
, Upload(..) , Upload(..)
, VersionRecord(versionRecordNumber, versionRecordPkgId) , VersionRecord(versionRecordNumber, versionRecordPkgId)
, unPkgRecordKey , unPkgRecordKey
) )
import Network.HTTP.Types ( status404 import Network.HTTP.Types ( status403
, status404
, status500 , status500
) )
import Settings import Settings
@@ -66,12 +75,15 @@ import Startlude ( ($)
, Applicative(pure) , Applicative(pure)
, Bool(..) , Bool(..)
, Eq , Eq
, Int
, Maybe(..) , Maybe(..)
, Monad((>>=)) , Monad((>>=))
, Show , Show
, SomeException(..) , SomeException(..)
, Text
, asum , asum
, fmap , fmap
, fromMaybe
, getCurrentTime , getCurrentTime
, guarded , guarded
, hush , hush
@@ -83,6 +95,7 @@ import Startlude ( ($)
, throwIO , throwIO
, toS , toS
, traverse , traverse
, void
, when , when
, zip , zip
) )
@@ -193,3 +206,39 @@ getPkgDeindexR = do
infixr 8 .* infixr 8 .*
(.*) :: (b -> c) -> (a1 -> a2 -> b) -> a1 -> a2 -> c (.*) :: (b -> c) -> (a1 -> a2 -> b) -> a1 -> a2 -> c
(.*) = (.) . (.) (.*) = (.) . (.)
data AddCategoryReq = AddCategoryReq
{ addCategoryDescription :: !(Maybe Text)
, addCategoryPriority :: !(Maybe Int)
}
instance FromJSON AddCategoryReq where
parseJSON = withObject "AddCategoryReq" $ \o -> do
addCategoryDescription <- o .:? "description"
addCategoryPriority <- o .:? "priority"
pure AddCategoryReq { .. }
instance ToJSON AddCategoryReq where
toJSON AddCategoryReq {..} = object ["description" .= addCategoryDescription, "priority" .= addCategoryPriority]
postCategoryR :: Text -> Handler ()
postCategoryR cat = do
AddCategoryReq {..} <- requireCheckJsonBody
now <- liftIO getCurrentTime
void . runDB $ upsert (Category now cat (fromMaybe "" addCategoryDescription) (fromMaybe 0 addCategoryPriority)) []
deleteCategoryR :: Text -> Handler ()
deleteCategoryR cat = runDB $ deleteBy (UniqueName cat)
postPkgCategorizeR :: Text -> PkgId -> Handler ()
postPkgCategorizeR cat pkg = runDB $ do
catEnt <- getBy (UniqueName cat) `orThrow` sendResponseText status404 [i|Category "#{cat}" does not exist|]
_pkgEnt <- get (PkgRecordKey pkg) `orThrow` sendResponseText status404 [i|Package "#{pkg}" does not exist|]
now <- liftIO getCurrentTime
void $ insertUnique (PkgCategory now (PkgRecordKey pkg) (entityKey catEnt)) `orThrow` sendResponseText
status403
[i|Package "#{pkg}" is already assigned to category "#{cat}"|]
deletePkgCategorizeR :: Text -> PkgId -> Handler ()
deletePkgCategorizeR cat pkg = runDB $ do
catEnt <- getBy (UniqueName cat) `orThrow` sendResponseText status404 [i|Category "#{cat}" does not exist|]
deleteBy (UniquePkgCategory (PkgRecordKey pkg) (entityKey catEnt))

View File

@@ -17,7 +17,10 @@ import Startlude ( ($)
) )
manualMigration :: Migration manualMigration :: Migration
manualMigration = [(0, 1) := migration_0_2_0] manualMigration = [(0, 1) := migration_0_2_0, (1, 2) := migration_0_2_1]
migration_0_2_1 :: [Operation]
migration_0_2_1 = [DropColumn ("category", "parent")]
migration_0_2_0 :: [Operation] migration_0_2_0 :: [Operation]
migration_0_2_0 = migration_0_2_0 =

View File

@@ -77,7 +77,6 @@ Metric
Category Category
createdAt UTCTime createdAt UTCTime
name Text name Text
parent CategoryId Maybe
description Text description Text
priority Int default=0 priority Int default=0
UniqueName name UniqueName name
@@ -88,6 +87,7 @@ PkgCategory
createdAt UTCTime createdAt UTCTime
pkgId PkgRecordId pkgId PkgRecordId
categoryId CategoryId categoryId CategoryId
UniquePkgCategory pkgId categoryId
deriving Eq deriving Eq
deriving Show deriving Show