Merge pull request #3127 from quay/project/download-tar

Q.E. Config User can update a config tarball pt 2
This commit is contained in:
Sam Chow 2018-06-29 16:53:28 -04:00 committed by GitHub
commit 31e4c6d380
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 318 additions and 1841 deletions

View file

@ -1,7 +1,4 @@
import logging import logging
import os
import subprocess
import signal
from flask import abort, request from flask import abort, request
@ -14,7 +11,6 @@ from data.users import get_federated_service_name, get_users_handler
from data.database import configure 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.validator import validate_service_for_config, ValidatorContext, is_valid_config_upload_filename from util.config.validator import validate_service_for_config, ValidatorContext, is_valid_config_upload_filename
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -69,44 +65,40 @@ class SuperUserConfig(ApiResource):
""" Updates the config override file. """ """ Updates the config override file. """
# Note: This method is called to set the database configuration before super users exists, # Note: This method is called to set the database configuration before super users exists,
# so we also allow it to be called if there is no valid registry configuration setup. # so we also allow it to be called if there is no valid registry configuration setup.
if not config_provider.config_exists(): config_object = request.get_json()['config']
config_object = request.get_json()['config'] hostname = request.get_json()['hostname']
hostname = request.get_json()['hostname']
# Add any enterprise defaults missing from the config. # Add any enterprise defaults missing from the config.
add_enterprise_config_defaults(config_object, app.config['SECRET_KEY'], hostname) add_enterprise_config_defaults(config_object, app.config['SECRET_KEY'], hostname)
# Write the configuration changes to the config override file. # Write the configuration changes to the config override file.
config_provider.save_config(config_object) config_provider.save_config(config_object)
# If the authentication system is federated, link the superuser account to the
# the authentication system chosen.
service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE'])
if service_name is not None:
current_user = get_authenticated_user()
if current_user is None:
abort(401)
# If the authentication system is federated, link the superuser account to the
# the authentication system chosen.
service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE']) service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE'])
if service_name is not None: if not model.has_federated_login(current_user.username, service_name):
current_user = get_authenticated_user() # Verify the user's credentials and retrieve the user's external username+email.
if current_user is None: handler = get_users_handler(config_object, config_provider, OVERRIDE_CONFIG_DIRECTORY)
abort(401) (result, err_msg) = handler.verify_credentials(current_user.username,
request.get_json().get('password', ''))
if not result:
logger.error('Could not save configuration due to external auth failure: %s', err_msg)
abort(400)
service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE']) # Link the existing user to the external user.
if not model.has_federated_login(current_user.username, service_name): model.attach_federated_login(current_user.username, service_name, result.username)
# Verify the user's credentials and retrieve the user's external username+email.
handler = get_users_handler(config_object, config_provider, OVERRIDE_CONFIG_DIRECTORY)
(result, err_msg) = handler.verify_credentials(current_user.username,
request.get_json().get('password', ''))
if not result:
logger.error('Could not save configuration due to external auth failure: %s', err_msg)
abort(400)
# Link the existing user to the external user.
model.attach_federated_login(current_user.username, service_name, result.username)
return {
'exists': True,
'config': config_object
}
abort(403)
return {
'exists': True,
'config': config_object
}
@resource('/v1/superuser/registrystatus') @resource('/v1/superuser/registrystatus')
@ -118,20 +110,6 @@ class SuperUserRegistryStatus(ApiResource):
def get(self): def get(self):
""" Returns the status of the registry. """ """ Returns the status of the registry. """
# If we have SETUP_COMPLETE, then we're ready to go!
if app.config.get('SETUP_COMPLETE', False):
return {
'provider_id': config_provider.provider_id,
'requires_restart': config_provider.requires_restart(app.config),
'status': 'ready'
}
# If there is no conf/stack volume, then report that status.
if not config_provider.volume_exists():
return {
'status': 'missing-config-dir'
}
# If there is no config file, we need to setup the database. # If there is no config file, we need to setup the database.
if not config_provider.config_exists(): if not config_provider.config_exists():
return { return {
@ -293,16 +271,14 @@ class SuperUserConfigValidate(ApiResource):
# Note: This method is called to validate the database configuration before super users exists, # Note: This method is called to validate the database configuration before super users exists,
# so we also allow it to be called if there is no valid registry configuration setup. Note that # so we also allow it to be called if there is no valid registry configuration setup. Note that
# this is also safe since this method does not access any information not given in the request. # this is also safe since this method does not access any information not given in the request.
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', ''), instance_keys=instance_keys,
instance_keys=instance_keys, ip_resolver=ip_resolver,
ip_resolver=ip_resolver, config_provider=config_provider)
config_provider=config_provider)
return validate_service_for_config(service, validator_context)
return validate_service_for_config(service, validator_context)
abort(403)
@resource('/v1/superuser/config/file/<filename>') @resource('/v1/superuser/config/file/<filename>')

