packages feed

salmon-apps-0.1.0.0: src/Migrator/Ops.hs

{-# LANGUAGE OverloadedRecordDot #-}

module Migrator.Ops where

import Data.Foldable (toList)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text

import Salmon.Builtin.Extension (Op, Track', ignoreTrack)
import qualified Salmon.Builtin.Migrations as Migrations
import qualified Salmon.Builtin.Nodes.Debian.OS as Debian
import Salmon.Builtin.Nodes.Filesystem as FS
import qualified Salmon.Builtin.Nodes.Postgres as Postgres
import qualified Salmon.Builtin.Nodes.Secrets as Secrets
import qualified SreBox.PostgresInit as PGInit
import qualified SreBox.PostgresMigrations as PGMigrate
import qualified SreBox.PostgresTemplate as PGTemplate
import System.FilePath (takeDirectory, (</>))

import Salmon.Op.G (G (..))
import Salmon.Op.OpGraph (inject, node)
import Salmon.Op.Track (Track (..))
import Salmon.Reporter

loadMigrations :: FilePath -> FilePath -> IO (G PGMigrate.MigrationFile)
loadMigrations root tip =
    let reader =
            Migrations.addFilePrefix root $
                PGMigrate.defaultMigrationReader
     in G . fmap node <$> Migrations.loadMigrations reader tip

prepare ::
    FilePath ->
    FilePath ->
    Text ->
    Text ->
    [Text] ->
    FilePath ->
    IO PGMigrate.MigrationSetup
prepare root tip dbname migrateusername extrausernames passfile =
    PGMigrate.MigrationSetup
        <$> loadMigrations root tip
        <*> pure (Postgres.User migrateusername)
        <*> pure (Postgres.Database dbname)
        <*> pure passfile
        <*> pure [(Postgres.User u, passfileForUser u) | u <- extrausernames]
  where
    passfileForUser :: Text -> FilePath
    passfileForUser u = takeDirectory passfile </> (Text.unpack $ u <> ".pass")

migrateSuperUser :: PGMigrate.MigrationSetup -> Op
migrateSuperUser arg =
    runMigration
  where
    runMigration :: Op
    runMigration =
        PGMigrate.applyAdminScriptMigration
            reportPrint
            Debian.psql
            (Track $ PGInit.setupNakedPG reportPrint)
            arg

migrate :: PGMigrate.MigrationSetup -> Op
migrate arg =
    runMigration
  where
    initPg = Track (\connstring -> initdb connstring `inject` passfile connstring)

    initdb :: Postgres.ConnString FilePath -> Op
    initdb ownerConnstring =
        PGInit.setupMultiUserPG
            reportPrint
            ownerConnstring
            [(u, p, [], []) | (u, p) <- users]
            []

    users :: [(Postgres.User, FS.File "passfile")]
    users = [(u, FS.Generated extrapassfile p) | (u, p) <- arg.setup_extra_users]

    passfile :: Postgres.ConnString FilePath -> Op
    passfile conn =
        Secrets.sharedSecretFile
            reportPrint
            Debian.openssl
            (Secrets.Secret Secrets.Hex 48 conn.connstring_user_pass)

    extrapassfile :: Track' FilePath
    extrapassfile = Track $ \path ->
        Secrets.sharedSecretFile
            reportPrint
            Debian.openssl
            (Secrets.Secret Secrets.Hex 48 path)

    runMigration :: Op
    runMigration =
        PGMigrate.applyUserScriptMigration
            reportPrint
            Debian.psql
            initPg
            arg

{- | The migrations of both sets, built into a template database.

Same two migration graphs, same order, same users as 'migrate' and
'migrateSuperUser' -- only walked from inside the template node, behind its
check, so that a template already built from these files is not touched at
all (see "SreBox.PostgresTemplate" for why it cannot be an ordinary
dependency).
-}
buildTemplate :: Text -> PGMigrate.MigrationSetup -> PGMigrate.MigrationSetup -> Op
buildTemplate fp superuser owner =
    PGTemplate.template
        reportPrint
        (Track $ Postgres.pgLocalCluster reportPrint Debian.postgres Debian.pg_ctlcluster)
        Debian.psql
        Postgres.localServer
        (PGTemplate.Template owner.setup_database.getDatabase fp)
        (migrate owner `inject` migrateSuperUser superuser)

{- | What a template is built from: every migration file's path and contents,
each set tagged so moving a file from one to the other counts as a change,
plus the database and roles, which end up baked into the template's
ownership and grants.
-}
{- | Copies a preview environment's database from a template already built
by 'buildTemplate' / @salmon-migrator template@ on this same cluster.

The template is named, not tracked: this binary has no graph position for
"the template got built", only a name it trusts is already there (an
already-run @template@ invocation, out of band -- see 'Salmon.Builtin.Extension.ignoreTrack').
'Postgres.cloneDatabase' refuses to run against a database it did not
itself create as a clone, so this is safe to re-run.
-}
cloneOp :: Postgres.Retention -> Postgres.Clone -> Op
cloneOp retention c =
    Postgres.cloneDatabase
        reportPrint
        Debian.psql
        Postgres.localServer.serverPort
        ignoreTrack
        retention
        c

fingerprintInputs :: PGMigrate.MigrationSetup -> PGMigrate.MigrationSetup -> IO Text
fingerprintInputs superuser owner = do
    superuserFiles <- PGTemplate.fileFingerprintParts (paths superuser)
    ownerFiles <- PGTemplate.fileFingerprintParts (paths owner)
    pure $
        PGTemplate.fingerprint $
            ["superuser"]
                <> superuserFiles
                <> ["owner"]
                <> ownerFiles
                <> ["database", Text.encodeUtf8 owner.setup_database.getDatabase, "owner", Text.encodeUtf8 owner.setup_user.userRole]
                <> ["user:" <> Text.encodeUtf8 u.userRole | (u, _) <- owner.setup_extra_users]
  where
    paths :: PGMigrate.MigrationSetup -> [FilePath]
    paths setup = [m.path | m <- toList setup.setup_migration]