packages feed

om-kubernetes-2.3.1.6: src/OM/Kubernetes.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}

{- |
  Description: Access the Kubernetes API from within the cluster.

  This module provides functions that access and operate on the
  Kubernetes API.  It is designed to be used from pods running within
  the K8s cluster itself and it won't work otherwise.
-}
module OM.Kubernetes (
  -- * Creating a handle
  newK8s,
  K8s,

  -- * Operations
  listPods,
  postPod,
  deletePod,
  getPodSpec,
  patchService,
  getServiceSpec,
  postService,
  postRoleBinding,
  postRole,
  postServiceAccount,
  postNamespace,
  getPodTemplate,
  queryPods,

  -- * Types
  JsonPatch(..),
  PodName(..),
  PodSpec(..),
  ServiceName(..),
  ServiceSpec(..),
  RoleBindingSpec(..),
  RoleSpec(..),
  ServiceAccountSpec(..),
  NamespaceSpec(..),
  Namespace(..),
  PodTemplateName(..),
  PodTemplateSpec(..),
  Pod(..),
) where


import Control.Exception.Safe (throw)
import Control.Monad ((>=>))
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson ((.:), FromJSON, FromJSONKey, ToJSON, ToJSONKey, Value,
  encode, parseJSON, withObject)
import Data.Default.Class (def)
import Data.String (IsString)
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.X509.CertificateStore (CertificateStore, readCertificateStore)
import Network.Connection (TLSSettings(TLSSettings))
import Network.HTTP.Client (Manager, newManager)
import Network.HTTP.Client.TLS (mkManagerSettings)
import Network.HTTP.Types (urlEncode)
import Network.TLS (clientShared, clientSupported,
  clientUseServerNameIndication, defaultParamsClient, sharedCAStore,
  supportedCiphers)
import Network.TLS.Extra.Cipher (ciphersuite_default)
import OM.HTTP (BearerToken(BearerToken))
import Servant.API (Accept(contentType), MimeRender(mimeRender),
  NoContent(NoContent), ToHttpApiData(toQueryParam), (:>), Capture,
  DeleteNoContent, Description, FromHttpApiData, Get, Header', JSON,
  Optional, PatchNoContent, PostNoContent, QueryParam', ReqBody,
  Required, Strict)
import Servant.API.Generic (GenericMode((:-)), Generic)
import Servant.Client (BaseUrl(BaseUrl), Scheme(Https), ClientEnv,
  ClientM, mkClientEnv, runClientM)
import Servant.Client.Generic (genericClient)
import qualified Data.ByteString as BS
import qualified Data.Text.IO as TIO


{- | A subset of the kubernetes api spec. -}
data KubernetesApi mode = KubernetesApi
  { kPostNamespaceR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> ReqBody '[JSON] NamespaceSpec
      :> PostNoContent
  , kListPodsR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "pods"
      :> Description "List pods"
      :> Get '[JSON] PodNameList
  , kQueryPodsR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "pods"
      :> QueryParam' '[Optional, Required] "labelSelectors" LabelSelectors
      :> Description "List pods"
      :> Get '[JSON] PodList
  , kPostPodR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "pods"
      :> Description "Post a pod definition"
      :> ReqBody '[JSON] PodSpec
      :> PostNoContent
  , kDeletePodR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "pods"
      :> Description "Delete a pod"
      :> Capture "pod-name" PodName
      :> DeleteNoContent
  , kGetPodSpecR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "pods"
      :> Description "Get a pod spec"
      :> Capture "pod-name" PodName
      :> Get '[JSON] PodSpec
  , kGetServiceSpecR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "services"
      :> Description "Get the cluster service."
      :> Capture "service-name" ServiceName
      :> Get '[JSON] ServiceSpec
  , kPostServiceR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "services"
      :> Description "Post a new serivce."
      :> ReqBody '[JSON] ServiceSpec
      :> PostNoContent
  , kPatchServiceR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "services"
      :> Description "Update the cluster spec annotation."
      :> Capture "service-name" ServiceName
      :> ReqBody '[JsonPatch] JsonPatch
      :> PatchNoContent
  , kPostRoleR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> Description "Roll API"
      :> "roles"
      :> ReqBody '[JSON] RoleSpec
      :> PostNoContent
  , kPostServiceAccountR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> Description "Service Account API"
      :> "serviceaccounts"
      :> ReqBody '[JSON] ServiceAccountSpec
      :> PostNoContent
  , kGetPodTemplateR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> "api"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> Description "Pod Templates API"
      :> "podtemplates"
      :> Capture "template-name" PodTemplateName
      :> Get '[JSON] PodTemplateSpec
  , kPostRoleBindingR :: mode
      :- Header' [Required, Strict] "Authorization" BearerToken
      :> Description "Role Binding API"
      :> "apis"
      :> "rbac.authorization.k8s.io"
      :> "v1"
      :> "namespaces"
      :> Capture "namespace" Namespace
      :> "rolebindings"
      :> ReqBody '[JSON] RoleBindingSpec
      :> PostNoContent
  }
  deriving stock (Generic)