View file

@ -4,15 +4,16 @@ import os
from flask import request, jsonify from flask import request, jsonify
from util.config.validator import EXTRA_CA_DIRECTORY
from config_app.config_endpoints.exception import InvalidRequest from config_app.config_endpoints.exception import InvalidRequest
from config_app.config_endpoints.api import resource, ApiResource, nickname from config_app.config_endpoints.api import resource, ApiResource, nickname
from config_app.config_endpoints.api.superuser_models_pre_oci import pre_oci_model
from config_app.config_util.ssl import load_certificate, CertInvalidException 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 app, config_provider
from config_app.config_endpoints.api.superuser_models_pre_oci import pre_oci_model
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
EXTRA_CA_DIRECTORY = 'extra_ca_certs'
@resource('/v1/superuser/customcerts/<certpath>') @resource('/v1/superuser/customcerts/<certpath>')

View file

@ -1,28 +1,59 @@
import os
import tempfile
import tarfile import tarfile
from flask import request, make_response from flask import request, make_response, send_file
from data.database import configure from data.database import configure
from config_app.c_app import app, config_provider from config_app.c_app import app, config_provider
from config_app.config_endpoints.api import resource, ApiResource, nickname from config_app.config_endpoints.api import resource, ApiResource, nickname
from config_app.config_util.tar import tarinfo_filter_partial, strip_absolute_path_and_add_trailing_dir
@resource('/v1/configapp/initialization')
class ConfigInitialization(ApiResource):
"""
Resource for dealing with any initialization logic for the config app
"""
@nickname('scStartNewConfig')
def post(self):
config_provider.new_config_dir()
return make_response('OK')
@resource('/v1/configapp/tarconfig') @resource('/v1/configapp/tarconfig')
class TarConfigLoader(ApiResource): class TarConfigLoader(ApiResource):
""" Resource for validating a block of configuration against an external service. """ """
Resource for dealing with configuration as a tarball,
including loading and generating functions
"""
@nickname('uploadTarballConfig') @nickname('scGetConfigTarball')
def get(self):
config_path = config_provider.get_config_dir_path()
tar_dir_prefix = strip_absolute_path_and_add_trailing_dir(config_path)
temp = tempfile.NamedTemporaryFile()
tar = tarfile.open(temp.name, mode="w|gz")
for name in os.listdir(config_path):
tar.add(os.path.join(config_path, name), filter=tarinfo_filter_partial(tar_dir_prefix))
tar.close()
return send_file(temp.name, mimetype='application/gzip')
@nickname('scUploadTarballConfig')
def put(self): def put(self):
""" Loads tarball config into the config provider """ """ Loads tarball config into the config provider """
# Generate a new empty dir to load the config into
config_provider.new_config_dir()
input_stream = request.stream input_stream = request.stream
tar_stream = tarfile.open(mode="r|gz", fileobj=input_stream) with tarfile.open(mode="r|gz", fileobj=input_stream) as tar_stream:
tar_stream.extractall(config_provider.get_config_dir_path())
config_provider.load_from_tar_stream(tar_stream) # now try to connect to the db provided in their config to validate it works
# now try to connect to the db provided in their config
combined = dict(**app.config) combined = dict(**app.config)
combined.update(config_provider.get_config()) combined.update(config_provider.get_config())
configure(combined) configure(combined)
return make_response('OK') return make_response('OK')

View file

