diff --git a/config_app/config_endpoints/api/kube_endpoints.py b/config_app/config_endpoints/api/kube_endpoints.py index 07ba03bd4..6e22169a6 100644 --- a/config_app/config_endpoints/api/kube_endpoints.py +++ b/config_app/config_endpoints/api/kube_endpoints.py @@ -1,3 +1,5 @@ +import logging + from flask import request, make_response from config_app.config_util.config import get_config_as_kube_secret @@ -7,6 +9,7 @@ from config_app.c_app import app, config_provider from config_app.config_endpoints.api import resource, ApiResource, nickname, kubernetes_only, validate_json_request from config_app.config_util.k8saccessor import KubernetesAccessorSingleton, K8sApiException +logger = logging.getLogger(__name__) @resource('/v1/kubernetes/deployments/') class SuperUserKubernetesDeployment(ApiResource): @@ -82,7 +85,7 @@ class QEDeploymentRollback(ApiResource): deployment_names = request.get_json()['deploymentNames'] # To roll back a deployment, we must do 2 things: - # 1. Roll back the config secret to its old value (discarding changes we made in this session + # 1. Roll back the config secret to its old value (discarding changes we made in this session) # 2. Trigger a rollback to the previous revision, so that the pods will be restarted with # the old config old_secret = get_config_as_kube_secret(config_provider.get_old_config_dir()) @@ -93,7 +96,8 @@ class QEDeploymentRollback(ApiResource): for name in deployment_names: kube_accessor.rollback_deployment(name) except K8sApiException as e: - return make_response(e.message, 500) + logger.exception('Failed to rollback deployment.') + return make_response(e.message, 503) return make_response('Ok', 204) @@ -109,7 +113,8 @@ class SuperUserKubernetesConfiguration(ApiResource): new_secret = get_config_as_kube_secret(config_provider.get_config_dir_path()) KubernetesAccessorSingleton.get_instance().replace_qe_secret(new_secret) except K8sApiException as e: - return make_response(e.message, 500) + logger.exception('Failed to deploy qe config secret to kubernetes.') + return make_response(e.message, 503) return make_response('Ok', 201) diff --git a/config_app/config_util/config/test/test_helpers.py b/config_app/config_util/config/test/test_helpers.py new file mode 100644 index 000000000..ceeae51ff --- /dev/null +++ b/config_app/config_util/config/test/test_helpers.py @@ -0,0 +1,75 @@ +import pytest +import os +import base64 + +from backports.tempfile import TemporaryDirectory + +from config_app.config_util.config import get_config_as_kube_secret +from util.config.validator import EXTRA_CA_DIRECTORY + + +def _create_temp_file_structure(file_structure): + temp_dir = TemporaryDirectory() + + for filename, data in file_structure.iteritems(): + if filename == EXTRA_CA_DIRECTORY: + extra_ca_dir_path = os.path.join(temp_dir.name, EXTRA_CA_DIRECTORY) + os.mkdir(extra_ca_dir_path) + + for name, cert_value in data: + with open(os.path.join(extra_ca_dir_path, name), 'w') as f: + f.write(cert_value) + else: + with open(os.path.join(temp_dir.name, filename), 'w') as f: + f.write(data) + + return temp_dir + + +@pytest.mark.parametrize('file_structure, expected_secret', [ + pytest.param({ + 'config.yaml': 'test:true', + }, + { + 'config.yaml': 'dGVzdDp0cnVl', + }, id='just a config value'), + pytest.param({ + 'config.yaml': 'test:true', + 'otherfile.ext': 'im a file' + }, + { + 'config.yaml': 'dGVzdDp0cnVl', + 'otherfile.ext': base64.b64encode('im a file') + }, id='config and another file'), + pytest.param({ + 'config.yaml': 'test:true', + 'extra_ca_certs': [ + ('cert.crt', 'im a cert!'), + ] + }, + { + 'config.yaml': 'dGVzdDp0cnVl', + 'extra_ca_certs_cert.crt': base64.b64encode('im a cert!'), + }, id='config and an extra cert'), + pytest.param({ + 'config.yaml': 'test:true', + 'otherfile.ext': 'im a file', + 'extra_ca_certs': [ + ('cert.crt', 'im a cert!'), + ('another.crt', 'im a different cert!'), + ] + }, + { + 'config.yaml': 'dGVzdDp0cnVl', + 'otherfile.ext': base64.b64encode('im a file'), + 'extra_ca_certs_cert.crt': base64.b64encode('im a cert!'), + 'extra_ca_certs_another.crt': base64.b64encode('im a different cert!'), + }, id='config, files, and extra certs!'), +]) +def test_get_config_as_kube_secret(file_structure, expected_secret): + temp_dir = _create_temp_file_structure(file_structure) + + secret = get_config_as_kube_secret(temp_dir.name) + assert secret == expected_secret + + temp_dir.cleanup() diff --git a/config_app/config_util/config/test/test_transient_dir_provider.py b/config_app/config_util/config/test/test_transient_dir_provider.py new file mode 100644 index 000000000..2d1f3f96c --- /dev/null +++ b/config_app/config_util/config/test/test_transient_dir_provider.py @@ -0,0 +1,68 @@ +import pytest +import os + +from config_app.config_util.config.TransientDirectoryProvider import TransientDirectoryProvider + + +@pytest.mark.parametrize('files_to_write, operations, expected_new_dir', [ + pytest.param({ + 'config.yaml': 'a config', + }, ([], [], []), { + 'config.yaml': 'a config', + }, id='just a config'), + pytest.param({ + 'config.yaml': 'a config', + 'oldfile': 'hmmm' + }, ([], [], ['oldfile']), { + 'config.yaml': 'a config', + }, id='delete a file'), + pytest.param({ + 'config.yaml': 'a config', + 'oldfile': 'hmmm' + }, ([('newfile', 'asdf')], [], ['oldfile']), { + 'config.yaml': 'a config', + 'newfile': 'asdf' + }, id='delete and add a file'), + pytest.param({ + 'config.yaml': 'a config', + 'somefile': 'before' + }, ([('newfile', 'asdf')], [('somefile', 'after')], []), { + 'config.yaml': 'a config', + 'newfile': 'asdf', + 'somefile': 'after', + }, id='add new files and change files'), +]) +def test_transient_dir_copy_config_dir(files_to_write, operations, expected_new_dir): + config_provider = TransientDirectoryProvider('', '', '') + + for name, data in files_to_write.iteritems(): + config_provider.write_volume_file(name, data) + + config_provider.create_copy_of_config_dir() + + for create in operations[0]: + (name, data) = create + config_provider.write_volume_file(name, data) + + for update in operations[1]: + (name, data) = update + config_provider.write_volume_file(name, data) + + for delete in operations[2]: + config_provider.remove_volume_file(delete) + + # check that the new directory matches expected state + for filename, data in expected_new_dir.iteritems(): + with open(os.path.join(config_provider.get_config_dir_path(), filename)) as f: + new_data = f.read() + assert new_data == data + + # Now check that the old dir matches the original state + saved = config_provider.get_old_config_dir() + + for filename, data in files_to_write.iteritems(): + with open(os.path.join(saved, filename)) as f: + new_data = f.read() + assert new_data == data + + config_provider.temp_dir.cleanup()