Create transient config provider, temp dir logic

Allows us to have a new config provider for each setup, with no overlap
of the directories used, and automatic cleanup of those directories.
This commit is contained in:
Sam Chow 2018-06-28 13:45:26 -04:00
parent 2d0a599aab
commit db757edcd2
9 changed files with 70 additions and 37 deletions

View file

@ -110,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 {

View file

@ -1,4 +1,5 @@
import os import os
import tempfile
import tarfile import tarfile
from flask import request, make_response, send_file from flask import request, make_response, send_file
@ -9,6 +10,19 @@ from util.config.validator import EXTRA_CA_DIRECTORY
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
@resource('/v1/configapp/initialization')
class ConfigInitialization(ApiResource):
"""
Resource for dealing with any initialization logic for the config app
"""
@nickname('scStartNewConfig')
def get(self):
config_provider.new_config_dir()
return make_response('OK')
@resource('/v1/configapp/tarconfig') @resource('/v1/configapp/tarconfig')
class TarConfigLoader(ApiResource): class TarConfigLoader(ApiResource):
""" """
@ -18,7 +32,7 @@ class TarConfigLoader(ApiResource):
@nickname('scGetConfigTarball') @nickname('scGetConfigTarball')
def get(self): def get(self):
config_path = config_provider.config_volume config_path = config_provider.get_config_dir_path()
# remove the initial trailing / from the prefix path, and add the last dir one # remove the initial trailing / from the prefix path, and add the last dir one
tar_dir_prefix = config_path[1:] + '/' tar_dir_prefix = config_path[1:] + '/'
@ -33,28 +47,28 @@ class TarConfigLoader(ApiResource):
return tarinfo return tarinfo
# Remove the tar if it already exists so we don't write on top of existing tarball temp = tempfile.NamedTemporaryFile()
if os.path.isfile('quay-config.tar.gz'):
os.remove('quay-config.tar.gz')
tar = tarfile.open('quay-config.tar.gz', mode="w|gz") tar = tarfile.open(temp.name, mode="w|gz")
for name in os.listdir(config_path): for name in os.listdir(config_path):
tar.add(os.path.join(config_path, name), filter=tarinfo_filter) tar.add(os.path.join(config_path, name), filter=tarinfo_filter)
tar.close() tar.close()
return send_file('quay-config.tar.gz', mimetype='application/gzip') return send_file(temp.name, mimetype='application/gzip')
@nickname('scUploadTarballConfig') @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
with tarfile.open(mode="r|gz", fileobj=input_stream) as tar_stream: with tarfile.open(mode="r|gz", fileobj=input_stream) as tar_stream:
# TODO: find a way to remove the contents of the directory on shutdown? tar_stream.extractall(config_provider.get_config_dir_path())
tar_stream.extractall(config_provider.config_volume)
# 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 = dict(**app.config)
combined.update(config_provider.get_config()) combined.update(config_provider.get_config())

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,5 +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.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):
@ -8,4 +9,4 @@ def get_config_provider(config_volume, yaml_filename, py_filename, testing=False
if testing: if testing:
return TestConfigProvider() return TestConfigProvider()
return FileConfigProvider(config_volume, yaml_filename, py_filename) return TransientDirectoryProvider(config_volume, yaml_filename, py_filename)

View file

@ -1,4 +1,4 @@
import { Component } 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');
/** /**
@ -17,12 +17,18 @@ export class ConfigSetupAppComponent {
private loadedConfig = false; private loadedConfig = false;
constructor() { 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 {

View file

@ -27,7 +27,7 @@ export class DownloadTarballModalComponent {
// We need to set the response type to 'blob', to ensure it's never encoded as a string // 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) // (string encoded binary data can be difficult to transform with js)
// and to make it easier to save (FileSaver expects a blob) // and to make it easier to save (FileSaver expects a blob)
this.ApiService.scGetConfigTarball(null, null, null, null, true).then(function(resp) { this.ApiService.scGetConfigTarball(null, null, null, null, 'blob').then(function(resp) {
FileSaver.saveAs(resp, 'quay-config.tar.gz'); FileSaver.saveAs(resp, 'quay-config.tar.gz');
}, errorDisplay); }, errorDisplay);
} }

View file

@ -417,10 +417,6 @@ angular.module("quay-config")
$scope.saveConfiguration = function() { $scope.saveConfiguration = function() {
$scope.savingConfiguration = true; $scope.savingConfiguration = true;
// Make sure to note that fully verified setup is completed. We use this as a signal
// in the setup tool.
// $scope.config['SETUP_COMPLETE'] = true;
var data = { var data = {
'config': $scope.config, 'config': $scope.config,
'hostname': window.location.host, 'hostname': window.location.host,
@ -441,7 +437,6 @@ angular.module("quay-config")
$('#validateAndSaveModal').modal('hide'); $('#validateAndSaveModal').modal('hide');
// $scope.configurationSaved({'config': $scope.config});
$scope.setupCompleted(); $scope.setupCompleted();
}, errorDisplay); }, errorDisplay);
}; };

View file

@ -212,17 +212,17 @@ 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, opt_blobresp) { 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_blobresp) { if (opt_background || opt_responseType) {
let httpConfig = {}; let httpConfig = {};
if (opt_background) { if (opt_background) {
httpConfig['ignoreLoadingBar'] = true; httpConfig['ignoreLoadingBar'] = true;
} }
if (opt_blobresp) { if (opt_responseType) {
httpConfig['responseType'] = 'blob'; httpConfig['responseType'] = opt_responseType;
} }
one.withHttpConfig(httpConfig); one.withHttpConfig(httpConfig);

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