packages feed

kubernetes-client 0.1.0.1 → 0.2.0.0

raw patch · 21 files changed

+1262/−230 lines, 21 filesdep +attoparsecdep +base64-bytestringdep +eitherdep ~aesondep ~bytestringdep ~connectionPVP ok

version bump matches the API change (PVP)

Dependencies added: attoparsec, base64-bytestring, either, file-embed, filepath, hoauth2, hspec-attoparsec, jose-jwt, jsonpath, oidc-client, stm, time, timerep, typed-process, uri-bytestring

Dependency ranges changed: aeson, bytestring, connection, containers, data-default-class, hspec, http-client-tls, kubernetes-client-core, microlens, mtl, pem, streaming-bytestring, x509, x509-store, x509-system, x509-validation, yaml

API changes (from Hackage documentation)

- Kubernetes.Client.Config: ParsePEMCertsException :: String -> ParsePEMCertsException
- Kubernetes.Client.Config: cluster :: (MonadIO m, MonadThrow m) => m (Manager, KubernetesClientConfig)
- Kubernetes.Client.Config: data ParsePEMCertsException
- Kubernetes.Client.Config: instance GHC.Exception.Type.Exception Kubernetes.Client.Config.ParsePEMCertsException
- Kubernetes.Client.Config: instance GHC.Show.Show Kubernetes.Client.Config.ParsePEMCertsException
+ Kubernetes.Client.Auth.ClientCert: CredentialLoadException :: String -> CredentialLoadException
+ Kubernetes.Client.Auth.ClientCert: clientCertDataAuth :: DetectAuth
+ Kubernetes.Client.Auth.ClientCert: clientCertFileAuth :: DetectAuth
+ Kubernetes.Client.Auth.ClientCert: data CredentialLoadException
+ Kubernetes.Client.Auth.ClientCert: disableValidateAuthMethods :: KubernetesClientConfig -> KubernetesClientConfig
+ Kubernetes.Client.Auth.ClientCert: instance GHC.Exception.Type.Exception Kubernetes.Client.Auth.ClientCert.CredentialLoadException
+ Kubernetes.Client.Auth.ClientCert: instance GHC.Show.Show Kubernetes.Client.Auth.ClientCert.CredentialLoadException
+ Kubernetes.Client.Auth.GCP: gcpAuth :: DetectAuth
+ Kubernetes.Client.Auth.GCP: instance GHC.Exception.Type.Exception Kubernetes.Client.Auth.GCP.GCPAuthParsingException
+ Kubernetes.Client.Auth.GCP: instance GHC.Exception.Type.Exception Kubernetes.Client.Auth.GCP.GCPGetTokenException
+ Kubernetes.Client.Auth.GCP: instance GHC.Show.Show Kubernetes.Client.Auth.GCP.GCPAuthParsingException
+ Kubernetes.Client.Auth.GCP: instance GHC.Show.Show Kubernetes.Client.Auth.GCP.GCPGetTokenException
+ Kubernetes.Client.Auth.GCP: instance Kubernetes.OpenAPI.Core.AuthMethod Kubernetes.Client.Auth.GCP.GCPAuth
+ Kubernetes.Client.Auth.Internal.Types: type DetectAuth = AuthInfo -> (ClientParams, KubernetesClientConfig) -> Maybe (IO (ClientParams, KubernetesClientConfig))
+ Kubernetes.Client.Auth.OIDC: cachedOIDCAuth :: OIDCCache -> DetectAuth
+ Kubernetes.Client.Auth.OIDC: instance GHC.Exception.Type.Exception Kubernetes.Client.Auth.OIDC.OIDCAuthParsingException
+ Kubernetes.Client.Auth.OIDC: instance GHC.Exception.Type.Exception Kubernetes.Client.Auth.OIDC.OIDCGetTokenException
+ Kubernetes.Client.Auth.OIDC: instance GHC.Show.Show Kubernetes.Client.Auth.OIDC.OIDCAuthParsingException
+ Kubernetes.Client.Auth.OIDC: instance GHC.Show.Show Kubernetes.Client.Auth.OIDC.OIDCGetTokenException
+ Kubernetes.Client.Auth.OIDC: instance Kubernetes.OpenAPI.Core.AuthMethod Kubernetes.Client.Auth.OIDC.OIDCAuth
+ Kubernetes.Client.Auth.OIDC: oidcAuth :: DetectAuth
+ Kubernetes.Client.Auth.OIDC: type OIDCCache = TVar (Map (Text, Text) OIDCAuth)
+ Kubernetes.Client.Auth.Token: setTokenAuth :: Text -> KubernetesClientConfig -> KubernetesClientConfig
+ Kubernetes.Client.Auth.Token: tokenAuth :: DetectAuth
+ Kubernetes.Client.Auth.TokenFile: TokenFileAuth :: TVar (Maybe Text) -> TVar (Maybe UTCTime) -> FilePath -> NominalDiffTime -> TokenFileAuth
+ Kubernetes.Client.Auth.TokenFile: [expiry] :: TokenFileAuth -> TVar (Maybe UTCTime)
+ Kubernetes.Client.Auth.TokenFile: [file] :: TokenFileAuth -> FilePath
+ Kubernetes.Client.Auth.TokenFile: [period] :: TokenFileAuth -> NominalDiffTime
+ Kubernetes.Client.Auth.TokenFile: [token] :: TokenFileAuth -> TVar (Maybe Text)
+ Kubernetes.Client.Auth.TokenFile: data TokenFileAuth
+ Kubernetes.Client.Auth.TokenFile: getCurrentToken :: TokenFileAuth -> IO (Maybe Text)
+ Kubernetes.Client.Auth.TokenFile: getToken :: TokenFileAuth -> IO Text
+ Kubernetes.Client.Auth.TokenFile: instance Kubernetes.OpenAPI.Core.AuthMethod Kubernetes.Client.Auth.TokenFile.TokenFileAuth
+ Kubernetes.Client.Auth.TokenFile: reloadToken :: TokenFileAuth -> IO Text
+ Kubernetes.Client.Auth.TokenFile: setTokenFileAuth :: FilePath -> KubernetesClientConfig -> IO KubernetesClientConfig
+ Kubernetes.Client.Auth.TokenFile: tokenFileAuth :: DetectAuth
+ Kubernetes.Client.Config: KubeConfigCluster :: KubeConfigSource
+ Kubernetes.Client.Config: KubeConfigFile :: FilePath -> KubeConfigSource
+ Kubernetes.Client.Config: addCACertData :: MonadThrow m => Config -> ClientParams -> m ClientParams
+ Kubernetes.Client.Config: addCACertFile :: Config -> FilePath -> ClientParams -> IO ClientParams
+ Kubernetes.Client.Config: applyAuthSettings :: OIDCCache -> AuthInfo -> (ClientParams, KubernetesClientConfig) -> IO (ClientParams, KubernetesClientConfig)
+ Kubernetes.Client.Config: data KubeConfigSource
+ Kubernetes.Client.Config: mkInClusterClientConfig :: (MonadIO m, MonadThrow m) => m (Manager, KubernetesClientConfig)
+ Kubernetes.Client.Config: mkKubeClientConfig :: OIDCCache -> KubeConfigSource -> IO (Manager, KubernetesClientConfig)
+ Kubernetes.Client.Config: tlsValidation :: Config -> ClientParams -> ClientParams
+ Kubernetes.Client.Internal.TLSUtils: Base64ParsingFailed :: String -> ParseCertException
+ Kubernetes.Client.Internal.TLSUtils: FailedToLoadCredential :: String -> ParseCertException
+ Kubernetes.Client.Internal.TLSUtils: PEMParsingFailed :: String -> ParseCertException
+ Kubernetes.Client.Internal.TLSUtils: clientHooksL :: Lens' ClientParams ClientHooks
+ Kubernetes.Client.Internal.TLSUtils: clientSharedL :: Lens' ClientParams Shared
+ Kubernetes.Client.Internal.TLSUtils: data ParseCertException
+ Kubernetes.Client.Internal.TLSUtils: defaultTLSClientParams :: IO ClientParams
+ Kubernetes.Client.Internal.TLSUtils: disableServerCertValidation :: ClientParams -> ClientParams
+ Kubernetes.Client.Internal.TLSUtils: disableServerNameValidation :: ClientParams -> ClientParams
+ Kubernetes.Client.Internal.TLSUtils: instance GHC.Exception.Type.Exception Kubernetes.Client.Internal.TLSUtils.ParseCertException
+ Kubernetes.Client.Internal.TLSUtils: instance GHC.Show.Show Kubernetes.Client.Internal.TLSUtils.ParseCertException
+ Kubernetes.Client.Internal.TLSUtils: loadB64EncodedCert :: MonadThrow m => ByteString -> ByteString -> m Credential
+ Kubernetes.Client.Internal.TLSUtils: loadPEMCerts :: (MonadIO m, MonadThrow m) => FilePath -> m [SignedCertificate]
+ Kubernetes.Client.Internal.TLSUtils: onCertificateRequestL :: Lens' ClientParams (([CertificateType], Maybe [HashAndSignatureAlgorithm], [DistinguishedName]) -> IO (Maybe (CertificateChain, PrivKey)))
+ Kubernetes.Client.Internal.TLSUtils: onServerCertificateL :: Lens' ClientParams (CertificateStore -> ValidationCache -> ServiceID -> CertificateChain -> IO [FailedReason])
+ Kubernetes.Client.Internal.TLSUtils: parsePEMCerts :: ByteString -> Either ParseCertException [SignedCertificate]
+ Kubernetes.Client.Internal.TLSUtils: setCAStore :: [SignedCertificate] -> ClientParams -> ClientParams
+ Kubernetes.Client.Internal.TLSUtils: setClientCert :: Credential -> ClientParams -> ClientParams
+ Kubernetes.Client.Internal.TLSUtils: sharedCAStoreL :: Lens' Shared CertificateStore
+ Kubernetes.Client.Internal.TLSUtils: updateClientParams :: ClientParams -> ByteString -> Either ParseCertException ClientParams
+ Kubernetes.Data.K8sJSONPath: JSONPath :: [JSONPathElement] -> K8sPathElement
+ Kubernetes.Data.K8sJSONPath: PlainText :: Text -> K8sPathElement
+ Kubernetes.Data.K8sJSONPath: data K8sPathElement
+ Kubernetes.Data.K8sJSONPath: encodeResult :: ExecutionResult Value -> Either String Text
+ Kubernetes.Data.K8sJSONPath: instance GHC.Classes.Eq Kubernetes.Data.K8sJSONPath.K8sPathElement
+ Kubernetes.Data.K8sJSONPath: instance GHC.Show.Show Kubernetes.Data.K8sJSONPath.K8sPathElement
+ Kubernetes.Data.K8sJSONPath: jsonToText :: Value -> Text
+ Kubernetes.Data.K8sJSONPath: jsonpathParser :: Parser K8sPathElement
+ Kubernetes.Data.K8sJSONPath: k8sJSONPath :: Parser [K8sPathElement]
+ Kubernetes.Data.K8sJSONPath: pathElementParser :: Parser K8sPathElement
+ Kubernetes.Data.K8sJSONPath: plainTextParser :: Parser K8sPathElement
+ Kubernetes.Data.K8sJSONPath: runJSONPath :: [K8sPathElement] -> Value -> Either String Text
+ Kubernetes.Data.K8sJSONPath: runPathElement :: K8sPathElement -> Value -> Either String Text
- Kubernetes.Client.Config: parsePEMCerts :: ByteString -> Either String [SignedCertificate]
+ Kubernetes.Client.Config: parsePEMCerts :: ByteString -> Either ParseCertException [SignedCertificate]

