diff --git a/config_app/c_app.py b/config_app/c_app.py index 3f0b13cf0..5ebd23013 100644 --- a/config_app/c_app.py +++ b/config_app/c_app.py @@ -24,7 +24,7 @@ is_kubernetes = IS_KUBERNETES logger.debug('Configuration is on a kubernetes deployment: %s' % IS_KUBERNETES) config_provider = get_config_provider(OVERRIDE_CONFIG_DIRECTORY, 'config.yaml', 'config.py', - testing=is_testing, kubernetes=is_kubernetes) + testing=is_testing) if is_testing: from test.testconfig import TestConfig diff --git a/config_app/config_endpoints/api/suconfig.py b/config_app/config_endpoints/api/suconfig.py index 9aebe0256..9c59f38fe 100644 --- a/config_app/config_endpoints/api/suconfig.py +++ b/config_app/config_endpoints/api/suconfig.py @@ -6,7 +6,7 @@ from config_app.config_endpoints.api.suconfig_models_pre_oci import pre_oci_mode from config_app.config_endpoints.api import resource, ApiResource, nickname, validate_json_request, kubernetes_only from config_app.c_app import (app, config_provider, superusers, ip_resolver, instance_keys, INIT_SCRIPTS_LOCATION, IS_KUBERNETES) -from config_app.config_util.k8sinterface import KubernetesAccessInterface +from config_app.config_util.k8sinterface import KubernetesAccessInterface, kubernetes_access_instance from data.database import configure from data.runmigration import run_alembic_migration @@ -266,15 +266,13 @@ class SuperUserKubernetesDeployment(ApiResource): @kubernetes_only @nickname('scGetNumDeployments') def get(self): - accessor = KubernetesAccessInterface() - res = accessor.get_num_qe_pods() - return res + return kubernetes_access_instance.get_qe_deployments() @resource('/v1/superuser/config/kubernetes') class SuperUserKubernetesConfiguration(ApiResource): """ Resource for fetching the status of config files and overriding them. """ @kubernetes_only - @nickname('scSaveConfigToKube') + @nickname('scDeployConfiguration') def post(self): return config_provider.save_configuration_to_kubernetes() diff --git a/config_app/config_util/config/TransientDirectoryProvider.py b/config_app/config_util/config/TransientDirectoryProvider.py index 69d85081b..15bb77738 100644 --- a/config_app/config_util/config/TransientDirectoryProvider.py +++ b/config_app/config_util/config/TransientDirectoryProvider.py @@ -2,19 +2,20 @@ import os from backports.tempfile import TemporaryDirectory from config_app.config_util.config.fileprovider import FileConfigProvider +from config_app.config_util.k8sinterface import kubernetes_access_instance + class TransientDirectoryProvider(FileConfigProvider): """ Implementation of the config provider that reads and writes the data from/to the file system, only using temporary directories, deleting old dirs and creating new ones as requested. """ - def __init__(self, config_volume, yaml_filename, py_filename, kubernetes=False): + def __init__(self, config_volume, yaml_filename, py_filename): # Create a temp directory that will be cleaned up when we change the config path # This should ensure we have no "pollution" of different configs: # no uploaded config should ever affect subsequent config modifications/creations temp_dir = TemporaryDirectory() self.temp_dir = temp_dir - self.kubernetes = kubernetes super(TransientDirectoryProvider, self).__init__(temp_dir.name, yaml_filename, py_filename) @property @@ -36,7 +37,10 @@ class TransientDirectoryProvider(FileConfigProvider): return self.config_volume def save_configuration_to_kubernetes(self): - if not self.kubernetes: - raise Exception("Not on kubernetes, cannot save configuration.") + config_path = self.get_config_dir_path() - print('do stuf') + for name in os.listdir(config_path): + file_path = os.path.join(self.config_volume, name) + kubernetes_access_instance.save_file_as_secret(name, file_path) + + return 200 diff --git a/config_app/config_util/config/__init__.py b/config_app/config_util/config/__init__.py index e12d013bf..b9edeba3a 100644 --- a/config_app/config_util/config/__init__.py +++ b/config_app/config_util/config/__init__.py @@ -3,10 +3,10 @@ from config_app.config_util.config.testprovider import TestConfigProvider from config_app.config_util.config.TransientDirectoryProvider import TransientDirectoryProvider -def get_config_provider(config_volume, yaml_filename, py_filename, testing=False, kubernetes=False): +def get_config_provider(config_volume, yaml_filename, py_filename, testing=False): """ Loads and returns the config provider for the current environment. """ if testing: return TestConfigProvider() - return TransientDirectoryProvider(config_volume, yaml_filename, py_filename, kubernetes=kubernetes) + return TransientDirectoryProvider(config_volume, yaml_filename, py_filename) diff --git a/config_app/config_util/k8sinterface.py b/config_app/config_util/k8sinterface.py index b71b50c71..41522fdfa 100644 --- a/config_app/config_util/k8sinterface.py +++ b/config_app/config_util/k8sinterface.py @@ -21,7 +21,7 @@ QE_NAMESPACE = os.environ.get('QE_K8S_NAMESPACE', 'quay-enterprise') QE_CONFIG_SECRET = os.environ.get('QE_K8S_CONFIG_SECRET', 'quay-enterprise-config-secret') # The name of the quay enterprise deployment (not config app) that is used to query & rollout -QE_DEPLOYMENT_SELECTOR = os.environ.get('QE_DEPLOYMENT_SELECTOR', 'quay-enterprise-app') +QE_DEPLOYMENT_SELECTOR = os.environ.get('QE_DEPLOYMENT_SELECTOR', 'app') class KubernetesAccessInterface: """ Implementation of the config provider that reads and writes configuration @@ -80,84 +80,89 @@ class KubernetesAccessInterface: # try: # flask_file.save(buf) # except IOError as ioe: - # raise CannotWriteConfigException(str(ioe)) + # raise Exception(str(ioe)) # # self._update_secret_file(relative_file_path, buf.getvalue()) # finally: # buf.close() - def get_num_qe_pods(self): - # TODO: change to get /deployments?labelSelector={labelName}%3D{labelValue} - # right now just get the hardcoded deployment name - deployment_url = 'namespaces/%s/deployments/%s' % (QE_NAMESPACE, QE_DEPLOYMENT_SELECTOR) - response = self._execute_k8s_api('GET', deployment_url, api_prefix='apis/extensions/v1beta1') + def save_file_as_secret(self, name, file_path): + with open(file_path) as f: + value = f.read() + self._update_secret_file(name, value) + + def get_qe_deployments(self): + pod_selector_url = 'namespaces/%s/deployments?labelSelector=quay-enterprise-component%%3D%s' % (QE_NAMESPACE, QE_DEPLOYMENT_SELECTOR) + response = self._execute_k8s_api('GET', pod_selector_url, api_prefix='apis/extensions/v1beta1') if response.status_code != 200: return None return json.loads(response.text) - # def _assert_success(self, response): - # if response.status_code != 200: - # logger.error('Kubernetes API call failed with response: %s => %s', response.status_code, - # response.text) - # raise CannotWriteConfigException('Kubernetes API call failed: %s' % response.text) + def _assert_success(self, response): + if response.status_code != 200: + logger.error('Kubernetes API call failed with response: %s => %s', response.status_code, + response.text) + raise Exception('Kubernetes API call failed: %s' % response.text) - # def _update_secret_file(self, relative_file_path, value=None): - # if '/' in relative_file_path: - # raise Exception('Expected path from get_volume_path, but found slashes') - # - # # Check first that the namespace for Quay Enterprise exists. If it does not, report that - # # as an error, as it seems to be a common issue. - # namespace_url = 'namespaces/%s' % (QE_NAMESPACE) - # response = self._execute_k8s_api('GET', namespace_url) - # if response.status_code // 100 != 2: - # msg = 'A Kubernetes namespace with name `%s` must be created to save config' % QE_NAMESPACE - # raise CannotWriteConfigException(msg) - # - # # Check if the secret exists. If not, then we create an empty secret and then update the file - # # inside. - # secret_url = 'namespaces/%s/secrets/%s' % (QE_NAMESPACE, QE_CONFIG_SECRET) - # secret = self._lookup_secret() - # if secret is None: - # self._assert_success(self._execute_k8s_api('POST', secret_url, { - # "kind": "Secret", - # "apiVersion": "v1", - # "metadata": { - # "name": QE_CONFIG_SECRET - # }, - # "data": {} - # })) - # - # # Update the secret to reflect the file change. - # secret['data'] = secret.get('data', {}) - # - # if value is not None: - # secret['data'][relative_file_path] = base64.b64encode(value) - # else: - # secret['data'].pop(relative_file_path) - # - # self._assert_success(self._execute_k8s_api('PUT', secret_url, secret)) - # - # # Wait until the local mounted copy of the secret has been updated, as - # # this is an eventual consistency operation, but the caller expects immediate - # # consistency. - # while True: - # matching_files = set() - # for secret_filename, encoded_value in secret['data'].iteritems(): - # expected_value = base64.b64decode(encoded_value) - # try: - # with self.get_volume_file(secret_filename) as f: - # contents = f.read() - # - # if contents == expected_value: - # matching_files.add(secret_filename) - # except IOError: - # continue - # - # if matching_files == set(secret['data'].keys()): - # break - # - # # Sleep for a second and then try again. - # time.sleep(1) + def _update_secret_file(self, relative_file_path, value=None): + if '/' in relative_file_path: + raise Exception('Expected path from get_volume_path, but found slashes') + + # Check first that the namespace for Quay Enterprise exists. If it does not, report that + # as an error, as it seems to be a common issue. + namespace_url = 'namespaces/%s' % (QE_NAMESPACE) + response = self._execute_k8s_api('GET', namespace_url) + if response.status_code // 100 != 2: + msg = 'A Kubernetes namespace with name `%s` must be created to save config' % QE_NAMESPACE + raise Exception(msg) + + # Check if the secret exists. If not, then we create an empty secret and then update the file + # inside. + secret_url = 'namespaces/%s/secrets/%s' % (QE_NAMESPACE, QE_CONFIG_SECRET) + secret = self._lookup_secret() + if secret is None: + self._assert_success(self._execute_k8s_api('POST', secret_url, { + "kind": "Secret", + "apiVersion": "v1", + "metadata": { + "name": QE_CONFIG_SECRET + }, + "data": {} + })) + + # Update the secret to reflect the file change. + secret['data'] = secret.get('data', {}) + + if value is not None: + secret['data'][relative_file_path] = base64.b64encode(value) + else: + secret['data'].pop(relative_file_path) + + self._assert_success(self._execute_k8s_api('PUT', secret_url, secret)) + + # Wait until the local mounted copy of the secret has been updated, as + # this is an eventual consistency operation, but the caller expects immediate + # consistency. + + # while True: + # matching_files = set() + # for secret_filename, encoded_value in secret['data'].iteritems(): + # expected_value = base64.b64decode(encoded_value) + # try: + # with self.get_volume_file(secret_filename) as f: + # contents = f.read() + # + # if contents == expected_value: + # matching_files.add(secret_filename) + # + # except IOError: + # continue + # + # if matching_files == set(secret['data'].keys()): + # break + # + # # Sleep for a second and then try again. + # time.sleep(1) def _lookup_secret(self): secret_url = 'namespaces/%s/secrets/%s' % (QE_NAMESPACE, QE_CONFIG_SECRET) @@ -180,3 +185,5 @@ class KubernetesAccessInterface: request = Request(method, url, data=data, headers=headers) return session.send(request.prepare(), verify=False, timeout=2) + +kubernetes_access_instance = KubernetesAccessInterface() \ No newline at end of file diff --git a/config_app/js/components/cor-loader/cor-loader.html b/config_app/js/components/cor-loader/cor-loader.html new file mode 100644 index 000000000..f0aab7afc --- /dev/null +++ b/config_app/js/components/cor-loader/cor-loader.html @@ -0,0 +1,5 @@ +