{- | A handle on the kubernetes service. -}
newtype K8s = K8s {
    kManager :: Manager
                {- ^
                  An http client manager configured to work against the
                  kubernetes api.
                -}
  }


{- | Create a new 'K8s'. -}
newK8s
  :: ( MonadIO m
     )
  => m K8s
newK8s = liftIO $
    readCertificateStore crtLocation >>= \case
      Nothing -> fail "Can't load K8S CA certificate."
      Just store -> do
        manager <-
          newManager
               (
                 mkManagerSettings
                   (k8sTLSSettings store)
                   Nothing
               )
        pure K8s {
            kManager = manager
          }
  where
    k8sTLSSettings :: CertificateStore -> TLSSettings
    k8sTLSSettings store =
      TLSSettings $
        (defaultParamsClient mempty mempty) {
          clientShared = def {
            sharedCAStore = store
          },
          clientSupported = def {
            supportedCiphers = ciphersuite_default
          },
          clientUseServerNameIndication = True
        }
    crtLocation :: FilePath
    crtLocation = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"


{- | Specify how to patch the pod template spec. -}
newtype JsonPatch = JsonPatch {
    unJsonPatch :: Value
  }
  deriving newtype (ToJSON)
instance Accept JsonPatch where
  contentType _proxy = "application/json-patch+json"
instance MimeRender JsonPatch JsonPatch where
 mimeRender _ = encode


{- | A list of pods. -}
newtype PodNameList = PodNameList {
    unPodNameList :: [PodName]
  }
instance FromJSON PodNameList where
  parseJSON = withObject "Pod List (Names)" $ \o -> do
    list <- o .: "items"
    PodNameList <$> mapM ((.: "metadata") >=> (.: "name")) list


{- ==================================== List all pods ======================= -}
{- | Get the list of pods. -}
kListPods :: BearerToken -> Namespace -> ClientM PodNameList

{- | List the pods, returning a list of names. -}
listPods :: (MonadIO m) => K8s -> Namespace -> m [PodName]
listPods k namespace = do
  token <- getServiceAccountToken
  let req = kListPods token namespace
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right list -> pure (unPodNameList list)


{- | Query the pods, returning the full JSON for each. -}
queryPods :: (MonadIO m) => K8s -> Namespace -> [(Text, Text)] -> m [Pod]
queryPods k namespace selectors = do
  token <- getServiceAccountToken
  let req = kQueryPodsR genericClient token namespace (LabelSelectors selectors)
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right (PodList list) -> pure list


newtype LabelSelectors = LabelSelectors
  { _unLabelSelectors :: [(Text, Text)]
  }
instance ToHttpApiData LabelSelectors where
  toQueryParam (LabelSelectors selectors) =
    decodeUtf8 $
      BS.intercalate
        ","
        [ urlEncode False (encodeUtf8 (name <> "=" <> value))
        | (name, value) <- selectors
        ]


newtype PodList = PodList
  { _unPodList :: [Pod]
  }
instance FromJSON PodList where
  parseJSON =
    withObject "Pod List" $ \o ->
      PodList . fmap Pod <$>
        (o .: "items")


newtype Pod = Pod
  { unPod :: Value
  }
  deriving newtype (FromJSON, ToJSON)
  deriving stock (Show)


{- ==================================== Post a new pod ====================== -}
{- | Create a new pod. -}
kPostPod :: BearerToken -> Namespace -> PodSpec -> ClientM NoContent

{- | Create a new pod. -}
postPod :: (MonadIO m) => K8s -> Namespace -> PodSpec -> m ()
postPod k namespace spec = do
  token <- getServiceAccountToken
  let req = kPostPod token namespace spec
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right NoContent -> pure ()


