Merge branch 'master' into delete-setup-page

This commit is contained in:
Sam Chow 2018-08-21 15:32:38 -04:00 committed by Sam Chow
commit cd6b0a6f46
29 changed files with 550 additions and 142 deletions

View file

@ -10,6 +10,9 @@ proxy_redirect off;
proxy_set_header Transfer-Encoding $http_transfer_encoding;
# The DB migrations sometimes take a while, so increase timeoutso we don't report an error
proxy_read_timeout 300s;
location / {
proxy_pass http://web_app_server;
}

View file

@ -150,10 +150,11 @@ def kubernetes_only(f):
nickname = partial(add_method_metadata, 'nickname')
import config_app.config_endpoints.api
import config_app.config_endpoints.api.discovery
import config_app.config_endpoints.api.kubeconfig
import config_app.config_endpoints.api.suconfig
import config_app.config_endpoints.api.superuser
import config_app.config_endpoints.api.user
import config_app.config_endpoints.api.tar_config_loader
import config_app.config_endpoints.api.user

View file

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

View file

@ -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
@ -270,47 +268,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/<filename>')
class SuperUserConfigFile(ApiResource):
""" Resource for fetching the status of config files and overriding them. """

View file

@ -55,8 +55,8 @@ class SuperUserCustomCertificate(ApiResource):
# Call the update script with config dir location to install the certificate immediately.
if not app.config['TESTING']:
if subprocess.call([os.path.join(INIT_SCRIPTS_LOCATION, 'certs_install.sh')],
env={ 'QUAYCONFIG': config_provider.get_config_dir_path() }) != 0:
cert_dir = os.path.join(config_provider.get_config_dir_path(), EXTRA_CA_DIRECTORY)
if subprocess.call([os.path.join(INIT_SCRIPTS_LOCATION, 'certs_install.sh')], env={ 'CERTDIR': cert_dir }) != 0:
raise Exception('Could not install certificates')
return '', 204

View file

@ -1,8 +1,12 @@
import os
import base64
from backports.tempfile import TemporaryDirectory
from config_app.config_util.config.fileprovider import FileConfigProvider
from config_app.config_util.k8saccessor import KubernetesAccessorSingleton
from util.config.validator import EXTRA_CA_DIRECTORY, EXTRA_CA_DIRECTORY_PREFIX
class TransientDirectoryProvider(FileConfigProvider):
@ -38,10 +42,25 @@ class TransientDirectoryProvider(FileConfigProvider):
return self.config_volume
def save_configuration_to_kubernetes(self):
config_path = self.get_config_dir_path()
data = {}
for name in os.listdir(config_path):
# Kubernetes secrets don't have sub-directories, so for the extra_ca_certs dir
# we have to put the extra certs in with a prefix, and then one of our init scripts
# (02_get_kube_certs.sh) will expand the prefixed certs into the equivalent directory
# so that they'll be installed correctly on startup by the certs_install script
certs_dir = os.path.join(self.config_volume, EXTRA_CA_DIRECTORY)
if os.path.exists(certs_dir):
for extra_cert in os.listdir(certs_dir):
with open(os.path.join(certs_dir, extra_cert)) as f:
data[EXTRA_CA_DIRECTORY_PREFIX + extra_cert] = base64.b64encode(f.read())
for name in os.listdir(self.config_volume):
file_path = os.path.join(self.config_volume, name)
KubernetesAccessorSingleton.get_instance().save_file_as_secret(name, file_path)
if not os.path.isdir(file_path):
with open(file_path) as f:
data[name] = base64.b64encode(f.read())
KubernetesAccessorSingleton.get_instance().replace_qe_secret(data)
return 200

View file

@ -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,10 +37,64 @@ class KubernetesAccessorSingleton(object):
return cls._instance
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 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)
def replace_qe_secret(self, new_secret_data):
"""
Removes the old config and replaces it with the new_secret_data as one action
"""
# 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' % (self.kube_config.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' % self.kube_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' % (self.kube_config.qe_namespace, self.kube_config.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": self.kube_config.qe_config_secret
},
"data": {}
}))
# Update the secret to reflect the file change.
secret['data'] = new_secret_data
self._assert_success(self._execute_k8s_api('PUT', secret_url, secret))
def get_qe_deployments(self):
""""

View file

@ -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 {

View file

@ -24,7 +24,7 @@ export class KubeDeployModalComponent {
this.state = 'loadingDeployments';
ApiService.scGetNumDeployments().then(resp => {
this.deploymentsStatus = resp.items.map(dep => ({ name: dep.metadata.name, numPods: dep.status.replicas }));
this.deploymentsStatus = resp.items.map(dep => ({ name: dep.metadata.name, numPods: dep.spec.replicas }));
this.state = 'readyToDeploy';
}).catch(err => {
this.state = 'error';
@ -37,7 +37,7 @@ export class KubeDeployModalComponent {
deployConfiguration(): void {
this.ApiService.scDeployConfiguration().then(() => {
const deploymentNames: string[]= this.deploymentsStatus.map(dep => dep.name);
const deploymentNames: string[] = this.deploymentsStatus.map(dep => dep.name);
this.ApiService.scCycleQEDeployments({ deploymentNames }).then(() => {
this.state = 'deployed'
@ -46,7 +46,6 @@ export class KubeDeployModalComponent {
this.errorMessage = `Could cycle the deployments with the new configuration. Error: ${err.toString()}`;
})
}).catch(err => {
console.log(err)
this.state = 'error';
this.errorMessage = `Could not deploy the configuration. Error: ${err.toString()}`;
})

View file

@ -209,19 +209,19 @@
</div>
<!-- Footer: SUPERUSER_ERROR -->
<div class="modal-footer alert alert-warning"
<div class="modal-footer alert alert-danger"
ng-show="isStep(currentStep, States.SUPERUSER_ERROR)">
{{ errors.SuperuserCreationError }}
</div>
<!-- Footer: DB_SETUP_ERROR -->
<div class="modal-footer alert alert-warning"
<div class="modal-footer alert alert-danger"
ng-show="isStep(currentStep, States.DB_SETUP_ERROR)">
Database Setup Failed. Please report this to support: {{ errors.DatabaseSetupError }}
Database Setup Failed: {{ errors.DatabaseSetupError }}
</div>
<!-- Footer: DB_ERROR -->
<div class="modal-footer alert alert-warning" ng-show="isStep(currentStep, States.DB_ERROR)">
<div class="modal-footer alert alert-danger" ng-show="isStep(currentStep, States.DB_ERROR)">
Database Validation Issue: {{ errors.DatabaseValidationError }}
</div>

View file

@ -28,6 +28,21 @@
text-decoration: none;
}
.quay-config-app .alert-danger {
padding: 25px;
display: flex;
}
.quay-config-app .alert-danger:before {
content: "\f071";
font-family: Font Awesome\ 5 Free;
font-weight: 900;
font-size: 30px;
padding-right: 15px;
color: #c53c3f;
text-align: center;
}
/* Overrides for fixing old quay styles*/