Merge pull request #3121 from quay/project/upload-tar
Q.E. User can upload a tarball config to modify
This commit is contained in:
commit
f32bbf1fdc
35 changed files with 398 additions and 75 deletions
|
@ -3,12 +3,13 @@ import logging
|
|||
|
||||
from flask import Flask
|
||||
|
||||
from data import database
|
||||
from data import database, model
|
||||
from util.config.superusermanager import SuperUserManager
|
||||
from util.ipresolver import NoopIPResolver
|
||||
|
||||
from config_app._init_config import ROOT_DIR
|
||||
from config_app.config_util.config import get_config_provider
|
||||
from util.security.instancekeys import InstanceKeys
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
@ -35,3 +36,6 @@ else:
|
|||
config_provider.update_app_config(app.config)
|
||||
superusers = SuperUserManager(app)
|
||||
ip_resolver = NoopIPResolver()
|
||||
instance_keys = InstanceKeys(app)
|
||||
|
||||
model.config.app_config = app.config
|
||||
|
|
|
@ -137,4 +137,5 @@ import config_endpoints.api.discovery
|
|||
import config_endpoints.api.suconfig
|
||||
import config_endpoints.api.superuser
|
||||
import config_endpoints.api.user
|
||||
import config_endpoints.api.tar_config_loader
|
||||
|
||||
|
|
|
@ -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 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 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 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__)
|
||||
|
||||
|
||||
def database_is_valid():
|
||||
""" Returns whether the database, as configured, is valid. """
|
||||
if app.config['TESTING']:
|
||||
return False
|
||||
|
||||
return model.is_valid()
|
||||
|
||||
|
||||
|
@ -103,9 +100,6 @@ class SuperUserConfig(ApiResource):
|
|||
# Link the existing user to the external user.
|
||||
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 {
|
||||
'exists': True,
|
||||
'config': config_object
|
||||
|
@ -182,11 +176,12 @@ class SuperUserSetupDatabase(ApiResource):
|
|||
|
||||
configure(combined)
|
||||
app.config['DB_URI'] = combined['DB_URI']
|
||||
db_uri = app.config['DB_URI']
|
||||
|
||||
log_handler = _AlembicLogHandler()
|
||||
|
||||
try:
|
||||
run_alembic_migration(log_handler)
|
||||
run_alembic_migration(db_uri, log_handler, setup_app=False)
|
||||
except Exception as ex:
|
||||
return {
|
||||
'error': str(ex)
|
||||
|
@ -301,9 +296,42 @@ class SuperUserConfigValidate(ApiResource):
|
|||
if not config_provider.config_exists():
|
||||
config = request.get_json()['config']
|
||||
validator_context = ValidatorContext.from_app(app, config, request.get_json().get('password', ''),
|
||||
ip_resolver=ip_resolver,
|
||||
config_provider=config_provider)
|
||||
instance_keys=instance_keys,
|
||||
ip_resolver=ip_resolver,
|
||||
config_provider=config_provider)
|
||||
return validate_service_for_config(service, validator_context)
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ from config_app.config_endpoints.api.suconfig_models_interface import SuperuserC
|
|||
|
||||
|
||||
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):
|
||||
try:
|
||||
list(User.select().limit(1))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import logging
|
||||
import pathvalidate
|
||||
import os
|
||||
|
||||
from flask import request, jsonify
|
||||
|
||||
from config_app.config_endpoints.exception import InvalidRequest
|
||||
|
@ -46,13 +47,14 @@ class SuperUserCustomCertificate(ApiResource):
|
|||
logger.exception('Got IO error for cert %s', certpath)
|
||||
return '', 204
|
||||
|
||||
# TODO(QUAY-991): properly install the custom certs provided by user
|
||||
# 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')
|
||||
# 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
|
||||
|
||||
|
|
28
config_app/config_endpoints/api/tar_config_loader.py
Normal file
28
config_app/config_endpoints/api/tar_config_loader.py
Normal 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')
|
|
@ -1,12 +1,12 @@
|
|||
from config_app.config_util.config.fileprovider import FileConfigProvider
|
||||
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):
|
||||
""" Loads and returns the config provider for the current environment. """
|
||||
|
||||
if testing:
|
||||
return TestConfigProvider()
|
||||
|
||||
return FileConfigProvider(config_volume, yaml_filename, py_filename)
|
||||
|
||||
|
||||
return InMemoryProvider()
|
||||
|
|
84
config_app/config_util/config/inmemoryprovider.py
Normal file
84
config_app/config_util/config/inmemoryprovider.py
Normal 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
|
|
@ -23,4 +23,4 @@
|
|||
</div>
|
||||
</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>
|
||||
|
|
|
@ -22,4 +22,8 @@ export class ConfigSetupAppComponent {
|
|||
private chooseLoad(): void {
|
||||
this.state = 'load';
|
||||
}
|
||||
|
||||
private configLoaded(): void {
|
||||
this.state = 'setup';
|
||||
}
|
||||
}
|
||||
|
|
3
config_app/js/components/cor-option/cor-option.html
Normal file
3
config_app/js/components/cor-option/cor-option.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<li>
|
||||
<a ng-click="optionClick()" ng-transclude></a>
|
||||
</li>
|
32
config_app/js/components/cor-option/cor-option.js
Normal file
32
config_app/js/components/cor-option/cor-option.js
Normal 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;
|
||||
});
|
|
@ -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>
|
|
@ -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>
|
2
config_app/js/components/cor-title/cor-title.html
Normal file
2
config_app/js/components/cor-title/cor-title.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<div class="co-nav-title" ng-transclude></div>
|
||||
|
31
config_app/js/components/cor-title/cor-title.js
Normal file
31
config_app/js/components/cor-title/cor-title.js
Normal 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;
|
||||
});
|
|
@ -17,6 +17,7 @@ angular.module('quay-config').directive('fileUploadBox', function () {
|
|||
'filesValidated': '&filesValidated',
|
||||
|
||||
'extensions': '<extensions',
|
||||
'apiEndpoint': '@apiEndpoint',
|
||||
|
||||
'reset': '=?reset'
|
||||
},
|
||||
|
@ -32,9 +33,9 @@ angular.module('quay-config').directive('fileUploadBox', function () {
|
|||
$scope.state = 'clear';
|
||||
$scope.selectedFiles = [];
|
||||
|
||||
var conductUpload = function(file, url, fileId, mimeType, progressCb, doneCb) {
|
||||
var conductUpload = function(file, apiEndpoint, fileId, mimeType, progressCb, doneCb) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', url, true);
|
||||
request.open('PUT', '/api/v1/' + apiEndpoint, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
|
@ -103,12 +104,7 @@ angular.module('quay-config').directive('fileUploadBox', function () {
|
|||
$scope.currentlyUploadingFile = currentFile;
|
||||
$scope.uploadProgress = 0;
|
||||
|
||||
ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
// Perform the upload.
|
||||
conductUpload(currentFile, resp.url, resp.file_id, mimeType, progressCb, doneCb);
|
||||
}, function() {
|
||||
callback(false, 'Could not retrieve upload URL');
|
||||
});
|
||||
conductUpload(currentFile, $scope.apiEndpoint, $scope.selectedFiles[0].name, mimeType, progressCb, doneCb);
|
||||
};
|
||||
|
||||
// Start the uploading.
|
||||
|
|
18
config_app/js/components/files-changed.js
Normal file
18
config_app/js/components/files-changed.js
Normal 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});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
|
@ -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 styleUrl = require('./load-config.css');
|
||||
|
||||
declare var bootbox: any;
|
||||
|
||||
@Component({
|
||||
selector: 'load-config',
|
||||
templateUrl,
|
||||
styleUrls: [ styleUrl ],
|
||||
})
|
||||
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;
|
||||
}
|
||||
}
|
0
config_app/js/components/load-config/load-config.css
Normal file
0
config_app/js/components/load-config/load-config.css
Normal file
|
@ -8,9 +8,20 @@
|
|||
<h4 class="modal-title"><span>Load Config</span></h4>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div>
|
||||
Please upload a tarball
|
||||
<input type="file" accept=".tar"></div>
|
||||
<div class="modal-body">
|
||||
<span>Please upload the previous configuration</span>
|
||||
<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><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
|
|
|
@ -20,10 +20,11 @@
|
|||
<td>Upload certificates:</td>
|
||||
<td>
|
||||
<div class="file-upload-box"
|
||||
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']"></div>
|
||||
api-endpoint="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']"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<!-- Basic Configuration -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-gears"></i> Basic Configuration
|
||||
<i class="fas fa-cogs"></i> Basic Configuration
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<table class="config-table">
|
||||
|
@ -456,7 +456,7 @@
|
|||
<!-- BitTorrent pull -->
|
||||
<div class="co-panel">
|
||||
<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 class="co-panel-body">
|
||||
<div class="description">
|
||||
|
@ -941,7 +941,7 @@
|
|||
<!-- GitHub Authentication -->
|
||||
<div class="co-panel">
|
||||
<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 class="co-panel-body">
|
||||
<div class="description">
|
||||
|
@ -1049,7 +1049,7 @@
|
|||
<!-- Google Authentication -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-google"></i> Google Authentication
|
||||
<i class="fab fa-google"></i> Google Authentication
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<div class="description">
|
||||
|
@ -1390,7 +1390,7 @@
|
|||
<!-- GitHub Trigger -->
|
||||
<div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;">
|
||||
<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 class="co-panel-body">
|
||||
<div class="description">
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<!-- Basic Configuration -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-gears"></i> Basic Configuration
|
||||
<i class="fas fa-cogs"></i> Basic Configuration
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<table class="config-table">
|
||||
|
@ -457,7 +457,7 @@
|
|||
<!-- BitTorrent pull -->
|
||||
<div class="co-panel">
|
||||
<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 class="co-panel-body">
|
||||
<div class="description">
|
||||
|
@ -942,7 +942,7 @@
|
|||
<!-- GitHub Authentication -->
|
||||
<div class="co-panel">
|
||||
<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 class="co-panel-body">
|
||||
<div class="description">
|
||||
|
@ -1050,7 +1050,7 @@
|
|||
<!-- Google Authentication -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-google"></i> Google Authentication
|
||||
<i class="fab fa-google"></i> Google Authentication
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<div class="description">
|
||||
|
@ -1391,7 +1391,7 @@
|
|||
<!-- GitHub Trigger -->
|
||||
<div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;">
|
||||
<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 class="co-panel-body">
|
||||
<div class="description">
|
||||
|
|
|
@ -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');
|
||||
|
@ -1115,7 +1116,7 @@ angular.module("quay-config")
|
|||
.directive('configContactField', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: urlContactsField,
|
||||
templateUrl: urlContactField,
|
||||
priority: 1,
|
||||
replace: false,
|
||||
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();
|
||||
// });
|
||||
|
||||
$scope.handleCertsSelected = function(files, callback) {
|
||||
$scope.certsUploading = true;
|
||||
|
|
|
@ -65,9 +65,6 @@ const templateUrl = require('./setup.html');
|
|||
// Database is being setup.
|
||||
'DB_SETUP': 'setup-db',
|
||||
|
||||
// Database setup has succeeded.
|
||||
'DB_SETUP_SUCCESS': 'setup-db-success',
|
||||
|
||||
// An error occurred when setting up the database.
|
||||
'DB_SETUP_ERROR': 'setup-db-error',
|
||||
|
||||
|
@ -262,7 +259,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;
|
||||
|
@ -277,7 +273,7 @@ const templateUrl = require('./setup.html');
|
|||
$scope.currentStep = $scope.States.DB_SETUP_ERROR;
|
||||
$scope.errors.DatabaseSetupError = resp['error'];
|
||||
} 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.'))
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div>
|
||||
<div id="padding-container">
|
||||
<div>
|
||||
<div class="cor-loader" ng-show="currentStep == States.LOADING"></div>
|
||||
<div class="page-content" ng-show="currentStep == States.CONFIG">
|
||||
|
@ -12,11 +12,11 @@
|
|||
<span class="cor-step-bar" progress="stepProgress">
|
||||
<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="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="Configure Registry" text="3"></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>
|
||||
|
||||
|
@ -24,8 +24,8 @@
|
|||
<div>Configure your Redis database and other settings below</div>
|
||||
</div>
|
||||
|
||||
<config-setup-tool is-active="isStep(currentStep, States.CONFIG)"
|
||||
configuration-saved="configurationSaved(config)"></config-setup-tool>
|
||||
<div class="config-setup-tool" is-active="isStep(currentStep, States.CONFIG)"
|
||||
configuration-saved="configurationSaved(config)"></divconfig-setup-tool>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,11 +39,11 @@
|
|||
<span class="cor-step-bar" progress="stepProgress">
|
||||
<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="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="Configure Registry" text="3"></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>
|
||||
<h4 class="modal-title">Setup</h4>
|
||||
|
@ -95,7 +95,7 @@
|
|||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
|
||||
<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
|
||||
</h4>
|
||||
This can take several minutes. If the container does not restart on its own,
|
||||
|
|
8
config_app/static/css/cor-option.css
Normal file
8
config_app/static/css/cor-option.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.cor-options-menu .fa-cog {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.open .fa-cog {
|
||||
color: #428BCA;
|
||||
}
|
4
config_app/static/css/cor-title.css
Normal file
4
config_app/static/css/cor-title.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.cor-title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
BIN
config_app/static/img/network-tile.png
Normal file
BIN
config_app/static/img/network-tile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
BIN
config_app/static/img/redis-small.png
Normal file
BIN
config_app/static/img/redis-small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
config_app/static/img/rocket.png
Normal file
BIN
config_app/static/img/rocket.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
|
@ -14,14 +14,23 @@ from peewee import SqliteDatabase
|
|||
from data.database import all_models, db
|
||||
from data.migrations.tester import NoopTester, PopulateTestDataTester
|
||||
|
||||
from app import app
|
||||
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
||||
from release import GIT_HEAD, REGION, SERVICE
|
||||
from util.morecollections import AttrDict
|
||||
|
||||
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.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name:
|
||||
|
@ -47,7 +56,7 @@ def get_tester():
|
|||
connecting to a production database.
|
||||
"""
|
||||
if os.environ.get('TEST_MIGRATE', '') == 'true':
|
||||
url = unquote(app.config['DB_URI'])
|
||||
url = unquote(DB_URI)
|
||||
if url.find('.quay.io') < 0:
|
||||
return PopulateTestDataTester()
|
||||
|
||||
|
@ -65,13 +74,12 @@ def run_migrations_offline():
|
|||
script output.
|
||||
|
||||
"""
|
||||
url = unquote(app.config['DB_URI'])
|
||||
url = unquote(DB_URI)
|
||||
context.configure(url=url, target_metadata=target_metadata, transactional_ddl=True)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations(tables=tables, tester=get_tester())
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
|
|
|
@ -5,12 +5,19 @@ from alembic.script import ScriptDirectory
|
|||
from alembic.environment import EnvironmentContext
|
||||
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:
|
||||
logging.getLogger(migration_name).addHandler(log_handler)
|
||||
|
||||
config = Config()
|
||||
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)
|
||||
|
||||
def fn(rev, context):
|
||||
|
|
|
@ -118,7 +118,7 @@ class SuperUserSetupDatabase(ApiResource):
|
|||
log_handler = _AlembicLogHandler()
|
||||
|
||||
try:
|
||||
run_alembic_migration(log_handler)
|
||||
run_alembic_migration(app.config['DB_URI'], log_handler)
|
||||
except Exception as ex:
|
||||
return {
|
||||
'error': str(ex)
|
||||
|
|
Reference in a new issue