Merge branch 'master' into delete-setup-page
This commit is contained in:
commit
cd6b0a6f46
29 changed files with 550 additions and 142 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
69
config_app/config_endpoints/api/kubeconfig.py
Normal file
69
config_app/config_endpoints/api/kubeconfig.py
Normal 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
|
|
@ -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. """
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
""""
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()}`;
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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*/
|
||||
|
||||
|
||||
|
|
Reference in a new issue