From 561522c6d335a76293ad67d972e3d4ac09586ff6 Mon Sep 17 00:00:00 2001 From: Sam Chow Date: Wed, 20 Jun 2018 11:41:22 -0400 Subject: [PATCH] Port cor-title and add file check endpoint Fix some FA5 regressions Fix uploading cert files Add fix some icons --- config_app/config_endpoints/api/suconfig.py | 35 +++++++++++++++++- config_app/config_endpoints/api/superuser.py | 17 ++------- .../config_endpoints/api/tar_config_loader.py | 5 +-- config_app/config_util/config/__init__.py | 6 +-- .../config_util/config/inmemoryprovider.py | 8 +++- .../js/components/cor-option/cor-option.html | 3 ++ .../js/components/cor-option/cor-option.js | 32 ++++++++++++++++ .../cor-option/cor-options-menu.html | 6 +++ .../cor-title/cor-title-content.html | 3 ++ .../js/components/cor-title/cor-title.html | 2 + .../js/components/cor-title/cor-title.js | 31 ++++++++++++++++ config_app/js/components/file-upload-box.js | 9 +---- .../components/load-config/load-config.html | 1 + .../config-certificates-field.html | 9 +++-- .../config-setup-tool.html | 10 ++--- .../core-config-setup/config-setup-tool.html | 10 ++--- .../js/core-config-setup/core-config-setup.js | 7 +--- config_app/js/setup/setup.component.js | 1 - config_app/js/setup/setup.html | 8 ++-- config_app/static/css/cor-option.css | 8 ++++ config_app/static/css/cor-title.css | 4 ++ config_app/static/img/network-tile.png | Bin 0 -> 6289 bytes config_app/static/img/redis-small.png | Bin 0 -> 1839 bytes config_app/static/img/rocket.png | Bin 0 -> 4246 bytes 24 files changed, 159 insertions(+), 56 deletions(-) create mode 100644 config_app/js/components/cor-option/cor-option.html create mode 100644 config_app/js/components/cor-option/cor-option.js create mode 100644 config_app/js/components/cor-option/cor-options-menu.html create mode 100644 config_app/js/components/cor-title/cor-title-content.html create mode 100644 config_app/js/components/cor-title/cor-title.html create mode 100644 config_app/js/components/cor-title/cor-title.js create mode 100644 config_app/static/css/cor-option.css create mode 100644 config_app/static/css/cor-title.css create mode 100644 config_app/static/img/network-tile.png create mode 100644 config_app/static/img/redis-small.png create mode 100644 config_app/static/img/rocket.png diff --git a/config_app/config_endpoints/api/suconfig.py b/config_app/config_endpoints/api/suconfig.py index 37eb72bd3..21b01762e 100644 --- a/config_app/config_endpoints/api/suconfig.py +++ b/config_app/config_endpoints/api/suconfig.py @@ -15,7 +15,7 @@ from data.database import configure from data.runmigration import run_alembic_migration from util.config.configutil import add_enterprise_config_defaults from util.config.database import sync_database_with_config -from util.config.validator import validate_service_for_config, ValidatorContext +from util.config.validator import validate_service_for_config, ValidatorContext, is_valid_config_upload_filename logger = logging.getLogger(__name__) @@ -302,3 +302,36 @@ class SuperUserConfigValidate(ApiResource): abort(403) + + +@resource('/v1/superuser/config/file/') +class SuperUserConfigFile(ApiResource): + """ Resource for fetching the status of config files and overriding them. """ + @nickname('scConfigFileExists') + def get(self, filename): + """ Returns whether the configuration file with the given name exists. """ + if not is_valid_config_upload_filename(filename): + abort(404) + + return { + 'exists': config_provider.volume_file_exists(filename) + } + + + @nickname('scUpdateConfigFile') + def post(self, filename): + """ Updates the configuration file with the given name. """ + if not is_valid_config_upload_filename(filename): + abort(404) + + # Note: This method can be called before the configuration exists + # to upload the database SSL cert. + uploaded_file = request.files['file'] + if not uploaded_file: + abort(400) + + config_provider.save_volume_file(filename, uploaded_file) + return { + 'status': True + } + diff --git a/config_app/config_endpoints/api/superuser.py b/config_app/config_endpoints/api/superuser.py index c672edb80..37c207383 100644 --- a/config_app/config_endpoints/api/superuser.py +++ b/config_app/config_endpoints/api/superuser.py @@ -1,4 +1,3 @@ -import os import logging import pathvalidate from flask import request, jsonify @@ -6,7 +5,7 @@ from flask import request, jsonify from config_app.config_endpoints.exception import InvalidRequest from config_app.config_endpoints.api import resource, ApiResource, nickname from config_app.config_util.ssl import load_certificate, CertInvalidException -from config_app.c_app import app, config_provider +from config_app.c_app import config_provider from config_app.config_endpoints.api.superuser_models_pre_oci import pre_oci_model @@ -37,8 +36,8 @@ class SuperUserCustomCertificate(ApiResource): # Validate the certificate. try: logger.debug('Loading custom certificate %s', certpath) - with config_provider.get_volume_file(cert_full_path) as f: - load_certificate(f.read()) + cert = config_provider.get_volume_file(cert_full_path) + load_certificate(cert) except CertInvalidException: logger.exception('Got certificate invalid error for cert %s', certpath) return '', 204 @@ -46,14 +45,6 @@ class SuperUserCustomCertificate(ApiResource): logger.exception('Got IO error for cert %s', certpath) return '', 204 - # Call the update script to install the certificate immediately. - if not app.config['TESTING']: - logger.debug('Calling certs_install.sh') - if os.system('/conf/init/certs_install.sh') != 0: - raise Exception('Could not install certificates') - - logger.debug('certs_install.sh completed') - return '', 204 @nickname('deleteCustomCertificate') @@ -79,7 +70,7 @@ class SuperUserCustomCertificates(ApiResource): cert_views = [] for extra_cert_path in extra_certs_found: try: - cert = config_provider.get_volume_path(EXTRA_CA_DIRECTORY, extra_cert_path) + cert = config_provider.get_volume_file(extra_cert_path) certificate = load_certificate(cert) cert_views.append({ 'path': extra_cert_path, diff --git a/config_app/config_endpoints/api/tar_config_loader.py b/config_app/config_endpoints/api/tar_config_loader.py index 32e907316..a63d6b5f5 100644 --- a/config_app/config_endpoints/api/tar_config_loader.py +++ b/config_app/config_endpoints/api/tar_config_loader.py @@ -1,4 +1,3 @@ -import logging import tarfile import cStringIO @@ -7,9 +6,7 @@ from flask import request, make_response from data.database import configure from config_app.c_app import app, config_provider -from config_app.config_endpoints.api import resource, ApiResource, nickname, validate_json_request - -logger = logging.getLogger(__name__) +from config_app.config_endpoints.api import resource, ApiResource, nickname @resource('/v1/configapp/tarconfig') class TarConfigLoader(ApiResource): diff --git a/config_app/config_util/config/__init__.py b/config_app/config_util/config/__init__.py index 1c2e77bfb..c344bb415 100644 --- a/config_app/config_util/config/__init__.py +++ b/config_app/config_util/config/__init__.py @@ -9,8 +9,4 @@ def get_config_provider(config_volume, yaml_filename, py_filename, testing=False if testing: return TestConfigProvider() - else: - return InMemoryProvider() - - - + return InMemoryProvider() diff --git a/config_app/config_util/config/inmemoryprovider.py b/config_app/config_util/config/inmemoryprovider.py index e900f3381..0f018f385 100644 --- a/config_app/config_util/config/inmemoryprovider.py +++ b/config_app/config_util/config/inmemoryprovider.py @@ -1,5 +1,6 @@ import logging import yaml +import io from config_app.config_util.config.baseprovider import BaseProvider @@ -50,14 +51,17 @@ class InMemoryProvider(BaseProvider): return [ name for name in self.files if name.startswith(path) ] def save_volume_file(self, filename, flask_file): - raise Exception('Not implemented yet') + self.files[filename] = flask_file.read() def requires_restart(self, app_config): raise Exception('Not implemented yet') def get_volume_path(self, directory, filename): # Here we can just access the filename since we're storing the tarball files with their full path - return self.files[filename] + if directory.endswith('/'): + return directory + filename + else: + return directory + '/' + filename def load_from_tarball(self, tarfile): for tarinfo in tarfile.getmembers(): diff --git a/config_app/js/components/cor-option/cor-option.html b/config_app/js/components/cor-option/cor-option.html new file mode 100644 index 000000000..8482a9050 --- /dev/null +++ b/config_app/js/components/cor-option/cor-option.html @@ -0,0 +1,3 @@ +
  • + +
  • diff --git a/config_app/js/components/cor-option/cor-option.js b/config_app/js/components/cor-option/cor-option.js new file mode 100644 index 000000000..880d83df3 --- /dev/null +++ b/config_app/js/components/cor-option/cor-option.js @@ -0,0 +1,32 @@ +const corOption = require('./cor-option.html'); +const corOptionsMenu = require('./cor-options-menu.html'); + +angular.module('quay-config') + .directive('corOptionsMenu', function() { + var directiveDefinitionObject = { + priority: 1, + templateUrl: corOptionsMenu, + replace: true, + transclude: true, + restrict: 'C', + scope: {}, + controller: function($rootScope, $scope, $element) { + } + }; + return directiveDefinitionObject; + }) + .directive('corOption', function() { + var directiveDefinitionObject = { + priority: 1, + templateUrl: corOption, + replace: true, + transclude: true, + restrict: 'C', + scope: { + 'optionClick': '&optionClick' + }, + controller: function($rootScope, $scope, $element) { + } + }; + return directiveDefinitionObject; + }); diff --git a/config_app/js/components/cor-option/cor-options-menu.html b/config_app/js/components/cor-option/cor-options-menu.html new file mode 100644 index 000000000..a234590a3 --- /dev/null +++ b/config_app/js/components/cor-option/cor-options-menu.html @@ -0,0 +1,6 @@ + + + diff --git a/config_app/js/components/cor-title/cor-title-content.html b/config_app/js/components/cor-title/cor-title-content.html new file mode 100644 index 000000000..0d3e13ddd --- /dev/null +++ b/config_app/js/components/cor-title/cor-title-content.html @@ -0,0 +1,3 @@ +
    +

    +
    diff --git a/config_app/js/components/cor-title/cor-title.html b/config_app/js/components/cor-title/cor-title.html new file mode 100644 index 000000000..63cfd322c --- /dev/null +++ b/config_app/js/components/cor-title/cor-title.html @@ -0,0 +1,2 @@ +
    + diff --git a/config_app/js/components/cor-title/cor-title.js b/config_app/js/components/cor-title/cor-title.js new file mode 100644 index 000000000..033112f23 --- /dev/null +++ b/config_app/js/components/cor-title/cor-title.js @@ -0,0 +1,31 @@ + +const titleUrl = require('./cor-title.html'); +const titleContentUrl = require('./cor-title-content.html'); + +angular.module('quay-config') + .directive('corTitleContent', function() { + var directiveDefinitionObject = { + priority: 1, + templateUrl: titleContentUrl, + replace: true, + transclude: true, + restrict: 'C', + scope: {}, + controller: function($rootScope, $scope, $element) { + } + }; + return directiveDefinitionObject; + }) + .directive('corTitle', function() { + var directiveDefinitionObject = { + priority: 1, + templateUrl: titleUrl, + replace: true, + transclude: true, + restrict: 'C', + scope: {}, + controller: function($rootScope, $scope, $element) { + } + }; + return directiveDefinitionObject; + }); diff --git a/config_app/js/components/file-upload-box.js b/config_app/js/components/file-upload-box.js index 6fee20a14..280517eeb 100644 --- a/config_app/js/components/file-upload-box.js +++ b/config_app/js/components/file-upload-box.js @@ -17,6 +17,7 @@ angular.module('quay-config').directive('fileUploadBox', function () { 'filesValidated': '&filesValidated', 'extensions': ' Please upload the previous configuration
    + api-endpoint="/api/v1/superuser/customcerts" + select-message="Select custom certificate to add to configuration. Must be in PEM format and end extension '.crt'" + files-selected="handleCertsSelected(files, callback)" + reset="resetUpload" + extensions="['.crt']">
    diff --git a/config_app/js/config-field-templates/config-setup-tool.html b/config_app/js/config-field-templates/config-setup-tool.html index 629e3b45f..ad86d3107 100644 --- a/config_app/js/config-field-templates/config-setup-tool.html +++ b/config_app/js/config-field-templates/config-setup-tool.html @@ -16,7 +16,7 @@
    - Basic Configuration + Basic Configuration
    @@ -456,7 +456,7 @@
    - BitTorrent-based download + BitTorrent-based download
    @@ -941,7 +941,7 @@
    - GitHub (Enterprise) Authentication + GitHub (Enterprise) Authentication
    @@ -1049,7 +1049,7 @@
    - Google Authentication + Google Authentication
    @@ -1390,7 +1390,7 @@
    - GitHub (Enterprise) Build Triggers + GitHub (Enterprise) Build Triggers
    diff --git a/config_app/js/core-config-setup/config-setup-tool.html b/config_app/js/core-config-setup/config-setup-tool.html index ec3faa1c7..9bec737fa 100644 --- a/config_app/js/core-config-setup/config-setup-tool.html +++ b/config_app/js/core-config-setup/config-setup-tool.html @@ -17,7 +17,7 @@
    - Basic Configuration + Basic Configuration
    @@ -457,7 +457,7 @@
    - BitTorrent-based download + BitTorrent-based download
    @@ -942,7 +942,7 @@
    - GitHub (Enterprise) Authentication + GitHub (Enterprise) Authentication
    @@ -1050,7 +1050,7 @@
    - Google Authentication + Google Authentication
    @@ -1391,7 +1391,7 @@
    - GitHub (Enterprise) Build Triggers + GitHub (Enterprise) Build Triggers
    diff --git a/config_app/js/core-config-setup/core-config-setup.js b/config_app/js/core-config-setup/core-config-setup.js index 4e7afdac2..07f048f58 100644 --- a/config_app/js/core-config-setup/core-config-setup.js +++ b/config_app/js/core-config-setup/core-config-setup.js @@ -7,6 +7,7 @@ const urlListField = require('../config-field-templates/config-list-field.html') const urlFileField = require('../config-field-templates/config-file-field.html'); const urlBoolField = require('../config-field-templates/config-bool-field.html'); const urlNumericField = require('../config-field-templates/config-numeric-field.html'); +const urlContactField = require('../config-field-templates/config-contact-field.html'); const urlContactsField = require('../config-field-templates/config-contacts-field.html'); const urlMapField = require('../config-field-templates/config-map-field.html'); const urlServiceKeyField = require('../config-field-templates/config-service-key-field.html'); @@ -1116,7 +1117,7 @@ angular.module("quay-config") .directive('configContactField', function () { var directiveDefinitionObject = { priority: 0, - templateUrl: urlContactsField, + templateUrl: urlContactField, priority: 1, replace: false, transclude: true, @@ -1414,11 +1415,7 @@ angular.module("quay-config") }); }; - // UserService.updateUserIn($scope, function(user) { - // console.log(user) - // no need to check for user, since it's all local loadCertificates(); - // }); $scope.handleCertsSelected = function(files, callback) { $scope.certsUploading = true; diff --git a/config_app/js/setup/setup.component.js b/config_app/js/setup/setup.component.js index 5e668ccaa..1ce709099 100644 --- a/config_app/js/setup/setup.component.js +++ b/config_app/js/setup/setup.component.js @@ -260,7 +260,6 @@ const templateUrl = require('./setup.html'); $scope.createSuperUser = function() { $scope.currentStep = $scope.States.CREATING_SUPERUSER; ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) { - // UserService.load(); $scope.checkStatus(); }, function(resp) { $scope.currentStep = $scope.States.SUPERUSER_ERROR; diff --git a/config_app/js/setup/setup.html b/config_app/js/setup/setup.html index 753c8cceb..93cc87a87 100644 --- a/config_app/js/setup/setup.html +++ b/config_app/js/setup/setup.html @@ -1,4 +1,4 @@ -
    +
    @@ -12,11 +12,11 @@ - + - + @@ -43,7 +43,7 @@ - + diff --git a/config_app/static/css/cor-option.css b/config_app/static/css/cor-option.css new file mode 100644 index 000000000..97ae7887d --- /dev/null +++ b/config_app/static/css/cor-option.css @@ -0,0 +1,8 @@ +.cor-options-menu .fa-cog { + color: #999; + cursor: pointer; +} + +.open .fa-cog { + color: #428BCA; +} diff --git a/config_app/static/css/cor-title.css b/config_app/static/css/cor-title.css new file mode 100644 index 000000000..ef199785a --- /dev/null +++ b/config_app/static/css/cor-title.css @@ -0,0 +1,4 @@ +.cor-title { + display: flex; + justify-content: center; +} diff --git a/config_app/static/img/network-tile.png b/config_app/static/img/network-tile.png new file mode 100644 index 0000000000000000000000000000000000000000..c27deaff2e853c710451b2c9e83dae95df7a3570 GIT binary patch literal 6289 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 PZND&yNG7b{ho$`sLaNCz literal 0 HcmV?d00001 diff --git a/config_app/static/img/rocket.png b/config_app/static/img/rocket.png new file mode 100644 index 0000000000000000000000000000000000000000..b9ffddf399300386b72f548722b66026dabc2691 GIT binary patch literal 4246 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 literal 0 HcmV?d00001