{- ==================================== Delete a pod ======================== -}
{- | Delete a pod. -}
kDeletePod :: BearerToken -> Namespace -> PodName -> ClientM NoContent

{- | Delete a pod. -}
deletePod :: (MonadIO m) => K8s -> Namespace -> PodName -> m ()
deletePod k namespace podName = do
  token <- getServiceAccountToken
  let req = kDeletePod token namespace podName
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right NoContent -> pure ()
  

{- ==================================== Delete a pod ======================== -}
{- | Get the spec of a specific pod. -}
kGetPodSpec :: BearerToken -> Namespace -> PodName -> ClientM PodSpec

{- | Get the spec of a specific pod. -}
getPodSpec :: (MonadIO m) => K8s -> Namespace -> PodName -> m PodSpec
getPodSpec k namespace podName = do
  token <- getServiceAccountToken
  let req = kGetPodSpec token namespace podName
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right spec -> pure spec
  

{- ==================================== Patch a service ===================== -}
{- | Patch a service. -}
kPatchService
  :: BearerToken
  -> Namespace
  -> ServiceName
  -> JsonPatch
  -> ClientM NoContent

{- | Patch a service. -}
patchService :: (MonadIO m) => K8s -> Namespace -> ServiceName -> JsonPatch -> m ()
patchService k namespace service patch = do
  token <- getServiceAccountToken
  let req = kPatchService token namespace service patch
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right NoContent -> pure ()


{- ==================================== Get a service Spec ================== -}
{- | Get the service spec. -}
kGetServiceSpec
  :: BearerToken
  -> Namespace
  -> ServiceName
  -> ClientM ServiceSpec

{- | Get the service spec. -}
getServiceSpec
  :: (MonadIO m)
  => K8s
  -> Namespace
  -> ServiceName
  -> m ServiceSpec
getServiceSpec k namespace service = do
  token <- getServiceAccountToken
  let req = kGetServiceSpec token namespace service
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right spec -> pure spec


{- ==================================== Get a Pod Template Spec ============= -}
{- | Get the pod template. -}
kGetPodTemplate
  :: BearerToken
  -> Namespace
  -> PodTemplateName
  -> ClientM PodTemplateSpec

{- | Get the pod template. -}
getPodTemplate
  :: (MonadIO m)
  => K8s
  -> Namespace
  -> PodTemplateName
  -> m PodTemplateSpec
getPodTemplate k namespace templateName = do
  token <- getServiceAccountToken
  let req = kGetPodTemplate token namespace templateName
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right spec -> pure spec


{- ==================================== Post a service ====================== -}
{- | Post a new service. -}
kPostService
  :: BearerToken
  -> Namespace
  -> ServiceSpec
  -> ClientM NoContent

{- | Post a new service. -}
postService :: (MonadIO m) => K8s -> Namespace -> ServiceSpec -> m ()
postService k namespace service = do
  token <- getServiceAccountToken
  let req = kPostService token namespace service
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right NoContent -> pure ()


{- ==================================== Post Role Binding =================== -}
{- | Post a role binding. -}
kPostRoleBinding
  :: BearerToken
  -> Namespace
  -> RoleBindingSpec
  -> ClientM NoContent

{- | Post a role binding. -}
postRoleBinding :: (MonadIO m) => K8s -> Namespace -> RoleBindingSpec -> m ()
postRoleBinding k namespace roleBinding = do
  token <- getServiceAccountToken
  let req = kPostRoleBinding token namespace roleBinding
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right NoContent -> pure ()


{- ==================================== Post Role =========================== -}
{- | Post a Role. -}
kPostRole :: BearerToken -> Namespace -> RoleSpec -> ClientM NoContent

{- | Post a Role. -}
postRole :: (MonadIO m) => K8s -> Namespace -> RoleSpec -> m ()
postRole k namespace role = do
  token <- getServiceAccountToken
  let req = kPostRole token namespace role
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right NoContent -> pure ()


{- ==================================== Post Service Account ================ -}
{- | Post a service account. -}
kPostServiceAccount
  :: BearerToken
  -> Namespace
  -> ServiceAccountSpec
  -> ClientM NoContent

