Merge pull request #3121 from quay/project/upload-tar

Q.E. User can upload a tarball config to modify
This commit is contained in:
Sam Chow 2018-06-22 14:50:21 -04:00 committed by GitHub
commit f32bbf1fdc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 398 additions and 75 deletions

View file

@ -3,12 +3,13 @@ import logging
from flask import Flask from flask import Flask
from data import database from data import database, model
from util.config.superusermanager import SuperUserManager from util.config.superusermanager import SuperUserManager
from util.ipresolver import NoopIPResolver from util.ipresolver import NoopIPResolver
from config_app._init_config import ROOT_DIR from config_app._init_config import ROOT_DIR
from config_app.config_util.config import get_config_provider from config_app.config_util.config import get_config_provider
from util.security.instancekeys import InstanceKeys
app = Flask(__name__) app = Flask(__name__)
@ -35,3 +36,6 @@ else:
config_provider.update_app_config(app.config) config_provider.update_app_config(app.config)
superusers = SuperUserManager(app) superusers = SuperUserManager(app)
ip_resolver = NoopIPResolver() ip_resolver = NoopIPResolver()
instance_keys = InstanceKeys(app)
model.config.app_config = app.config

View file

@ -137,4 +137,5 @@ import config_endpoints.api.discovery
import config_endpoints.api.suconfig import config_endpoints.api.suconfig
import config_endpoints.api.superuser import config_endpoints.api.superuser
import config_endpoints.api.user import config_endpoints.api.user
import config_endpoints.api.tar_config_loader

View file

