From cd70ae3527e716259a08f9c705d31609ef0cd5f5 Mon Sep 17 00:00:00 2001 From: vikaschoudhary16 Date: Fri, 3 Mar 2017 13:54:38 +0530 Subject: [PATCH] Add support for HTTPS client Add support to use cert and key files for watching a HTTPS enabled K8S api server. Change-Id: I0978531caa2c35031041450f86db9e90ce5efbb0 Closes-bug: #1670346 --- README.rst | 22 ++++++++ kuryr_kubernetes/config.py | 9 +++ kuryr_kubernetes/k8s_client.py | 35 ++++++++++-- .../tests/unit/test_k8s_client.py | 56 ++++++++++++++++--- 4 files changed, 109 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 3f3d9bf0a..86058f74e 100644 --- a/README.rst +++ b/README.rst @@ -104,6 +104,28 @@ running. 4GB memory and 2 vCPUs, is the minimum resource requirement for the VM: Now launch pods using kubectl, Undercloud Neutron will serve the networking. + +How to watch K8S api-server over HTTPS : +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add absolute path of client side cert file and key file for K8S server in kuryr.conf:: + + [kubernetes] + ssl_client_crt_file = + ssl_client_key_file = + +If server ssl certification verification is also to be enabled, add absolute path to the ca cert:: + + [kubernetes] + ssl_ca_crt_file = + ssl_verify_server_crt = True + +If want to query HTTPS K8S api server with "--insecure" mode:: + + [kubernetes] + ssl_verify_server_crt = False + + Features -------- diff --git a/kuryr_kubernetes/config.py b/kuryr_kubernetes/config.py index 43e138811..10682832a 100644 --- a/kuryr_kubernetes/config.py +++ b/kuryr_kubernetes/config.py @@ -33,6 +33,15 @@ k8s_opts = [ cfg.StrOpt('api_root', help=_("The root URL of the Kubernetes API"), default=os.environ.get('K8S_API', 'http://localhost:8080')), + cfg.StrOpt('ssl_client_crt_file', + help=_("Absolute path to client cert to connect to HTTPS K8S_API")), + cfg.StrOpt('ssl_client_key_file', + help=_("Absolute path client key file to connect to HTTPS K8S_API")), + cfg.StrOpt('ssl_ca_crt_file', + help=_("Absolute path to ca cert file to connect to HTTPS K8S_API")), + cfg.BoolOpt('ssl_verify_server_crt', + help=_("HTTPS K8S_API server identity verification"), + default=False), cfg.StrOpt('pod_project_driver', help=_("The driver to determine OpenStack project for pod ports"), default='default'), diff --git a/kuryr_kubernetes/k8s_client.py b/kuryr_kubernetes/k8s_client.py index 1ab82c938..8c6a6c502 100644 --- a/kuryr_kubernetes/k8s_client.py +++ b/kuryr_kubernetes/k8s_client.py @@ -12,14 +12,16 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - import contextlib import itertools +import os from oslo_log import log as logging from oslo_serialization import jsonutils import requests +from kuryr.lib._i18n import _ +from kuryr_kubernetes import config from kuryr_kubernetes import exceptions as exc LOG = logging.getLogger(__name__) @@ -31,11 +33,33 @@ class K8sClient(object): def __init__(self, base_url): self._base_url = base_url + cert_file = config.CONF.kubernetes.ssl_client_crt_file + key_file = config.CONF.kubernetes.ssl_client_key_file + ca_crt_file = config.CONF.kubernetes.ssl_ca_crt_file + self.verify_server = config.CONF.kubernetes.ssl_verify_server_crt + if cert_file and not os.path.exists(cert_file): + raise RuntimeError( + _("Unable to find ssl cert_file : %s") % cert_file) + if key_file and not os.path.exists(key_file): + raise RuntimeError( + _("Unable to find ssl key_file : %s") % key_file) + if self.verify_server: + if not ca_crt_file: + raise RuntimeError( + _("ssl_ca_crt_file cannot be None")) + elif not os.path.exists(ca_crt_file): + raise RuntimeError( + _("Unable to find ca cert_file : %s") % ca_crt_file) + else: + self.verify_server = ca_crt_file + + self.cert = (cert_file, key_file) def get(self, path): LOG.debug("Get %(path)s", {'path': path}) url = self._base_url + path - response = requests.get(url) + response = requests.get(url, cert=self.cert, + verify=self.verify_server) if not response.ok: raise exc.K8sClientException(response.text) return response.json() @@ -61,7 +85,7 @@ class K8sClient(object): response = requests.patch(url, data=data, headers={ 'Content-Type': 'application/merge-patch+json', 'Accept': 'application/json', - }) + }, cert=self.cert, verify=self.verify_server) if response.ok: return response.json()['metadata']['annotations'] if response.status_code == requests.codes.conflict: @@ -88,8 +112,9 @@ class K8sClient(object): # TODO(ivc): handle connection errors and retry on failure while True: - with contextlib.closing(requests.get(url, params=params, - stream=True)) as response: + with contextlib.closing( + requests.get(url, params=params, stream=True, + cert=self.cert, verify=self.verify_server)) as response: if not response.ok: raise exc.K8sClientException(response.text) for line in response.iter_lines(delimiter='\n'): diff --git a/kuryr_kubernetes/tests/unit/test_k8s_client.py b/kuryr_kubernetes/tests/unit/test_k8s_client.py index e42c042d7..49e6c3ea5 100644 --- a/kuryr_kubernetes/tests/unit/test_k8s_client.py +++ b/kuryr_kubernetes/tests/unit/test_k8s_client.py @@ -29,6 +29,38 @@ class TestK8sClient(test_base.TestCase): super(TestK8sClient, self).setUp() self.base_url = 'http://127.0.0.1:12345' self.client = k8s_client.K8sClient(self.base_url) + default_cert = (None, None) + self.assertEqual(default_cert, self.client.cert) + self.assertEqual(False, self.client.verify_server) + + @mock.patch('os.path.exists') + @mock.patch('kuryr_kubernetes.config.CONF') + def test_https_client_init(self, m_cfg, m_exist): + m_cfg.kubernetes.ssl_client_crt_file = 'dummy_crt_file_path' + m_cfg.kubernetes.ssl_client_key_file = 'dummy_key_file_path' + m_cfg.kubernetes.ssl_ca_crt_file = 'dummy_ca_file_path' + m_cfg.kubernetes.ssl_verify_server_crt = True + m_exist.return_value = True + test_client = k8s_client.K8sClient(self.base_url) + cert = ('dummy_crt_file_path', 'dummy_key_file_path') + self.assertEqual(cert, test_client.cert) + self.assertEqual('dummy_ca_file_path', test_client.verify_server) + + @mock.patch('kuryr_kubernetes.config.CONF') + def test_https_client_init_invalid_client_crt_path(self, m_cfg): + m_cfg.kubernetes.ssl_client_crt_file = 'dummy_crt_file_path' + m_cfg.kubernetes.ssl_client_key_file = 'dummy_key_file_path' + self.assertRaises(RuntimeError, k8s_client.K8sClient, self.base_url) + + @mock.patch('os.path.exists') + @mock.patch('kuryr_kubernetes.config.CONF') + def test_https_client_init_invalid_ca_path(self, m_cfg, m_exist): + m_cfg.kubernetes.ssl_client_crt_file = 'dummy_crt_file_path' + m_cfg.kubernetes.ssl_client_key_file = 'dummy_key_file_path' + m_cfg.kubernetes.ssl_ca_crt_file = None + m_cfg.kubernetes.ssl_verify_server_crt = True + m_exist.return_value = True + self.assertRaises(RuntimeError, k8s_client.K8sClient, self.base_url) @mock.patch('requests.get') def test_get(self, m_get): @@ -41,7 +73,8 @@ class TestK8sClient(test_base.TestCase): m_get.return_value = m_resp self.assertEqual(ret, self.client.get(path)) - m_get.assert_called_once_with(self.base_url + path) + m_get.assert_called_once_with(self.base_url + path, + cert=(None, None), verify=False) @mock.patch('requests.get') def test_get_exception(self, m_get): @@ -72,7 +105,8 @@ class TestK8sClient(test_base.TestCase): self.assertEqual(annotations, self.client.annotate( path, annotations, resource_version=resource_version)) m_patch.assert_called_once_with(self.base_url + path, - data=data, headers=mock.ANY) + data=data, headers=mock.ANY, + cert=(None, None), verify=False) @mock.patch('itertools.count') @mock.patch('requests.patch') @@ -120,10 +154,12 @@ class TestK8sClient(test_base.TestCase): m_patch.assert_has_calls([ mock.call(self.base_url + path, data=conflicting_data, - headers=mock.ANY), + headers=mock.ANY, + cert=(None, None), verify=False), mock.call(self.base_url + path, data=good_data, - headers=mock.ANY)]) + headers=mock.ANY, + cert=(None, None), verify=False)]) @mock.patch('itertools.count') @mock.patch('requests.patch') @@ -162,10 +198,12 @@ class TestK8sClient(test_base.TestCase): m_patch.assert_has_calls([ mock.call(self.base_url + path, data=annotating_data, - headers=mock.ANY), + headers=mock.ANY, + cert=(None, None), verify=False), mock.call(self.base_url + path, data=resolution_data, - headers=mock.ANY)]) + headers=mock.ANY, + cert=(None, None), verify=False)]) @mock.patch('itertools.count') @mock.patch('requests.patch') @@ -196,7 +234,8 @@ class TestK8sClient(test_base.TestCase): resource_version=resource_version) m_patch.assert_called_once_with(self.base_url + path, data=conflicting_data, - headers=mock.ANY) + headers=mock.ANY, + cert=(None, None), verify=False) @mock.patch('requests.get') def test_watch(self, m_get): @@ -218,7 +257,8 @@ class TestK8sClient(test_base.TestCase): self.assertEqual(cycles, m_get.call_count) self.assertEqual(cycles, m_resp.close.call_count) m_get.assert_called_with(self.base_url + path, stream=True, - params={'watch': 'true'}) + params={'watch': 'true'}, cert=(None, None), + verify=False) @mock.patch('requests.get') def test_watch_exception(self, m_get):