{- | Post a service account. -}
postServiceAccount :: (MonadIO m) => K8s -> Namespace -> ServiceAccountSpec -> m ()
postServiceAccount k namespace serviceAccount = do
  token <- getServiceAccountToken
  let req = kPostServiceAccount token namespace serviceAccount
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right NoContent -> pure ()


{- ==================================== Post a Namspace ===================== -}
{- | Post a Namespace. -}
kPostNamespace :: BearerToken -> NamespaceSpec -> ClientM NoContent

{- | Post a Namespace. -}
postNamespace :: (MonadIO m) => K8s -> NamespaceSpec -> m ()
postNamespace k namespace = do
  token <- getServiceAccountToken
  let req = kPostNamespace token namespace
  liftIO $ runClientM req (mkEnv k) >>= \case
    Left err -> liftIO (throw err)
    Right NoContent -> pure ()


{- ==================================== Other stuff ========================= -}

KubernetesApi
    { kPostNamespaceR = kPostNamespace
    , kListPodsR = kListPods
    , kPostPodR = kPostPod
    , kDeletePodR = kDeletePod
    , kGetPodSpecR = kGetPodSpec
    , kGetServiceSpecR = kGetServiceSpec
    , kPostServiceR = kPostService
    , kPatchServiceR = kPatchService
    , kPostRoleR = kPostRole
    , kPostServiceAccountR = kPostServiceAccount
    , kGetPodTemplateR = kGetPodTemplate
    , kPostRoleBindingR = kPostRoleBinding
    }
  =
    genericClient


{- | The name of a service. -}
newtype ServiceName = ServiceName {
    unServiceName :: Text
  }
  deriving newtype (ToHttpApiData)


{- | The specification of a service. -}
newtype ServiceSpec = ServiceSpec {
    unServiceSpec :: Value
  }
  deriving newtype (FromJSON, ToJSON)


{- | The name of a pod template. -}
newtype PodTemplateName =  PodTemplateName
  { unPodTemplateName :: Text
  }
  deriving newtype (
    Eq, Ord, Show, IsString, ToHttpApiData, FromHttpApiData, ToJSON,
    FromJSON, ToJSONKey, FromJSONKey
  )


{- | The specification of a pod template.  -}
newtype PodTemplateSpec = PodTempalteSpec
  { unPodTemplateSpec :: Value
  }
  deriving newtype (FromJSON, ToJSON)


{- | A pod specification. -}
newtype PodSpec = PodSpec {
    unPodSpec :: Value
  }
  deriving newtype (FromJSON, ToJSON)


{- | A Kubernetes namespace. -}
newtype Namespace = Namespace
  { unNamespace :: Text
  }
  deriving newtype (
    Eq, Ord, Show, IsString, ToHttpApiData, FromHttpApiData, ToJSON,
    FromJSON, ToJSONKey, FromJSONKey
  )


{- | The name of a pod. -}
newtype PodName = PodName {
    unPodName :: Text
  }
  deriving newtype (
    Eq, Ord, Show, IsString, ToHttpApiData, FromHttpApiData, ToJSON,
    FromJSON, ToJSONKey, FromJSONKey
  )


{- | The representation of Role Binding. -}
newtype RoleBindingSpec = RoleBindingSpec {
    unRoleBindingSpec :: Value
  }
  deriving newtype (ToJSON, FromJSON)


{- | Get the k8s service account token. -}
getServiceAccountToken :: (MonadIO m) => m BearerToken
getServiceAccountToken =
  fmap BearerToken
  . liftIO
  . TIO.readFile
  $ "/var/run/secrets/kubernetes.io/serviceaccount/token"


mkEnv :: K8s -> ClientEnv
mkEnv =
    mkEnv_ . kManager 
  where
    mkEnv_ :: Manager -> ClientEnv
    mkEnv_ manager =
      mkClientEnv
        manager
        (BaseUrl Https "kubernetes.default.svc" 443 "")


{- | The representation of a Role. -}
newtype RoleSpec = RoleSpec {
    unRoleSpec :: Value
  }
  deriving newtype (ToJSON, FromJSON)


{- | The representation of a service account. -}
newtype ServiceAccountSpec = ServiceAccountSpec {
    unServiceAccountSpec :: Value
  }
  deriving newtype (ToJSON, FromJSON)


{- | The representation of a Namespace specification. -}
newtype NamespaceSpec = NamespaceSpec {
    unNamespaceSpec :: Value
  }
  deriving newtype (ToJSON, FromJSON)