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:
commit
31e4c6d380
24 changed files with 318 additions and 1841 deletions
|
@ -1,7 +1,4 @@
|
|||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import signal
|
||||
|
||||
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.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, is_valid_config_upload_filename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -69,44 +65,40 @@ class SuperUserConfig(ApiResource):
|
|||
""" Updates the config override file. """
|
||||
# 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.
|
||||
if not config_provider.config_exists():
|
||||
config_object = request.get_json()['config']
|
||||
hostname = request.get_json()['hostname']
|
||||
config_object = request.get_json()['config']
|
||||
hostname = request.get_json()['hostname']
|
||||
|
||||
# Add any enterprise defaults missing from the config.
|
||||
add_enterprise_config_defaults(config_object, app.config['SECRET_KEY'], hostname)
|
||||
# Add any enterprise defaults missing from the config.
|
||||
add_enterprise_config_defaults(config_object, app.config['SECRET_KEY'], hostname)
|
||||
|
||||
# Write the configuration changes to the config override file.
|
||||
config_provider.save_config(config_object)
|
||||
# Write the configuration changes to the config override file.
|
||||
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'])
|
||||
if service_name is not None:
|
||||
current_user = get_authenticated_user()
|
||||
if current_user is None:
|
||||
abort(401)
|
||||
if not model.has_federated_login(current_user.username, service_name):
|
||||
# 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)
|
||||
|
||||
service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE'])
|
||||
if not model.has_federated_login(current_user.username, service_name):
|
||||
# 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)
|
||||
# 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
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/superuser/registrystatus')
|
||||
|
@ -118,20 +110,6 @@ class SuperUserRegistryStatus(ApiResource):
|
|||
def get(self):
|
||||
""" 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 not config_provider.config_exists():
|
||||
return {
|
||||
|
@ -293,16 +271,14 @@ class SuperUserConfigValidate(ApiResource):
|
|||
# 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
|
||||
# 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']
|
||||
validator_context = ValidatorContext.from_app(app, config, request.get_json().get('password', ''),
|
||||
instance_keys=instance_keys,
|
||||
ip_resolver=ip_resolver,
|
||||
config_provider=config_provider)
|
||||
return validate_service_for_config(service, validator_context)
|
||||
config = request.get_json()['config']
|
||||
validator_context = ValidatorContext.from_app(app, config, request.get_json().get('password', ''),
|
||||
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>')
|
||||
|
|
|
@ -4,15 +4,16 @@ import os
|
|||
|
||||
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.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.c_app import app, config_provider
|
||||
|
||||
from config_app.config_endpoints.api.superuser_models_pre_oci import pre_oci_model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
EXTRA_CA_DIRECTORY = 'extra_ca_certs'
|
||||
|
||||
|
||||
@resource('/v1/superuser/customcerts/<certpath>')
|
||||
|
|
|
@ -1,28 +1,59 @@
|
|||
import os
|
||||
import tempfile
|
||||
import tarfile
|
||||
|
||||
from flask import request, make_response
|
||||
from flask import request, make_response, send_file
|
||||
|
||||
from data.database import configure
|
||||
|
||||
from config_app.c_app import app, config_provider
|
||||
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')
|
||||
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):
|
||||
""" 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
|
||||
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
|
||||
# now try to connect to the db provided in their config to validate it works
|
||||
combined = dict(**app.config)
|
||||
combined.update(config_provider.get_config())
|
||||
|
||||
configure(combined)
|
||||
|
||||
return make_response('OK')
|
||||
|
|
30
config_app/config_util/config/TransientDirectoryProvider.py
Normal file
30
config_app/config_util/config/TransientDirectoryProvider.py
Normal 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
|
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
from config_app.config_util.config.TransientDirectoryProvider import TransientDirectoryProvider
|
||||
|
||||
|
||||
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:
|
||||
return TestConfigProvider()
|
||||
|
||||
return InMemoryProvider()
|
||||
return TransientDirectoryProvider(config_volume, yaml_filename, py_filename)
|
||||
|
|
|
@ -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
|
20
config_app/config_util/tar.py
Normal file
20
config_app/config_util/tar.py
Normal 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
|
29
config_app/config_util/test/test_tar.py
Normal file
29
config_app/config_util/test/test_tar.py
Normal 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
|
|
@ -22,5 +22,6 @@
|
|||
</div><!-- /.modal-dialog -->
|
||||
</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>
|
||||
<download-tarball-modal ng-if="$ctrl.state === 'download'" loaded-config="$ctrl.loadedConfig"></download-tarball-modal>
|
||||
|
|
|
@ -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');
|
||||
|
||||
/**
|
||||
|
@ -9,21 +9,38 @@ const templateUrl = require('./config-setup-app.component.html');
|
|||
templateUrl: templateUrl,
|
||||
})
|
||||
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';
|
||||
}
|
||||
|
||||
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 {
|
||||
this.state = 'load';
|
||||
this.loadedConfig = true;
|
||||
}
|
||||
|
||||
private configLoaded(): void {
|
||||
this.state = 'setup';
|
||||
}
|
||||
|
||||
private setupCompleted(): void {
|
||||
this.state = 'download';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -2,6 +2,7 @@ import { NgModule } from 'ng-metadata/core';
|
|||
import * as restangular from 'restangular';
|
||||
|
||||
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';
|
||||
|
||||
const quayDependencies: string[] = [
|
||||
|
@ -42,6 +43,7 @@ function provideConfig($provide: ng.auto.IProvideService,
|
|||
imports: [ DependencyConfig ],
|
||||
declarations: [
|
||||
ConfigSetupAppComponent,
|
||||
DownloadTarballModalComponent,
|
||||
LoadConfigComponent,
|
||||
],
|
||||
providers: []
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,6 @@
|
|||
<div class="config-setup-tool-element">
|
||||
<div class="cor-loader" ng-if="!config"></div>
|
||||
<div ng-show="true">
|
||||
<!--<div ng-show="config && config['SUPER_USERS']">-->
|
||||
<form id="configform" name="configform">
|
||||
|
||||
<!-- Custom SSL certificates -->
|
||||
|
@ -1455,7 +1454,7 @@
|
|||
<!-- BitBucket Trigger -->
|
||||
<div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;">
|
||||
<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 class="co-panel-body">
|
||||
<div class="description">
|
||||
|
@ -1497,7 +1496,7 @@
|
|||
<!-- GitLab Trigger -->
|
||||
<div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;">
|
||||
<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 class="co-panel-body">
|
||||
<div class="description">
|
||||
|
@ -1632,7 +1631,7 @@
|
|||
<button class="btn btn-primary"
|
||||
ng-click="saveConfiguration()"
|
||||
ng-disabled="savingConfiguration">
|
||||
<i class="fa fa-upload" style="margin-right: 10px;"></i>Save Configuration
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -27,7 +27,8 @@ angular.module("quay-config")
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'isActive': '=isActive',
|
||||
'configurationSaved': '&configurationSaved'
|
||||
'configurationSaved': '&configurationSaved',
|
||||
'setupCompleted': '&setupCompleted',
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, $timeout, ApiService) {
|
||||
var authPassword = null;
|
||||
|
@ -434,7 +435,7 @@ angular.module("quay-config")
|
|||
|
||||
$('#validateAndSaveModal').modal('hide');
|
||||
|
||||
$scope.configurationSaved({'config': $scope.config});
|
||||
$scope.setupCompleted();
|
||||
}, errorDisplay);
|
||||
};
|
||||
|
||||
|
|
|
@ -212,12 +212,20 @@ angular.module('quay-config').factory('ApiService', ['Restangular', '$q', 'UtilS
|
|||
var urlPath = path['x-path'];
|
||||
|
||||
// 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));
|
||||
if (opt_background) {
|
||||
one.withHttpConfig({
|
||||
'ignoreLoadingBar': true
|
||||
});
|
||||
|
||||
if (opt_background || opt_responseType) {
|
||||
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);
|
||||
|
|
|
@ -15,7 +15,8 @@ const templateUrl = require('./setup.html');
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'isActive': '=isActive',
|
||||
'configurationSaved': '&configurationSaved'
|
||||
'configurationSaved': '&configurationSaved',
|
||||
'setupCompleted': '&setupCompleted',
|
||||
},
|
||||
controller: SetupCtrl,
|
||||
};
|
||||
|
|
|
@ -25,7 +25,9 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -20,6 +20,7 @@ azure-storage-blob==1.1.0
|
|||
azure-storage-common==1.1.0
|
||||
azure-storage-nspkg==3.0.0
|
||||
Babel==2.5.3
|
||||
backports.tempfile==1.0
|
||||
beautifulsoup4==4.6.0
|
||||
bencode==1.0
|
||||
bintrees==2.0.7
|
||||
|
|
|
@ -7,6 +7,8 @@ from storage.azurestorage import AzureStorage
|
|||
from storage.downloadproxy import DownloadProxy
|
||||
from util.ipresolver import NoopIPResolver
|
||||
|
||||
TYPE_LOCAL_STORAGE = 'LocalStorage'
|
||||
|
||||
STORAGE_DRIVER_CLASSES = {
|
||||
'LocalStorage': LocalStorage,
|
||||
'S3Storage': S3Storage,
|
||||
|
|
|
@ -135,15 +135,15 @@ class ValidatorContext(object):
|
|||
url_scheme_and_hostname = URLSchemeAndHostname.from_app_config(app.config)
|
||||
|
||||
return cls(config,
|
||||
user_password,
|
||||
client or app.config['HTTPCLIENT'],
|
||||
app.app_context,
|
||||
url_scheme_and_hostname,
|
||||
app.config.get('JWT_AUTH_MAX_FRESH_S', 300),
|
||||
app.config['REGISTRY_TITLE'],
|
||||
ip_resolver,
|
||||
instance_keys,
|
||||
app.config.get('FEATURE_SECURITY_SCANNER', False),
|
||||
app.config.get('TESTING', False),
|
||||
get_blob_download_uri_getter(app.test_request_context('/'), url_scheme_and_hostname),
|
||||
config_provider)
|
||||
user_password=user_password,
|
||||
http_client=client or app.config['HTTPCLIENT'],
|
||||
context=app.app_context,
|
||||
url_scheme_and_hostname=url_scheme_and_hostname,
|
||||
jwt_auth_max=app.config.get('JWT_AUTH_MAX_FRESH_S', 300),
|
||||
registry_title=app.config['REGISTRY_TITLE'],
|
||||
ip_resolver=ip_resolver,
|
||||
feature_sec_scanner=app.config.get('FEATURE_SECURITY_SCANNER', False),
|
||||
is_testing=app.config.get('TESTING', False),
|
||||
uri_creator=get_blob_download_uri_getter(app.test_request_context('/'), url_scheme_and_hostname),
|
||||
config_provider=config_provider,
|
||||
instance_keys=instance_keys)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
@ -20,6 +20,11 @@ class StorageValidator(BaseValidator):
|
|||
raise ConfigValidationException('Storage configuration required')
|
||||
|
||||
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:
|
||||
if replication_enabled and storage_type == 'LocalStorage':
|
||||
raise ConfigValidationException('Locally mounted directory not supported ' +
|
||||
|
|
Reference in a new issue