packages feed

salmon-ops-0.1.0.0: src/Salmon/Builtin/Nodes/Nginx.hs

{- | nginx as a reverse-proxy load balancer, configured purely from a list of
DNS-name -> upstream(s) mappings — one @server{}@ block per name, proxying
to a load-balanced upstream group.
-}
module Salmon.Builtin.Nodes.Nginx where

import Salmon.Builtin.Extension
import Salmon.Builtin.Nodes.Binary (Binary, justInstall)
import qualified Salmon.Builtin.Nodes.Filesystem as FS
import qualified Salmon.Builtin.Nodes.Systemd as Systemd
import Salmon.Op.OpGraph (inject)
import Salmon.Op.Track
import Salmon.Reporter

import Data.Text (Text)
import qualified Data.Text as Text

import System.FilePath ((</>))

-------------------------------------------------------------------------------

type ServerName = Text

data Upstream
    = Upstream
    { upstream_host :: Text
    , upstream_port :: Int
    }
    deriving (Show)

data Scheme
    = Http
    | Https
    deriving (Show)

schemeTxt :: Scheme -> Text
schemeTxt Http = "http"
schemeTxt Https = "https"

-- | One DNS name routed to a (possibly load-balanced) set of backends.
data VHost
    = VHost
    { vhost_server_name :: ServerName
    , vhost_listen_port :: Int
    , vhost_upstream_scheme :: Scheme
    , vhost_upstreams :: [Upstream]
    }

data NginxConfig
    = NginxConfig
    { nginx_sites_dir :: FilePath
    -- ^ e.g. \/etc\/nginx\/conf.d
    , nginx_vhosts :: [VHost]
    }

vhostFileName :: VHost -> FilePath
vhostFileName v = Text.unpack v.vhost_server_name <> ".conf"

-------------------------------------------------------------------------------

configFiles :: NginxConfig -> Op
configFiles cfg =
    op "nginx-config" (deps $ fmap vhostFile cfg.nginx_vhosts) id
  where
    vhostFile v = FS.filecontents $ FS.FileContents (cfg.nginx_sites_dir </> vhostFileName v) (renderVHost v)

renderVHost :: VHost -> Text
renderVHost v =
    Text.unlines $
        mconcat
            [
                [ "upstream " <> upstreamName <> " {"
                ]
            , fmap renderUpstream v.vhost_upstreams
            ,
                [ "}"
                , ""
                , "server {"
                , "    listen " <> Text.pack (show v.vhost_listen_port) <> ";"
                , "    server_name " <> v.vhost_server_name <> ";"
                , ""
                , "    location / {"
                , "        proxy_pass " <> schemeTxt v.vhost_upstream_scheme <> "://" <> upstreamName <> ";"
                , "        proxy_set_header Host $host;"
                , "        proxy_set_header X-Real-IP $remote_addr;"
                , "        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
                , "        proxy_set_header X-Forwarded-Proto $scheme;"
                , "    }"
                , "}"
                ]
            ]
  where
    upstreamName :: Text
    upstreamName = "upstream_" <> Text.replace "." "_" v.vhost_server_name

    renderUpstream :: Upstream -> Text
    renderUpstream u = mconcat ["    server ", u.upstream_host, ":", Text.pack (show u.upstream_port), ";"]

-------------------------------------------------------------------------------

-- | Installs nginx, renders every vhost's config, and reloads the (package-shipped) service.
setup :: Reporter Systemd.Report -> Track' (Binary "systemctl") -> Track' (Binary "nginx") -> NginxConfig -> Op
setup r systemctl nginxBin cfg =
    Systemd.restartService r systemctl "nginx.service"
        `inject` configFiles cfg
        `inject` justInstall nginxBin