packages feed

terraform-http-backend-pass-0.1.0.0: src/Terraform/HttpBackend/Pass/Api.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module Terraform.HttpBackend.Pass.Api where

import Control.Monad (unless)
import Data.Aeson (ToJSON (..), Value, (.=))
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Text as Aeson
import qualified Data.ByteString.Lazy as LBS
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import qualified Data.Text.Encoding as Text
import qualified Data.Text.Lazy as LText
import GHC.Generics (Generic)
import Servant
import Servant.API.Generic (ToServant, ToServantApi)
import qualified Servant.Server.Generic as Servant
import Terraform.HttpBackend.Pass.App (AppT)
import Terraform.HttpBackend.Pass.Crypt (MonadPass (..))
import Terraform.HttpBackend.Pass.Git (MonadGit (..))

data StateNotFound = StateNotFound

instance HasStatus StateNotFound where
  type StatusOf StateNotFound = 404

instance ToJSON StateNotFound where
  toJSON _ = Aeson.object ["error" .= ("state not found" :: Text)]

newtype StateCorrupt = StateCorrupt {err :: String}

instance HasStatus StateCorrupt where
  type StatusOf StateCorrupt = 500

instance ToJSON StateCorrupt where
  toJSON (StateCorrupt err) =
    Aeson.object
      [ "error" .= ("state corrupt" :: Text),
        "message" .= err
      ]

type GetResponse = '[WithStatus 200 Value, StateNotFound, StateCorrupt]

type GetState = "state" :> Capture "name" Text :> UVerb 'GET '[JSON] GetResponse

type UpdateState = "state" :> Capture "name" Text :> ReqBody '[JSON] Value :> PostNoContent

type DeleteState = "state" :> Capture "name" Text :> DeleteNoContent

type Api = GetState :<|> UpdateState :<|> DeleteState

api :: Proxy Api
api = Proxy

server :: (Monad m, MonadPass m, MonadGit m) => ServerT Api m
server =
  getStateImpl
    :<|> updateStateImpl
    :<|> purgeStateImpl

getStateImpl :: (Monad m, MonadGit m, MonadPass m) => Text -> m (Union GetResponse)
getStateImpl name = do
  gitPull
  let path = stateFilePath name
  stateExists <- exists path
  if stateExists
    then do
      eitherState <-
        Aeson.eitherDecode @Value . LBS.fromStrict . Text.encodeUtf8
          <$> decrypt path
      case eitherState of
        Left err -> respond $ StateCorrupt err
        Right state -> respond (WithStatus @200 state)
    else respond StateNotFound

updateStateImpl :: (Monad m, MonadPass m, MonadGit m) => Text -> Value -> m NoContent
updateStateImpl name tfstate = do
  gitPull
  let path = stateFilePath name
  -- Also commits
  encrypt path $ LText.toStrict $ Aeson.encodeToLazyText tfstate

  gitPush
  pure NoContent

purgeStateImpl :: (MonadGit m, MonadPass m, Monad m) => Text -> m NoContent
purgeStateImpl name = do
  gitPull
  let path = stateFilePath name
  stateExists <- exists path
  unless stateExists $ do
    purge (stateFilePath name)
    gitPush
  pure NoContent

stateFilePath :: Text -> Text
stateFilePath name = name <> "/terraform.tfstate"