{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-|

Module      : Kubernetes.Webhook
Copyright   : (c) Earnest Research, 2020
License     : MIT
Maintainer  : amarrella@earnestresearch.com
Stability   : experimental
Portability : POSIX

This module lets you create [Kubernetes Admission Webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/).

Example with Servant (note: webhooks in Kubernetes require TLS):

@
  module Kubernetes.Example
      ( startApp,
        app,
      )
    where

  import Control.Monad.IO.Class
  import qualified Data.Aeson as A
  import qualified Data.ByteString as BS
  import qualified Data.HashMap.Strict as HM
  import Data.Text
  import GHC.Generics
  import qualified Kubernetes.Webhook as W
  import Network.Wai
  import Network.Wai.Handler.Warp
  import Network.Wai.Handler.WarpTLS
  import Servant
  import System.Environment

  type API =
    "mutate" :> ReqBody '[JSON] W.AdmissionReviewRequest :> Post '[JSON] W.AdmissionReviewResponse

  data Toleration
    = Toleration
        { effect :: Maybe TolerationEffect,
          key :: Maybe Text,
          operator :: Maybe TolerationOperator,
          tolerationSeconds :: Maybe Integer,
          value :: Maybe Text
        }
    deriving (Generic, A.ToJSON)

  data TolerationEffect = NoSchedule | PreferNoSchedule | NoExecute deriving (Generic, A.ToJSON)

  data TolerationOperator = Exists | Equal deriving (Generic, A.ToJSON)

  testToleration :: Toleration
  testToleration =
    Toleration
      { effect = Just NoSchedule,
        key = Just "dedicated",
        operator = Just Equal,
        tolerationSeconds = Nothing,
        value = Just "test"
      }

  startApp :: IO ()
  startApp = do
    let tlsOpts = tlsSettings "/certs/tls.crt" "/certs/tls.key"
        warpOpts = setPort 8080 defaultSettings
    runTLS tlsOpts warpOpts app

  app :: Application
  app = serve api server

  api :: Proxy API
  api = Proxy

  server :: Server API
  server = mutate

  mutate :: W.AdmissionReviewRequest -> Handler W.AdmissionReviewResponse
  mutate req = pure $ W.mutatingWebhook req (\_ -> Right W.Allowed) addToleration

  addToleration :: W.Patch
  addToleration = 
    W.Patch
      [ W.PatchOperation
          { op = W.Add,
            path = "/spec/tolerations/-",
            from = Nothing,
            value = Just $ A.toJSON testToleration
          }
      ]
@

-}

module Kubernetes.Webhook (
  mutatingWebhook,
  validatingWebhook,
  Allowed(..),
  module Kubernetes.Webhook.Types
) where

import Kubernetes.Webhook.Types
import Data.Either

data Allowed = Allowed

-- | Lets you create a mutating admission webhook 
mutatingWebhook :: AdmissionReviewRequest -- ^ the request the webhook receives from Kubernetes
                -> (AdmissionRequest -> Either Status Allowed) -- ^ logic to validate the request or reject it with an error
                -> Patch -- ^ the change to apply to the object
                -> AdmissionReviewResponse -- ^ the response sent back to Kubernetes
mutatingWebhook AdmissionReviewRequest { request = req } allow patch =
    admissionReviewResponse AdmissionResponse {
      uid = rid,
      allowed = isRight processedRequest,
      patch = Just patch,
      status = either Just (const Nothing) processedRequest,
      patchType = Just JSONPatch,
      auditAnnotations = Nothing
    }
  where
    AdmissionRequest { uid = rid } = req
    processedRequest = allow req

-- | Lets you create a validating admission webhook
validatingWebhook :: AdmissionReviewRequest  -- ^ the request the webhook receives from Kubernetes
                  -> (AdmissionRequest -> Either Status Allowed)  -- ^ logic to validate the request or reject it with an error
                  -> AdmissionReviewResponse -- ^ the response sent back to Kubernetes
validatingWebhook AdmissionReviewRequest { request = req } allow =
    admissionReviewResponse AdmissionResponse {
      uid = rid,
      allowed = isRight processedRequest,
      patch = Nothing,
      status = either Just (const Nothing) processedRequest,
      patchType = Nothing,
      auditAnnotations = Nothing
    }
  where
    AdmissionRequest { uid = rid } = req
    processedRequest = allow req