@ -0,0 +1,30 @@
import os
from backports.tempfile import TemporaryDirectory
from config_app.config_util.config.fileprovider import FileConfigProvider
class TransientDirectoryProvider(FileConfigProvider):
""" Implementation of the config provider that reads and writes the data
from/to the file system, only using temporary directories,
deleting old dirs and creating new ones as requested.
"""
def __init__(self, config_volume, yaml_filename, py_filename):
# Create a temp directory that will be cleaned up when we change the config path
# This should ensure we have no "pollution" of different configs:
# no uploaded config should ever affect subsequent config modifications/creations
temp_dir = TemporaryDirectory()
self.temp_dir = temp_dir
super(TransientDirectoryProvider, self).__init__(temp_dir.name, yaml_filename, py_filename)
def new_config_dir(self):
"""
Update the path with a new temporary directory, deleting the old one in the process
"""
temp_dir = TemporaryDirectory()
self.config_volume = temp_dir.name
self.temp_dir = temp_dir
self.yaml_path = os.path.join(temp_dir.name, self.yaml_filename)
def get_config_dir_path(self):
return self.config_volume

View file

@ -1,6 +1,6 @@
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 from config_app.config_util.config.TransientDirectoryProvider import TransientDirectoryProvider
def get_config_provider(config_volume, yaml_filename, py_filename, testing=False): def get_config_provider(config_volume, yaml_filename, py_filename, testing=False):
@ -9,4 +9,4 @@ def get_config_provider(config_volume, yaml_filename, py_filename, testing=False
if testing: if testing:
return TestConfigProvider() return TestConfigProvider()
return InMemoryProvider() return TransientDirectoryProvider(config_volume, yaml_filename, py_filename)

View file

@ -1,84 +0,0 @@
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

@ -0,0 +1,20 @@
from util.config.validator import EXTRA_CA_DIRECTORY
def strip_absolute_path_and_add_trailing_dir(path):
"""
Removes the initial trailing / from the prefix path, and add the last dir one
"""
return path[1:] + '/'
def tarinfo_filter_partial(prefix):
def tarinfo_filter(tarinfo):
# remove leading directory info
tarinfo.name = tarinfo.name.replace(prefix, '')
# ignore any directory that isn't the specified extra ca one:
if tarinfo.isdir() and not tarinfo.name == EXTRA_CA_DIRECTORY:
return None
return tarinfo
return tarinfo_filter

View file

@ -0,0 +1,29 @@
import pytest
from config_app.config_util.tar import tarinfo_filter_partial
from util.config.validator import EXTRA_CA_DIRECTORY
from test.fixtures import *
class MockTarInfo:
def __init__(self, name, isdir):
self.name = name
self.isdir = lambda: isdir
def __eq__(self, other):
return other is not None and self.name == other.name
@pytest.mark.parametrize('prefix,tarinfo,expected', [
# It should handle simple files
('Users/sam/', MockTarInfo('Users/sam/config.yaml', False), MockTarInfo('config.yaml', False)),
# It should allow the extra CA dir
('Users/sam/', MockTarInfo('Users/sam/%s' % EXTRA_CA_DIRECTORY, True), MockTarInfo('%s' % EXTRA_CA_DIRECTORY, True)),
# it should allow a file in that extra dir
('Users/sam/', MockTarInfo('Users/sam/%s/cert.crt' % EXTRA_CA_DIRECTORY, False), MockTarInfo('%s/cert.crt' % EXTRA_CA_DIRECTORY, False)),
# it should not allow a directory that isn't the CA dir
('Users/sam/', MockTarInfo('Users/sam/dirignore', True), None),
])
def test_tarinfo_filter(prefix, tarinfo, expected):
partial = tarinfo_filter_partial(prefix)
assert partial(tarinfo) == expected

View file

@ -22,5 +22,6 @@
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
</div> </div>
</div> </div>
<div ng-if="$ctrl.state === 'setup'" class="setup"></div> <div ng-if="$ctrl.state === 'setup'" class="setup" setup-completed="$ctrl.setupCompleted()"></div>
<load-config ng-if="$ctrl.state === 'load'" config-loaded="$ctrl.configLoaded()"></load-config> <load-config ng-if="$ctrl.state === 'load'" config-loaded="$ctrl.configLoaded()"></load-config>
<download-tarball-modal ng-if="$ctrl.state === 'download'" loaded-config="$ctrl.loadedConfig"></download-tarball-modal>

View file

@ -1,4 +1,4 @@
import { Input, Component, Inject } from 'ng-metadata/core'; import { Component, Inject } from 'ng-metadata/core';
const templateUrl = require('./config-setup-app.component.html'); const templateUrl = require('./config-setup-app.component.html');
/** /**
@ -9,21 +9,38 @@ const templateUrl = require('./config-setup-app.component.html');
templateUrl: templateUrl, templateUrl: templateUrl,
}) })
export class ConfigSetupAppComponent { export class ConfigSetupAppComponent {
private state: 'choice' | 'setup' | 'load'; private state
: 'choice'
| 'setup'
| 'load'
| 'download';
constructor() { private loadedConfig = false;
constructor(@Inject('ApiService') private apiService) {
this.state = 'choice'; this.state = 'choice';
} }
private chooseSetup(): void { private chooseSetup(): void {
this.state = 'setup'; this.apiService.scStartNewConfig()
.then(() => {
this.state = 'setup';
})
.catch(this.apiService.errorDisplay(
'Could not initialize new setup. Please report this error'
));
} }
private chooseLoad(): void { private chooseLoad(): void {
this.state = 'load'; this.state = 'load';
this.loadedConfig = true;
} }
private configLoaded(): void { private configLoaded(): void {
this.state = 'setup'; this.state = 'setup';
} }
private setupCompleted(): void {
this.state = 'download';
}
} }

View file

@ -0,0 +1,46 @@
<div>
<div class="co-dialog modal fade initial-setup-modal in" id="setupModal" style="display: block;">
<div class="modal-backdrop fade in" style="height: 1000px;"></div>
<div class="modal-dialog fade in">
<div class="modal-content">
<!-- Header -->
<div class="modal-header">
<h4 class="modal-title"><span>Download Configuration</span></h4>
</div>
<!-- Body -->
<div class="modal-body download-tarball-modal">
<div ng-if="$ctrl.loadedConfig">
Please download your updated configuration. To deploy these changes to your Quay Enterprise instances, please
<a target="_blank" href="https://coreos.com/quay-enterprise/docs/latest/initial-setup.html">
see the docs.
</a>
<div class="modal__warning-box">
<div class="fas co-alert co-alert-warning">
<strong>Warning: </strong>
Your configuration and certificates are kept <i>unencrypted</i>. Please keep this file secure.
</div>
</div>
</div>
<div ng-if="!$ctrl.loadedConfig">
Please download your new configuration. For more information, and next steps, please
<a target="_blank" href="https://coreos.com/quay-enterprise/docs/latest/initial-setup.html">
see the docs.
</a>
<div class="modal__warning-box">
<div class="fas co-alert co-alert-warning">
<strong>Warning: </strong>
Your configuration and certificates are kept <i>unencrypted</i>. Please keep this file secure.
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary"
ng-click="$ctrl.downloadTarball()">
<i class="fa fa-download" style="margin-right: 10px;"></i>Download Configuration
</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
</div>

View file

@ -0,0 +1,34 @@
import { Input, Component, Inject } from 'ng-metadata/core';
const templateUrl = require('./download-tarball-modal.component.html');
const styleUrl = require('./download-tarball-modal.css');
declare const FileSaver: any;
/**
* Initial Screen and Choice in the Config App
*/
@Component({
selector: 'download-tarball-modal',
templateUrl: templateUrl,
styleUrls: [ styleUrl ],
})
export class DownloadTarballModalComponent {
@Input('<') public loadedConfig;
constructor(@Inject('ApiService') private ApiService) {
}
private downloadTarball() {
const errorDisplay: Function = this.ApiService.errorDisplay(
'Could not save configuration. Please report this error.'
);
// We need to set the response type to 'blob', to ensure it's never encoded as a string
// (string encoded binary data can be difficult to transform with js)
// and to make it easier to save (FileSaver expects a blob)
this.ApiService.scGetConfigTarball(null, null, null, null, 'blob').then(function(resp) {
FileSaver.saveAs(resp, 'quay-config.tar.gz');
}, errorDisplay);
}
}

View file

@ -0,0 +1,11 @@
.co-dialog .modal-body.download-tarball-modal {
padding: 15px;
}
.modal__warning-box {
margin-top: 15px;
}
.modal__warning-box .co-alert.co-alert-warning::before {
font-family:Font Awesome\ 5 Free;
}

View file

@ -2,6 +2,7 @@ import { NgModule } from 'ng-metadata/core';
import * as restangular from 'restangular'; import * as restangular from 'restangular';
import { ConfigSetupAppComponent } from './components/config-setup-app/config-setup-app.component'; import { ConfigSetupAppComponent } from './components/config-setup-app/config-setup-app.component';
import { DownloadTarballModalComponent } from './components/download-tarball-modal/download-tarball-modal.component';
import { LoadConfigComponent } from './components/load-config/load-config.component'; import { LoadConfigComponent } from './components/load-config/load-config.component';
const quayDependencies: string[] = [ const quayDependencies: string[] = [
@ -42,6 +43,7 @@ function provideConfig($provide: ng.auto.IProvideService,
imports: [ DependencyConfig ], imports: [ DependencyConfig ],
declarations: [ declarations: [
ConfigSetupAppComponent, ConfigSetupAppComponent,
DownloadTarballModalComponent,
LoadConfigComponent, LoadConfigComponent,
], ],
providers: [] providers: []

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,6 @@
<div class="config-setup-tool-element"> <div class="config-setup-tool-element">
<div class="cor-loader" ng-if="!config"></div> <div class="cor-loader" ng-if="!config"></div>
<div ng-show="true"> <div ng-show="true">
<!--<div ng-show="config && config['SUPER_USERS']">-->
<form id="configform" name="configform"> <form id="configform" name="configform">
<!-- Custom SSL certificates --> <!-- Custom SSL certificates -->
@ -1455,7 +1454,7 @@
<!-- BitBucket Trigger --> <!-- BitBucket 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-bitbucket"></i> BitBucket Build Triggers <i class="fab fa-bitbucket"></i> BitBucket Build Triggers
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
@ -1497,7 +1496,7 @@
<!-- GitLab Trigger --> <!-- GitLab 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-gitlab"></i> GitLab Build Triggers <i class="fab fa-gitlab"></i> GitLab Build Triggers
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
@ -1632,7 +1631,7 @@
<button class="btn btn-primary" <button class="btn btn-primary"
ng-click="saveConfiguration()" ng-click="saveConfiguration()"
ng-disabled="savingConfiguration"> ng-disabled="savingConfiguration">
<i class="fa fa-upload" style="margin-right: 10px;"></i>Save Configuration Next
</button> </button>
</div> </div>

View file

@ -27,7 +27,8 @@ angular.module("quay-config")
restrict: 'C', restrict: 'C',
scope: { scope: {
'isActive': '=isActive', 'isActive': '=isActive',
'configurationSaved': '&configurationSaved' 'configurationSaved': '&configurationSaved',
'setupCompleted': '&setupCompleted',
}, },
controller: function($rootScope, $scope, $element, $timeout, ApiService) { controller: function($rootScope, $scope, $element, $timeout, ApiService) {
var authPassword = null; var authPassword = null;
@ -434,7 +435,7 @@ angular.module("quay-config")
$('#validateAndSaveModal').modal('hide'); $('#validateAndSaveModal').modal('hide');
$scope.configurationSaved({'config': $scope.config}); $scope.setupCompleted();
}, errorDisplay); }, errorDisplay);
}; };

