{-# LANGUAGE DeriveGeneric #-}
module SreBox.Postgrest where
import Data.Aeson (FromJSON, ToJSON)
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import GHC.Generics (Generic)
import System.Directory
import System.FilePath
import qualified Salmon.Builtin.CommandLine as CLI
import Salmon.Builtin.Extension
import qualified Salmon.Builtin.Migrations as Migrations
import qualified Salmon.Builtin.Nodes.Certificates as Certs
import qualified Salmon.Builtin.Nodes.Continuation as Continuation
import qualified Salmon.Builtin.Nodes.Debian.OS as Debian
import qualified Salmon.Builtin.Nodes.Filesystem as FS
import qualified Salmon.Builtin.Nodes.Postgres as Postgres
import qualified Salmon.Builtin.Nodes.Rsync as Rsync
import qualified Salmon.Builtin.Nodes.Secrets as Secrets
import qualified Salmon.Builtin.Nodes.Self as Self
import qualified Salmon.Builtin.Nodes.Ssh as Ssh
import qualified Salmon.Builtin.Nodes.Systemd as Systemd
import Salmon.Op.G (G (..))
import Salmon.Op.OpGraph (OpGraph (..), inject)
import Salmon.Op.Ref (mkRef)
import Salmon.Op.Track (Track (..), Tracked (..), trackedGraph, using, (>*<))
import Salmon.Reporter
import qualified Salmon.Builtin.Nodes.Git as Git
import SreBox.CabalBuilding (cabalBinUpload, postgrest)
import qualified SreBox.CabalBuilding as CabalBuilding
import SreBox.Environment
import qualified SreBox.PostgresInit as PGInit
import qualified SreBox.PostgresMigrations as PGMigrate
-------------------------------------------------------------------------------
data Report
= Build !CabalBuilding.Report
| Upload !CabalBuilding.Report
| CallSelf !Self.Report
| UploadSelf !Self.Report
| UploadFile !Rsync.Report
| GetSources !Git.Report
| InitializeDB !PGInit.Report
| Migration !PGMigrate.Report
| SetupSystemd !Systemd.Report
deriving (Show)
-------------------------------------------------------------------------------
type ServiceName = Text
type PortNumber = Int
data PostgrestConfig
= PostgrestConfig
{ postgrest_cfg_serviceName :: ServiceName
, postgrest_cfg_portnum :: PortNumber
, postgrest_cfg_anonRole :: Postgres.Group
, postgrest_cfg_connectionString :: Postgres.ConnString FilePath
, postgrest_cfg_configPath :: FilePath
-- TODO: schemas
}
data PostgrestSetup
= PostgrestSetup
{ postgrest_setup_serviceName :: ServiceName
, postgrest_setup_localBinPath :: FilePath
, postgrest_setup_localConfigPath :: FilePath
, postgrest_setup_localConnstringPath :: FilePath
, postgrest_setup_localJwtKeySecretPath :: FilePath
}
deriving (Generic)
instance FromJSON PostgrestSetup
instance ToJSON PostgrestSetup
setupPostgrest ::
(FromJSON directive, ToJSON directive) =>
Reporter Report ->
Track' Ssh.Remote ->
Track' directive ->
Self.Remote ->
Self.SelfPath ->
(PostgrestSetup -> directive) ->
Track' FilePath ->
FilePath ->
FilePaths ->
PostgrestConfig ->
Op
setupPostgrest r mkRemote simulate selfRemote selfpath toSpec makeJwt jwtFilePath paths cfg =
using uploadbuiltfile $ \remotepath ->
let
setup =
PostgrestSetup
cfg.postgrest_cfg_serviceName
remotepath
remoteConfig
remoteConnstring
remoteJwtSecret
in
trackedGraph (continueRemotely setup) `inject` configUploads
where
uploadbuiltfile =
cabalBinUpload
(contramap Upload r)
(postgrest (contramap Build r))
rsyncRemote
rsyncRemote :: Rsync.Remote
rsyncRemote = (\(Self.Remote a b) -> Rsync.Remote a b) selfRemote
configUploads =
let opdeps = deps [uploadConfig, uploadConnStringFile, uploadJwtFile]
in op "uploads-postgrest-configs" opdeps $ \actions ->
actions
{ notes =
[ "for service: " <> cfg.postgrest_cfg_serviceName
]
, ref = mkRef "postgrest-uploads" cfg.postgrest_cfg_serviceName
}
-- recursive call
continueRemotely setup =
Self.uploadAndCallSelfAsSudo
(contramap UploadSelf r)
(contramap CallSelf r)
"tmp"
selfRemote
selfpath
mkRemote
simulate
CLI.Up
(toSpec setup)
-- upload config file
remoteConfig = Text.unpack $ "tmp/postgrest-" <> cfg.postgrest_cfg_serviceName <> ".config"
remoteConnstring = Text.unpack $ "tmp/postgrest-" <> cfg.postgrest_cfg_serviceName <> ".connstring"
remoteJwtSecret = Text.unpack $ "tmp/postgrest-" <> cfg.postgrest_cfg_serviceName <> ".shared-secret"
upload gen localpath distpath =
Rsync.sendFile (contramap UploadFile r) Debian.rsync (FS.Generated gen localpath) rsyncRemote distpath
uploadConfig =
upload makeConfig cfg.postgrest_cfg_configPath remoteConfig
where
-- make config using remote filepaths as contents
makeConfig =
Track $ \path ->
let connectionString = paths.connstringPath cfg.postgrest_cfg_serviceName
jwtSigningKey = paths.jwtSigningKeyPath cfg.postgrest_cfg_serviceName
in FS.filecontents $
(FS.FileContents path (renderConfig cfg jwtSigningKey connectionString))
uploadConnStringFile =
upload makeConnstring connstringFilePath remoteConnstring
where
makeConnstring = PGMigrate.pgConnstringFile (contramap Migration r) (cfg.postgrest_cfg_connectionString)
connstringFilePath =
Text.unpack $ mconcat ["./configs/posgtrest/postgrest-", cfg.postgrest_cfg_serviceName, ".connstring"]
uploadJwtFile =
upload makeJwt jwtFilePath remoteJwtSecret
renderConfig :: PostgrestConfig -> FilePath -> FilePath -> Text
renderConfig cfg secretFilePath connstringFilePath =
Text.unlines
[ kv_text "db-anon-role" $ Postgres.groupRole cfg.postgrest_cfg_anonRole
, kv_text "db-channel" "pgrst"
, kv_bool "db-channel-enabled" True
, kv_bool "db-config" True
, kv_text "db-extra-search-path" "public"
, kv_int "db-pool" 10
, kv_bool "db-prepared-statements" True
, kv_text "db-schemas" "public"
, kv_text "db-tx-end" "commit"
, kv_text "db-uri" (Text.pack $ '@' : connstringFilePath)
, kv_text "jwt-role-claim-key" ".role"
, kv_text "jwt-secret" (Text.pack $ '@' : secretFilePath)
, kv_bool "jwt-secret-is-base64" True
, kv_text "log-level" "error"
, kv_text "openapi-mode" "follow-privileges"
, kv_text "openapi-server-proxy-uri" ""
, kv_text "server-host" "!4"
, kv_int "server-port" cfg.postgrest_cfg_portnum
, kv_bool "server-timing-enabled" False
]
where
kv_text k v = Text.unwords [k, "=", "\"" <> v <> "\""]
kv_bool k v = Text.unwords [k, "=", if v then "true" else "false"]
kv_int k v = Text.unwords [k, "=", Text.pack $ show v]
systemdPostgrest :: Reporter Report -> FilePaths -> PostgrestSetup -> Op
systemdPostgrest r paths setup =
Systemd.systemdService (contramap SetupSystemd r) Debian.systemctl trackConfig config
where
trackConfig :: Track' Systemd.Config
trackConfig = Track $ \cfg ->
let
execPath = Systemd.start_path $ Systemd.service_execStart $ Systemd.config_service $ cfg
copybin = FS.fileCopy (postgrest_setup_localBinPath setup) execPath
copyConfig = FS.fileCopy (postgrest_setup_localConfigPath setup) cpath
copyConnstringSecret = FS.fileCopy (postgrest_setup_localConnstringPath setup) sconnstringpath
copyJwtKeySecret = FS.fileCopy (postgrest_setup_localJwtKeySecretPath setup) sjwtpath
in
op "setup-systemd-for-postgrest" (deps [copybin, copyConfig, copyJwtKeySecret, copyConnstringSecret]) $ \actions ->
actions
{ ref = mkRef "postgrest-systemd" setup.postgrest_setup_serviceName
}
config :: Systemd.Config
config = Systemd.Config Systemd.System "/etc/systemd/system" tgt unit service install
tgt :: Systemd.UnitTarget
tgt = "salmon-postgrest-" <> setup.postgrest_setup_serviceName <> ".service"
unit :: Systemd.Unit
unit = Systemd.Unit (mconcat ["Postgrest from Salmon (", setup.postgrest_setup_serviceName, ")"]) "network-online.target"
service :: Systemd.Service
service = Systemd.Service Systemd.Simple "root" "root" "007" start Systemd.OnFailure Systemd.Process "/opt/rundir/postgrest"
start :: Systemd.Start
start =
Systemd.Start
"/opt/rundir/postgrest/bin/postgrest"
[ Text.pack cpath
]
cpath :: FilePath
cpath = paths.configPath setup.postgrest_setup_serviceName
sconnstringpath :: FilePath
sconnstringpath = paths.connstringPath setup.postgrest_setup_serviceName
sjwtpath :: FilePath
sjwtpath = paths.jwtSigningKeyPath setup.postgrest_setup_serviceName
install :: Systemd.Install
install = Systemd.Install "multi-user.target"
-------------------------------------------------------------------------------
data FilePaths
= FilePaths
{ configPath :: ServiceName -> FilePath
, connstringPath :: ServiceName -> FilePath
, jwtSigningKeyPath :: ServiceName -> FilePath
}
defaultFilePaths :: FilePaths
defaultFilePaths =
FilePaths
configPath
connstringPath
jwtSigningKeyPath
where
configPath name =
"/opt/rundir/postgrest" </> Text.unpack name </> "config.txt"
connstringPath name =
"/opt/rundir/postgrest" </> Text.unpack name </> "connstring.connstring"
jwtSigningKeyPath name =
"/opt/rundir/postgrest" </> Text.unpack name </> "jwt-shared-secret"
-------------------------------------------------------------------------------
data PostgrestMigratedApiConfig
= PostgrestMigratedApiConfig
{ pma_serviceName :: Text
, pma_apiPort :: Int
, pma_repo :: Git.Repo
, pma_migrationTip :: FilePath
, pma_migrationsPrefix :: FilePath
, pma_migrate_connstring :: Postgres.ConnString FilePath
, pma_setrole_connstring :: Postgres.ConnString FilePath
, pma_fallback_role :: Postgres.Group
, pma_token_protected_roles :: [Postgres.Group]
}
postgrestMigratedApi ::
(FromJSON directive, ToJSON directive) =>
Reporter Report ->
Track' directive ->
(PGInit.InitSetup FilePath -> directive) ->
(Either PGMigrate.PrepareRemoteMigrationSetup PGMigrate.MigrationSetup -> directive) ->
(PostgrestSetup -> directive) ->
Self.SelfPath ->
Self.Remote ->
Track' FilePath ->
FilePath ->
FilePaths ->
PostgrestMigratedApiConfig ->
Op
postgrestMigratedApi r simulate toSpec0 toSpec1 toSpec2 selfpath remoteSelf mkJwt jwtPath paths cfg =
op "postgrest-api" (deps [prest `inject` migrateDBWithUser `inject` initDB]) $ \actions ->
actions
{ ref = mkRef "prest-api" cfg.pma_serviceName
}
where
inputMigrations :: IO (G PGMigrate.MigrationFile)
inputMigrations =
let reader =
Migrations.addFilePrefix (Git.clonedir cfg.pma_repo </> cfg.pma_migrationsPrefix) $
PGMigrate.defaultMigrationReader
in G . fmap node <$> Migrations.loadMigrations reader cfg.pma_migrationTip
exposedDatabase = cfg.pma_migrate_connstring.connstring_db
migrateUser = cfg.pma_migrate_connstring.connstring_user
setroleUser = cfg.pma_setrole_connstring.connstring_user
anonRoleGroup = cfg.pma_fallback_role
extraNonAnonRoleGroups = cfg.pma_token_protected_roles
cloneSource = Track $ const $ Git.repo (contramap GetSources r) Debian.git cfg.pma_repo
initDB =
PGInit.remoteSetupPG
(contramap InitializeDB r)
simulate
remoteSelf
selfpath
toSpec0
( PGInit.InitSetup
exposedDatabase
(migrateUser, cfg.pma_migrate_connstring.connstring_user_pass)
[ (migrateUser, cfg.pma_migrate_connstring.connstring_user_pass, [Postgres.CONNECT, Postgres.CREATE], [])
, (setroleUser, cfg.pma_setrole_connstring.connstring_user_pass, [Postgres.CONNECT], anonRoleGroup : extraNonAnonRoleGroups)
]
[ (anonRoleGroup, [])
]
)
migrateDBWithUser =
PGMigrate.remoteMigrateOpaqueSetup
cfg.pma_serviceName
(contramap Migration r)
simulate
remoteSelf
selfpath
toSpec1
( PGMigrate.RemoteMigrateConfig
(TrackedIO $ Tracked cloneSource inputMigrations)
(PGMigrate.defaultRemoteMigrationPath exposedDatabase.getDatabase)
migrateUser
exposedDatabase
cfg.pma_migrate_connstring.connstring_user_pass
[]
)
prest = op "postgrest" (deps [setupPrest]) $ \actions ->
actions
{ ref = mkRef "prest-service" cfg.pma_serviceName
}
setupPrest =
setupPostgrest
r
Ssh.preExistingRemoteMachine
simulate
remoteSelf
selfpath
toSpec2
mkJwt
jwtPath
paths
prestConfig
serviceName = "postgrest-api-" <> cfg.pma_serviceName
prestConfigPath = Text.unpack $ mconcat ["./configs/posgtrest/postgrest-", cfg.pma_serviceName, ".config"]
prestConfig =
PostgrestConfig
serviceName
cfg.pma_apiPort
anonRoleGroup
cfg.pma_setrole_connstring
prestConfigPath