mirror of
https://github.com/Start9Labs/registry.git
synced 2026-03-31 04:03:40 +00:00
finalize persistence ofapp download metrics
This commit is contained in:
@@ -32,3 +32,11 @@ app-compatibility-path: "_env:APP_COMPATIBILITY_CONFIG:/etc/start9/registry/comp
|
|||||||
resources-path: "_env:RESOURCES_PATH:/var/www/html/resources"
|
resources-path: "_env:RESOURCES_PATH:/var/www/html/resources"
|
||||||
ssl-path: "_env:SSL_PATH:/var/ssl"
|
ssl-path: "_env:SSL_PATH:/var/ssl"
|
||||||
registry-hostname: "_env:REGISTRY_HOSTNAME:registry.start9labs.com"
|
registry-hostname: "_env:REGISTRY_HOSTNAME:registry.start9labs.com"
|
||||||
|
|
||||||
|
database:
|
||||||
|
database: "_env:PG_DATABASE:start9_registry"
|
||||||
|
poolsize: "_env:PG_POOL_SIZE:2"
|
||||||
|
user: "_env:PG_USER:"
|
||||||
|
password: "_env:PG_PASSWORD:"
|
||||||
|
host: "_env:PG_HOST:localhost"
|
||||||
|
port: "_env:PG_PORT:5432"
|
||||||
@@ -38,6 +38,7 @@ dependencies:
|
|||||||
- monad-loops
|
- monad-loops
|
||||||
- persistent
|
- persistent
|
||||||
- persistent-sqlite
|
- persistent-sqlite
|
||||||
|
- persistent-postgresql
|
||||||
- persistent-template
|
- persistent-template
|
||||||
- process
|
- process
|
||||||
- protolude
|
- protolude
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ module Application
|
|||||||
|
|
||||||
import Startlude
|
import Startlude
|
||||||
|
|
||||||
import Control.Monad.Logger (liftLoc)
|
import Control.Monad.Logger (liftLoc, runLoggingT)
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Default
|
import Data.Default
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
|
import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr, pgPoolSize)
|
||||||
import Language.Haskell.TH.Syntax (qLocation)
|
import Language.Haskell.TH.Syntax (qLocation)
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
import Network.Wai.Handler.Warp (Settings, defaultSettings, defaultShouldDisplayException,
|
import Network.Wai.Handler.Warp (Settings, defaultSettings, defaultShouldDisplayException,
|
||||||
@@ -57,7 +58,6 @@ import Lib.Ssl
|
|||||||
import Settings
|
import Settings
|
||||||
import System.Posix.Process
|
import System.Posix.Process
|
||||||
|
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||||
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
|
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
|
||||||
-- comments there for more details.
|
-- comments there for more details.
|
||||||
@@ -84,13 +84,20 @@ makeFoundation appSettings = do
|
|||||||
-- logging function. To get out of this loop, we initially create a
|
-- logging function. To get out of this loop, we initially create a
|
||||||
-- temporary foundation without a real connection pool, get a log function
|
-- temporary foundation without a real connection pool, get a log function
|
||||||
-- from there, and then create the real foundation.
|
-- from there, and then create the real foundation.
|
||||||
let mkFoundation = AgentCtx {..}
|
let mkFoundation appConnPool = AgentCtx {..}
|
||||||
-- The AgentCtx {..} syntax is an example of record wild cards. For more
|
-- The AgentCtx {..} syntax is an example of record wild cards. For more
|
||||||
-- information, see:
|
-- information, see:
|
||||||
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
|
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
|
||||||
|
tempFoundation = mkFoundation $ panic "connPool forced in tempFoundation"
|
||||||
|
logFunc = messageLoggerSource tempFoundation appLogger
|
||||||
|
|
||||||
|
-- Create the database connection pool
|
||||||
|
pool <- flip runLoggingT logFunc $ createPostgresqlPool
|
||||||
|
(pgConnStr $ appDatabaseConf appSettings)
|
||||||
|
(pgPoolSize . appDatabaseConf $ appSettings)
|
||||||
|
|
||||||
-- Return the foundation
|
-- Return the foundation
|
||||||
return $ mkFoundation
|
return $ mkFoundation pool
|
||||||
|
|
||||||
-- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and
|
-- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and
|
||||||
-- applying some additional middlewares.
|
-- applying some additional middlewares.
|
||||||
|
|||||||
36
src/Database/Queries.hs
Normal file
36
src/Database/Queries.hs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE RankNTypes #-}
|
||||||
|
|
||||||
|
module Database.Queries where
|
||||||
|
|
||||||
|
import Startlude
|
||||||
|
import Lib.Types.Semver
|
||||||
|
import Database.Persist.Sql
|
||||||
|
import Model
|
||||||
|
import Settings
|
||||||
|
|
||||||
|
fetchApp :: MonadIO m => AppIdentifier -> AppVersion -> ReaderT SqlBackend m (Maybe (Entity App))
|
||||||
|
fetchApp appId appVersion = selectFirst [AppAppId ==. appId, AppSemver ==. appVersion] []
|
||||||
|
|
||||||
|
createApp :: MonadIO m => AppIdentifier -> AppSeed -> ReaderT SqlBackend m (Key App)
|
||||||
|
createApp appId AppSeed{..} = do
|
||||||
|
time <- liftIO $ getCurrentTime
|
||||||
|
insert $ App
|
||||||
|
time
|
||||||
|
Nothing
|
||||||
|
title
|
||||||
|
appId
|
||||||
|
descShort
|
||||||
|
descLong
|
||||||
|
semver
|
||||||
|
releaseNotes
|
||||||
|
iconType
|
||||||
|
|
||||||
|
createMetric :: MonadIO m => Maybe (Key App) -> AppIdentifier -> ReaderT SqlBackend m (Key Metric)
|
||||||
|
createMetric appId event = do
|
||||||
|
time <- liftIO $ getCurrentTime
|
||||||
|
insert $ Metric
|
||||||
|
time
|
||||||
|
appId
|
||||||
|
event
|
||||||
@@ -19,6 +19,7 @@ import qualified Yesod.Core.Unsafe as Unsafe
|
|||||||
|
|
||||||
import Lib.Types.Semver
|
import Lib.Types.Semver
|
||||||
import Settings
|
import Settings
|
||||||
|
import Yesod.Persist.Core
|
||||||
|
|
||||||
-- | The foundation datatype for your application. This can be a good place to
|
-- | The foundation datatype for your application. This can be a good place to
|
||||||
-- keep settings and values requiring initialization before your application
|
-- keep settings and values requiring initialization before your application
|
||||||
@@ -31,6 +32,7 @@ data AgentCtx = AgentCtx
|
|||||||
, appLogger :: Logger
|
, appLogger :: Logger
|
||||||
, appWebServerThreadId :: IORef (Maybe ThreadId)
|
, appWebServerThreadId :: IORef (Maybe ThreadId)
|
||||||
, appCompatibilityMap :: HM.HashMap AppVersion AppVersion
|
, appCompatibilityMap :: HM.HashMap AppVersion AppVersion
|
||||||
|
, appConnPool :: ConnectionPool
|
||||||
}
|
}
|
||||||
|
|
||||||
setWebProcessThreadId :: ThreadId -> AgentCtx -> IO ()
|
setWebProcessThreadId :: ThreadId -> AgentCtx -> IO ()
|
||||||
@@ -85,6 +87,17 @@ instance Yesod AgentCtx where
|
|||||||
makeLogger :: AgentCtx -> IO Logger
|
makeLogger :: AgentCtx -> IO Logger
|
||||||
makeLogger = return . appLogger
|
makeLogger = return . appLogger
|
||||||
|
|
||||||
|
-- How to run database actions.
|
||||||
|
instance YesodPersist AgentCtx where
|
||||||
|
type YesodPersistBackend AgentCtx = SqlBackend
|
||||||
|
runDB :: SqlPersistT Handler a -> Handler a
|
||||||
|
runDB action = runSqlPool action . appConnPool =<< getYesod
|
||||||
|
|
||||||
|
instance YesodPersistRunner AgentCtx where
|
||||||
|
getDBRunner :: Handler (DBRunner AgentCtx, Handler ())
|
||||||
|
getDBRunner = defaultGetDBRunner appConnPool
|
||||||
|
|
||||||
|
|
||||||
unsafeHandler :: AgentCtx -> Handler a -> IO a
|
unsafeHandler :: AgentCtx -> Handler a -> IO a
|
||||||
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import qualified GHC.Show (Show (..))
|
|||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import System.Directory
|
import System.Directory
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
|
import Yesod.Persist.Core
|
||||||
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Lib.Registry
|
import Lib.Registry
|
||||||
@@ -26,6 +28,9 @@ import Lib.Semver
|
|||||||
import System.FilePath ((<.>), (</>))
|
import System.FilePath ((<.>), (</>))
|
||||||
import System.Posix.Files (fileSize, getFileStatus)
|
import System.Posix.Files (fileSize, getFileStatus)
|
||||||
import Settings
|
import Settings
|
||||||
|
import Database.Queries
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
import Database.Persist
|
||||||
|
|
||||||
pureLog :: Show a => a -> Handler a
|
pureLog :: Show a => a -> Handler a
|
||||||
pureLog = liftA2 (*>) ($logInfo . show) pure
|
pureLog = liftA2 (*>) ($logInfo . show) pure
|
||||||
@@ -54,7 +59,7 @@ getAppR e = do
|
|||||||
getApp appResourceDir e
|
getApp appResourceDir e
|
||||||
|
|
||||||
getApp :: KnownSymbol a => FilePath -> Extension a -> Handler TypedContent
|
getApp :: KnownSymbol a => FilePath -> Extension a -> Handler TypedContent
|
||||||
getApp rootDir ext = do
|
getApp rootDir ext@(Extension appId) = do
|
||||||
specString <- T.filter (not . isSpace) . fromMaybe "*" <$> lookupGetParam "spec"
|
specString <- T.filter (not . isSpace) . fromMaybe "*" <$> lookupGetParam "spec"
|
||||||
spec <- case readMaybe specString of
|
spec <- case readMaybe specString of
|
||||||
Nothing -> sendResponseStatus status400 ("Invalid App Version Specification" :: Text)
|
Nothing -> sendResponseStatus status400 ("Invalid App Version Specification" :: Text)
|
||||||
@@ -63,13 +68,25 @@ getApp rootDir ext = do
|
|||||||
putStrLn $ "valid appversion for " <> (show ext :: String) <> ": " <> show appVersions
|
putStrLn $ "valid appversion for " <> (show ext :: String) <> ": " <> show appVersions
|
||||||
case getSpecifiedAppVersion spec appVersions of
|
case getSpecifiedAppVersion spec appVersions of
|
||||||
Nothing -> notFound
|
Nothing -> notFound
|
||||||
Just (RegisteredAppVersion (_, filePath)) -> do
|
Just (RegisteredAppVersion (appVersion, filePath)) -> do
|
||||||
exists <- liftIO $ doesFileExist filePath
|
exists <- liftIO $ doesFileExist filePath
|
||||||
if exists
|
if exists
|
||||||
then do
|
then do
|
||||||
|
let appId' = T.pack appId
|
||||||
|
ai <- runDB $ fetchApp appId' appVersion
|
||||||
|
_ <- case ai of
|
||||||
|
Nothing -> do
|
||||||
|
-- save the app if it does not yet exist in db at particular version (automatic eventual transfer from using app.yaml to db record)
|
||||||
|
rd <- resourcesDir . appSettings <$> getYesod
|
||||||
|
manifest <- liftIO $ getAppManifest rd
|
||||||
|
deets <- case HM.lookup appId' $ unAppManifest manifest of
|
||||||
|
Nothing -> sendResponseStatus status400 ("App not present in manifest" :: Text)
|
||||||
|
Just x -> pure x
|
||||||
|
appKey <- runDB $ createApp appId' deets
|
||||||
|
-- log app download
|
||||||
|
runDB $ createMetric (Just appKey) appId'
|
||||||
|
Just a -> runDB $ createMetric (Just $ entityKey a) appId'
|
||||||
sz <- liftIO $ fileSize <$> getFileStatus filePath
|
sz <- liftIO $ fileSize <$> getFileStatus filePath
|
||||||
addHeader "Content-Length" (show sz)
|
addHeader "Content-Length" (show sz)
|
||||||
respondSource typePlain $ CB.sourceFile filePath .| awaitForever sendChunkBS
|
respondSource typePlain $ CB.sourceFile filePath .| awaitForever sendChunkBS
|
||||||
else notFound
|
else notFound
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,5 +44,3 @@ instance HasAppVersion AppVersion where
|
|||||||
appVersionMax :: HasAppVersion a => [a] -> Maybe a
|
appVersionMax :: HasAppVersion a => [a] -> Maybe a
|
||||||
appVersionMax [] = Nothing
|
appVersionMax [] = Nothing
|
||||||
appVersionMax as = Just $ maximumBy (\a1 a2 -> version a1 `compare` version a2) as
|
appVersionMax as = Just $ maximumBy (\a1 a2 -> version a1 `compare` version a2) as
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
module Lib.Types.Semver where
|
module Lib.Types.Semver where
|
||||||
|
|
||||||
import Startlude hiding (break)
|
import Startlude hiding (break)
|
||||||
@@ -13,6 +15,7 @@ import Data.Char (isDigit)
|
|||||||
import Data.String.Interpolate
|
import Data.String.Interpolate
|
||||||
import Data.Text
|
import Data.Text
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
|
import Database.Persist.Sql
|
||||||
|
|
||||||
------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
-- Semver AppVersion
|
-- Semver AppVersion
|
||||||
@@ -58,6 +61,13 @@ instance FromJSONKey AppVersion where
|
|||||||
Nothing -> fail "invalid app version"
|
Nothing -> fail "invalid app version"
|
||||||
Just x -> pure x
|
Just x -> pure x
|
||||||
|
|
||||||
|
instance PersistField AppVersion where
|
||||||
|
toPersistValue = toPersistValue @String . show
|
||||||
|
fromPersistValue = note "" . readMaybe <=< fromPersistValue
|
||||||
|
|
||||||
|
instance PersistFieldSql AppVersion where
|
||||||
|
sqlType _ = SqlString
|
||||||
|
|
||||||
------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
-- Semver AppVersionSpecification
|
-- Semver AppVersionSpecification
|
||||||
------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
34
src/Model.hs
Normal file
34
src/Model.hs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{-# LANGUAGE GADTs #-}
|
||||||
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
|
||||||
|
module Model where
|
||||||
|
|
||||||
|
import Startlude
|
||||||
|
import Database.Persist.TH
|
||||||
|
import Lib.Types.Semver
|
||||||
|
|
||||||
|
|
||||||
|
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
|
||||||
|
App
|
||||||
|
createdAt UTCTime
|
||||||
|
updatedAt UTCTime Maybe
|
||||||
|
title Text
|
||||||
|
appId Text
|
||||||
|
descShort Text
|
||||||
|
descLong Text
|
||||||
|
semver AppVersion
|
||||||
|
releaseNotes Text
|
||||||
|
iconType Text
|
||||||
|
deriving Eq
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
Metric
|
||||||
|
createdAt UTCTime
|
||||||
|
appId AppId Maybe default=null
|
||||||
|
event Text
|
||||||
|
deriving Eq
|
||||||
|
deriving Show
|
||||||
|
|]
|
||||||
@@ -16,17 +16,21 @@ import Data.Aeson.Types
|
|||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
import Data.FileEmbed (embedFile)
|
import Data.FileEmbed (embedFile)
|
||||||
import Data.Yaml (decodeEither')
|
import Data.Yaml (decodeEither')
|
||||||
|
import Database.Persist.Postgresql (PostgresConf)
|
||||||
import Network.Wai.Handler.Warp (HostPreference)
|
import Network.Wai.Handler.Warp (HostPreference)
|
||||||
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
|
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
|
||||||
import Paths_start9_registry (version)
|
import Paths_start9_registry (version)
|
||||||
import Lib.Types.Semver
|
import Lib.Types.Semver
|
||||||
import System.FilePath ((</>))
|
import System.FilePath ((</>))
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
import Data.Yaml.Config
|
||||||
|
|
||||||
-- | Runtime settings to configure this application. These settings can be
|
-- | Runtime settings to configure this application. These settings can be
|
||||||
-- loaded from various sources: defaults, environment variables, config files,
|
-- loaded from various sources: defaults, environment variables, config files,
|
||||||
-- theoretically even a database.
|
-- theoretically even a database.
|
||||||
data AppSettings = AppSettings
|
data AppSettings = AppSettings
|
||||||
{ appHost :: HostPreference
|
{ appDatabaseConf :: PostgresConf
|
||||||
|
, appHost :: HostPreference
|
||||||
-- ^ Host/interface the server should bind to.
|
-- ^ Host/interface the server should bind to.
|
||||||
, appPort :: Word16
|
, appPort :: Word16
|
||||||
-- ^ Port to listen on
|
-- ^ Port to listen on
|
||||||
@@ -44,12 +48,13 @@ data AppSettings = AppSettings
|
|||||||
, registryHostname :: Text
|
, registryHostname :: Text
|
||||||
, registryVersion :: AppVersion
|
, registryVersion :: AppVersion
|
||||||
, sslKeyLocation :: FilePath
|
, sslKeyLocation :: FilePath
|
||||||
, sslCsrLocation :: FilePath
|
, sslCsrLocation :: FilePath
|
||||||
, sslCertLocation :: FilePath
|
, sslCertLocation :: FilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
instance FromJSON AppSettings where
|
instance FromJSON AppSettings where
|
||||||
parseJSON = withObject "AppSettings" $ \o -> do
|
parseJSON = withObject "AppSettings" $ \o -> do
|
||||||
|
appDatabaseConf <- o .: "database"
|
||||||
appHost <- fromString <$> o .: "host"
|
appHost <- fromString <$> o .: "host"
|
||||||
appPort <- o .: "port"
|
appPort <- o .: "port"
|
||||||
appIpFromHeader <- o .: "ip-from-header"
|
appIpFromHeader <- o .: "ip-from-header"
|
||||||
@@ -82,3 +87,59 @@ compileTimeAppSettings =
|
|||||||
case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of
|
case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of
|
||||||
Error e -> panic $ toS e
|
Error e -> panic $ toS e
|
||||||
Success settings -> settings
|
Success settings -> settings
|
||||||
|
|
||||||
|
getAppManifest :: FilePath -> IO AppManifest
|
||||||
|
getAppManifest resourcesDir = do
|
||||||
|
let appResourceDir = (</> "apps" </> "apps.yaml") $ resourcesDir
|
||||||
|
loadYamlSettings [appResourceDir] [] useEnv
|
||||||
|
|
||||||
|
type AppIdentifier = Text
|
||||||
|
data AppSeed = AppSeed
|
||||||
|
{ title :: Text
|
||||||
|
, descShort :: Text
|
||||||
|
, descLong :: Text
|
||||||
|
, semver :: AppVersion
|
||||||
|
, releaseNotes :: Text
|
||||||
|
, iconType :: Text
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
newtype AppManifest = AppManifest { unAppManifest :: HM.HashMap AppIdentifier AppSeed}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
instance FromJSON AppManifest where
|
||||||
|
parseJSON = withObject "app details to seed" $ \o -> do
|
||||||
|
apps <- for (HM.toList o) $ \(appId', c) -> do
|
||||||
|
appId <- parseJSON $ String appId'
|
||||||
|
config <- parseJSON c
|
||||||
|
title <- config .: "title"
|
||||||
|
iconType <- config .: "icon-type"
|
||||||
|
desc <- config .: "description"
|
||||||
|
ver <- config .: "version-info"
|
||||||
|
let descShort = short desc
|
||||||
|
let descLong = long desc
|
||||||
|
let semver = version' ver
|
||||||
|
let releaseNotes = notes ver
|
||||||
|
return $ (appId, AppSeed {..})
|
||||||
|
return $ AppManifest (HM.fromList apps)
|
||||||
|
|
||||||
|
data VersionInfo = VersionInfo
|
||||||
|
{ version' :: AppVersion
|
||||||
|
, notes :: Text
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
instance FromJSON VersionInfo where
|
||||||
|
parseJSON = withObject "version info" $ \o -> do
|
||||||
|
version' <- o .: "version"
|
||||||
|
notes <- o .: "release-notes"
|
||||||
|
pure VersionInfo {..}
|
||||||
|
|
||||||
|
data AppDescription = AppDescription
|
||||||
|
{ short :: Text
|
||||||
|
, long :: Text
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
instance FromJSON AppDescription where
|
||||||
|
parseJSON = withObject "app desc" $ \o -> do
|
||||||
|
short <- o .: "short"
|
||||||
|
long <- o .: "long"
|
||||||
|
pure AppDescription {..}
|
||||||
Reference in New Issue
Block a user