View file

@ -212,12 +212,20 @@ angular.module('quay-config').factory('ApiService', ['Restangular', '$q', 'UtilS
var urlPath = path['x-path']; var urlPath = path['x-path'];
// Add the operation itself. // Add the operation itself.
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forceget) { apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forceget, opt_responseType) {
var one = Restangular.one(buildUrl(urlPath, opt_parameters)); var one = Restangular.one(buildUrl(urlPath, opt_parameters));
if (opt_background) {
one.withHttpConfig({ if (opt_background || opt_responseType) {
'ignoreLoadingBar': true let httpConfig = {};
});
if (opt_background) {
httpConfig['ignoreLoadingBar'] = true;
}
if (opt_responseType) {
httpConfig['responseType'] = opt_responseType;
}
one.withHttpConfig(httpConfig);
} }
var opObj = one[opt_forceget ? 'get' : 'custom' + method.toUpperCase()](opt_options); var opObj = one[opt_forceget ? 'get' : 'custom' + method.toUpperCase()](opt_options);

View file

@ -15,7 +15,8 @@ const templateUrl = require('./setup.html');
restrict: 'C', restrict: 'C',
scope: { scope: {
'isActive': '=isActive', 'isActive': '=isActive',
'configurationSaved': '&configurationSaved' 'configurationSaved': '&configurationSaved',
'setupCompleted': '&setupCompleted',
}, },
controller: SetupCtrl, controller: SetupCtrl,
}; };