@ -7,7 +7,7 @@ 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.suconfig_models_pre_oci import pre_oci_model as model
from config_app.config_endpoints.api import resource, ApiResource, nickname, validate_json_request from config_app.config_endpoints.api import resource, ApiResource, nickname, validate_json_request
from config_app.c_app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY, ip_resolver from config_app.c_app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY, ip_resolver, instance_keys
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from data.users import get_federated_service_name, get_users_handler from data.users import get_federated_service_name, get_users_handler
@ -15,16 +15,13 @@ from data.database import configure
from data.runmigration import run_alembic_migration from data.runmigration import run_alembic_migration
from util.config.configutil import add_enterprise_config_defaults from util.config.configutil import add_enterprise_config_defaults
from util.config.database import sync_database_with_config 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__) logger = logging.getLogger(__name__)
def database_is_valid(): def database_is_valid():
""" Returns whether the database, as configured, is valid. """ """ Returns whether the database, as configured, is valid. """
if app.config['TESTING']:
return False
return model.is_valid() return model.is_valid()
@ -103,9 +100,6 @@ class SuperUserConfig(ApiResource):
# Link the existing user to the external user. # Link the existing user to the external user.
model.attach_federated_login(current_user.username, service_name, result.username) model.attach_federated_login(current_user.username, service_name, result.username)
# Ensure database is up-to-date with config
sync_database_with_config(config_object)
return { return {
'exists': True, 'exists': True,
'config': config_object 'config': config_object
@ -182,11 +176,12 @@ class SuperUserSetupDatabase(ApiResource):
configure(combined) configure(combined)
app.config['DB_URI'] = combined['DB_URI'] app.config['DB_URI'] = combined['DB_URI']
db_uri = app.config['DB_URI']
log_handler = _AlembicLogHandler() log_handler = _AlembicLogHandler()
try: try:
run_alembic_migration(log_handler) run_alembic_migration(db_uri, log_handler, setup_app=False)
except Exception as ex: except Exception as ex:
return { return {
'error': str(ex) 'error': str(ex)
@ -301,9 +296,42 @@ class SuperUserConfigValidate(ApiResource):
if not config_provider.config_exists(): if not config_provider.config_exists():
config = request.get_json()['config'] config = request.get_json()['config']
validator_context = ValidatorContext.from_app(app, config, request.get_json().get('password', ''), validator_context = ValidatorContext.from_app(app, config, request.get_json().get('password', ''),
ip_resolver=ip_resolver, instance_keys=instance_keys,
config_provider=config_provider) ip_resolver=ip_resolver,
config_provider=config_provider)
return validate_service_for_config(service, validator_context) return validate_service_for_config(service, validator_context)
abort(403) abort(403)
@resource('/v1/superuser/config/file/<filename>')
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
}

View file

@ -4,6 +4,8 @@ from config_app.config_endpoints.api.suconfig_models_interface import SuperuserC
class PreOCIModel(SuperuserConfigDataInterface): class PreOCIModel(SuperuserConfigDataInterface):
# Note: this method is different than has_users: the user select will throw if the user
# table does not exist, whereas has_users assumes the table is valid
def is_valid(self): def is_valid(self):
try: try:
list(User.select().limit(1)) list(User.select().limit(1))

View file

@ -1,6 +1,7 @@
import os
import logging import logging
import pathvalidate import pathvalidate
import os
from flask import request, jsonify from flask import request, jsonify
from config_app.config_endpoints.exception import InvalidRequest from config_app.config_endpoints.exception import InvalidRequest
@ -46,13 +47,14 @@ class SuperUserCustomCertificate(ApiResource):
logger.exception('Got IO error for cert %s', certpath) logger.exception('Got IO error for cert %s', certpath)
return '', 204 return '', 204
# TODO(QUAY-991): properly install the custom certs provided by user
# Call the update script to install the certificate immediately. # Call the update script to install the certificate immediately.
if not app.config['TESTING']: # if not app.config['TESTING']:
logger.debug('Calling certs_install.sh') # logger.debug('Calling certs_install.sh')
if os.system('/conf/init/certs_install.sh') != 0: # if os.system('/conf/init/certs_install.sh') != 0:
raise Exception('Could not install certificates') # raise Exception('Could not install certificates')
#
logger.debug('certs_install.sh completed') # logger.debug('certs_install.sh completed')
return '', 204 return '', 204

View file

@ -0,0 +1,28 @@
import tarfile
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
@resource('/v1/configapp/tarconfig')
class TarConfigLoader(ApiResource):
""" Resource for validating a block of configuration against an external service. """
@nickname('uploadTarballConfig')
def put(self):
""" Loads tarball config into the config provider """
input_stream = request.stream
tar_stream = tarfile.open(mode="r|gz", fileobj=input_stream)
config_provider.load_from_tar_stream(tar_stream)
# now try to connect to the db provided in their config
combined = dict(**app.config)
combined.update(config_provider.get_config())
configure(combined)
return make_response('OK')

View file

@ -1,12 +1,12 @@
from config_app.config_util.config.fileprovider import FileConfigProvider from config_app.config_util.config.fileprovider import FileConfigProvider
from config_app.config_util.config.testprovider import TestConfigProvider from config_app.config_util.config.testprovider import TestConfigProvider
from config_app.config_util.config.inmemoryprovider import InMemoryProvider
def get_config_provider(config_volume, yaml_filename, py_filename, testing=False): def get_config_provider(config_volume, yaml_filename, py_filename, testing=False):
""" Loads and returns the config provider for the current environment. """ """ Loads and returns the config provider for the current environment. """
if testing: if testing:
return TestConfigProvider() return TestConfigProvider()
return FileConfigProvider(config_volume, yaml_filename, py_filename) return InMemoryProvider()

View file

@ -0,0 +1,84 @@
import logging
import yaml
import io
import os
from config_app.config_util.config.baseprovider import BaseProvider
logger = logging.getLogger(__name__)
CONFIG_FILENAME = 'config.yaml'
class InMemoryProvider(BaseProvider):
def __init__(self):
self.files = {}
self.config = {}
self.was_loaded = False
@property
def provider_id(self):
return 'memory'
def update_app_config(self, app_config):
self.config = app_config
def get_config(self):
return self.config
def save_config(self, config_object):
self.config = config_object
self.was_loaded = True
def config_exists(self):
return self.was_loaded
def volume_exists(self):
return True
def volume_file_exists(self, filename):
return any([name.startswith(filename) for name in self.files])
def get_volume_file(self, filename, mode='r'):
return io.BytesIO(self.files[filename])
def write_volume_file(self, filename, contents):
raise Exception('Not implemented yet')
def remove_volume_file(self, filename):
raise Exception('Not implemented yet')
def list_volume_directory(self, path):
def strip_directory(string):
if '/' in string:
return string[string.rfind('/') + 1:]
return string
return [strip_directory(name) for name in self.files if name.startswith(path)]
def save_volume_file(self, filename, flask_file):
self.files[filename] = flask_file.read()
def requires_restart(self, app_config):
raise Exception('Not implemented yet')
def get_volume_path(self, directory, filename):
return os.path.join(directory, filename)
def load_from_tarball(self, tarfile):
for tarinfo in tarfile.getmembers():
if tarinfo.isfile():
self.files[tarinfo.name] = tarfile.extractfile(tarinfo.name).read()
if self.files.has_key(CONFIG_FILENAME):
self.config = yaml.load(self.files.get(CONFIG_FILENAME))
self.was_loaded = True
def load_from_tar_stream(self, tarfile):
for tarinfo in tarfile:
if tarinfo.isfile():
self.files[tarinfo.name] = tarfile.extractfile(tarinfo).read()
if self.files.has_key(CONFIG_FILENAME):
self.config = yaml.load(self.files.get(CONFIG_FILENAME))
self.was_loaded = True

View file

@ -23,4 +23,4 @@
</div> </div>
</div> </div>
<div ng-if="$ctrl.state === 'setup'" class="setup"></div> <div ng-if="$ctrl.state === 'setup'" class="setup"></div>
<load-config ng-if="$ctrl.state === 'load'"></load-config> <load-config ng-if="$ctrl.state === 'load'" config-loaded="$ctrl.configLoaded()"></load-config>

View file

@ -22,4 +22,8 @@ export class ConfigSetupAppComponent {
private chooseLoad(): void { private chooseLoad(): void {
this.state = 'load'; this.state = 'load';
} }
private configLoaded(): void {
this.state = 'setup';
}
} }

View file

@ -0,0 +1,3 @@
<li>
<a ng-click="optionClick()" ng-transclude></a>
</li>

View file

@ -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;
});