Files

README.md view
@@ -2,6 +2,52 @@  ## Example +### Load KubeConfig file++```haskell+import Control.Concurrent.STM (atomically, newTVar)+import Kubernetes.Client      (KubeConfigSource (..), mkKubeClientConfig)+import Kubernetes.OpenAPI     (Accept (..), MimeJSON (..), dispatchMime)++import qualified Data.Map                      as Map+import qualified Kubernetes.OpenAPI.API.CoreV1 as CoreV1++main :: IO ()+main = do+    oidcCache <- atomically $ newTVar $ Map.fromList []+    (mgr, kcfg) <- mkKubeClientConfig oidcCache $ KubeConfigFile "/path/to/kubeconfig"+    dispatchMime+            mgr+            kcfg+            (CoreV1.listPodForAllNamespaces (Accept MimeJSON))+        >>= print+```++### Load InCluster Config++```haskell+import Control.Concurrent.STM (atomically, newTVar)+import Data.Function          ((&))+import Kubernetes.Client      (KubeConfigSource (..), mkKubeClientConfig)+import Kubernetes.OpenAPI     (Accept (..), MimeJSON (..), dispatchMime)+import Network.TLS            (credentialLoadX509)++import qualified Data.Map                      as Map+import qualified Kubernetes.OpenAPI.API.CoreV1 as CoreV1++main :: IO ()+main = do+    oidcCache <- atomically $ newTVar $ Map.fromList []+    (mgr, kcfg) <- mkKubeClientConfig oidcCache KubeConfigCluster+    dispatchMime+            mgr+            kcfg+            (CoreV1.listPodForAllNamespaces (Accept MimeJSON))+        >>= print+```++### Load config from URL and paths+ ```haskell {-# LANGUAGE OverloadedStrings #-} 
example/App.hs view
@@ -2,18 +2,20 @@  module Main where -import           Data.Function                 ((&))-import           Kubernetes.Client             (defaultTLSClientParams,-                                                disableServerCertValidation,-                                                disableServerNameValidation,-                                                disableValidateAuthMethods,-                                                loadPEMCerts, newManager,-                                                setCAStore, setClientCert,-                                                setMasterURI, setTokenAuth)-import           Kubernetes.OpenAPI            (Accept (..), MimeJSON (..),-                                                dispatchMime, newConfig)+import Control.Concurrent.STM (atomically, newTVar)+import Data.Function          ((&))+import Kubernetes.Client      (KubeConfigSource (..), defaultTLSClientParams,+                               disableServerCertValidation,+                               disableServerNameValidation,+                               disableValidateAuthMethods, mkKubeClientConfig,+                               loadPEMCerts, newManager, setCAStore,+                               setClientCert, setMasterURI, setTokenAuth)+import Kubernetes.OpenAPI     (Accept (..), MimeJSON (..), dispatchMime,+                               newConfig)+import Network.TLS            (credentialLoadX509)++import qualified Data.Map                      as Map import qualified Kubernetes.OpenAPI.API.CoreV1 as CoreV1-import           Network.TLS                   (credentialLoadX509)  example :: IO () example = do@@ -38,6 +40,26 @@     manager <- newManager tlsParams     dispatchMime             manager+            kcfg+            (CoreV1.listPodForAllNamespaces (Accept MimeJSON))+        >>= print++exampleWithKubeConfig :: IO ()+exampleWithKubeConfig = do+    oidcCache <- atomically $ newTVar $ Map.fromList []+    (mgr, kcfg) <- mkKubeClientConfig oidcCache $ KubeConfigFile "/path/to/kubeconfig"+    dispatchMime+            mgr+            kcfg+            (CoreV1.listPodForAllNamespaces (Accept MimeJSON))+        >>= print++exampleWithInClusterConfig :: IO ()+exampleWithInClusterConfig = do+    oidcCache <- atomically $ newTVar $ Map.fromList []+    (mgr, kcfg) <- mkKubeClientConfig oidcCache KubeConfigCluster+    dispatchMime+            mgr             kcfg             (CoreV1.listPodForAllNamespaces (Accept MimeJSON))         >>= print
kubernetes-client.cabal view
@@ -1,111 +1,174 @@-cabal-version: 1.12-name: kubernetes-client-version: 0.1.0.1-license: Apache-2.0-license-file: LICENSE-maintainer: Shimin Guo <smguo2001@gmail.com>,-            Akshay Mankar <itsakshaymankar@gmail.com>-synopsis: Client library for Kubernetes+cabal-version:      1.12+name:               kubernetes-client+version:            0.2.0.0+license:            Apache-2.0+license-file:       LICENSE+maintainer:+    Shimin Guo <smguo2001@gmail.com>,+    Akshay Mankar <itsakshaymankar@gmail.com>++synopsis:           Client library for Kubernetes description:     Client library for interacting with a Kubernetes cluster.     .     This package contains hand-written code while kubernetes-client-core contains code auto-generated from the OpenAPI spec.-category: Web-build-type: Simple++category:           Web+build-type:         Simple extra-source-files:+    test/testdata/certs/certificate.pem+    test/testdata/certs/private-key.pem     test/testdata/kubeconfig.yaml+    test/testdata/tokens/token1+    test/testdata/tokens/token2     README.md  library     exposed-modules:         Kubernetes.Client+        Kubernetes.Client.Auth.ClientCert+        Kubernetes.Client.Auth.GCP+        Kubernetes.Client.Auth.Internal.Types+        Kubernetes.Client.Auth.OIDC+        Kubernetes.Client.Auth.Token+        Kubernetes.Client.Auth.TokenFile         Kubernetes.Client.Config+        Kubernetes.Client.Internal.TLSUtils         Kubernetes.Client.KubeConfig         Kubernetes.Client.Watch-    hs-source-dirs: src-    other-modules:-        Paths_kubernetes_client+        Kubernetes.Data.K8sJSONPath++    hs-source-dirs:   src+    other-modules:    Paths_kubernetes_client     default-language: Haskell2010+    ghc-options:      -Wall     build-depends:-        aeson >=1.2.2 && <1.5,+        aeson >=1.2 && <1.5,+        attoparsec ==0.13.*,         base >=4.7 && <5.0,-        bytestring >=0.10.0 && <0.11,-        connection >=0.2.8 && <0.3,-        containers >=0.6.0.1 && <0.7,-        data-default-class >=0.1.2.0 && <0.2,+        base64-bytestring >=1.0.0.2 && <1.1,+        bytestring ==0.10.*,+        connection >=0.2 && <0.4,+        containers >=0.5 && <0.7,+        data-default-class ==0.1.*,+        either ==5.0.*,+        filepath ==1.4.*,+        hoauth2 ==1.8.*,         http-client >=0.5 && <0.7,-        http-client-tls >=0.3.5.3 && <0.4,-        kubernetes-client-core ==0.1.0.1,-        microlens >=0.4.3 && <0.5,-        mtl >=2.2.1 && <2.3,-        pem >=0.2.4 && <0.3,+        http-client-tls ==0.3.*,+        jose-jwt ==0.8.*,+        jsonpath ==0.1.*,+        kubernetes-client-core ==0.2.0.0,+        microlens ==0.4.*,+        mtl ==2.2.*,+        oidc-client ==0.4.*,+        pem ==0.2.*,         safe-exceptions >=0.1.0.0 && <0.2,-        streaming-bytestring >=0.1.5 && <0.2.0,+        stm >=2.4 && <2.6,+        streaming-bytestring >=0.1 && <0.2.0,         text >=0.11 && <1.3,+        time ==1.8.*,+        timerep ==2.0.*,         tls >=1.4.1 && <1.5,-        x509 >=1.7.5 && <1.8,-        x509-store >=1.6.7 && <1.7,-        x509-system >=1.6.6 && <1.7,-        x509-validation >=1.6.11 && <1.7+        typed-process ==0.2.*,+        uri-bytestring ==0.3.*,+        x509 ==1.7.*,+        x509-store ==1.6.*,+        x509-system ==1.6.*,+        x509-validation ==1.6.*,+        yaml >=0.8.32 && <0.12  test-suite example-    type: exitcode-stdio-1.0-    main-is: App.hs-    hs-source-dirs: example-    other-modules:-        Paths_kubernetes_client+    type:             exitcode-stdio-1.0+    main-is:          App.hs+    hs-source-dirs:   example+    other-modules:    Paths_kubernetes_client     default-language: Haskell2010     build-depends:-        aeson >=1.2.2 && <1.5,+        aeson >=1.2 && <1.5,+        attoparsec ==0.13.*,         base >=4.7 && <5.0,-        bytestring >=0.10.0 && <0.11,-        connection >=0.2.8 && <0.3,-        containers >=0.6.0.1 && <0.7,-        data-default-class >=0.1.2.0 && <0.2,+        base64-bytestring >=1.0.0.2 && <1.1,+        bytestring ==0.10.*,+        connection >=0.2 && <0.4,+        containers >=0.5 && <0.7,+        data-default-class ==0.1.*,+        either ==5.0.*,+        filepath ==1.4.*,+        hoauth2 ==1.8.*,         http-client >=0.5 && <0.7,-        http-client-tls >=0.3.5.3 && <0.4,+        http-client-tls ==0.3.*,+        jose-jwt ==0.8.*,+        jsonpath ==0.1.*,         kubernetes-client -any,-        kubernetes-client-core ==0.1.0.1,-        microlens >=0.4.3 && <0.5,-        mtl >=2.2.1 && <2.3,-        pem >=0.2.4 && <0.3,+        kubernetes-client-core ==0.2.0.0,+        microlens ==0.4.*,+        mtl ==2.2.*,+        oidc-client ==0.4.*,+        pem ==0.2.*,         safe-exceptions >=0.1.0.0 && <0.2,-        streaming-bytestring >=0.1.5 && <0.2.0,+        stm >=2.4 && <2.6,+        streaming-bytestring >=0.1 && <0.2.0,         text >=0.11 && <1.3,+        time ==1.8.*,+        timerep ==2.0.*,         tls >=1.4.1 && <1.5,-        x509 >=1.7.5 && <1.8,-        x509-store >=1.6.7 && <1.7,-        x509-system >=1.6.6 && <1.7,-        x509-validation >=1.6.11 && <1.7+        typed-process ==0.2.*,+        uri-bytestring ==0.3.*,+        x509 ==1.7.*,+        x509-store ==1.6.*,+        x509-system ==1.6.*,+        x509-validation ==1.6.*,+        yaml >=0.8.32 && <0.12  test-suite spec-    type: exitcode-stdio-1.0-    main-is: Spec.hs-    hs-source-dirs: test+    type:             exitcode-stdio-1.0+    main-is:          Spec.hs+    hs-source-dirs:   test     other-modules:+        Kubernetes.Client.Auth.ClientCertSpec+        Kubernetes.Client.Auth.TokenFileSpec+        Kubernetes.Client.KubeConfigSpec+        Kubernetes.Data.K8sJSONPathSpec         Paths_kubernetes_client+     default-language: Haskell2010     build-depends:-        aeson >=1.2.2 && <1.5,+        aeson >=1.2 && <1.5,+        attoparsec ==0.13.*,         base >=4.7 && <5.0,-        bytestring >=0.10.0 && <0.11,-        connection >=0.2.8 && <0.3,-        containers >=0.6.0.1 && <0.7,-        data-default-class >=0.1.2.0 && <0.2,-        hspec >=2.6.1 && <2.7,+        base64-bytestring >=1.0.0.2 && <1.1,+        bytestring ==0.10.*,+        connection >=0.2 && <0.4,+        containers >=0.5 && <0.7,+        data-default-class ==0.1.*,+        either ==5.0.*,+        file-embed >=0.0.11 && <0.1,+        filepath ==1.4.*,+        hoauth2 ==1.8.*,+        hspec >=2.7.1 && <2.8,+        hspec-attoparsec >=0.1.0.2 && <0.2,         http-client >=0.5 && <0.7,-        http-client-tls >=0.3.5.3 && <0.4,+        http-client-tls ==0.3.*,+        jose-jwt ==0.8.*,+        jsonpath ==0.1.*,         kubernetes-client -any,-        kubernetes-client-core ==0.1.0.1,-        microlens >=0.4.3 && <0.5,-        mtl >=2.2.1 && <2.3,-        pem >=0.2.4 && <0.3,+        kubernetes-client-core ==0.2.0.0,+        microlens ==0.4.*,+        mtl ==2.2.*,+        oidc-client ==0.4.*,+        pem ==0.2.*,         safe-exceptions >=0.1.0.0 && <0.2,-        streaming-bytestring >=0.1.5 && <0.2.0,+        stm >=2.4 && <2.6,+        streaming-bytestring >=0.1 && <0.2.0,         text >=0.11 && <1.3,+        time ==1.8.*,+        timerep ==2.0.*,         tls >=1.4.1 && <1.5,-        x509 >=1.7.5 && <1.8,-        x509-store >=1.6.7 && <1.7,-        x509-system >=1.6.6 && <1.7,-        x509-validation >=1.6.11 && <1.7,-        yaml >=0.11.0.0 && <0.12+        typed-process ==0.2.*,+        uri-bytestring ==0.3.*,+        x509 ==1.7.*,+        x509-store ==1.6.*,+        x509-system ==1.6.*,+        x509-validation ==1.6.*,+        yaml >=0.11.1.2 && <0.12
+ src/Kubernetes/Client/Auth/ClientCert.hs view
@@ -0,0 +1,41 @@+module Kubernetes.Client.Auth.ClientCert where++import Control.Exception.Safe                (Exception, throwM)+import Data.Text.Encoding+import Kubernetes.Client.Auth.Internal.Types+import Kubernetes.Client.Internal.TLSUtils+import Kubernetes.Client.KubeConfig+import Kubernetes.OpenAPI                    (KubernetesClientConfig (..))+import Network.TLS++-- | Detects if kuebconfig file provides 'client-certificate', if it configures TLS client params with the client certificate+clientCertFileAuth :: DetectAuth+clientCertFileAuth auth (tlsParams, cfg) = do+  certFile <- clientCertificate auth+  keyFile <- clientKey auth+  return $ do+    cert <- credentialLoadX509 certFile keyFile+            >>= either (throwM . CredentialLoadException) return+    let newParams = (setClientCert cert tlsParams)+        newCfg = (disableValidateAuthMethods cfg)+    return (newParams, newCfg)++-- | Detects if kuebconfig file provides 'client-certificate-data', if it configures TLS client params with the client certificate+clientCertDataAuth :: DetectAuth+clientCertDataAuth auth (tlsParams, cfg) = do+  certB64 <- encodeUtf8 <$> clientCertificateData auth+  keyB64 <- encodeUtf8 <$> clientKeyData auth+  Just $  do+    cert <- loadB64EncodedCert certB64 keyB64+    let newParams = (setClientCert cert tlsParams)+        newCfg = (disableValidateAuthMethods cfg)+    return (newParams, newCfg)++-- |Disables the client-side auth methods validation. This is necessary if you are using client cert authentication.+disableValidateAuthMethods :: KubernetesClientConfig -> KubernetesClientConfig+disableValidateAuthMethods kcfg = kcfg { configValidateAuthMethods = False }++data CredentialLoadException = CredentialLoadException String+  deriving Show++instance Exception CredentialLoadException
+ src/Kubernetes/Client/Auth/GCP.hs view
@@ -0,0 +1,136 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+module Kubernetes.Client.Auth.GCP+  ( gcpAuth )+where++import Control.Concurrent.STM+import Control.Exception.Safe                (Exception, throwM)+import Data.Attoparsec.Text+import Data.Either.Combinators+import Data.Function                         ((&))+import Data.JSONPath+import Data.Map                              (Map)+import Data.Monoid                           ((<>))+import Data.Text                             (Text)+import Data.Time.Clock+import Data.Time.LocalTime+import Data.Time.RFC3339+import Kubernetes.Client.Auth.Internal.Types+import Kubernetes.Client.KubeConfig+import Kubernetes.Data.K8sJSONPath+import Kubernetes.OpenAPI.Core+import System.Process.Typed++import qualified Data.Aeson         as Aeson+import qualified Data.Map           as Map+import qualified Data.Text          as Text+import qualified Data.Text.Encoding as Text+import qualified Lens.Micro         as L++-- TODO: Add support for scopes based token fetching+data GCPAuth = GCPAuth { gcpAccessToken :: TVar(Maybe Text)+                       , gcpTokenExpiry :: TVar(Maybe UTCTime)+                       , gcpCmd         :: ProcessConfig () () ()+                       , gcpTokenKey    :: [K8sPathElement]+                       , gcpExpiryKey   :: [K8sPathElement]+                       }++instance AuthMethod GCPAuth where+  applyAuthMethod _ gcp req = do+    token <- getToken gcp+             >>= either throwM pure+    pure+      $ setHeader req [("Authorization", "Bearer " <> (Text.encodeUtf8 token))]+      & L.set rAuthTypesL []++-- |Detects if auth-provier name is gcp, if it is configures the 'KubernetesClientConfig' with GCPAuth 'AuthMethod'+gcpAuth :: DetectAuth+gcpAuth AuthInfo{authProvider = Just(AuthProviderConfig "gcp" (Just cfg))} (tlsParams, kubecfg)+  = Just $ do+      configOrErr <- parseGCPAuthInfo cfg+      case configOrErr of+        Left err  -> throwM err+        Right gcp -> pure (tlsParams, addAuthMethod kubecfg gcp)+gcpAuth _ _ = Nothing++data GCPAuthParsingException = GCPAuthMissingInformation String+                             | GCPAuthInvalidExpiry String+                             | GCPAuthInvalidTokenJSONPath String+                             | GCPAuthInvalidExpiryJSONPath String+  deriving Show+instance Exception GCPAuthParsingException++data GCPGetTokenException = GCPCmdProducedInvalidJSON String+                          | GCPTokenNotFound String+                          | GCPTokenExpiryNotFound String+                          | GCPTokenExpiryInvalid String+  deriving Show+instance Exception GCPGetTokenException++getToken :: GCPAuth -> IO (Either GCPGetTokenException Text)+getToken auth@(GCPAuth{..}) = getCurrentToken auth+                              >>= maybe (fetchToken auth) (return . Right)++getCurrentToken :: GCPAuth -> IO (Maybe Text)+getCurrentToken (GCPAuth{..}) = do+  now <- getCurrentTime+  maybeExpiry <- readTVarIO gcpTokenExpiry+  maybeToken <- readTVarIO gcpAccessToken+  return $ do+    expiry <- maybeExpiry+    if expiry > now+      then maybeToken+      else Nothing++fetchToken :: GCPAuth -> IO (Either GCPGetTokenException Text)+fetchToken GCPAuth{..} = do+  (stdOut, _) <- readProcess_ gcpCmd+  case parseTokenAndExpiry stdOut of+    Left err -> return $ Left err+    Right (token, expiry) -> do+      atomically $ do+        writeTVar gcpAccessToken (Just token)+        writeTVar gcpTokenExpiry (Just expiry)+      return $ Right token+  where+    parseTokenAndExpiry credsStr = do+      credsJSON <- Aeson.eitherDecode credsStr+                   & mapLeft GCPCmdProducedInvalidJSON+      token <- runJSONPath gcpTokenKey credsJSON+               & mapLeft GCPTokenNotFound+      expText <- runJSONPath gcpExpiryKey credsJSON+                 & mapLeft GCPTokenExpiryNotFound+      expiry <- parseExpiryTime expText+                & mapLeft GCPTokenExpiryInvalid+      return (token, expiry)++parseGCPAuthInfo :: Map Text Text -> IO (Either GCPAuthParsingException GCPAuth)+parseGCPAuthInfo authInfo = do+  gcpAccessToken <- atomically $ newTVar $ Map.lookup "access-token" authInfo+  eitherGCPExpiryToken <- sequence $ fmap (atomically . newTVar) lookupAndParseExpiry+  return $ do+    gcpTokenExpiry <- mapLeft GCPAuthInvalidExpiry eitherGCPExpiryToken+    cmdPath <- Text.unpack <$> lookupEither "cmd-path"+    cmdArgs <- Text.splitOn " " <$> lookupEither "cmd-args"+    gcpTokenKey <- readJSONPath "token-key" [JSONPath [KeyChild "token_expiry"]]+                   & mapLeft GCPAuthInvalidTokenJSONPath+    gcpExpiryKey <- readJSONPath "expiry-key" [JSONPath [KeyChild "access_token"]]+                    & mapLeft GCPAuthInvalidExpiryJSONPath+    let gcpCmd = proc cmdPath (map Text.unpack cmdArgs)+    pure $ GCPAuth{..}+  where+    lookupAndParseExpiry =+      case Map.lookup "expiry" authInfo of+        Nothing         -> Right Nothing+        Just expiryText -> Just <$> parseExpiryTime expiryText+    lookupEither key = Map.lookup key authInfo+                       & maybeToRight (GCPAuthMissingInformation $ Text.unpack key)+    parseK8sJSONPath = parseOnly (k8sJSONPath <* endOfInput)+    readJSONPath key defaultPath =+      maybe (Right defaultPath) parseK8sJSONPath $ Map.lookup key authInfo++parseExpiryTime :: Text -> Either String UTCTime+parseExpiryTime expiryText =+  zonedTimeToUTC <$> parseTimeRFC3339 expiryText+  & maybeToRight ("failed to parse token expiry time " <> Text.unpack expiryText)
+ src/Kubernetes/Client/Auth/Internal/Types.hs view
@@ -0,0 +1,9 @@+module Kubernetes.Client.Auth.Internal.Types where++import Network.TLS as TLS+import Kubernetes.Client.KubeConfig+import Kubernetes.OpenAPI (KubernetesClientConfig)++type DetectAuth = AuthInfo+                  -> (TLS.ClientParams, KubernetesClientConfig)+                  -> Maybe (IO (TLS.ClientParams, KubernetesClientConfig))
+ src/Kubernetes/Client/Auth/OIDC.hs view
@@ -0,0 +1,184 @@+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+module Kubernetes.Client.Auth.OIDC+  (oidcAuth, OIDCCache, cachedOIDCAuth)+where++import Control.Applicative+import Control.Concurrent.STM+import Control.Exception.Safe                (Exception, throwM)+import Data.Either.Combinators+import Data.Function                         ((&))+import Data.Map                              (Map)+import Data.Maybe+import Data.Monoid                           ((<>))+import Data.Text+import Data.Time.Clock.POSIX                 (getPOSIXTime)+import Jose.Jwt+import Kubernetes.Client.Auth.Internal.Types+import Kubernetes.Client.Internal.TLSUtils+import Kubernetes.Client.KubeConfig+import Kubernetes.OpenAPI.Core+import Network.HTTP.Client+import Network.HTTP.Client.TLS+import Network.OAuth.OAuth2                  as OAuth+import Network.TLS                           as TLS+import URI.ByteString+import Web.OIDC.Client.Discovery             as OIDC++import qualified Data.ByteString                   as BS+import qualified Data.ByteString.Base64            as B64+import qualified Data.Map                          as Map+import qualified Data.Text                         as Text+import qualified Data.Text.Encoding                as Text+import qualified Lens.Micro                        as L+import qualified Network.OAuth.OAuth2.TokenRequest as OAuth2TokenRequest++data OIDCAuth = OIDCAuth { issuerURL        :: Text+                         , clientID         :: Text+                         , clientSecret     :: Text+                         , tlsParams        :: TLS.ClientParams+                         , idTokenTVar      :: TVar(Maybe Text)+                         , refreshTokenTVar :: TVar(Maybe Text)+                         }++-- | Cache OIDCAuth based on issuerURL and clientID.+type OIDCCache = TVar (Map (Text, Text) OIDCAuth)++instance AuthMethod OIDCAuth where+  applyAuthMethod _ oidc req = do+    token <- getToken oidc+    pure+      $ setHeader req [("Authorization", "Bearer " <> (Text.encodeUtf8 token))]+      & L.set rAuthTypesL []++data OIDCGetTokenException = OIDCOAuthException (OAuth2Error OAuth2TokenRequest.Errors)+                           | OIDCURIException URIParseError+                           | OIDCGetTokenException String+  deriving Show+instance Exception OIDCGetTokenException++data OIDCAuthParsingException = OIDCAuthCAParsingFailed ParseCertException+                              | OIDCAuthMissingInformation String+  deriving Show+instance Exception OIDCAuthParsingException++-- TODO: Consider a token expired few seconds before actual expiry to account for time skew+getToken :: OIDCAuth -> IO Text+getToken auth@(OIDCAuth{..}) = do+  now <- getPOSIXTime+  maybeIdToken <- readTVarIO idTokenTVar+  case maybeIdToken of+    Nothing -> fetchToken auth+    Just idToken -> do+      let maybeExpiry = do+            (_, claims) <- decodeClaims (Text.encodeUtf8 idToken)+                           & rightToMaybe+            jwtExp claims+      case maybeExpiry of+        Nothing -> fetchToken auth+        Just (IntDate expiryDate) ->+          if now < expiryDate+          then pure idToken+          else fetchToken auth++fetchToken :: OIDCAuth -> IO Text+fetchToken auth@(OIDCAuth{..}) = do+  mgr <- newManager tlsManagerSettings+  maybeToken <- readTVarIO refreshTokenTVar+  case maybeToken of+    Nothing -> throwM $ OIDCGetTokenException "cannot refresh id-token without a refresh token"+    Just token -> do+      tokenEndpoint <- fetchTokenEndpoint mgr auth+      tokenURI <- parseURI strictURIParserOptions (Text.encodeUtf8 tokenEndpoint)+                  & either (throwM . OIDCURIException) pure+      let oauth = OAuth2{ oauthClientId = clientID+                        , oauthClientSecret = clientSecret+                        , oauthAccessTokenEndpoint = tokenURI+                        , oauthOAuthorizeEndpoint = tokenURI+                        , oauthCallback = Nothing+                        }+      oauthToken <- refreshAccessToken mgr oauth (RefreshToken token)+                    >>= either (throwM . OIDCOAuthException) pure+      case OAuth.idToken oauthToken of+        Nothing -> throwM $ OIDCGetTokenException "token response did not contain an id_token, either the scope \"openid\" wasn't requested upon login, or the provider doesn't support id_tokens as part of the refresh response."+        Just (IdToken t) -> do+          _ <- atomically $ writeTVar idTokenTVar (Just t)+          return t++fetchTokenEndpoint :: Manager -> OIDCAuth -> IO Text+fetchTokenEndpoint mgr OIDCAuth{..} = do+  discover issuerURL mgr+    & (fmap configuration)+    & (fmap tokenEndpoint)++{-+   Detects if auth-provier name is oidc, if it is configures the 'KubernetesClientConfig' with OIDCAuth 'AuthMethod'.+   Does not use cache, consider using 'cachedOIDCAuth'.+-}+oidcAuth :: DetectAuth+oidcAuth AuthInfo{authProvider = Just(AuthProviderConfig "oidc" (Just cfg))} (tls, kubecfg)+  = Just+    $ parseOIDCAuthInfo cfg+    >>= either throwM (\oidc -> pure (tls, addAuthMethod kubecfg oidc))+oidcAuth _ _ = Nothing++-- TODO: Consider doing this whole function atomically, as two threads may miss the cache simultaneously+{-+   Detects if auth-provier name is oidc, if it is configures the 'KubernetesClientConfig' with OIDCAuth 'AuthMethod'.+   First looks for Auth information to be present in 'OIDCCache'. If found returns that, otherwise creates new Auth information and persists it in cache.+-}+cachedOIDCAuth :: OIDCCache -> DetectAuth+cachedOIDCAuth cache AuthInfo{authProvider = Just(AuthProviderConfig "oidc" (Just cfg))} (tls, kubecfg) = Just $ do+  latestCache <- readTVarIO cache+  issuerURL <- lookupOrThrow "idp-issuer-url"+  clientID <- lookupOrThrow "client-id"+  case Map.lookup (issuerURL, clientID) latestCache of+    Just cacheHit -> return $ newTLSAndAuth cacheHit+    Nothing -> do+      parsedAuth <- parseOIDCAuthInfo cfg+                    >>= either throwM pure+      let newCache = Map.insert (issuerURL, clientID) parsedAuth latestCache+      _ <- atomically $ swapTVar cache newCache+      return $ newTLSAndAuth parsedAuth+  where lookupOrThrow k = Map.lookup k cfg+                         & maybe (throwM $ OIDCAuthMissingInformation $ Text.unpack k) pure+        newTLSAndAuth auth = (tls, addAuthMethod kubecfg auth)+cachedOIDCAuth _ _ _ = Nothing++parseOIDCAuthInfo :: Map Text Text -> IO (Either OIDCAuthParsingException OIDCAuth)+parseOIDCAuthInfo authInfo = do+  eitherTLSParams <- parseCA authInfo+  idTokenTVar <- atomically $ newTVar $ Map.lookup "id-token" authInfo+  refreshTokenTVar <- atomically $ newTVar $ Map.lookup "refresh-token" authInfo+  return $ do+    tlsParams <- mapLeft OIDCAuthCAParsingFailed eitherTLSParams+    issuerURL <- lookupEither "idp-issuer-url"+    clientID <- lookupEither "client-id"+    clientSecret <- lookupEither "client-secret"+    return OIDCAuth{..}+    where lookupEither k = Map.lookup k authInfo+                           & maybeToRight (OIDCAuthMissingInformation $ Text.unpack k)++parseCA :: Map Text Text -> IO (Either ParseCertException TLS.ClientParams)+parseCA authInfo = do+  tlsParams <- defaultTLSClientParams+  let maybeNewParams = (parseCAFile tlsParams authInfo+                        <|> parseCAData tlsParams authInfo)+  fromMaybe (pure $ Right tlsParams) maybeNewParams++parseCAFile :: TLS.ClientParams -> Map Text Text -> Maybe (IO (Either ParseCertException TLS.ClientParams))+parseCAFile tlsParams authInfo = do+  caFile <- Text.unpack <$> Map.lookup "idp-certificate-authority" authInfo+  Just $ do+    caText <- BS.readFile caFile+    return $ updateClientParams tlsParams caText++parseCAData :: TLS.ClientParams -> Map Text Text -> Maybe (IO (Either ParseCertException TLS.ClientParams))+parseCAData tlsParams authInfo = do+  caBase64 <- Map.lookup "idp-certificate-authority-data" authInfo+  Just $ pure $ do+    caText <- B64.decode (Text.encodeUtf8 caBase64)+              & mapLeft Base64ParsingFailed+    updateClientParams tlsParams caText
+ src/Kubernetes/Client/Auth/Token.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE OverloadedStrings #-}+module Kubernetes.Client.Auth.Token where++import           Data.Monoid                    ( (<>) )+import           Kubernetes.Client.Auth.Internal.Types+import           Kubernetes.Client.KubeConfig   ( AuthInfo(..) )+import           Kubernetes.OpenAPI.Core        ( AnyAuthMethod(..)+                                                , KubernetesClientConfig(..)+                                                )+import           Kubernetes.OpenAPI.Model       ( AuthApiKeyBearerToken(..) )++import qualified Data.Text                     as T++-- |Detects if token is specified in AuthConfig, if it is configures 'KubernetesClientConfig' with 'AuthApiKeyBearerToken'+tokenAuth :: DetectAuth+tokenAuth auth (tlsParams, cfg) = do+  t <- token auth+  return $ return (tlsParams, setTokenAuth t cfg)++-- |Configures the 'KubernetesClientConfig' to use token authentication.+setTokenAuth+  :: T.Text                 -- ^Authentication token+  -> KubernetesClientConfig+  -> KubernetesClientConfig+setTokenAuth t kcfg = kcfg+  { configAuthMethods = [AnyAuthMethod (AuthApiKeyBearerToken $ "Bearer " <> t)]+  }
+ src/Kubernetes/Client/Auth/TokenFile.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NamedFieldPuns #-}+module Kubernetes.Client.Auth.TokenFile where++import           Control.Concurrent.STM+import           Data.Function                  ( (&) )+import           Data.Monoid                    ( (<>) )+import           Data.Text                      ( Text )+import           Data.Time.Clock+import           Kubernetes.Client.Auth.Internal.Types+import           Kubernetes.OpenAPI.Core+import           Kubernetes.Client.KubeConfig+                                         hiding ( token )+import qualified Data.Text                     as T+import qualified Data.Text.IO                  as T+import qualified Lens.Micro                    as L++data TokenFileAuth = TokenFileAuth { token :: TVar(Maybe Text)+                                   , expiry :: TVar(Maybe UTCTime)+                                   , file :: FilePath+                                   , period :: NominalDiffTime+                                   }++instance AuthMethod TokenFileAuth where+  applyAuthMethod _ tokenFile req = do+    t <- getToken tokenFile+    pure+      $           req+      `setHeader` toHeader ("authorization", "Bearer " <> t)+      &           L.set rAuthTypesL []++-- |Detects if token-file is specified in AuthConfig.+tokenFileAuth :: DetectAuth+tokenFileAuth auth (tlsParams, cfg) = do+  file <- tokenFile auth+  return $ do+    c <- setTokenFileAuth file cfg+    return (tlsParams, c)++-- |Configures the 'KubernetesClientConfig' to use TokenFile authentication.+setTokenFileAuth+  :: FilePath -> KubernetesClientConfig -> IO KubernetesClientConfig+setTokenFileAuth f kcfg = atomically $ do+  t <- newTVar (Nothing :: Maybe Text)+  e <- newTVar (Nothing :: Maybe UTCTime)+  return kcfg+    { configAuthMethods =+      [ AnyAuthMethod+          (TokenFileAuth { token = t, expiry = e, file = f, period = 60 })+      ]+    }++getToken :: TokenFileAuth -> IO Text+getToken auth = getCurrentToken auth >>= maybe (reloadToken auth) return++getCurrentToken :: TokenFileAuth -> IO (Maybe Text)+getCurrentToken TokenFileAuth { token, expiry } = do+  now         <- getCurrentTime+  maybeExpiry <- readTVarIO expiry+  maybeToken  <- readTVarIO token+  return $ do+    e <- maybeExpiry+    if e > now then maybeToken else Nothing++reloadToken :: TokenFileAuth -> IO Text+reloadToken TokenFileAuth { token, expiry, file, period } = do+  content <- T.readFile file+  let t = T.strip content+  now <- getCurrentTime+  atomically $ do+    writeTVar token  (Just t)+    writeTVar expiry (Just (addUTCTime period now))+  return t
src/Kubernetes/Client/Config.hs view
@@ -1,136 +1,174 @@-{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE OverloadedStrings #-} -module Kubernetes.Client.Config where+module Kubernetes.Client.Config+  ( KubeConfigSource(..)+  , addCACertData+  , addCACertFile+  , applyAuthSettings+  , clientHooksL+  , defaultTLSClientParams+  , disableServerCertValidation+  , disableServerNameValidation+  , disableValidateAuthMethods+  , loadPEMCerts+  , mkInClusterClientConfig+  , mkKubeClientConfig+  , newManager+  , onCertificateRequestL+  , onServerCertificateL+  , parsePEMCerts+  , serviceAccountDir+  , setCAStore+  , setClientCert+  , setMasterURI+  , setTokenAuth+  , tlsValidation+  )+where -import qualified Kubernetes.OpenAPI.Core            as K-import qualified Kubernetes.OpenAPI.Model           as K+import qualified Kubernetes.OpenAPI.Core       as K -import           Control.Exception.Safe     (Exception, MonadThrow, throwM)-import           Control.Monad.IO.Class     (MonadIO, liftIO)-import qualified Data.ByteString            as B-import qualified Data.ByteString.Lazy       as LazyB-import           Data.Default.Class         (def)-import           Data.Either                (rights)-import           Data.Monoid                ((<>))-import           Data.PEM                   (pemContent, pemParseBS)-import qualified Data.Text                  as T-import qualified Data.Text.Encoding         as T-import qualified Data.Text.IO               as T-import           Data.Typeable              (Typeable)-import           Data.X509                  (SignedCertificate,-                                             decodeSignedCertificate)-import qualified Data.X509                  as X509-import           Data.X509.CertificateStore (CertificateStore, makeCertificateStore)-import qualified Data.X509.Validation       as X509-import           Lens.Micro                 (Lens', lens, set)-import           Network.Connection         (TLSSettings (..))-import qualified Network.HTTP.Client        as NH-import           Network.HTTP.Client.TLS    (mkManagerSettings)-import           Network.TLS                (Credential, defaultParamsClient)-import qualified Network.TLS                as TLS-import qualified Network.TLS.Extra          as TLS-import           System.Environment         (getEnv)-import           System.X509                (getSystemCertificateStore)+import           Control.Applicative            ( (<|>) )+import           Control.Exception.Safe         ( MonadThrow+                                                , throwM+                                                )+import           Control.Monad.IO.Class         ( MonadIO+                                                , liftIO+                                                )+import qualified Data.ByteString               as B+import qualified Data.ByteString.Base64        as B64+import qualified Data.ByteString.Lazy          as LazyB+import           Data.Either.Combinators+import           Data.Function                  ( (&) )+import           Data.Maybe+import qualified Data.Text                     as T+import qualified Data.Text.Encoding            as T+import           Data.Yaml+import           Kubernetes.Client.Auth.ClientCert+import           Kubernetes.Client.Auth.GCP+import           Kubernetes.Client.Auth.OIDC+import           Kubernetes.Client.Auth.Token+import           Kubernetes.Client.Auth.TokenFile+import           Kubernetes.Client.Internal.TLSUtils+import           Kubernetes.Client.KubeConfig+import           Network.Connection             ( TLSSettings(..) )+import qualified Network.HTTP.Client           as NH+import           Network.HTTP.Client.TLS        ( mkManagerSettings )+import qualified Network.TLS                   as TLS+import           System.Environment             ( getEnv )+import           System.FilePath --- |Sets the master URI in the 'K.KubernetesClientConfig'.-setMasterURI-    :: T.Text                -- ^ Master URI-    -> K.KubernetesClientConfig-    -> K.KubernetesClientConfig-setMasterURI server kcfg =-    kcfg { K.configHost = (LazyB.fromStrict . T.encodeUtf8) server }+data KubeConfigSource = KubeConfigFile FilePath+                      | KubeConfigCluster --- |Disables the client-side auth methods validation. This is necessary if you are using client cert authentication.-disableValidateAuthMethods :: K.KubernetesClientConfig -> K.KubernetesClientConfig-disableValidateAuthMethods kcfg = kcfg { K.configValidateAuthMethods = False }+{-|+  Creates 'NH.Manager' and 'K.KubernetesClientConfig' for a given+  'KubeConfigSource'. It is recommended that multiple 'kubeClient' invocations+  across an application share an 'OIDCCache', this makes sure updation of OAuth+  token is synchronized across all the different clients being used.+-}+mkKubeClientConfig+  :: OIDCCache -> KubeConfigSource -> IO (NH.Manager, K.KubernetesClientConfig)+mkKubeClientConfig oidcCache (KubeConfigFile f) = do+  kubeConfig <- decodeFileThrow f+  masterURI  <-+    server+    <$> getCluster kubeConfig+    &   either (const $ pure "localhost:8080") return+  tlsParams <- configureTLSParams kubeConfig (takeDirectory f)+  clientConfig <- K.newConfig & fmap (setMasterURI masterURI)+  (tlsParamsWithAuth, clientConfigWithAuth) <- case getAuthInfo kubeConfig of+    Left _ -> return (tlsParams, clientConfig)+    Right (_, auth) ->+      applyAuthSettings oidcCache auth (tlsParams, clientConfig)+  mgr <- newManager tlsParamsWithAuth+  return (mgr, clientConfigWithAuth)+mkKubeClientConfig _ KubeConfigCluster = mkInClusterClientConfig --- |Configures the 'K.KubernetesClientConfig' to use token authentication.-setTokenAuth-    :: T.Text             -- ^Authentication token-    -> K.KubernetesClientConfig-    -> K.KubernetesClientConfig-setTokenAuth token kcfg = kcfg-    { K.configAuthMethods = [K.AnyAuthMethod (K.AuthApiKeyBearerToken $ "Bearer " <> token)]-    }+-- |Creates 'NH.Manager' and 'K.KubernetesClientConfig' assuming it is being executed in a pod+mkInClusterClientConfig+  :: (MonadIO m, MonadThrow m) => m (NH.Manager, K.KubernetesClientConfig)+mkInClusterClientConfig = do+  caStore <- loadPEMCerts $ serviceAccountDir ++ "/ca.crt"+  defTlsParams <- liftIO defaultTLSClientParams+  mgr <- liftIO . newManager . setCAStore caStore $ disableServerNameValidation+    defTlsParams+  host <- liftIO $ getEnv "KUBERNETES_SERVICE_HOST"+  port <- liftIO $ getEnv "KUBERNETES_SERVICE_PORT"+  cfg  <- setMasterURI (T.pack $ "https://" ++ host ++ ":" ++ port) <$> liftIO+    (K.newConfig >>= setTokenFileAuth (serviceAccountDir ++ "/token"))+  return (mgr, cfg) +-- |Sets the master URI in the 'K.KubernetesClientConfig'.+setMasterURI+  :: T.Text                -- ^ Master URI+  -> K.KubernetesClientConfig+  -> K.KubernetesClientConfig+setMasterURI masterURI kcfg =+  kcfg { K.configHost = (LazyB.fromStrict . T.encodeUtf8) masterURI }+ -- |Creates a 'NH.Manager' that can handle TLS. newManager :: TLS.ClientParams -> IO NH.Manager newManager cp = NH.newManager (mkManagerSettings (TLSSettings cp) Nothing) --- |Default TLS settings using the system CA store.-defaultTLSClientParams :: IO TLS.ClientParams-defaultTLSClientParams = do-    let defParams = defaultParamsClient "" ""-    systemCAStore <- getSystemCertificateStore-    return defParams-        { TLS.clientSupported = def-            { TLS.supportedCiphers = TLS.ciphersuite_strong-            }-        , TLS.clientShared    = (TLS.clientShared defParams)-            { TLS.sharedCAStore = systemCAStore-            }-        }--clientHooksL :: Lens' TLS.ClientParams TLS.ClientHooks-clientHooksL = lens TLS.clientHooks (\cp ch -> cp { TLS.clientHooks = ch })--onServerCertificateL :: Lens' TLS.ClientParams (CertificateStore -> TLS.ValidationCache -> X509.ServiceID -> X509.CertificateChain -> IO [X509.FailedReason])-onServerCertificateL =-  clientHooksL . lens TLS.onServerCertificate (\ch osc -> ch { TLS.onServerCertificate = osc })---- |Don't check whether the cert presented by the server matches the name of the server you are connecting to.--- This is necessary if you specify the server host by its IP address.-disableServerNameValidation :: TLS.ClientParams -> TLS.ClientParams-disableServerNameValidation =-  set onServerCertificateL (X509.validate X509.HashSHA256 def (def { X509.checkFQHN = False }))---- |Insecure mode. The client will not validate the server cert at all.-disableServerCertValidation :: TLS.ClientParams -> TLS.ClientParams-disableServerCertValidation = set onServerCertificateL (\_ _ _ _ -> return [])---- |Use a custom CA store.-setCAStore :: [SignedCertificate] -> TLS.ClientParams -> TLS.ClientParams-setCAStore certs cp = cp-    { TLS.clientShared = (TLS.clientShared cp)-        { TLS.sharedCAStore = (makeCertificateStore certs)-        }-    }--onCertificateRequestL :: Lens' TLS.ClientParams (([TLS.CertificateType], Maybe [TLS.HashAndSignatureAlgorithm], [X509.DistinguishedName]) -> IO (Maybe (X509.CertificateChain, TLS.PrivKey)))-onCertificateRequestL =-  clientHooksL . lens TLS.onCertificateRequest (\ch ocr -> ch { TLS.onCertificateRequest = ocr })---- |Use a client cert for authentication.-setClientCert :: Credential -> TLS.ClientParams -> TLS.ClientParams-setClientCert cred = set onCertificateRequestL (\_ -> return $ Just cred)---- |Parses a PEM-encoded @ByteString@ into a list of certificates.-parsePEMCerts :: B.ByteString -> Either String [SignedCertificate]-parsePEMCerts b = do-    pems <- pemParseBS b-    return $ rights $ map (decodeSignedCertificate . pemContent) pems+serviceAccountDir :: FilePath+serviceAccountDir = "/var/run/secrets/kubernetes.io/serviceaccount" -data ParsePEMCertsException = ParsePEMCertsException String deriving (Typeable, Show)+configureTLSParams :: Config -> FilePath -> IO TLS.ClientParams+configureTLSParams cfg dir = do+  defaultTLS     <- defaultTLSClientParams+  withCACertData <- addCACertData cfg defaultTLS+  withCACertFile <- addCACertFile cfg dir withCACertData+  return $ tlsValidation cfg withCACertFile -instance Exception ParsePEMCertsException+tlsValidation :: Config -> TLS.ClientParams -> TLS.ClientParams+tlsValidation cfg tlsParams = case getCluster cfg of+  Left  _ -> tlsParams+  Right c -> case insecureSkipTLSVerify c of+    Just True -> disableServerCertValidation tlsParams+    _         -> tlsParams --- |Loads certificates from a PEM-encoded file.-loadPEMCerts :: (MonadIO m, MonadThrow m) => FilePath -> m [SignedCertificate]-loadPEMCerts p = do-    liftIO (B.readFile p)-        >>= either (throwM . ParsePEMCertsException) return-        .   parsePEMCerts+addCACertData+  :: (MonadThrow m) => Config -> TLS.ClientParams -> m TLS.ClientParams+addCACertData cfg tlsParams =+  let+    eitherCertText =+      getCluster cfg+        & (>>= (maybeToRight "cert data not provided" . certificateAuthorityData+               )+          )+  in  case eitherCertText of+        Left  _          -> pure tlsParams+        Right certBase64 -> do+          certText <-+            B64.decode (T.encodeUtf8 certBase64)+              & either (throwM . Base64ParsingFailed) pure+          updateClientParams tlsParams certText & either throwM return -serviceAccountDir :: FilePath-serviceAccountDir = "/var/run/secrets/kubernetes.io/serviceaccount"+addCACertFile :: Config -> FilePath -> TLS.ClientParams -> IO TLS.ClientParams+addCACertFile cfg dir tlsParams = do+  let eitherCertFile =+        getCluster cfg+          >>= maybeToRight "cert file not provided"+          .   certificateAuthority+          &   fmap T.unpack+          &   fmap (dir </>)+  case eitherCertFile of+    Left  _        -> return tlsParams+    Right certFile -> do+      certText <- B.readFile certFile+      return $ updateClientParams tlsParams certText & fromRight tlsParams -cluster :: (MonadIO m, MonadThrow m) => m (NH.Manager, K.KubernetesClientConfig)-cluster = do-  caStore <- loadPEMCerts $ serviceAccountDir ++ "/ca.crt"-  defTlsParams <- liftIO defaultTLSClientParams-  mgr <- liftIO . newManager . setCAStore caStore $ disableServerNameValidation defTlsParams-  tok <- liftIO . T.readFile $ serviceAccountDir ++ "/token"-  host <- liftIO $ getEnv "KUBERNETES_SERVICE_HOST"-  port <- liftIO $ getEnv "KUBERNETES_SERVICE_PORT"-  config <- setTokenAuth tok . setMasterURI (T.pack $ "https://" ++ host ++ ":" ++ port) <$> liftIO K.newConfig-  return (mgr, config)+applyAuthSettings+  :: OIDCCache+  -> AuthInfo+  -> (TLS.ClientParams, K.KubernetesClientConfig)+  -> IO (TLS.ClientParams, K.KubernetesClientConfig)+applyAuthSettings oidcCache auth input =+  fromMaybe (pure input)+    $   clientCertFileAuth auth input+    <|> clientCertDataAuth auth input+    <|> tokenAuth auth input+    <|> tokenFileAuth auth input+    <|> gcpAuth auth input+    <|> cachedOIDCAuth oidcCache auth input
+ src/Kubernetes/Client/Internal/TLSUtils.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE OverloadedStrings #-}+module Kubernetes.Client.Internal.TLSUtils where++import Control.Exception.Safe     (Exception, MonadThrow, throwM)+import Control.Monad.IO.Class     (MonadIO, liftIO)+import Data.ByteString            (ByteString)+import Data.Default.Class         (def)+import Data.Either                (rights)+import Data.Either.Combinators    (mapLeft)+import Data.Function              ((&))+import Data.PEM                   (pemContent, pemParseBS)+import Data.X509                  (SignedCertificate, decodeSignedCertificate)+import Data.X509.CertificateStore (CertificateStore, makeCertificateStore)+import Lens.Micro+import Network.TLS                (Credential, defaultParamsClient)+import Network.TLS+import System.X509                (getSystemCertificateStore)++import qualified Data.ByteString        as B+import qualified Data.ByteString.Base64 as B64+import qualified Data.X509              as X509+import qualified Data.X509.Validation   as X509+import qualified Network.TLS            as TLS+import qualified Network.TLS.Extra      as TLS++-- |Default TLS settings using the system CA store.+defaultTLSClientParams :: IO TLS.ClientParams+defaultTLSClientParams = do+    let defParams = defaultParamsClient "" ""+    systemCAStore <- getSystemCertificateStore+    return defParams+        { TLS.clientSupported = def+            { TLS.supportedCiphers = TLS.ciphersuite_strong+            }+        , TLS.clientShared    = (TLS.clientShared defParams)+            { TLS.sharedCAStore = systemCAStore+            }+        }++-- |Parses a PEM-encoded @ByteString@ into a list of certificates.+parsePEMCerts :: B.ByteString -> Either ParseCertException [SignedCertificate]+parsePEMCerts pemBS = do+    pems <- pemParseBS pemBS+            & mapLeft PEMParsingFailed+    return $ rights $ map (decodeSignedCertificate . pemContent) pems++-- | Updates client params, sets CA store to passed bytestring of CA certificates+updateClientParams :: TLS.ClientParams -> ByteString -> Either ParseCertException TLS.ClientParams+updateClientParams cp certText = parsePEMCerts certText+                                 & (fmap (flip setCAStore cp))++-- |Use a custom CA store.+setCAStore :: [SignedCertificate] -> TLS.ClientParams -> TLS.ClientParams+setCAStore certs tlsParams =+  tlsParams & clientSharedL . sharedCAStoreL .~ makeCertificateStore certs++-- |Use a client cert for authentication.+setClientCert :: Credential -> TLS.ClientParams -> TLS.ClientParams+setClientCert cred = set onCertificateRequestL (\_ -> return $ Just cred)++clientHooksL :: Lens' TLS.ClientParams TLS.ClientHooks+clientHooksL = lens TLS.clientHooks (\cp ch -> cp { TLS.clientHooks = ch })++onServerCertificateL :: Lens' TLS.ClientParams (CertificateStore -> TLS.ValidationCache -> X509.ServiceID -> X509.CertificateChain -> IO [X509.FailedReason])+onServerCertificateL =+  clientHooksL . lens TLS.onServerCertificate (\ch osc -> ch { TLS.onServerCertificate = osc })++clientSharedL :: Lens' TLS.ClientParams TLS.Shared+clientSharedL = lens TLS.clientShared (\tlsParams cs -> tlsParams {TLS.clientShared = cs} )++sharedCAStoreL :: Lens' TLS.Shared CertificateStore+sharedCAStoreL = lens TLS.sharedCAStore (\shared store -> shared{TLS.sharedCAStore = store})++-- |Don't check whether the cert presented by the server matches the name of the server you are connecting to.+-- This is necessary if you specify the server host by its IP address.+disableServerNameValidation :: TLS.ClientParams -> TLS.ClientParams+disableServerNameValidation =+  set onServerCertificateL (X509.validate X509.HashSHA256 def (def { X509.checkFQHN = False }))++-- |Insecure mode. The client will not validate the server cert at all.+disableServerCertValidation :: TLS.ClientParams -> TLS.ClientParams+disableServerCertValidation = set onServerCertificateL (\_ _ _ _ -> return [])++onCertificateRequestL :: Lens' TLS.ClientParams (([TLS.CertificateType], Maybe [TLS.HashAndSignatureAlgorithm], [X509.DistinguishedName]) -> IO (Maybe (X509.CertificateChain, TLS.PrivKey)))+onCertificateRequestL =+  clientHooksL . lens TLS.onCertificateRequest (\ch ocr -> ch { TLS.onCertificateRequest = ocr })++-- |Loads certificates from a PEM-encoded file.+loadPEMCerts :: (MonadIO m, MonadThrow m) => FilePath -> m [SignedCertificate]+loadPEMCerts pemFile = do+    liftIO (B.readFile pemFile)+        >>= (either throwM return)+        .   parsePEMCerts++-- |Loads Base64 encoded certificate and private key+loadB64EncodedCert :: (MonadThrow m) => B.ByteString -> B.ByteString -> m Credential+loadB64EncodedCert certB64 keyB64 = either throwM pure $ do+  certText <- B64.decode certB64+              & mapLeft Base64ParsingFailed+  keyText <- B64.decode keyB64+              & mapLeft Base64ParsingFailed+  credentialLoadX509FromMemory certText keyText+    & mapLeft FailedToLoadCredential++data ParseCertException = PEMParsingFailed String+                        | Base64ParsingFailed String+                        | FailedToLoadCredential String+  deriving Show++instance Exception ParseCertException
+ src/Kubernetes/Data/K8sJSONPath.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedStrings #-}+module Kubernetes.Data.K8sJSONPath where++import Data.Aeson+import Data.Aeson.Text+import Data.JSONPath+import Data.Monoid     ((<>))+import Data.Text       as Text++import Control.Applicative  ((<|>))+import Data.Attoparsec.Text+import Data.Text.Lazy       (toStrict)++data K8sPathElement = PlainText Text+                    | JSONPath [JSONPathElement]+  deriving  (Show, Eq)++k8sJSONPath :: Parser [K8sPathElement]+k8sJSONPath = many1 pathElementParser++pathElementParser :: Parser K8sPathElement+pathElementParser = jsonpathParser <|> plainTextParser++plainTextParser :: Parser K8sPathElement+plainTextParser = PlainText <$> takeWhile1 (/= '{')++jsonpathParser :: Parser K8sPathElement+jsonpathParser = JSONPath <$> (char '{' *> jsonPath <* char '}')++runJSONPath :: [K8sPathElement] -> Value -> Either String Text+runJSONPath [] _ = pure ""+runJSONPath (e:es) v = do+  res <- runPathElement e v+  rest <- runJSONPath es v+  pure $ res <> rest++runPathElement :: K8sPathElement -> Value -> Either String Text+runPathElement (PlainText t) _ = pure t+runPathElement (JSONPath p) v  = encodeResult $ executeJSONPath p v++encodeResult :: ExecutionResult Value -> Either String Text+encodeResult (ResultValue val) = return $ jsonToText val+encodeResult (ResultList vals) = return $ (intercalate " " $ Prelude.map jsonToText vals)+encodeResult (ResultError err) = Left err++jsonToText :: Value -> Text+jsonToText (String t) = t+jsonToText x          = toStrict $ encodeToLazyText x
+ test/Kubernetes/Client/Auth/ClientCertSpec.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}+module Kubernetes.Client.Auth.ClientCertSpec where++import Test.Hspec++import Data.FileEmbed+import Data.Maybe                        (isJust, isNothing)+import Data.Text.Encoding                (decodeUtf8)+import Kubernetes.Client.Auth.ClientCert+import Kubernetes.Client.KubeConfig+import Kubernetes.OpenAPI+import Network.TLS                       (defaultParamsClient)++import qualified Data.ByteString.Base64 as B64++emptyAuthInfo :: AuthInfo+emptyAuthInfo = AuthInfo Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing++spec :: Spec+spec = do+  let inputTLSParams = defaultParamsClient "" ""+      certFilePath = "test/testdata/certs/certificate.pem"+      keyFilePath =  "test/testdata/certs/private-key.pem"+      base64EncodedCert = decodeUtf8 $ B64.encode $(embedFile $ "test/testdata/certs/certificate.pem")+      base64EncodedKey = decodeUtf8 $ B64.encode $(embedFile $ "test/testdata/certs/private-key.pem")+  describe "ClientCert File Authentication" $ do+    context "when cert and key file are provided in AuthInfo" $ do+      let auth = emptyAuthInfo { clientCertificate = Just $ certFilePath+                               , clientKey = Just $ keyFilePath }+      it "should detect client cert file auth" $ do+        inputConfig <- newConfig+        isJust (clientCertFileAuth auth (inputTLSParams, inputConfig)) `shouldBe` True++      it "should disable validate auth method" $ do+        inputConfig <- newConfig+        case clientCertFileAuth auth (inputTLSParams, inputConfig) of+          Nothing -> expectationFailure "expected to detect client cert file auth"+          Just detectedAuth -> do+            (_, cfg) <- detectedAuth+            configValidateAuthMethods cfg `shouldBe` False++    it "should return Nothing if the cert file is not provided" $ do+      let auth = emptyAuthInfo {clientKey = Just "/some/file"}+      inputConfig <- newConfig+      isNothing (clientCertFileAuth auth (inputTLSParams, inputConfig)) `shouldBe` True++    it "should return Nothing if the key file is not provided" $ do+      let auth = emptyAuthInfo {clientCertificate = Just "/some/file"}+      inputConfig <- newConfig+      isNothing (clientCertFileAuth auth (undefined, inputConfig)) `shouldBe` True++  describe "ClientCert Data Authentication" $ do+    context "when cert and key file are provided in AuthInfo" $ do+      let auth = emptyAuthInfo { clientCertificateData = Just base64EncodedCert+                               , clientKeyData = Just base64EncodedKey}+      it "should detect client cert data auth" $ do+        inputConfig <- newConfig+        isJust (clientCertDataAuth auth (inputTLSParams, inputConfig)) `shouldBe` True++      it "should disable validate auth method" $ do+        inputConfig <- newConfig+        case clientCertDataAuth auth (inputTLSParams, inputConfig) of+          Nothing -> expectationFailure "expected to detect client cert file auth"+          Just detectedAuth -> do+            (_, cfg) <- detectedAuth+            configValidateAuthMethods cfg `shouldBe` False++    it "should return Nothing if the cert file is not provided" $ do+      let auth = emptyAuthInfo {clientKeyData = Just base64EncodedKey}+      inputConfig <- newConfig+      isNothing (clientCertDataAuth auth (inputTLSParams, inputConfig)) `shouldBe` True++    it "should return Nothing if the key file is not provided" $ do+      let auth = emptyAuthInfo {clientCertificateData = Just base64EncodedCert}+      inputConfig <- newConfig+      isNothing (clientCertDataAuth auth (inputTLSParams, inputConfig)) `shouldBe` True
+ test/Kubernetes/Client/Auth/TokenFileSpec.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE OverloadedStrings #-}+module Kubernetes.Client.Auth.TokenFileSpec where++import           Test.Hspec+import           Control.Concurrent+import           Data.Function                  ( (&) )+import           Data.FileEmbed+import           Data.Typeable+import           Data.Maybe                     ( isJust+                                                , isNothing+                                                )+import           Data.Text                      ( Text )+import           Data.Text.Encoding             ( decodeUtf8 )+import           Kubernetes.Client.Auth.TokenFile+import           Kubernetes.Client.KubeConfig+import           Kubernetes.OpenAPI+import           Kubernetes.OpenAPI.Core        ( _applyAuthMethods+                                                , _mkRequest+                                                )+import           Network.TLS                    ( defaultParamsClient )++import qualified Data.ByteString.Base64        as B64++emptyAuthInfo :: AuthInfo+emptyAuthInfo = AuthInfo Nothing+                         Nothing+                         Nothing+                         Nothing+                         Nothing+                         Nothing+                         Nothing+                         Nothing+                         Nothing+                         Nothing+                         Nothing+                         Nothing++spec :: Spec+spec = do+  let testTLSParams  = defaultParamsClient "" ""+      token1FilePath = "test/testdata/tokens/token1"+      token2FilePath = "test/testdata/tokens/token2"+  describe "TokenFile Authentication" $ do+    it "should return Nothing if the file is not provided" $ do+      testConfig <- newConfig+      isNothing (tokenFileAuth emptyAuthInfo (testTLSParams, testConfig))+        `shouldBe` True++    it "should reload token after expiry" $ do+      let auth = emptyAuthInfo { tokenFile = Just token1FilePath }+      testConfig <- newConfig+      case tokenFileAuth auth (testTLSParams, testConfig) of+        Nothing -> expectationFailure "expected to detect TokenFile auth"+        Just detectedAuth -> do+          (_, cfg@(KubernetesClientConfig { configAuthMethods = AnyAuthMethod (a) : as })) <-+            detectedAuth+          case cast a :: Maybe TokenFileAuth of+            Nothing -> expectationFailure "expected to be TokenFile auth"+            Just tf -> do+              let tfWithShorterPeriod = tf { period = 5 }+              x <- getToken tfWithShorterPeriod+              x `shouldBe` ("token1" :: Text)++              let tfWithShorterPeriod' =+                    tfWithShorterPeriod { file = token2FilePath }+              threadDelay 2000000 -- sleep 2 seconds+              x <- getToken tfWithShorterPeriod'+              x `shouldBe` ("token1" :: Text)++              threadDelay 3000000 -- sleep 3 seconds+              x <- getToken tfWithShorterPeriod'+              x `shouldBe` ("token2" :: Text)+
+ test/Kubernetes/Client/KubeConfigSpec.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Kubernetes.Client.KubeConfigSpec where++import           Data.Aeson                   (decode, encode, parseJSON,+                                               toJSON)+import           Data.Maybe                   (fromJust)+import           Data.Yaml                    (decodeFile)+import           Kubernetes.Client.KubeConfig (AuthInfo (..), Cluster (..),+                                               Config, Context (..),+                                               getAuthInfo, getCluster,+                                               getContext)+import           Test.Hspec++spec :: Spec+spec = do+  let getConfig :: IO Config+      getConfig = fromJust <$> decodeFile "test/testdata/kubeconfig.yaml"+  describe "FromJSON and ToJSON instances" $ do+    it "roundtrips successfully" $ do+      config <- getConfig+      decode (encode (toJSON config)) `shouldBe` Just config+  describe "getContext" $ do+    it "returns the correct context" $ do+      config <- getConfig+      getContext config `shouldBe` (Right (Context "cluster-aaa" "user-aaa" Nothing))++  describe "getCluster" $ do+    it "returns the correct cluster" $ do+      config <- getConfig+      server <$> getCluster config `shouldBe` (Right "https://aaa.example.com")++  describe "getAuthInfo" $ do+    it "returns the correct authInfo" $ do+      config <- getConfig+      fst <$> getAuthInfo config `shouldBe` (Right "user-aaa")
+ test/Kubernetes/Data/K8sJSONPathSpec.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}+module Kubernetes.Data.K8sJSONPathSpec where++import Test.Hspec+import Test.Hspec.Attoparsec++import Kubernetes.Data.K8sJSONPath+import Data.Text+import Data.JSONPath+import Data.Aeson++spec :: Spec+spec = do+  describe "K8sJSONPath" $ do+    describe "Parsing" $ do+      it "should parse plain text" $ do+        ("plain" :: Text) ~> k8sJSONPath+          `shouldParse` [PlainText "plain"]++      it "should parse jsonpath" $ do+        ("{.foo}" :: Text) ~> k8sJSONPath+          `shouldParse` [JSONPath [KeyChild "foo"]]++      it "should parse K8sJSONPath with both text and jsonpath" $ do+        ("kind is {.kind}" :: Text) ~> k8sJSONPath+          `shouldParse` [PlainText "kind is ", JSONPath [KeyChild "kind"]]++    describe "Running" $ do+      it "should interpolate string with json values" $ do+        let path = [PlainText "kind is ", JSONPath [KeyChild "kind"]]+            val = (object ["kind" .= ("Pod" :: Text)])+        runJSONPath path val `shouldBe` Right "kind is Pod"+
test/Spec.hs view
@@ -1,31 +1,1 @@-{-# LANGUAGE OverloadedStrings   #-}-{-# LANGUAGE ScopedTypeVariables #-}--import           Data.Aeson                   (decode, encode, parseJSON,-                                               toJSON)-import           Data.Maybe                   (fromJust)-import           Data.Yaml                    (decodeFile)-import           Kubernetes.Client.KubeConfig (AuthInfo (..), Cluster (..),-                                               Config, Context (..),-                                               getAuthInfo, getCluster,-                                               getContext)-import           Test.Hspec--main :: IO ()-main = do-  config :: Config <- fromJust <$> decodeFile "test/testdata/kubeconfig.yaml"-  hspec $ do-    describe "FromJSON and ToJSON instances" $ do-      it "roundtrips successfully" $ do-        decode (encode (toJSON config)) `shouldBe` Just config-    describe "getContext" $ do-      it "returns the correct context" $ do-        getContext config `shouldBe` (Right (Context "cluster-aaa" "user-aaa" Nothing))--    describe "getCluster" $ do-      it "returns the correct cluster" $ do-        server <$> getCluster config `shouldBe` (Right "https://aaa.example.com")--    describe "getAuthInfo" $ do-      it "returns the correct authInfo" $ do-        fst <$> getAuthInfo config `shouldBe` (Right "user-aaa")+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/testdata/certs/certificate.pem view
@@ -0,0 +1,17 @@+-----BEGIN CERTIFICATE-----+MIICsDCCAZgCCQCyRyEmfEJy9jANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5z+ZWxmLXNpZ25lZC1jYTAgFw0xOTA3MjMyMTUwMThaGA8yMTI5MDEyNzIxNTAxOFow+GTEXMBUGA1UEAwwOc2VsZi1zaWduZWQtY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB+DwAwggEKAoIBAQDbxmgIae5FUbLu76NmsUbbo7D0Bk6cUpII/lhREQptoL4YkOo3+MnLVQ6bkTG1hdWkN2A+eDHDJklNR6zzrCAA395lKhCZWSxq9o3AFWFMI8TWSptxL+X/fl/vDh1pH4u/nGjKC1fSf2F9nvxywIgd0PNttWG8bNMzl3gZULfg0TWKvSX6Ln+9SLA9Whsr3WMQno46JiKauB7z1+9Qc+26uEOi6kpc2mYIDTCUO5+umUEsV+seQS++TUe9lMQCLuZVmnWoygbCx5+eXm//iYOBSz8Z5jpEOuA6mY9FJAUU3uoGR656NImZ+IBjgVSZBIDleq2V/lmdbjHy8GpQTMtButMorAgMBAAEwDQYJKoZIhvcNAQEFBQAD+ggEBALHSlOI+Ra6QWr5V3FQS2ypkddK19RZ3TxEvCt8Brt2uNBw6Oxw/qLq17xiB+C7bJV1SA9MNqdv1grk1kVil5aUeAGLIQtVD3C1/qiCgfP+HN1Fb0QnMcxJwESRmc+TDQoYEkZp/TqgggDbmJD6S91EpXs1QiAvNA5+9L8ikOrOJQeHkzFen9PXoSqkVTl+XPwOsCIzcBnuq+agt3pkiFcHtm348y4Yjv3kOimAhzanR96DH2DtTBCmDE/cYW7j+s5aOI/MKlrZxh2gFvgqBZMjVUv8bNuDAU+8prDH4YzsDFeGKD42lDvnL+XQ69xKE+oLBoM+xTo5QwQp0ZWW3srw47BKo=+-----END CERTIFICATE-----
+ test/testdata/certs/private-key.pem view
@@ -0,0 +1,27 @@+-----BEGIN RSA PRIVATE KEY-----+MIIEowIBAAKCAQEA28ZoCGnuRVGy7u+jZrFG26Ow9AZOnFKSCP5YUREKbaC+GJDq+NzJy1UOm5ExtYXVpDdgPngxwyZJTUes86wgAN/eZSoQmVksavaNwBVhTCPE1kqbc+S1/35f7w4daR+Lv5xoygtX0n9hfZ78csCIHdDzbbVhvGzTM5d4GVC34NE1ir0l+i+5/UiwPVobK91jEJ6OOiYimrge89fvUHPturhDoupKXNpmCA0wlDufrplBLFfrHkE+vk1HvZTEAi7mVZp1qMoGwsefnl5v/4mDgUs/GeY6RDrgOpmPRSQFFN7qBkeuejSJ+mSAY4FUmQSA5Xqtlf5ZnW4x8vBqUEzLQbrTKKwIDAQABAoIBACG0nRHlRSCmdf3F+DNdcCtT2ltXl/bplw3XTpDHSnjnP9DeKShFrEEd616advgy7WABCiaqgl8+iPFsM+68vT70ymEYFnIQYNAK3i2fRH5nwxmhjCtHhu4HMKlWDdaoeuNJFp0d/jsPRCFi96+6Vrop8GElUDwg53G5GJaokQf8dtsbg05Qv0C+BLMjYoXt+OCSYn0Jnzc3YLyn6pf+0Ubhb5X73+pSFcY0JindJzHo/c3k5255VOorimxSA9Kr0glYJ2MlOuWM7khZfWjJ+yJPEfa0hM/mPkR2WZ3Rif1Hb01BcrpjsNmQKFDKG+gumxL0CYzUlxOrVI5RO5+yC+wKw8FYkCgYEA/QSzpNxc+qtYj9lJL3p2sxN8cQNOhDOF3DdEZmwToPF2JQKqnDo0+5LDugNgUuOilVFYGxmLNC9RMB+PgOT7kHgZY99cZFuUCww85j2NIbdoATXFZvUdD+O2E9o4X4RQ4+WQTZZ01sfjxMa6Y+t5HC49wvwHwdY0SMA3w6jPkBer8CgYEA3l1q+yJObFV6rorCoeJIpLPGs1lQpXb0qgENmE6tnwYQ56mIuP0mLiSvqkISWwQyEiPjZ+g9O70adOLom/0l9ssIsE1bHk/k/391rzYsvXvHM/uuIFNd7yqYApBLXq3jiNZbfq+CeoWkymez0VOWzgIQxh2UHZ+5+qRSMDgV6hq55UCgYBr00ofcs2pAcZvHyFCO4VE+UYSRwOAAFNjx/ReIMny29M/te9JrW57Y6tHpVKyYFIUIiNTATLCnXuS75A/VNYkP+hpL5o9AMYrInoGBeS+g88E96sViWAj2Tm6AiBODFxQkq9JcVn/ghX98NbT6DCnos+ktRCymHXwQmOHq3xD9jijwKBgGX3KFQ5e0/dTY8Yuugu/bqiR8MwbJeTer2+Kjyy+yK0wWO5lfxd+PgH0pWcHpal4d/3nPrb4jJOiyHMGr3NkVo7N8LWdEYicWvSOPDT9+jDvaDUtBAWqmhVe8cRK76Ktl+1C9eRB6y0dIOo6JFVk25HL/8KEM9Tybj2txJm6L+yBnRAoGBAN3K6EhZ6eO0BorVNi8/5KaE8D0f3ZFEgUBEpVwiJLmL1hXbsiv2JyjE++dR+reScpl8NODycnnFzBzlIijB52CZWo+cxyFSvZxz3RrJZ8XaM/fg5MgNmcwLQ+zejmoH7LuYD4z/OaPzgupJZlRUZNADOqZ8alsxSIvOHkImUAT9Q2+-----END RSA PRIVATE KEY-----
+ test/testdata/tokens/token1 view
@@ -0,0 +1,1 @@+token1
+ test/testdata/tokens/token2 view
@@ -0,0 +1,1 @@+token2