summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoraxeman <>2019-07-11 07:07:00 (GMT)
committerhdiff <hdiff@hdiff.luite.com>2019-07-11 07:07:00 (GMT)
commit586e49f64122857978026944d905cec52d973a3f (patch)
treee88585cd1edca34fcd92a77f30e96b5a2dc6e0d7
version 0.1.0.00.1.0.0
-rw-r--r--LICENSE201
-rw-r--r--example/App.hs46
-rw-r--r--kubernetes-client.cabal109
-rw-r--r--src/Kubernetes/Client.hs9
-rw-r--r--src/Kubernetes/Client/Config.hs136
-rw-r--r--src/Kubernetes/Client/KubeConfig.hs184
-rw-r--r--src/Kubernetes/Client/Watch.hs95
-rw-r--r--test/Spec.hs31
-rw-r--r--test/testdata/kubeconfig.yaml35
9 files changed, 846 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/example/App.hs b/example/App.hs
new file mode 100644
index 0000000..6f10379
--- /dev/null
+++ b/example/App.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+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 qualified Kubernetes.OpenAPI.API.CoreV1 as CoreV1
+import Network.TLS (credentialLoadX509)
+
+example :: IO ()
+example = do
+ -- We need to first create a Kubernetes.Core.KubernetesConfig and a Network.HTTP.Client.Manager.
+ -- Currently we need to construct these objects manually. Work is underway to construct these
+ -- objects automatically from a kubeconfig file. See https://github.com/kubernetes-client/haskell/issues/2.
+ kcfg <-
+ newConfig
+ & fmap (setMasterURI "https://mycluster.example.com") -- fill in master URI
+ & fmap (setTokenAuth "mytoken") -- if using token auth
+ & fmap disableValidateAuthMethods -- if using client cert auth
+ myCAStore <- loadPEMCerts "/path/to/ca.crt" -- if using custom CA certs
+ myCert <- -- if using client cert
+ credentialLoadX509 "/path/to/client.crt" "/path/to/client.key"
+ >>= either error return
+ tlsParams <-
+ defaultTLSClientParams
+ & fmap disableServerNameValidation -- if master address is specified as an IP address
+ & fmap disableServerCertValidation -- if you don't want to validate the server cert at all (insecure)
+ & fmap (setCAStore myCAStore) -- if using custom CA certs
+ & fmap (setClientCert myCert) -- if using client cert
+ manager <- newManager tlsParams
+ dispatchMime
+ manager
+ kcfg
+ (CoreV1.listPodForAllNamespaces (Accept MimeJSON))
+ >>= print
+
+main :: IO ()
+main = return ()
diff --git a/kubernetes-client.cabal b/kubernetes-client.cabal
new file mode 100644
index 0000000..69af00f
--- /dev/null
+++ b/kubernetes-client.cabal
@@ -0,0 +1,109 @@
+cabal-version: 1.12
+name: kubernetes-client
+version: 0.1.0.0
+license: Apache-2.0
+license-file: LICENSE
+maintainer: Shimin Guo <smguo2001@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
+extra-source-files:
+ test/testdata/kubeconfig.yaml
+
+library
+ exposed-modules:
+ Kubernetes.Client
+ Kubernetes.Client.Config
+ Kubernetes.Client.KubeConfig
+ Kubernetes.Client.Watch
+ hs-source-dirs: src
+ other-modules:
+ Paths_kubernetes_client
+ default-language: Haskell2010
+ build-depends:
+ aeson >=1.2.2 && <1.5,
+ 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,
+ 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,
+ safe-exceptions >=0.1.0.0 && <0.2,
+ streaming-bytestring >=0.1.5 && <0.2.0,
+ text >=0.11 && <1.3,
+ 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
+
+test-suite example
+ 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,
+ 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,
+ http-client >=0.5 && <0.7,
+ http-client-tls >=0.3.5.3 && <0.4,
+ 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,
+ safe-exceptions >=0.1.0.0 && <0.2,
+ streaming-bytestring >=0.1.5 && <0.2.0,
+ text >=0.11 && <1.3,
+ 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
+
+test-suite spec
+ type: exitcode-stdio-1.0
+ main-is: Spec.hs
+ hs-source-dirs: test
+ other-modules:
+ Paths_kubernetes_client
+ default-language: Haskell2010
+ build-depends:
+ aeson >=1.2.2 && <1.5,
+ 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,
+ http-client >=0.5 && <0.7,
+ http-client-tls >=0.3.5.3 && <0.4,
+ 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,
+ safe-exceptions >=0.1.0.0 && <0.2,
+ streaming-bytestring >=0.1.5 && <0.2.0,
+ text >=0.11 && <1.3,
+ 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
diff --git a/src/Kubernetes/Client.hs b/src/Kubernetes/Client.hs
new file mode 100644
index 0000000..f646114
--- /dev/null
+++ b/src/Kubernetes/Client.hs
@@ -0,0 +1,9 @@
+module Kubernetes.Client
+ ( module Kubernetes.Client.Config
+ , module Kubernetes.Client.KubeConfig
+ , module Kubernetes.Client.Watch
+ ) where
+
+import Kubernetes.Client.Config
+import Kubernetes.Client.KubeConfig
+import Kubernetes.Client.Watch
diff --git a/src/Kubernetes/Client/Config.hs b/src/Kubernetes/Client/Config.hs
new file mode 100644
index 0000000..7dd0de2
--- /dev/null
+++ b/src/Kubernetes/Client/Config.hs
@@ -0,0 +1,136 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Kubernetes.Client.Config where
+
+import qualified Kubernetes.OpenAPI.Core as K
+import qualified Kubernetes.OpenAPI.Model 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)
+
+-- |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 }
+
+-- |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 }
+
+-- |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 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
+
+data ParsePEMCertsException = ParsePEMCertsException String deriving (Typeable, Show)
+
+instance Exception ParsePEMCertsException
+
+-- |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
+
+serviceAccountDir :: FilePath
+serviceAccountDir = "/var/run/secrets/kubernetes.io/serviceaccount"
+
+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)
diff --git a/src/Kubernetes/Client/KubeConfig.hs b/src/Kubernetes/Client/KubeConfig.hs
new file mode 100644
index 0000000..211726e
--- /dev/null
+++ b/src/Kubernetes/Client/KubeConfig.hs
@@ -0,0 +1,184 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+{-|
+Module : Kubernetes.KubeConfig
+Description : Data model for the kubeconfig.
+
+This module contains the definition of the data model of the kubeconfig.
+
+The official definition of the kubeconfig is defined in https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/api/v1/types.go.
+
+This is a mostly straightforward translation into Haskell, with 'FromJSON' and 'ToJSON' instances defined.
+-}
+module Kubernetes.Client.KubeConfig where
+
+import Data.Aeson (FromJSON (..), Options, ToJSON (..),
+ Value (..), camelTo2, defaultOptions,
+ fieldLabelModifier, genericParseJSON,
+ genericToJSON, object, omitNothingFields,
+ withObject, (.:), (.=))
+import qualified Data.Map as Map
+import Data.Proxy
+import Data.Semigroup ((<>))
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Typeable
+import GHC.Generics
+import GHC.TypeLits
+
+camelToWithOverrides :: Char -> Map.Map String String -> Options
+camelToWithOverrides c overrides = defaultOptions
+ { fieldLabelModifier = modifier
+ , omitNothingFields = True
+ }
+ where modifier s = Map.findWithDefault (camelTo2 c s) s overrides
+
+-- |Represents a kubeconfig.
+data Config = Config
+ { kind :: Maybe Text
+ , apiVersion :: Maybe Text
+ , preferences :: Maybe Preferences
+ , clusters :: [NamedEntity Cluster "cluster"]
+ , authInfos :: [NamedEntity AuthInfo "user"]
+ , contexts :: [NamedEntity Context "context"]
+ , currentContext :: Text
+ } deriving (Eq, Generic, Show)
+
+configJSONOptions = camelToWithOverrides
+ '-'
+ (Map.fromList [("apiVersion", "apiVersion"), ("authInfos", "users")])
+
+instance ToJSON Config where
+ toJSON = genericToJSON configJSONOptions
+
+instance FromJSON Config where
+ parseJSON = genericParseJSON configJSONOptions
+
+newtype Preferences = Preferences
+ { colors :: Maybe Bool
+ } deriving (Eq, Generic, Show)
+
+instance ToJSON Preferences where
+ toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty
+
+instance FromJSON Preferences where
+ parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty
+
+data Cluster = Cluster
+ { server :: Text
+ , insecureSkipTLSVerify :: Maybe Bool
+ , certificateAuthority :: Maybe Text
+ , certificateAuthorityData :: Maybe Text
+ } deriving (Eq, Generic, Show, Typeable)
+
+instance ToJSON Cluster where
+ toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty
+
+instance FromJSON Cluster where
+ parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty
+
+data NamedEntity a (typeKey :: Symbol) = NamedEntity
+ { name :: Text
+ , entity :: a } deriving (Eq, Generic, Show)
+
+instance (FromJSON a, Typeable a, KnownSymbol s) =>
+ FromJSON (NamedEntity a s) where
+ parseJSON = withObject ("Named" <> (show $ typeOf (undefined :: a))) $ \v ->
+ NamedEntity <$> v .: "name" <*> v .: T.pack (symbolVal (Proxy :: Proxy s))
+
+instance (ToJSON a, KnownSymbol s) =>
+ ToJSON (NamedEntity a s) where
+ toJSON (NamedEntity {..}) = object
+ ["name" .= toJSON name, T.pack (symbolVal (Proxy :: Proxy s)) .= toJSON entity]
+
+toMap :: [NamedEntity a s] -> Map.Map Text a
+toMap = Map.fromList . fmap (\NamedEntity {..} -> (name, entity))
+
+data AuthInfo = AuthInfo
+ { clientCertificate :: Maybe FilePath
+ , clientCertificateData :: Maybe Text
+ , clientKey :: Maybe FilePath
+ , clientKeyData :: Maybe Text
+ , token :: Maybe Text
+ , tokenFile :: Maybe FilePath
+ , impersonate :: Maybe Text
+ , impersonateGroups :: Maybe [Text]
+ , impersonateUserExtra :: Maybe (Map.Map Text [Text])
+ , username :: Maybe Text
+ , password :: Maybe Text
+ , authProvider :: Maybe AuthProviderConfig
+ } deriving (Eq, Generic, Show, Typeable)
+
+authInfoJSONOptions = camelToWithOverrides
+ '-'
+ ( Map.fromList
+ [ ("tokenFile" , "tokenFile")
+ , ("impersonate" , "as")
+ , ("impersonateGroups" , "as-groups")
+ , ("impersonateUserExtra", "as-user-extra")
+ ]
+ )
+
+instance ToJSON AuthInfo where
+ toJSON = genericToJSON authInfoJSONOptions
+
+instance FromJSON AuthInfo where
+ parseJSON = genericParseJSON authInfoJSONOptions
+
+data Context = Context
+ { cluster :: Text
+ , authInfo :: Text
+ , namespace :: Maybe Text
+ } deriving (Eq, Generic, Show, Typeable)
+
+contextJSONOptions =
+ camelToWithOverrides '-' (Map.fromList [("authInfo", "user")])
+
+instance ToJSON Context where
+ toJSON = genericToJSON contextJSONOptions
+
+instance FromJSON Context where
+ parseJSON = genericParseJSON contextJSONOptions
+
+data AuthProviderConfig = AuthProviderConfig
+ { name :: Text
+ , config :: Maybe (Map.Map Text Text)
+ } deriving (Eq, Generic, Show)
+
+instance ToJSON AuthProviderConfig where
+ toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty
+
+instance FromJSON AuthProviderConfig where
+ parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty
+
+-- |Returns the currently active context.
+getContext :: Config -> Either String Context
+getContext Config {..} =
+ let maybeContext = Map.lookup currentContext (toMap contexts)
+ in case maybeContext of
+ Just ctx -> Right ctx
+ Nothing -> Left ("No context named " <> T.unpack currentContext)
+
+-- |Returns the currently active user.
+getAuthInfo :: Config -> Either String (Text, AuthInfo)
+getAuthInfo cfg@Config {..} = do
+ Context {..} <- getContext cfg
+ let maybeAuth = Map.lookup authInfo (toMap authInfos)
+ case maybeAuth of
+ Just auth -> Right (authInfo, auth)
+ Nothing -> Left ("No user named " <> T.unpack authInfo)
+
+-- |Returns the currently active cluster.
+getCluster :: Config -> Either String Cluster
+getCluster cfg@Config {clusters=clusters} = do
+ Context {cluster=clusterName} <- getContext cfg
+ let maybeCluster = Map.lookup clusterName (toMap clusters)
+ case maybeCluster of
+ Just cluster -> Right cluster
+ Nothing -> Left ("No cluster named " <> T.unpack clusterName)
diff --git a/src/Kubernetes/Client/Watch.hs b/src/Kubernetes/Client/Watch.hs
new file mode 100644
index 0000000..eede579
--- /dev/null
+++ b/src/Kubernetes/Client/Watch.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Kubernetes.Client.Watch
+ ( WatchEvent
+ , eventType
+ , eventObject
+ , dispatchWatch
+ ) where
+
+import Control.Monad
+import Control.Monad.Trans (lift)
+import Data.Aeson
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Streaming.Char8 as Q
+import qualified Data.Text as T
+import Kubernetes.OpenAPI.Core
+import Kubernetes.OpenAPI.Client
+import Kubernetes.OpenAPI.MimeTypes
+import Kubernetes.OpenAPI.Model (Watch(..))
+import Network.HTTP.Client
+
+data WatchEvent a = WatchEvent
+ { _eventType :: T.Text
+ , _eventObject :: a
+ } deriving (Eq, Show)
+
+instance FromJSON a => FromJSON (WatchEvent a) where
+ parseJSON (Object x) = WatchEvent <$> x .: "type" <*> x .: "object"
+ parseJSON _ = fail "Expected an object"
+
+instance ToJSON a => ToJSON (WatchEvent a) where
+ toJSON x = object
+ [ "type" .= _eventType x
+ , "object" .= _eventObject x
+ ]
+
+-- | Type of the 'WatchEvent'.
+eventType :: WatchEvent a -> T.Text
+eventType = _eventType
+
+-- | Object within the 'WatchEvent'.
+eventObject :: WatchEvent a -> a
+eventObject = _eventObject
+
+{-| Dispatch a request setting watch to true. Takes a consumer function
+which consumes the 'Q.ByteString' stream. Following is a simple example which
+just streams to stdout. First some setup - this assumes kubernetes is accessible
+at http://localhost:8001, e.g. after running /kubectl proxy/:
+
+@
+import qualified Data.ByteString.Streaming.Char8 as Q
+
+manager <- newManager defaultManagerSettings
+defaultConfig <- newConfig
+config = defaultConfig { configHost = "http://localhost:8001", configValidateAuthMethods = False }
+request = listEndpointsForAllNamespaces (Accept MimeJSON)
+@
+
+Launching 'dispatchWatch' with the above we get a stream of endpoints data:
+
+@
+ > dispatchWatch manager config request Q.stdout
+ {"type":\"ADDED\","object":{"kind":\"Endpoints\","apiVersion":"v1","metadata":{"name":"heapster" ....
+@
+-}
+dispatchWatch ::
+ (HasOptionalParam req Watch, MimeType accept, MimeType contentType) =>
+ Manager
+ -> KubernetesClientConfig
+ -> KubernetesRequest req contentType resp accept
+ -> (Q.ByteString IO () -> IO a)
+ -> IO a
+dispatchWatch manager config request apply = do
+ let watchRequest = applyOptionalParam request (Watch True)
+ (InitRequest req) <- _toInitRequest config watchRequest
+ withHTTP req manager $ \resp -> apply $ responseBody resp
+
+withHTTP ::
+ Request
+ -> Manager
+ -> (Response (Q.ByteString IO ()) -> IO a)
+ -> IO a
+withHTTP request manager f = withResponse request manager f'
+ where
+ f' resp = do
+ let p = (from . brRead . responseBody) resp
+ f (resp {responseBody = p})
+ from :: IO B.ByteString -> Q.ByteString IO ()
+ from io = go
+ where
+ go = do
+ bs <- lift io
+ unless (B.null bs) $ do
+ Q.chunk bs
+ go
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
index 0000000..378cc2e
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,31 @@
+{-# 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")
diff --git a/test/testdata/kubeconfig.yaml b/test/testdata/kubeconfig.yaml
new file mode 100644
index 0000000..029a82f
--- /dev/null
+++ b/test/testdata/kubeconfig.yaml
@@ -0,0 +1,35 @@
+apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: fake-ca-data
+ server: https://aaa.example.com
+ name: cluster-aaa
+- cluster:
+ certificate-authority-data: fake-ca-data
+ server: https://bbb.example.com
+ name: cluster-bbb
+contexts:
+- context:
+ cluster: cluster-aaa
+ user: user-aaa
+ name: aaa
+- context:
+ cluster: cluster-bbb
+ user: user-bbb
+ name: bbb
+current-context: aaa
+kind: Config
+preferences: {}
+users:
+- name: user-aaa
+ user:
+ auth-provider:
+ config:
+ access-token: fake-token
+ expiry: 2017-06-06 22:53:31
+ expiry-key: '{.credential.token_expiry}'
+ token-key: '{.credential.access_token}'
+ name: gcp
+- name: user-bbb
+ user:
+ token: fake-token