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 +46/−0
- example/App.hs +33/−11
- kubernetes-client.cabal +131/−68
- src/Kubernetes/Client/Auth/ClientCert.hs +41/−0
- src/Kubernetes/Client/Auth/GCP.hs +136/−0
- src/Kubernetes/Client/Auth/Internal/Types.hs +9/−0
- src/Kubernetes/Client/Auth/OIDC.hs +184/−0
- src/Kubernetes/Client/Auth/Token.hs +27/−0
- src/Kubernetes/Client/Auth/TokenFile.hs +73/−0
- src/Kubernetes/Client/Config.hs +158/−120
- src/Kubernetes/Client/Internal/TLSUtils.hs +110/−0
- src/Kubernetes/Data/K8sJSONPath.hs +48/−0
- test/Kubernetes/Client/Auth/ClientCertSpec.hs +77/−0
- test/Kubernetes/Client/Auth/TokenFileSpec.hs +73/−0
- test/Kubernetes/Client/KubeConfigSpec.hs +36/−0
- test/Kubernetes/Data/K8sJSONPathSpec.hs +33/−0
- test/Spec.hs +1/−31
- test/testdata/certs/certificate.pem +17/−0
- test/testdata/certs/private-key.pem +27/−0
- test/testdata/tokens/token1 +1/−0
- test/testdata/tokens/token2 +1/−0
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