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
This commit is contained in:
vikaschoudhary16 2017-03-03 13:54:38 +05:30
parent 5bd2d40f1d
commit cd70ae3527
4 changed files with 109 additions and 13 deletions

View File

@ -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 = <absolute file path eg. /etc/kubernetes/admin.crt>
ssl_client_key_file = <absolute file path eg. /etc/kubernetes/admin.key>
If server ssl certification verification is also to be enabled, add absolute path to the ca cert::
[kubernetes]
ssl_ca_crt_file = <absolute file path eg. /etc/kubernetes/ca.crt>
ssl_verify_server_crt = True
If want to query HTTPS K8S api server with "--insecure" mode::
[kubernetes]
ssl_verify_server_crt = False
Features
--------

View File

@ -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'),

View File

@ -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'):

View File

@ -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):