From df25ec00612eceaeed57521926835f4050eb35e0 Mon Sep 17 00:00:00 2001 From: Sam Chow Date: Mon, 20 Aug 2018 13:47:10 -0400 Subject: [PATCH] Add ability to populate config from kube cluster --- config_app/config_endpoints/api/__init__.py | 3 +- config_app/config_endpoints/api/kubeconfig.py | 69 +++++++++++++++++++ config_app/config_endpoints/api/suconfig.py | 45 +----------- config_app/config_util/k8saccessor.py | 26 +++++++ .../config-setup-app.component.ts | 11 ++- config_app/js/setup/setup.html | 8 +-- .../static/css/config-setup-app-component.css | 15 ++++ 7 files changed, 126 insertions(+), 51 deletions(-) create mode 100644 config_app/config_endpoints/api/kubeconfig.py diff --git a/config_app/config_endpoints/api/__init__.py b/config_app/config_endpoints/api/__init__.py index 21d13cc19..d9425670e 100644 --- a/config_app/config_endpoints/api/__init__.py +++ b/config_app/config_endpoints/api/__init__.py @@ -152,8 +152,9 @@ nickname = partial(add_method_metadata, 'nickname') import config_endpoints.api import config_endpoints.api.discovery +import config_endpoints.api.kubeconfig import config_endpoints.api.suconfig import config_endpoints.api.superuser -import config_endpoints.api.user import config_endpoints.api.tar_config_loader +import config_endpoints.api.user diff --git a/config_app/config_endpoints/api/kubeconfig.py b/config_app/config_endpoints/api/kubeconfig.py new file mode 100644 index 000000000..a2f9219c8 --- /dev/null +++ b/config_app/config_endpoints/api/kubeconfig.py @@ -0,0 +1,69 @@ +from flask import request + +from data.database import configure + +from config_app.c_app import app, config_provider +from config_app.config_endpoints.api import resource, ApiResource, nickname, kubernetes_only +from config_app.config_util.k8saccessor import KubernetesAccessorSingleton + + +@resource('/v1/kubernetes/deployments/') +class SuperUserKubernetesDeployment(ApiResource): + """ Resource for the getting the status of Quay Enterprise deployments and cycling them """ + schemas = { + 'ValidateDeploymentNames': { + 'type': 'object', + 'description': 'Validates deployment names for cycling', + 'required': [ + 'deploymentNames' + ], + 'properties': { + 'deploymentNames': { + 'type': 'array', + 'description': 'The names of the deployments to cycle' + }, + }, + } + } + + @kubernetes_only + @nickname('scGetNumDeployments') + def get(self): + return KubernetesAccessorSingleton.get_instance().get_qe_deployments() + + @kubernetes_only + @nickname('scCycleQEDeployments') + def put(self): + deployment_names = request.get_json()['deploymentNames'] + return KubernetesAccessorSingleton.get_instance().cycle_qe_deployments(deployment_names) + + +@resource('/v1/superuser/config/kubernetes') +class SuperUserKubernetesConfiguration(ApiResource): + """ Resource for saving the config files to kubernetes secrets. """ + + @kubernetes_only + @nickname('scDeployConfiguration') + def post(self): + return config_provider.save_configuration_to_kubernetes() + + +@resource('/v1/kubernetes/config/populate') +class KubernetesConfigurationPopulator(ApiResource): + """ Resource for populating the local configuration from the cluster's kubernetes secrets. """ + + @kubernetes_only + @nickname('scKubePopulateConfig') + def post(self): + # Get a clean transient directory to write the config into + config_provider.new_config_dir() + KubernetesAccessorSingleton.get_instance().save_secret_to_directory(config_provider.get_config_dir_path()) + + # We update the db configuration to connect to their specified one + # (Note, even if this DB isn't valid, it won't affect much in the config app, since we'll report an error, + # and all of the options create a new clean dir, so we'll never pollute configs) + combined = dict(**app.config) + combined.update(config_provider.get_config()) + configure(combined) + + return 200 diff --git a/config_app/config_endpoints/api/suconfig.py b/config_app/config_endpoints/api/suconfig.py index ce6d34539..01532c47d 100644 --- a/config_app/config_endpoints/api/suconfig.py +++ b/config_app/config_endpoints/api/suconfig.py @@ -3,11 +3,9 @@ import logging from flask import abort, request from config_app.config_endpoints.api.suconfig_models_pre_oci import pre_oci_model as model -from config_app.config_endpoints.api import resource, ApiResource, nickname, validate_json_request, \ - kubernetes_only +from config_app.config_endpoints.api import resource, ApiResource, nickname, validate_json_request from config_app.c_app import (app, config_provider, superusers, ip_resolver, instance_keys, INIT_SCRIPTS_LOCATION) -from config_app.config_util.k8saccessor import KubernetesAccessorSingleton from data.database import configure from data.runmigration import run_alembic_migration @@ -265,47 +263,6 @@ class SuperUserConfigValidate(ApiResource): return validate_service_for_config(service, validator_context) -@resource('/v1/kubernetes/deployments/') -class SuperUserKubernetesDeployment(ApiResource): - """ Resource for the getting the status of Quay Enterprise deployments and cycling them """ - schemas = { - 'ValidateDeploymentNames': { - 'type': 'object', - 'description': 'Validates deployment names for cycling', - 'required': [ - 'deploymentNames' - ], - 'properties': { - 'deploymentNames': { - 'type': 'array', - 'description': 'The names of the deployments to cycle' - }, - }, - } - } - - @kubernetes_only - @nickname('scGetNumDeployments') - def get(self): - return KubernetesAccessorSingleton.get_instance().get_qe_deployments() - - @kubernetes_only - @nickname('scCycleQEDeployments') - def put(self): - deployment_names = request.get_json()['deploymentNames'] - return KubernetesAccessorSingleton.get_instance().cycle_qe_deployments(deployment_names) - - -@resource('/v1/superuser/config/kubernetes') -class SuperUserKubernetesConfiguration(ApiResource): - """ Resource for saving the config files to kubernetes secrets. """ - - @kubernetes_only - @nickname('scDeployConfiguration') - def post(self): - return config_provider.save_configuration_to_kubernetes() - - @resource('/v1/superuser/config/file/') class SuperUserConfigFile(ApiResource): """ Resource for fetching the status of config files and overriding them. """ diff --git a/config_app/config_util/k8saccessor.py b/config_app/config_util/k8saccessor.py index d498ede4d..dd7f596e2 100644 --- a/config_app/config_util/k8saccessor.py +++ b/config_app/config_util/k8saccessor.py @@ -2,8 +2,10 @@ import logging import json import base64 import datetime +import os from requests import Request, Session +from util.config.validator import EXTRA_CA_DIRECTORY, EXTRA_CA_DIRECTORY_PREFIX from config_app.config_util.k8sconfig import KubernetesConfig @@ -35,6 +37,30 @@ class KubernetesAccessorSingleton(object): return cls._instance + def save_secret_to_directory(self, dir_path): + """ + Saves all files in the kubernetes secret to a local directory. + Assumes the directory is empty. + """ + secret = self._lookup_secret() + + secret_data = secret.get('data', {}) + + # Make the `extra_ca_certs` dir to ensure we can populate extra certs + extra_ca_dir_path = os.path.join(dir_path, EXTRA_CA_DIRECTORY) + os.mkdir(extra_ca_dir_path) + + for secret_filename, data in secret_data.iteritems(): + write_path = os.path.join(dir_path, secret_filename) + + if EXTRA_CA_DIRECTORY_PREFIX in secret_filename: + write_path = os.path.join(extra_ca_dir_path, secret_filename.replace(EXTRA_CA_DIRECTORY_PREFIX, '')) + + with open(write_path, 'w') as f: + f.write(base64.b64decode(data)) + + return 200 + def save_file_as_secret(self, name, file_pointer): value = file_pointer.read() self._update_secret_file(name, value) diff --git a/config_app/js/components/config-setup-app/config-setup-app.component.ts b/config_app/js/components/config-setup-app/config-setup-app.component.ts index 16a796ed4..3b05c304c 100644 --- a/config_app/js/components/config-setup-app/config-setup-app.component.ts +++ b/config_app/js/components/config-setup-app/config-setup-app.component.ts @@ -15,7 +15,6 @@ export class ConfigSetupAppComponent { : 'choice' | 'setup' | 'load' - | 'populate' | 'download' | 'deploy'; @@ -45,7 +44,15 @@ export class ConfigSetupAppComponent { } private choosePopulate(): void { - this.state = 'populate'; + this.apiService.scKubePopulateConfig() + .then(() => { + this.state = 'setup'; + }) + .catch(err => { + this.apiService.errorDisplay( + `Could not populate the configuration from your cluster. Please report this error: ${JSON.stringify(err)}` + )() + }) } private configLoaded(): void { diff --git a/config_app/js/setup/setup.html b/config_app/js/setup/setup.html index 04f959770..5548e52f5 100644 --- a/config_app/js/setup/setup.html +++ b/config_app/js/setup/setup.html @@ -209,19 +209,19 @@ - - -