View file

@ -25,7 +25,9 @@
</div> </div>
<div class="config-setup-tool" is-active="isStep(currentStep, States.CONFIG)" <div class="config-setup-tool" is-active="isStep(currentStep, States.CONFIG)"
configuration-saved="configurationSaved(config)"></divconfig-setup-tool> configuration-saved="configurationSaved(config)"
setup-completed="setupCompleted()"
></div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -20,6 +20,7 @@ azure-storage-blob==1.1.0
azure-storage-common==1.1.0 azure-storage-common==1.1.0
azure-storage-nspkg==3.0.0 azure-storage-nspkg==3.0.0
Babel==2.5.3 Babel==2.5.3
backports.tempfile==1.0
beautifulsoup4==4.6.0 beautifulsoup4==4.6.0
bencode==1.0 bencode==1.0
bintrees==2.0.7 bintrees==2.0.7

View file

@ -7,6 +7,8 @@ from storage.azurestorage import AzureStorage
from storage.downloadproxy import DownloadProxy from storage.downloadproxy import DownloadProxy
from util.ipresolver import NoopIPResolver from util.ipresolver import NoopIPResolver
TYPE_LOCAL_STORAGE = 'LocalStorage'
STORAGE_DRIVER_CLASSES = { STORAGE_DRIVER_CLASSES = {
'LocalStorage': LocalStorage, 'LocalStorage': LocalStorage,
'S3Storage': S3Storage, 'S3Storage': S3Storage,

View file

@ -135,15 +135,15 @@ class ValidatorContext(object):
url_scheme_and_hostname = URLSchemeAndHostname.from_app_config(app.config) url_scheme_and_hostname = URLSchemeAndHostname.from_app_config(app.config)
return cls(config, return cls(config,
user_password, user_password=user_password,
client or app.config['HTTPCLIENT'], http_client=client or app.config['HTTPCLIENT'],
app.app_context, context=app.app_context,
url_scheme_and_hostname, url_scheme_and_hostname=url_scheme_and_hostname,
app.config.get('JWT_AUTH_MAX_FRESH_S', 300), jwt_auth_max=app.config.get('JWT_AUTH_MAX_FRESH_S', 300),
app.config['REGISTRY_TITLE'], registry_title=app.config['REGISTRY_TITLE'],
ip_resolver, ip_resolver=ip_resolver,
instance_keys, feature_sec_scanner=app.config.get('FEATURE_SECURITY_SCANNER', False),
app.config.get('FEATURE_SECURITY_SCANNER', False), is_testing=app.config.get('TESTING', False),
app.config.get('TESTING', False), uri_creator=get_blob_download_uri_getter(app.test_request_context('/'), url_scheme_and_hostname),
get_blob_download_uri_getter(app.test_request_context('/'), url_scheme_and_hostname), config_provider=config_provider,
config_provider) instance_keys=instance_keys)

View file

@ -1,4 +1,4 @@
from storage import get_storage_driver from storage import get_storage_driver, TYPE_LOCAL_STORAGE
from util.config.validators import BaseValidator, ConfigValidationException from util.config.validators import BaseValidator, ConfigValidationException
@ -20,6 +20,11 @@ class StorageValidator(BaseValidator):
raise ConfigValidationException('Storage configuration required') raise ConfigValidationException('Storage configuration required')
for name, (storage_type, driver) in providers: for name, (storage_type, driver) in providers:
# We can skip localstorage validation, since we can't guarantee that
# this will be the same machine Q.E. will run under
if storage_type == TYPE_LOCAL_STORAGE:
continue
try: try:
if replication_enabled and storage_type == 'LocalStorage': if replication_enabled and storage_type == 'LocalStorage':
raise ConfigValidationException('Locally mounted directory not supported ' + raise ConfigValidationException('Locally mounted directory not supported ' +