View file

@ -0,0 +1,6 @@
<span class="co-options-menu">
<div class="dropdown" style="text-align: left;">
<i class="fas fa-cog fa-lg dropdown-toggle" data-toggle="dropdown" data-title="Options" bs-tooltip></i>
<ul class="dropdown-menu pull-right" ng-transclude></ul>
</div>
</span>

View file

@ -0,0 +1,3 @@
<div class="col-lg-6 col-md-6 col-sm-5 col-xs-12">
<h2 class="co-nav-title-content co-fx-text-shadow" ng-transclude></h2>
</div>

View file

@ -0,0 +1,2 @@
<div class="co-nav-title" ng-transclude></div>

View file

@ -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;
});

View file

@ -17,6 +17,7 @@ angular.module('quay-config').directive('fileUploadBox', function () {
'filesValidated': '&filesValidated', 'filesValidated': '&filesValidated',
'extensions': '<extensions', 'extensions': '<extensions',
'apiEndpoint': '@apiEndpoint',
'reset': '=?reset' 'reset': '=?reset'
}, },
@ -32,9 +33,9 @@ angular.module('quay-config').directive('fileUploadBox', function () {
$scope.state = 'clear'; $scope.state = 'clear';
$scope.selectedFiles = []; $scope.selectedFiles = [];
var conductUpload = function(file, url, fileId, mimeType, progressCb, doneCb) { var conductUpload = function(file, apiEndpoint, fileId, mimeType, progressCb, doneCb) {
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open('PUT', url, true); request.open('PUT', '/api/v1/' + apiEndpoint, true);
request.setRequestHeader('Content-Type', mimeType); request.setRequestHeader('Content-Type', mimeType);
request.onprogress = function(e) { request.onprogress = function(e) {
$scope.$apply(function() { $scope.$apply(function() {
@ -103,12 +104,7 @@ angular.module('quay-config').directive('fileUploadBox', function () {
$scope.currentlyUploadingFile = currentFile; $scope.currentlyUploadingFile = currentFile;
$scope.uploadProgress = 0; $scope.uploadProgress = 0;
ApiService.getFiledropUrl(data).then(function(resp) { conductUpload(currentFile, $scope.apiEndpoint, $scope.selectedFiles[0].name, mimeType, progressCb, doneCb);
// Perform the upload.
conductUpload(currentFile, resp.url, resp.file_id, mimeType, progressCb, doneCb);
}, function() {
callback(false, 'Could not retrieve upload URL');
});
}; };
// Start the uploading. // Start the uploading.

View file

@ -0,0 +1,18 @@
/**
* Raises the 'filesChanged' event on the scope if a file on the marked <input type="file"> exists.
*/
angular.module('quay-config').directive("filesChanged", [function () {
return {
restrict: 'A',
scope: {
'filesChanged': "&"
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
scope.$apply(function() {
scope.filesChanged({'files': changeEvent.target.files});
});
});
}
}
}]);

View file

@ -1,11 +1,58 @@
import { Input, Component, Inject } from 'ng-metadata/core'; import {Component, EventEmitter, Inject, Output} from 'ng-metadata/core';
const templateUrl = require('./load-config.html'); const templateUrl = require('./load-config.html');
const styleUrl = require('./load-config.css');
declare var bootbox: any;
@Component({ @Component({
selector: 'load-config', selector: 'load-config',
templateUrl, templateUrl,
styleUrls: [ styleUrl ],
}) })
export class LoadConfigComponent { export class LoadConfigComponent {
constructor() { private readyToSubmit: boolean = false;
private uploadFunc: Function;
@Output() public configLoaded: EventEmitter<any> = new EventEmitter();
private constructor(@Inject('ApiService') private apiService: any) {
}
private handleTarballSelected(files: File[], callback: Function) {
this.readyToSubmit = true;
callback(true)
}
private handleTarballCleared() {
this.readyToSubmit = false;
}
private uploadTarball() {
this.uploadFunc(success => {
if (success) {
this.configLoaded.emit({});
} else {
bootbox.dialog({
"message": 'Could not upload configuration. Please check you have provided a valid tar file' +
'If this problem persists, please contact support',
"title": 'Error Loading Configuration',
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
}
});
}
/**
* When files are validated, this is called by the child to give us
* the callback function to upload
* @param files: files to upload
* @param uploadFiles: function to call to upload files
*/
private tarballValidatedByUploadBox(files, uploadFiles) {
this.uploadFunc = uploadFiles;
} }
} }

View file

@ -8,9 +8,20 @@
<h4 class="modal-title"><span>Load Config</span></h4> <h4 class="modal-title"><span>Load Config</span></h4>
</div> </div>
<!-- Body --> <!-- Body -->
<div> <div class="modal-body">
Please upload a tarball <span>Please upload the previous configuration</span>
<input type="file" accept=".tar"></div> <div class="file-upload-box"
api-endpoint="configapp/tarconfig"
select-message="Select a previous configuration to modify. Must be in tar.gz format"
files-selected="$ctrl.handleTarballSelected(files, callback)"
files-cleared="$ctrl.handleFilesCleared()"
files-validated="$ctrl.tarballValidatedByUploadBox(files, uploadFiles)"
extensions="['application/gzip', '.gz']"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="$ctrl.uploadTarball()" ng-disabled="!$ctrl.readyToSubmit">
Upload Configuration
</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->

View file

@ -20,10 +20,11 @@
<td>Upload certificates:</td> <td>Upload certificates:</td>
<td> <td>
<div class="file-upload-box" <div class="file-upload-box"
select-message="Select custom certificate to add to configuration. Must be in PEM format and end extension '.crt'" api-endpoint="superuser/customcerts"
files-selected="handleCertsSelected(files, callback)" select-message="Select custom certificate to add to configuration. Must be in PEM format and end extension '.crt'"
reset="resetUpload" files-selected="handleCertsSelected(files, callback)"
extensions="['.crt']"></div> reset="resetUpload"
extensions="['.crt']"></div>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -16,7 +16,7 @@
<!-- Basic Configuration --> <!-- Basic Configuration -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-gears"></i> Basic Configuration <i class="fas fa-cogs"></i> Basic Configuration
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<table class="config-table"> <table class="config-table">
@ -456,7 +456,7 @@
<!-- BitTorrent pull --> <!-- BitTorrent pull -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-cloud-download"></i> BitTorrent-based download <i class="fas fa-cloud-download-alt"></i> BitTorrent-based download
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
@ -941,7 +941,7 @@
<!-- GitHub Authentication --> <!-- GitHub Authentication -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-github"></i> GitHub (Enterprise) Authentication <i class="fab fa-github"></i> GitHub (Enterprise) Authentication
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
@ -1049,7 +1049,7 @@
<!-- Google Authentication --> <!-- Google Authentication -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-google"></i> Google Authentication <i class="fab fa-google"></i> Google Authentication
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
@ -1390,7 +1390,7 @@
<!-- GitHub Trigger --> <!-- GitHub Trigger -->
<div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;"> <div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-github"></i> GitHub (Enterprise) Build Triggers <i class="fab fa-github"></i> GitHub (Enterprise) Build Triggers
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">

View file

@ -17,7 +17,7 @@
<!-- Basic Configuration --> <!-- Basic Configuration -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-gears"></i> Basic Configuration <i class="fas fa-cogs"></i> Basic Configuration
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<table class="config-table"> <table class="config-table">
@ -457,7 +457,7 @@
<!-- BitTorrent pull --> <!-- BitTorrent pull -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-cloud-download"></i> BitTorrent-based download <i class="fas fa-cloud-download-alt"></i> BitTorrent-based download
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
@ -942,7 +942,7 @@
<!-- GitHub Authentication --> <!-- GitHub Authentication -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-github"></i> GitHub (Enterprise) Authentication <i class="fab fa-github"></i> GitHub (Enterprise) Authentication
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
@ -1050,7 +1050,7 @@
<!-- Google Authentication --> <!-- Google Authentication -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-google"></i> Google Authentication <i class="fab fa-google"></i> Google Authentication
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
@ -1391,7 +1391,7 @@
<!-- GitHub Trigger --> <!-- GitHub Trigger -->
<div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;"> <div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;">
<div class="co-panel-heading"> <div class="co-panel-heading">
<i class="fa fa-github"></i> GitHub (Enterprise) Build Triggers <i class="fab fa-github"></i> GitHub (Enterprise) Build Triggers
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">

View file

@ -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 urlFileField = require('../config-field-templates/config-file-field.html');
const urlBoolField = require('../config-field-templates/config-bool-field.html'); const urlBoolField = require('../config-field-templates/config-bool-field.html');
const urlNumericField = require('../config-field-templates/config-numeric-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 urlContactsField = require('../config-field-templates/config-contacts-field.html');
const urlMapField = require('../config-field-templates/config-map-field.html'); const urlMapField = require('../config-field-templates/config-map-field.html');
const urlServiceKeyField = require('../config-field-templates/config-service-key-field.html'); const urlServiceKeyField = require('../config-field-templates/config-service-key-field.html');
@ -1115,7 +1116,7 @@ angular.module("quay-config")
.directive('configContactField', function () { .directive('configContactField', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
templateUrl: urlContactsField, templateUrl: urlContactField,
priority: 1, priority: 1,
replace: false, replace: false,
transclude: true, transclude: true,
@ -1413,11 +1414,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(); loadCertificates();
// });
$scope.handleCertsSelected = function(files, callback) { $scope.handleCertsSelected = function(files, callback) {
$scope.certsUploading = true; $scope.certsUploading = true;

View file

@ -65,9 +65,6 @@ const templateUrl = require('./setup.html');
// Database is being setup. // Database is being setup.
'DB_SETUP': 'setup-db', 'DB_SETUP': 'setup-db',
// Database setup has succeeded.
'DB_SETUP_SUCCESS': 'setup-db-success',
// An error occurred when setting up the database. // An error occurred when setting up the database.
'DB_SETUP_ERROR': 'setup-db-error', 'DB_SETUP_ERROR': 'setup-db-error',
@ -262,7 +259,6 @@ const templateUrl = require('./setup.html');
$scope.createSuperUser = function() { $scope.createSuperUser = function() {
$scope.currentStep = $scope.States.CREATING_SUPERUSER; $scope.currentStep = $scope.States.CREATING_SUPERUSER;
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) { ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
UserService.load();
$scope.checkStatus(); $scope.checkStatus();
}, function(resp) { }, function(resp) {
$scope.currentStep = $scope.States.SUPERUSER_ERROR; $scope.currentStep = $scope.States.SUPERUSER_ERROR;
@ -277,7 +273,7 @@ const templateUrl = require('./setup.html');
$scope.currentStep = $scope.States.DB_SETUP_ERROR; $scope.currentStep = $scope.States.DB_SETUP_ERROR;
$scope.errors.DatabaseSetupError = resp['error']; $scope.errors.DatabaseSetupError = resp['error'];
} else { } else {
$scope.currentStep = $scope.States.DB_SETUP_SUCCESS; $scope.currentStep = $scope.States.CREATE_SUPERUSER;
} }
}, ApiService.errorDisplay('Could not setup database. Please report this to support.')) }, ApiService.errorDisplay('Could not setup database. Please report this to support.'))
}; };

View file

@ -1,4 +1,4 @@
<div> <div id="padding-container">
<div> <div>
<div class="cor-loader" ng-show="currentStep == States.LOADING"></div> <div class="cor-loader" ng-show="currentStep == States.LOADING"></div>
<div class="page-content" ng-show="currentStep == States.CONFIG"> <div class="page-content" ng-show="currentStep == States.CONFIG">
@ -12,11 +12,11 @@
<span class="cor-step-bar" progress="stepProgress"> <span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span> <span class="cor-step" title="Configure Database" text="1"></span>
<span class="cor-step" title="Setup Database" icon="database"></span> <span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span> <span class="cor-step" title="Container Restart" icon="sync"></span>
<span class="cor-step" title="Create Superuser" text="2"></span> <span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span> <span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span> <span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span> <span class="cor-step" title="Container Restart" icon="sync"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span> <span class="cor-step" title="Setup Complete" icon="check"></span>
</span> </span>
@ -24,8 +24,8 @@
<div>Configure your Redis database and other settings below</div> <div>Configure your Redis database and other settings below</div>
</div> </div>
<config-setup-tool is-active="isStep(currentStep, States.CONFIG)" <div class="config-setup-tool" is-active="isStep(currentStep, States.CONFIG)"
configuration-saved="configurationSaved(config)"></config-setup-tool> configuration-saved="configurationSaved(config)"></divconfig-setup-tool>
</div> </div>
</div> </div>
</div> </div>
@ -39,11 +39,11 @@
<span class="cor-step-bar" progress="stepProgress"> <span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span> <span class="cor-step" title="Configure Database" text="1"></span>
<span class="cor-step" title="Setup Database" icon="database"></span> <span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span> <span class="cor-step" title="Container Restart" icon="sync"></span>
<span class="cor-step" title="Create Superuser" text="2"></span> <span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span> <span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span> <span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span> <span class="cor-step" title="Container Restart" icon="sync"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span> <span class="cor-step" title="Setup Complete" icon="check"></span>
</span> </span>
<h4 class="modal-title">Setup</h4> <h4 class="modal-title">Setup</h4>
@ -95,7 +95,7 @@
<div class="modal-body" style="padding: 20px;" <div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)"> ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
<h4 style="margin-bottom: 20px;"> <h4 style="margin-bottom: 20px;">
<i class="fa fa-lg fa-refresh" style="margin-right: 10px;"></i> <i class="fa fa-lg fa-sync" style="margin-right: 10px;"></i>
<span class="registry-name"></span> is currently being restarted <span class="registry-name"></span> is currently being restarted
</h4> </h4>
This can take several minutes. If the container does not restart on its own, This can take several minutes. If the container does not restart on its own,

View file

@ -0,0 +1,8 @@
.cor-options-menu .fa-cog {
color: #999;
cursor: pointer;
}
.open .fa-cog {
color: #428BCA;
}

View file

@ -0,0 +1,4 @@
.cor-title {
display: flex;
justify-content: center;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -14,14 +14,23 @@ from peewee import SqliteDatabase
from data.database import all_models, db from data.database import all_models, db
from data.migrations.tester import NoopTester, PopulateTestDataTester from data.migrations.tester import NoopTester, PopulateTestDataTester
from app import app
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
from release import GIT_HEAD, REGION, SERVICE from release import GIT_HEAD, REGION, SERVICE
from util.morecollections import AttrDict from util.morecollections import AttrDict
config = context.config config = context.config
config.set_main_option('sqlalchemy.url', unquote(app.config['DB_URI'])) DB_URI = config.get_main_option('db_uri', 'sqlite:///test/data/test.db')
# This option exists because alembic needs the db proxy to be configured in order
# to perform migrations. The app import does the init of the proxy, but we don't
# want that in the case of the config app, as we are explicitly connecting to a
# db that the user has passed in, and we can't have import dependency on app
if config.get_main_option('alembic_setup_app', 'True') == 'True':
from app import app
DB_URI = app.config['DB_URI']
config.set_main_option('sqlalchemy.url', unquote(DB_URI))
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
# This line sets up loggers basically. # This line sets up loggers basically.
if config.config_file_name: if config.config_file_name:
@ -47,7 +56,7 @@ def get_tester():
connecting to a production database. connecting to a production database.
""" """
if os.environ.get('TEST_MIGRATE', '') == 'true': if os.environ.get('TEST_MIGRATE', '') == 'true':
url = unquote(app.config['DB_URI']) url = unquote(DB_URI)
if url.find('.quay.io') < 0: if url.find('.quay.io') < 0:
return PopulateTestDataTester() return PopulateTestDataTester()
@ -65,13 +74,12 @@ def run_migrations_offline():
script output. script output.
""" """
url = unquote(app.config['DB_URI']) url = unquote(DB_URI)
context.configure(url=url, target_metadata=target_metadata, transactional_ddl=True) context.configure(url=url, target_metadata=target_metadata, transactional_ddl=True)
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations(tables=tables, tester=get_tester()) context.run_migrations(tables=tables, tester=get_tester())
def run_migrations_online(): def run_migrations_online():
"""Run migrations in 'online' mode. """Run migrations in 'online' mode.

View file

@ -5,12 +5,19 @@ from alembic.script import ScriptDirectory
from alembic.environment import EnvironmentContext from alembic.environment import EnvironmentContext
from alembic.migration import __name__ as migration_name from alembic.migration import __name__ as migration_name
def run_alembic_migration(log_handler=None): def run_alembic_migration(db_uri, log_handler=None, setup_app=True):
if log_handler: if log_handler:
logging.getLogger(migration_name).addHandler(log_handler) logging.getLogger(migration_name).addHandler(log_handler)
config = Config() config = Config()
config.set_main_option("script_location", "data:migrations") config.set_main_option("script_location", "data:migrations")
config.set_main_option("db_uri", db_uri)
if setup_app:
config.set_main_option('alembic_setup_app', 'True')
else:
config.set_main_option('alembic_setup_app', '')
script = ScriptDirectory.from_config(config) script = ScriptDirectory.from_config(config)
def fn(rev, context): def fn(rev, context):

View file

@ -118,7 +118,7 @@ class SuperUserSetupDatabase(ApiResource):
log_handler = _AlembicLogHandler() log_handler = _AlembicLogHandler()
try: try:
run_alembic_migration(log_handler) run_alembic_migration(app.config['DB_URI'], log_handler)
except Exception as ex: except Exception as ex:
return { return {
'error': str(ex) 'error': str(ex)