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 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,7 +65,6 @@ 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']
|
||||||
|
|
||||||
|
@ -105,9 +100,6 @@ class SuperUserConfig(ApiResource):
|
||||||
'config': config_object
|
'config': config_object
|
||||||
}
|
}
|
||||||
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/superuser/registrystatus')
|
@resource('/v1/superuser/registrystatus')
|
||||||
class SuperUserRegistryStatus(ApiResource):
|
class SuperUserRegistryStatus(ApiResource):
|
||||||
|
@ -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,17 +271,15 @@ 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>')
|
||||||
class SuperUserConfigFile(ApiResource):
|
class SuperUserConfigFile(ApiResource):
|
||||||
|
|
|
@ -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>')
|
||||||
|
|
|
@ -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')
|
||||||
|
|
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.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)
|
||||||
|
|
|
@ -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><!-- /.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>
|
||||||
|
|
|
@ -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.apiService.scStartNewConfig()
|
||||||
|
.then(() => {
|
||||||
this.state = 'setup';
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 * 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
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 || opt_responseType) {
|
||||||
|
let httpConfig = {};
|
||||||
|
|
||||||
if (opt_background) {
|
if (opt_background) {
|
||||||
one.withHttpConfig({
|
httpConfig['ignoreLoadingBar'] = true;
|
||||||
'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);
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 ' +
|
||||||
|
|
Reference in a new issue