Implement new step-by-step setup

This commit is contained in:
Joseph Schorr 2015-01-23 17:19:15 -05:00
parent 28d319ad26
commit c8229b9c8a
20 changed files with 1393 additions and 599 deletions

View file

@ -18,7 +18,8 @@ config.set_main_option('sqlalchemy.url', unquote(app.config['DB_URI']))
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
if config.config_file_name:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support

20
data/runmigration.py Normal file
View file

@ -0,0 +1,20 @@
import logging
from alembic.config import Config
from alembic.script import ScriptDirectory
from alembic.environment import EnvironmentContext
from alembic.migration import __name__ as migration_name
def run_alembic_migration(log_handler=None):
if log_handler:
logging.getLogger(migration_name).addHandler(log_handler)
config = Config()
config.set_main_option("script_location", "data:migrations")
script = ScriptDirectory.from_config(config)
def fn(rev, context):
return script._upgrade_revs('head', rev)
with EnvironmentContext(config, script, fn=fn, destination_rev='head'):
script.run_env()

View file

@ -1,19 +1,22 @@
import logging
import os
import json
import signal
from flask import abort
from flask import abort, Response
from endpoints.api import (ApiResource, nickname, resource, internal_only, show_if,
require_fresh_login, request, validate_json_request, verify_not_prod)
from endpoints.common import common_login
from app import app, CONFIG_PROVIDER, superusers
from data import model
from data.database import configure
from auth.permissions import SuperUserPermission
from auth.auth_context import get_authenticated_user
from data.database import User
from util.config.configutil import add_enterprise_config_defaults
from util.config.validator import validate_service_for_config, SSL_FILENAMES
from data.runmigration import run_alembic_migration
import features
@ -21,12 +24,16 @@ logger = logging.getLogger(__name__)
def database_is_valid():
""" Returns whether the database, as configured, is valid. """
if app.config['TESTING']:
return False
try:
User.select().limit(1)
list(User.select().limit(1))
return True
except:
return False
def database_has_users():
""" Returns whether the database has any users defined. """
return bool(list(User.select().limit(1)))
@ -42,17 +49,107 @@ class SuperUserRegistryStatus(ApiResource):
@nickname('scRegistryStatus')
@verify_not_prod
def get(self):
""" Returns whether a valid configuration, database and users exist. """
file_exists = CONFIG_PROVIDER.yaml_exists()
""" Returns the status of the registry. """
# 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.yaml_exists():
return {
'status': 'config-db'
}
# If the database isn't yet valid, then we need to set it up.
if not database_is_valid():
return {
'status': 'setup-db'
}
# If we have SETUP_COMPLETE, then we're ready to go!
if app.config.get('SETUP_COMPLETE', False):
return {
'requires_restart': CONFIG_PROVIDER.requires_restart(app.config),
'status': 'ready'
}
return {
'dir_exists': CONFIG_PROVIDER.volume_exists(),
'file_exists': file_exists,
'is_testing': app.config['TESTING'],
'valid_db': database_is_valid(),
'ready': not app.config['TESTING'] and file_exists and superusers.has_superusers()
'status': 'create-superuser' if not database_has_users() else 'config'
}
class _AlembicLogHandler(logging.Handler):
def __init__(self):
super(_AlembicLogHandler, self).__init__()
self.records = []
def emit(self, record):
self.records.append({
'level': record.levelname,
'message': record.getMessage()
})
@resource('/v1/superuser/setupdb')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserSetupDatabase(ApiResource):
""" Resource for invoking alembic to setup the database. """
@verify_not_prod
@nickname('scSetupDatabase')
def get(self):
""" Invokes the alembic upgrade process. """
# Note: This method is called after the database configured is saved, but before the
# database has any tables. Therefore, we only allow it to be run in that unique case.
if CONFIG_PROVIDER.yaml_exists() and not database_is_valid():
# Note: We need to reconfigure the database here as the config has changed.
combined = dict(**app.config)
combined.update(CONFIG_PROVIDER.get_yaml())
configure(combined)
app.config['DB_URI'] = combined['DB_URI']
log_handler = _AlembicLogHandler()
try:
run_alembic_migration(log_handler)
except Exception as ex:
return {
'error': str(ex)
}
return {
'logs': log_handler.records
}
abort(403)
@resource('/v1/superuser/shutdown')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserShutdown(ApiResource):
""" Resource for sending a shutdown signal to the container. """
@verify_not_prod
@nickname('scShutdownContainer')
def post(self):
""" Sends a signal to the phusion init system to shut down the container. """
# 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 app.config['TESTING'] or not database_has_users() or SuperUserPermission().can():
# Note: We skip if debugging locally.
if app.config.get('DEBUGGING') == True:
return {}
os.kill(1, signal.SIGINT)
return {}
abort(403)
@resource('/v1/superuser/config')
@internal_only
@show_if(features.SUPER_USERS)

View file

@ -98,6 +98,7 @@ def organizations():
def user():
return index('')
@web.route('/superuser/')
@no_cache
@route_show_if(features.SUPER_USERS)
@ -105,6 +106,13 @@ def superuser():
return index('')
@web.route('/setup/')
@no_cache
@route_show_if(features.SUPER_USERS)
def setup():
return index('')
@web.route('/signin/')
@no_cache
def signin(redirect=None):

View file

@ -366,10 +366,6 @@
bottom: 0px;
}
.config-setup-tool .cor-floating-bottom-bar {
text-align: right;
}
.config-setup-tool .cor-floating-bottom-bar button i.fa {
margin-right: 6px;
}
@ -418,3 +414,285 @@
font-family: Consolas, "Lucida Console", Monaco, monospace;
font-size: 12px;
}
.co-m-loader, .co-m-inline-loader {
min-width: 28px; }
.co-m-loader {
display: block;
position: absolute;
left: 50%;
top: 50%;
margin: -11px 0 0 -13px; }
.co-m-inline-loader {
display: inline-block;
cursor: default; }
.co-m-inline-loader:hover {
text-decoration: none; }
.co-m-loader-dot__one, .co-m-loader-dot__two, .co-m-loader-dot__three {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
animation-fill-mode: both;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-ms-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-name: bouncedelay;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-delay: 0;
animation-direction: normal;
animation-iteration-count: infinite;
animation-fill-mode: forwards;
animation-play-state: running;
-webkit-animation-name: bouncedelay;
-webkit-animation-duration: 1s;
-webkit-animation-timing-function: ease-in-out;
-webkit-animation-delay: 0;
-webkit-animation-direction: normal;
-webkit-animation-iteration-count: infinite;
-webkit-animation-fill-mode: forwards;
-webkit-animation-play-state: running;
-moz-animation-name: bouncedelay;
-moz-animation-duration: 1s;
-moz-animation-timing-function: ease-in-out;
-moz-animation-delay: 0;
-moz-animation-direction: normal;
-moz-animation-iteration-count: infinite;
-moz-animation-fill-mode: forwards;
-moz-animation-play-state: running;
display: inline-block;
height: 6px;
width: 6px;
background: #419eda;
border-radius: 100%;
display: inline-block; }
.co-m-loader-dot__one {
animation-delay: -0.32s;
-webkit-animation-delay: -0.32s;
-moz-animation-delay: -0.32s;
-ms-animation-delay: -0.32s;
-o-animation-delay: -0.32s; }
.co-m-loader-dot__two {
animation-delay: -0.16s;
-webkit-animation-delay: -0.16s;
-moz-animation-delay: -0.16s;
-ms-animation-delay: -0.16s;
-o-animation-delay: -0.16s; }
@-webkit-keyframes bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0.25, 0.25);
-moz-transform: scale(0.25, 0.25);
-ms-transform: scale(0.25, 0.25);
-o-transform: scale(0.25, 0.25);
transform: scale(0.25, 0.25); }
40% {
-webkit-transform: scale(1, 1);
-moz-transform: scale(1, 1);
-ms-transform: scale(1, 1);
-o-transform: scale(1, 1);
transform: scale(1, 1); } }
@-moz-keyframes bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0.25, 0.25);
-moz-transform: scale(0.25, 0.25);
-ms-transform: scale(0.25, 0.25);
-o-transform: scale(0.25, 0.25);
transform: scale(0.25, 0.25); }
40% {
-webkit-transform: scale(1, 1);
-moz-transform: scale(1, 1);
-ms-transform: scale(1, 1);
-o-transform: scale(1, 1);
transform: scale(1, 1); } }
@-ms-keyframes bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0.25, 0.25);
-moz-transform: scale(0.25, 0.25);
-ms-transform: scale(0.25, 0.25);
-o-transform: scale(0.25, 0.25);
transform: scale(0.25, 0.25); }
40% {
-webkit-transform: scale(1, 1);
-moz-transform: scale(1, 1);
-ms-transform: scale(1, 1);
-o-transform: scale(1, 1);
transform: scale(1, 1); } }
@keyframes bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0.25, 0.25);
-moz-transform: scale(0.25, 0.25);
-ms-transform: scale(0.25, 0.25);
-o-transform: scale(0.25, 0.25);
transform: scale(0.25, 0.25); }
40% {
-webkit-transform: scale(1, 1);
-moz-transform: scale(1, 1);
-ms-transform: scale(1, 1);
-o-transform: scale(1, 1);
transform: scale(1, 1); } }
.co-dialog .modal-body {
padding: 10px;
min-height: 100px;
}
.co-dialog .modal-content {
border-radius: 0px;
}
.co-dialog.fatal-error .modal-content {
padding-left: 175px;
}
.co-dialog.fatal-error .alert-icon-container-container {
position: absolute;
top: -36px;
left: -175px;
bottom: 20px;
}
.co-dialog.fatal-error .alert-icon-container {
height: 100%;
display: table;
}
.co-dialog.fatal-error .alert-icon {
display: table-cell;
vertical-align: middle;
border-right: 1px solid #eee;
margin-right: 20px;
}
.co-dialog.fatal-error .alert-icon:before {
content: "\f071";
font-family: FontAwesome;
font-size: 60px;
padding-left: 50px;
padding-right: 50px;
color: #c53c3f;
text-align: center;
}
.co-dialog .modal-header .cor-step-bar {
float: right;
}
.co-dialog .modal-footer.working {
text-align: left;
}
.co-dialog .modal-footer.working .cor-loader-inline {
margin-right: 10px;
}
.co-dialog .modal-footer .left-align {
float: left;
vertical-align: middle;
font-size: 16px;
margin-top: 8px;
}
.co-dialog .modal-footer .left-align i.fa-warning {
color: #ffba35;
display: inline-block;
margin-right: 6px;
}
.co-dialog .modal-footer .left-align i.fa-check {
color: green;
display: inline-block;
margin-right: 6px;
}
.co-step-bar .co-step-element {
cursor: default;
display: inline-block;
width: 28px;
height: 28px;
position: relative;
color: #ddd;
text-align: center;
line-height: 24px;
font-size: 16px;
}
.co-step-bar .co-step-element.text {
margin-left: 24px;
background: white;
}
.co-step-bar .co-step-element.icon {
margin-left: 22px;
}
.co-step-bar .co-step-element:first-child {
margin-left: 0px;
}
.co-step-bar .co-step-element.active {
color: #53a3d9;
}
.co-step-bar .co-step-element:first-child:before {
display: none;
}
.co-step-bar .co-step-element:before {
content: "";
position: absolute;
top: 12px;
width: 14px;
border-top: 2px solid #ddd;
}
.co-step-bar .co-step-element.icon:before {
left: -20px;
}
.co-step-bar .co-step-element.text:before {
left: -22px;
}
.co-step-bar .co-step-element.active:before {
border-top: 2px solid #53a3d9;
}
.co-step-bar .co-step-element.text {
border-radius: 100%;
border: 2px solid #ddd;
}
.co-step-bar .co-step-element.text.active {
border: 2px solid #53a3d9;
}
@media screen and (min-width: 900px) {
.co-dialog .modal-dialog {
width: 800px;
}
}
.co-alert .co-step-bar {
float: right;
margin-top: 6px;
}

View file

@ -553,42 +553,15 @@
</div>
<!-- Modal message dialog -->
<div class="modal fade initial-setup-modal" id="validateAndSaveModal">
<div class="modal co-dialog fade initial-setup-modal" id="validateAndSaveModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title"
ng-show="mapped.$hasChanges && validationStatus(validating) == 'validating'">
Validating Configuration...
</h4>
<h4 class="modal-title"
ng-show="mapped.$hasChanges && validationStatus(validating) == 'failed'">
<i class="fa fa-warning"></i> Configuration Validation Failed
</h4>
<h4 class="modal-title"
ng-show="mapped.$hasChanges && validationStatus(validating) == 'success'">
<i class="fa fa-check-circle"></i> Configuration Validation Succeeded!
</h4>
<h4 class="modal-title" ng-show="!mapped.$hasChanges">
Configuration Changes Saved
<h4 class="modal-title">
Checking your settings
</h4>
</div>
<div class="modal-body" ng-show="!mapped.$hasChanges">
<div class="verified">
<i class="fa fa-check-circle"></i> Configuration Changes Saved
</div>
<p>Your configuration changes have been saved to <code>config.yaml</code> in the mounted config
volume and will be applied the next time the <span class="registry-title"></span> container is restarted.</p>
<p>
<strong>
It is highly recommended that you restart your container now and test these changes!
</strong>
</p>
</div>
<div class="modal-body" ng-show="mapped.$hasChanges">
<div class="modal-body">
<div class="service-verification">
<div class="service-verification-row" ng-repeat="serviceInfo in validating">
<span class="quay-spinner" ng-show="serviceInfo.status == 'validating'"></span>
@ -601,33 +574,49 @@
</div>
</div>
<div class="modal-footer" ng-show="!mapped.$hasChanges">
<button class="btn btn-default" data-dismiss="modal">
Close
<!-- Footer: Saving configuration -->
<div class="modal-footer working" ng-show="savingConfiguration">
<span class="cor-loader-inline"></span> Saving Configuration...
</div>
<!-- Footer: Validating -->
<div class="modal-footer working"
ng-show="!savingConfiguration && validationStatus(validating) == 'validating'">
<span class="cor-loader-inline"></span> Validating settings...
<button class="btn btn-default" ng-click="cancelValidation()">
Stop Validating
</button>
</div>
<div class="modal-footer" ng-show="mapped.$hasChanges">
<span ng-show="validating.length == 0">Please Wait...</span>
<button class="btn btn-default"
ng-show="validationStatus(validating) == 'validating'"
ng-click="cancelValidation()">
Stop Validating
</button>
<!-- Footer: Valid Config -->
<div class="modal-footer"
ng-show="!savingConfiguration && validationStatus(validating) == 'success'">
<span class="left-align">
<i class="fa fa-check"></i>
Configuration Validated
</span>
<button class="btn btn-primary"
ng-show="validationStatus(validating) == 'success'"
ng-click="saveConfiguration()"
ng-disabled="savingConfiguration">
<i class="fa fa-upload" style="margin-right: 10px;"></i>Save Configuration
</button>
<button class="btn btn-default"
ng-show="validationStatus(validating) == 'failed'"
data-dismiss="modal">
Continue Editing Configuration
</div>
<!-- Footer: Invalid Config -->
<div class="modal-footer"
ng-show="!savingConfiguration && validationStatus(validating) == 'failed'">
<span class="left-align">
<i class="fa fa-warning"></i>
Problem Detected
</span>
<button class="btn btn-default" data-dismiss="modal">
Continue Editing
</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -0,0 +1,5 @@
<div class="co-m-inline-loader co-an-fade-in-out">
<div class="co-m-loader-dot__one"></div>
<div class="co-m-loader-dot__two"></div>
<div class="co-m-loader-dot__three"></div>
</div>

View file

@ -0,0 +1,5 @@
<div class="co-m-loader co-an-fade-in-out">
<div class="co-m-loader-dot__one"></div>
<div class="co-m-loader-dot__two"></div>
<div class="co-m-loader-dot__three"></div>
</div>

View file

@ -0,0 +1,3 @@
<div class="co-step-bar">
<span class="transclude" ng-transclude/>
</div>

View file

@ -0,0 +1,6 @@
<span ng-class="text ? 'co-step-element text' : 'co-step-element icon'">
<span data-title="{{ title }}" bs-tooltip>
<span class="text" ng-if="text">{{ text }}</span>
<i class="fa fa-lg" ng-if="icon" ng-class="'fa-' + icon"></i>
</span>
</span>

View file

@ -2225,8 +2225,10 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}).
when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html',
reloadOnSearch: false, controller: UserAdminCtrl}).
when('/superuser/', {title: 'Enterprise Registry Setup', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
when('/superuser/', {title: 'Enterprise Registry Management', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
reloadOnSearch: false, controller: SuperUserAdminCtrl, newLayout: true}).
when('/setup/', {title: 'Enterprise Registry Setup', description:'Setup for ' + title, templateUrl: '/static/partials/setup.html',
reloadOnSearch: false, controller: SetupCtrl, newLayout: true}).
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title,
templateUrl: '/static/partials/guide.html',
controller: GuideCtrl}).
@ -3908,9 +3910,11 @@ quayApp.directive('registryName', function () {
replace: false,
transclude: true,
restrict: 'C',
scope: {},
scope: {
'isShort': '=isShort'
},
controller: function($scope, $element, Config) {
$scope.name = Config.REGISTRY_TITLE;
$scope.name = $scope.isShort ? Config.REGISTRY_TITLE_SHORT : Config.REGISTRY_TITLE;
}
};
return directiveDefinitionObject;
@ -6865,7 +6869,7 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
if (activeTab) {
changeTab(activeTab);
}
}, 100); // 100ms to make sure angular has rendered.
}, 400); // 400ms to make sure angular has rendered.
});
var initallyChecked = false;

View file

@ -2809,346 +2809,6 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
loadApplicationInfo();
}
function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, AngularPollChannel) {
if (!Features.SUPER_USERS) {
return;
}
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
$scope.configStatus = null;
$scope.logsCounter = 0;
$scope.newUser = {};
$scope.createdUser = null;
$scope.systemUsage = null;
$scope.debugServices = null;
$scope.debugLogs = null;
$scope.pollChannel = null;
$scope.logsScrolled = false;
$scope.csrf_token = window.__token;
$scope.showCreateUser = function() {
$scope.createdUser = null;
$('#createUserModal').modal('show');
};
$scope.viewSystemLogs = function(service) {
if ($scope.pollChannel) {
$scope.pollChannel.stop();
}
$scope.debugService = service;
$scope.debugLogs = null;
$scope.pollChannel = AngularPollChannel.create($scope, $scope.loadServiceLogs, 2 * 1000 /* 2s */);
$scope.pollChannel.start();
};
$scope.loadServiceLogs = function(callback) {
if (!$scope.debugService) { return; }
var params = {
'service': $scope.debugService
};
var errorHandler = ApiService.errorDisplay('Cannot load system logs. Please contact support.',
function() {
callback(false);
})
ApiService.getSystemLogs(null, params, /* background */true).then(function(resp) {
$scope.debugLogs = resp['logs'];
callback(true);
}, errorHandler);
};
$scope.loadDebugServices = function() {
if ($scope.pollChannel) {
$scope.pollChannel.stop();
}
$scope.debugService = null;
ApiService.listSystemLogServices().then(function(resp) {
$scope.debugServices = resp['services'];
}, ApiService.errorDisplay('Cannot load system logs. Please contact support.'))
};
$scope.getUsage = function() {
if ($scope.systemUsage) { return; }
ApiService.getSystemUsage().then(function(resp) {
$scope.systemUsage = resp;
}, ApiService.errorDisplay('Cannot load system usage. Please contact support.'))
}
$scope.loadUsageLogs = function() {
$scope.logsCounter++;
};
$scope.loadUsers = function() {
if ($scope.users) {
return;
}
$scope.loadUsersInternal();
};
$scope.loadUsersInternal = function() {
ApiService.listAllUsers().then(function(resp) {
$scope.users = resp['users'];
$scope.showInterface = true;
}, function(resp) {
$scope.users = [];
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
});
};
$scope.showChangePassword = function(user) {
$scope.userToChange = user;
$('#changePasswordModal').modal({});
};
$scope.createUser = function() {
$scope.creatingUser = true;
$scope.createdUser = null;
var errorHandler = ApiService.errorDisplay('Cannot create user', function() {
$scope.creatingUser = false;
$('#createUserModal').modal('hide');
});
ApiService.createInstallUser($scope.newUser, null).then(function(resp) {
$scope.creatingUser = false;
$scope.newUser = {};
$scope.createdUser = resp;
$scope.loadUsersInternal();
}, errorHandler)
};
$scope.showDeleteUser = function(user) {
if (user.username == UserService.currentUser().username) {
bootbox.dialog({
"message": 'Cannot delete yourself!',
"title": "Cannot delete user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
return;
}
$scope.userToDelete = user;
$('#confirmDeleteUserModal').modal({});
};
$scope.changeUserPassword = function(user) {
$('#changePasswordModal').modal('hide');
var params = {
'username': user.username
};
var data = {
'password': user.password
};
ApiService.changeInstallUser(data, params).then(function(resp) {
$scope.loadUsersInternal();
}, ApiService.errorDisplay('Could not change user'));
};
$scope.deleteUser = function(user) {
$('#confirmDeleteUserModal').modal('hide');
var params = {
'username': user.username
};
ApiService.deleteInstallUser(null, params).then(function(resp) {
$scope.loadUsersInternal();
}, ApiService.errorDisplay('Cannot delete user'));
};
$scope.sendRecoveryEmail = function(user) {
var params = {
'username': user.username
};
ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) {
bootbox.dialog({
"message": "A recovery email has been sent to " + resp['email'],
"title": "Recovery email sent",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
}, ApiService.errorDisplay('Cannot send recovery email'))
};
$scope.parseDbUri = function(value) {
if (!value) { return null; }
// Format: mysql+pymysql://<username>:<url escaped password>@<hostname>/<database_name>
var uri = URI(value);
return {
'kind': uri.protocol(),
'username': uri.username(),
'password': uri.password(),
'server': uri.host(),
'database': uri.path() ? uri.path().substr(1) : ''
};
};
$scope.serializeDbUri = function(fields) {
if (!fields['server']) { return '' };
try {
var uri = URI();
uri = uri && uri.host(fields['server']);
uri = uri && uri.protocol(fields['kind']);
uri = uri && uri.username(fields['username']);
uri = uri && uri.password(fields['password']);
uri = uri && uri.path('/' + (fields['database'] || ''));
uri = uri && uri.toString();
} catch (ex) {
return '';
}
return uri;
};
$scope.createSuperUser = function() {
$scope.createSuperuserIssue = null;
$scope.configStep = 'creating-superuser';
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
UserService.load();
$('#createSuperuserModal').modal('hide');
$scope.checkContainerStatus();
}, function(resp) {
$scope.configStep = 'create-superuser';
$scope.createSuperuserIssue = ApiService.getErrorMessage(resp, 'Could not create superuser');
});
};
$scope.checkContainerStatus = function() {
var errorHandler = function(resp) {
if ((resp.status == 404 || resp.status == 502) && $scope.configStep == 'valid-database') {
// Container has not yet come back up, so we schedule another check.
$scope.waitForValidConfig();
return;
}
return ApiService.errorDisplay('Cannot load status. Please report this to support')(resp);
};
ApiService.scRegistryStatus(null, null).then(function(resp) {
$scope.configStatus = resp;
// !dir_exists -> No mounted directory.
if (!$scope.configStatus.dir_exists) {
bootbox.dialog({
"message": "No volume was found mounted at path <code>/conf/stack</code>. " +
"Please rerun the container with the volume mounted and refresh this page." +
"<br><br>For more information: " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Enterprise Registry Setup Guide</a>",
"title": "Missing mounted configuration volume",
"buttons": {},
"closeButton": false
});
return;
}
// is_testing = False -> valid config
// ready = False -> no valid superusers yet
if (!$scope.configStatus.is_testing && !$scope.configStatus.ready) {
$('#initializeConfigModal').modal('hide');
$scope.superUser = {};
$scope.configStep = 'create-superuser';
$('#createSuperuserModal').modal({
keyboard: false,
backdrop: 'static'
});
return;
}
// file_exists -> config file, but possibly invalid DB
// valid_db = False -> invalid DB
// is_testing = True -> still in testing mode
if (!$scope.configStatus.file_exists || !$scope.configStatus.valid_db ||
$scope.configStatus.is_testing) {
$('#createSuperuserModal').modal('hide');
$scope.databaseUri = '';
$scope.configStep = 'enter-database';
// Handle the case where they have entered a valid DB config, refreshed, but have not
// yet restarted the DB container.
if ($scope.configStatus.file_exists && $scope.configStatus.is_testing) {
$scope.waitForValidConfig();
}
$('#initializeConfigModal').modal({
keyboard: false,
backdrop: 'static'
});
return;
}
}, errorHandler, /* background */true);
};
$scope.waitForValidConfig = function() {
$scope.configStep = 'valid-database';
$timeout(function() {
$scope.checkContainerStatus();
}, 3000);
};
$scope.validateDatabase = function() {
$scope.configStep = 'validating-database';
$scope.databaseInvalid = null;
var data = {
'config': {
'DB_URI': $scope.databaseUri
},
'hostname': window.location.host
};
var params = {
'service': 'database'
};
ApiService.scValidateConfig(data, params).then(function(resp) {
var status = resp.status;
if (status) {
$scope.configStep = 'updating-config';
ApiService.scUpdateConfig(data, null).then(function(resp) {
$scope.waitForValidConfig();
}, ApiService.errorDisplay('Cannot update config. Please report this to support'));
} else {
$scope.configStep = 'invalid-database';
$scope.databaseInvalid = resp.reason;
}
}, ApiService.errorDisplay('Cannot validate database. Please report this to support'));
};
// Load the configuration status.
$scope.checkContainerStatus();
}
function TourCtrl($scope, $location) {
$scope.kind = $location.path().substring('/tour/'.length);
}

View file

@ -0,0 +1,281 @@
function SetupCtrl($scope, $timeout, ApiService, Features, UserService, AngularPollChannel, CoreDialog) {
if (!Features.SUPER_USERS) {
return;
}
// Note: The values of the enumeration are important for isStepFamily. For example,
// *all* states under the "configuring db" family must start with "config-db".
$scope.States = {
// Loading the state of the product.
'LOADING': 'loading',
// The configuration directory is missing.
'MISSING_CONFIG_DIR': 'missing-config-dir',
// The config.yaml exists but it is invalid.
'INVALID_CONFIG': 'config-invalid',
// DB is being configured.
'CONFIG_DB': 'config-db',
// DB information is being validated.
'VALIDATING_DB': 'config-db-validating',
// DB information is being saved to the config.
'SAVING_DB': 'config-db-saving',
// A validation error occurred with the database.
'DB_ERROR': 'config-db-error',
// Database is being setup.
'DB_SETUP': 'setup-db',
// Database setup has succeeded.
'DB_SETUP_SUCCESS': 'setup-db-success',
// An error occurred when setting up the database.
'DB_SETUP_ERROR': 'setup-db-error',
// The container is being restarted for the database changes.
'DB_RESTARTING': 'setup-db-restarting',
// A superuser is being configured.
'CREATE_SUPERUSER': 'create-superuser',
// The superuser is being created.
'CREATING_SUPERUSER': 'create-superuser-creating',
// An error occurred when setting up the superuser.
'SUPERUSER_ERROR': 'create-superuser-error',
// The superuser was created successfully.
'SUPERUSER_CREATED': 'create-superuser-created',
// General configuration is being setup.
'CONFIG': 'config',
// The configuration is fully valid.
'VALID_CONFIG': 'valid-config',
// The container is being restarted for the configuration changes.
'CONFIG_RESTARTING': 'config-restarting',
// The product is ready for use.
'READY': 'ready'
}
$scope.csrf_token = window.__token;
$scope.currentStep = $scope.States.LOADING;
$scope.errors = {};
$scope.stepProgress = [];
$scope.$watch('currentStep', function(currentStep) {
$scope.stepProgress = $scope.getProgress(currentStep);
switch (currentStep) {
case $scope.States.CONFIG:
$('#setupModal').modal('hide');
break;
case $scope.States.MISSING_CONFIG_DIR:
$scope.showMissingConfigDialog();
break;
case $scope.States.INVALID_CONFIG:
$scope.showInvalidConfigDialog();
break;
case $scope.States.DB_SETUP:
$scope.performDatabaseSetup();
// Fall-through.
case $scope.States.CREATE_SUPERUSER:
case $scope.States.DB_RESTARTING:
case $scope.States.CONFIG_DB:
case $scope.States.VALID_CONFIG:
case $scope.States.READY:
$('#setupModal').modal({
keyboard: false,
backdrop: 'static'
});
break;
}
});
$scope.showSuperuserPanel = function() {
$('#setupModal').modal('hide');
window.location = '/superuser';
};
$scope.configurationSaved = function() {
$scope.currentStep = $scope.States.VALID_CONFIG;
};
$scope.getProgress = function(step) {
var isStep = $scope.isStep;
var isStepFamily = $scope.isStepFamily;
var States = $scope.States;
return [
isStepFamily(step, States.CONFIG_DB),
isStepFamily(step, States.DB_SETUP),
isStep(step, States.DB_RESTARTING),
isStepFamily(step, States.CREATE_SUPERUSER),
isStep(step, States.CONFIG),
isStep(step, States.VALID_CONFIG),
isStep(step, States.CONFIG_RESTARTING),
isStep(step, States.READY)
];
};
$scope.isStepFamily = function(step, family) {
if (!step) { return false; }
return step.indexOf(family) == 0;
};
$scope.isStep = function(step) {
for (var i = 1; i < arguments.length; ++i) {
if (arguments[i] == step) {
return true;
}
}
return false;
};
$scope.showInvalidConfigDialog = function() {
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
var title = "Invalid configuration file";
CoreDialog.fatal(title, message);
};
$scope.showMissingConfigDialog = function() {
var message = "A volume should be mounted into the container at <code>/conf/stack</code>: " +
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
"<br>Once fixed, restart the container. For more information, " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Read the Setup Guide</a>"
var title = "Missing configuration volume";
CoreDialog.fatal(title, message);
};
$scope.restartContainer = function(restartState) {
$scope.currentStep = restartState;
ApiService.scShutdownContainer(null, null).then(function(resp) {
$timeout(function() {
$scope.checkStatus();
}, 2000);
}, ApiService.errorDisplay('Cannot restart container. Please report this to support.'))
};
$scope.scheduleStatusCheck = function() {
$timeout(function() {
$scope.checkStatus();
}, 3000);
};
$scope.checkStatus = function() {
var errorHandler = function(resp) {
if (resp.status == 404 || resp.status == 502) {
// Container has not yet come back up, so we schedule another check.
$scope.scheduleStatusCheck();
return;
}
return ApiService.errorDisplay('Cannot load status. Please report this to support')(resp);
};
ApiService.scRegistryStatus(null, null).then(function(resp) {
$scope.currentStep = resp['status'];
}, errorHandler, /* background */true);
};
$scope.parseDbUri = function(value) {
if (!value) { return null; }
// Format: mysql+pymysql://<username>:<url escaped password>@<hostname>/<database_name>
var uri = URI(value);
return {
'kind': uri.protocol(),
'username': uri.username(),
'password': uri.password(),
'server': uri.host(),
'database': uri.path() ? uri.path().substr(1) : ''
};
};
$scope.serializeDbUri = function(fields) {
if (!fields['server']) { return '' };
try {
var uri = URI();
uri = uri && uri.host(fields['server']);
uri = uri && uri.protocol(fields['kind']);
uri = uri && uri.username(fields['username']);
uri = uri && uri.password(fields['password']);
uri = uri && uri.path('/' + (fields['database'] || ''));
uri = uri && uri.toString();
} catch (ex) {
return '';
}
return uri;
};
$scope.createSuperUser = function() {
$scope.currentStep = $scope.States.CREATING_SUPERUSER;
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
UserService.load();
$scope.checkStatus();
}, function(resp) {
$scope.currentStep = $scope.States.SUPERUSER_ERROR;
$scope.errors.SuperuserCreationError = ApiService.getErrorMessage(resp, 'Could not create superuser');
});
};
$scope.performDatabaseSetup = function() {
$scope.currentStep = $scope.States.DB_SETUP;
ApiService.scSetupDatabase(null, null).then(function(resp) {
if (resp['error']) {
$scope.currentStep = $scope.States.DB_SETUP_ERROR;
$scope.errors.DatabaseSetupError = resp['error'];
} else {
$scope.currentStep = $scope.States.DB_SETUP_SUCCESS;
}
}, ApiService.errorDisplay('Could not setup database. Please report this to support.'))
};
$scope.validateDatabase = function() {
$scope.currentStep = $scope.States.VALIDATING_DB;
$scope.databaseInvalid = null;
var data = {
'config': {
'DB_URI': $scope.databaseUri
},
'hostname': window.location.host
};
var params = {
'service': 'database'
};
ApiService.scValidateConfig(data, params).then(function(resp) {
var status = resp.status;
if (status) {
$scope.currentStep = $scope.States.SAVING_DB;
ApiService.scUpdateConfig(data, null).then(function(resp) {
$scope.checkStatus();
}, ApiService.errorDisplay('Cannot update config. Please report this to support'));
} else {
$scope.currentStep = $scope.States.DB_ERROR;
$scope.errors.DatabaseValidationError = resp.reason;
}
}, ApiService.errorDisplay('Cannot validate database. Please report this to support'));
};
// Load the initial status.
$scope.checkStatus();
}

View file

@ -0,0 +1,205 @@
function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, AngularPollChannel, CoreDialog) {
if (!Features.SUPER_USERS) {
return;
}
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
$scope.configStatus = null;
$scope.logsCounter = 0;
$scope.newUser = {};
$scope.createdUser = null;
$scope.systemUsage = null;
$scope.debugServices = null;
$scope.debugLogs = null;
$scope.pollChannel = null;
$scope.logsScrolled = false;
$scope.csrf_token = window.__token;
$scope.showCreateUser = function() {
$scope.createdUser = null;
$('#createUserModal').modal('show');
};
$scope.viewSystemLogs = function(service) {
if ($scope.pollChannel) {
$scope.pollChannel.stop();
}
$scope.debugService = service;
$scope.debugLogs = null;
$scope.pollChannel = AngularPollChannel.create($scope, $scope.loadServiceLogs, 2 * 1000 /* 2s */);
$scope.pollChannel.start();
};
$scope.loadServiceLogs = function(callback) {
if (!$scope.debugService) { return; }
var params = {
'service': $scope.debugService
};
var errorHandler = ApiService.errorDisplay('Cannot load system logs. Please contact support.',
function() {
callback(false);
})
ApiService.getSystemLogs(null, params, /* background */true).then(function(resp) {
$scope.debugLogs = resp['logs'];
callback(true);
}, errorHandler);
};
$scope.loadDebugServices = function() {
if ($scope.pollChannel) {
$scope.pollChannel.stop();
}
$scope.debugService = null;
ApiService.listSystemLogServices().then(function(resp) {
$scope.debugServices = resp['services'];
}, ApiService.errorDisplay('Cannot load system logs. Please contact support.'))
};
$scope.getUsage = function() {
if ($scope.systemUsage) { return; }
ApiService.getSystemUsage().then(function(resp) {
$scope.systemUsage = resp;
}, ApiService.errorDisplay('Cannot load system usage. Please contact support.'))
}
$scope.loadUsageLogs = function() {
$scope.logsCounter++;
};
$scope.loadUsers = function() {
if ($scope.users) {
return;
}
$scope.loadUsersInternal();
};
$scope.loadUsersInternal = function() {
ApiService.listAllUsers().then(function(resp) {
$scope.users = resp['users'];
$scope.showInterface = true;
}, function(resp) {
$scope.users = [];
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
});
};
$scope.showChangePassword = function(user) {
$scope.userToChange = user;
$('#changePasswordModal').modal({});
};
$scope.createUser = function() {
$scope.creatingUser = true;
$scope.createdUser = null;
var errorHandler = ApiService.errorDisplay('Cannot create user', function() {
$scope.creatingUser = false;
$('#createUserModal').modal('hide');
});
ApiService.createInstallUser($scope.newUser, null).then(function(resp) {
$scope.creatingUser = false;
$scope.newUser = {};
$scope.createdUser = resp;
$scope.loadUsersInternal();
}, errorHandler)
};
$scope.showDeleteUser = function(user) {
if (user.username == UserService.currentUser().username) {
bootbox.dialog({
"message": 'Cannot delete yourself!',
"title": "Cannot delete user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
return;
}
$scope.userToDelete = user;
$('#confirmDeleteUserModal').modal({});
};
$scope.changeUserPassword = function(user) {
$('#changePasswordModal').modal('hide');
var params = {
'username': user.username
};
var data = {
'password': user.password
};
ApiService.changeInstallUser(data, params).then(function(resp) {
$scope.loadUsersInternal();
}, ApiService.errorDisplay('Could not change user'));
};
$scope.deleteUser = function(user) {
$('#confirmDeleteUserModal').modal('hide');
var params = {
'username': user.username
};
ApiService.deleteInstallUser(null, params).then(function(resp) {
$scope.loadUsersInternal();
}, ApiService.errorDisplay('Cannot delete user'));
};
$scope.sendRecoveryEmail = function(user) {
var params = {
'username': user.username
};
ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) {
bootbox.dialog({
"message": "A recovery email has been sent to " + resp['email'],
"title": "Recovery email sent",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
}, ApiService.errorDisplay('Cannot send recovery email'))
};
$scope.checkStatus = function() {
ApiService.scRegistryStatus(null, null).then(function(resp) {
$scope.configStatus = resp['status'];
if ($scope.configStatus == 'ready') {
$scope.loadUsers();
} else {
var message = "Installation of this product has not yet been completed." +
"<br><br>Please read the " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Setup Guide</a>"
var title = "Installation Incomplete";
CoreDialog.fatal(title, message);
}
}, ApiService.errorDisplay('Cannot load status. Please report this to support'), /* background */true);
};
// Load the initial status.
$scope.checkStatus();
}

View file

@ -7,7 +7,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
transclude: true,
restrict: 'C',
scope: {
'isActive': '=isActive'
'isActive': '=isActive',
'configurationSaved': '&configurationSaved'
},
controller: function($rootScope, $scope, $element, $timeout, ApiService) {
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$';
@ -166,6 +167,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
$scope.saveConfiguration = function() {
$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 = {
'config': $scope.config,
'hostname': window.location.host
@ -173,7 +178,9 @@ angular.module("core-config-setup", ['angularFileUpload'])
ApiService.scUpdateConfig(data).then(function(resp) {
$scope.savingConfiguration = false;
$scope.mapped.$hasChanges = false
$scope.mapped.$hasChanges = false;
$('#validateAndSaveModal').modal('hide');
$scope.configurationSaved({});
}, ApiService.errorDisplay('Could not save configuration. Please report this error.'));
};

View file

@ -1,4 +1,19 @@
angular.module("core-ui", [])
.factory('CoreDialog', [function() {
var service = {};
service['fatal'] = function(title, message) {
bootbox.dialog({
"title": title,
"message": "<div class='alert-icon-container-container'><div class='alert-icon-container'><div class='alert-icon'></div></div></div>" + message,
"buttons": {},
"className": "co-dialog fatal-error",
"closeButton": false
});
};
return service;
}])
.directive('corLogBox', function() {
var directiveDefinitionObject = {
priority: 1,
@ -210,7 +225,7 @@ angular.module("core-ui", [])
$scope.$on('$destroy', function() {
$(window).off("resize", handler);
$(window).off("scroll", handler);
$internval.stop(stop);
$interval.cancel(stop);
});
}
};
@ -218,6 +233,32 @@ angular.module("core-ui", [])
})
.directive('corLoaderInline', function() {
var directiveDefinitionObject = {
templateUrl: '/static/directives/cor-loader-inline.html',
replace: true,
restrict: 'C',
scope: {
},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corLoader', function() {
var directiveDefinitionObject = {
templateUrl: '/static/directives/cor-loader.html',
replace: true,
restrict: 'C',
scope: {
},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corTab', function() {
var directiveDefinitionObject = {
priority: 4,
@ -235,4 +276,54 @@ angular.module("core-ui", [])
}
};
return directiveDefinitionObject;
})
.directive('corStep', function() {
var directiveDefinitionObject = {
priority: 4,
templateUrl: '/static/directives/cor-step.html',
replace: true,
transclude: false,
requires: '^corStepBar',
restrict: 'C',
scope: {
'icon': '@icon',
'title': '@title',
'text': '@text'
},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corStepBar', function() {
var directiveDefinitionObject = {
priority: 4,
templateUrl: '/static/directives/cor-step-bar.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'progress': '=progress'
},
controller: function($rootScope, $scope, $element) {
$scope.$watch('progress', function(progress) {
var index = 0;
for (var i = 0; i < progress.length; ++i) {
if (progress[i]) {
index = i;
}
}
$element.find('.transclude').children('.co-step-element').each(function(i, elem) {
$(elem).removeClass('active');
if (i <= index) {
$(elem).addClass('active');
}
});
});
}
};
return directiveDefinitionObject;
});

289
static/partials/setup.html Normal file
View file

@ -0,0 +1,289 @@
<div>
<div class="cor-loader" ng-show="currentStep == States.LOADING"></div>
<div class="page-content" quay-show="Features.SUPER_USERS && currentStep == States.CONFIG">
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">Enterprise Registry Setup</span>
</div>
<div class="cor-tab-panel" style="padding: 20px;">
<div class="co-alert alert alert-info">
<span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span>
<span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span>
</span>
<div><strong>Almost done!</strong></div>
<div>Configure your Redis database and other settings below</div>
</div>
<div class="config-setup-tool" is-active="isStep(currentStep, States.CONFIG)"
configuration-saved="configurationSaved()"></div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="co-dialog modal fade initial-setup-modal" id="setupModal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Header -->
<div class="modal-header">
<span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span>
<span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span>
</span>
<h4 class="modal-title"><span><span class="registry-name" is-short="true"></span> Setup</h4>
</div>
<form id="superuserForm" name="superuserForm" ng-submit="createSuperUser()">
<!-- Content: CREATE_SUPERUSER or SUPERUSER_ERROR or CREATING_SUPERUSER -->
<div class="modal-body config-setup-tool-element" style="padding: 20px"
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR, States.CREATING_SUPERUSER)">
<p>A superuser is the main administrator of your <span class="registry-name" is-short="true"></span>. Only superusers can edit configuration settings.</p>
<div class="form-group">
<label>Username</label>
<input class="form-control" type="text" ng-model="superUser.username"
ng-pattern="/^[a-z0-9_]{4,30}$/" required>
<div class="help-text">Minimum 4 characters in length</div>
</div>
<div class="form-group">
<label>Email address</label>
<input class="form-control" type="email" ng-model="superUser.email" required>
</div>
<div class="form-group">
<label>Password</label>
<input class="form-control" type="password" ng-model="superUser.password"
ng-pattern="/^[^\s]+$/"
ng-minlength="8" required>
<div class="help-text">Minimum 8 characters in length</div>
</div>
<div class="form-group">
<label>Repeat Password</label>
<input class="form-control" type="password" ng-model="superUser.repeatPassword"
match="superUser.password" required>
</div>
</div>
<!-- Footer: CREATE_SUPERUSER or SUPERUSER_ERROR -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR)">
<button type="submit" class="btn btn-primary" ng-disabled="!superuserForm.$valid">
Create Super User
</button>
</div>
</form>
<!-- Content: DB_RESTARTING or CONFIG_RESTARTING -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
<i class="fa fa-lg fa-refresh" style="margin-right: 10px;"></i>
<span class="registry-name"></span> is currently being restarted.
<br><br>
This can take several minutes. If the container does not restart on its own,
please reexecute the <code>docker run</code> command.
</div>
<!-- Content: READY -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.READY)">
Installation and setup of <span class="registry-name"></span> is complete. You can
now invite users to join, create organizations and start pushing and pulling
repositories.
</div>
<!-- Content: VALID_CONFIG -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.VALID_CONFIG)">
All configuration has been validated and saved. The container must be restarted to
apply the configuration changes.
</div>
<!-- Content: DB_SETUP_SUCCESS -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
The database has been setup and is ready. The container must be restarted to
apply the configuration changes.
</div>
<!-- Content: DB_SETUP or DB_SETUP_ERROR -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_SETUP, States.DB_SETUP_ERROR)">
<i class="fa fa-lg fa-database" style="margin-right: 10px;"></i>
<span class="registry-name"></span> is currently setting up its database
schema.
<br><br>
This can take several minutes.
</div>
<!-- Content: CONFIG_DB or DB_ERROR or VALIDATING_DB or SAVING_DB -->
<div class="modal-body validate-database config-setup-tool-element"
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR, States.VALIDATING_DB, States.SAVING_DB)">
<p>
Please enter the connection details for your <strong>empty</strong> database. The schema will be created in the following step.</p>
</p>
<div class="config-parsed-field" binding="databaseUri"
parser="parseDbUri(value)"
serializer="serializeDbUri(fields)">
<table class="config-table">
<tr>
<td class="non-input">Database Type:</td>
<td>
<select ng-model="fields.kind">
<option value="mysql+pymysql">MySQL</option>
<option value="postgresql">Postgres</option>
</select>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Database Server:</td>
<td>
<span class="config-string-field" binding="fields.server"
placeholder="dbserverhost"></span>
<div class="help-text">
The server (and optionally, custom port) where the database lives
</div>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Username:</td>
<td>
<span class="config-string-field" binding="fields.username"
placeholder="someuser"></span>
<div class="help-text">This user must have <strong>full access</strong> to the database</div>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Password:</td>
<td>
<input class="form-control" type="password" ng-model="fields.password"></span>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Database Name:</td>
<td>
<span class="config-string-field" binding="fields.database"
placeholder="registry-database"></span>
</td>
</tr>
</table>
</div>
</div>
<!-- Footer: CREATING_SUPERUSER -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.CREATING_SUPERUSER)">
<span class="cor-loader-inline"></span> Creating superuser...
</div>
<!-- Footer: SUPERUSER_ERROR -->
<div class="modal-footer alert alert-warning"
ng-show="isStep(currentStep, States.SUPERUSER_ERROR)">
{{ errors.SuperuserCreationError }}
</div>
<!-- Footer: DB_SETUP_ERROR -->
<div class="modal-footer alert alert-warning"
ng-show="isStep(currentStep, States.DB_SETUP_ERROR)">
Database Setup Failed. Please report this to support: {{ errors.DatabaseSetupError }}
</div>
<!-- Footer: DB_ERROR -->
<div class="modal-footer alert alert-warning" ng-show="isStep(currentStep, States.DB_ERROR)">
Database Validation Issue: {{ errors.DatabaseValidationError }}
</div>
<!-- Footer: CONFIG_DB or DB_ERROR -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR)">
<span class="left-align" ng-show="isStep(currentStep, States.DB_ERROR)">
<i class="fa fa-warning"></i>
Problem Detected
</span>
<button type="submit" class="btn btn-primary" ng-disabled="!databaseUri"
ng-click="validateDatabase()">
Validate Database Settings
</button>
</div>
<!-- Footer: READY -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.READY)">
<span class="left-align">
<i class="fa fa-check"></i>
Installation Complete!
</span>
<a href="javascript:void(0)" ng-click="showSuperuserPanel()" class="btn btn-primary">
View Superuser Panel
</a>
</div>
<!-- Footer: VALID_CONFIG -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.VALID_CONFIG)">
<span class="left-align">
<i class="fa fa-check"></i>
Configuration Validated and Saved
</span>
<button type="submit" class="btn btn-primary"
ng-click="restartContainer(States.CONFIG_RESTARTING)">
Restart Container
</button>
</div>
<!-- Footer: DB_SETUP_SUCCESS -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
<span class="left-align">
<i class="fa fa-check"></i>
Database Setup and Ready
</span>
<button type="submit" class="btn btn-primary"
ng-click="restartContainer(States.DB_RESTARTING)">
Restart Container
</button>
</div>
<!-- Footer: DB_SETUP -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.DB_SETUP)">
<span class="cor-loader-inline"></span> Setting up database...
</div>
<!-- Footer: SAVING_DB -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.SAVING_DB)">
<span class="cor-loader-inline"></span> Saving database configuration...
</div>
<!-- Footer: VALIDATING_DB -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.VALIDATING_DB)">
<span class="cor-loader-inline"></span> Testing database settings...
</div>
<!-- Footer: DB_RESTARTING or CONFIG_RESTARTING-->
<div class="modal-footer working"
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
<span class="cor-loader-inline"></span> Waiting for container to restart...
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -1,6 +1,6 @@
<div>
<div class="quay-spinner" ng-show="!configStatus"></div>
<div class="page-content" quay-show="Features.SUPER_USERS && configStatus.ready">
<div class="cor-loader" ng-show="!configStatus"></div>
<div class="page-content" quay-show="Features.SUPER_USERS && configStatus == 'ready'">
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">Enterprise Registry Management</span>
@ -8,11 +8,8 @@
<div class="cor-tab-panel">
<div class="cor-tabs">
<span class="cor-tab" tab-active="true" tab-title="Registry Settings" tab-target="#setup"
tab-init="loadConfig()">
<i class="fa fa-cog"></i>
</span>
<span class="cor-tab" tab-title="Manage Users" tab-target="#users" tab-init="loadUsers()">
<span class="cor-tab" tab-active="true" tab-title="Manage Users"
tab-target="#users" tab-init="loadUsers()">
<i class="fa fa-group"></i>
</span>
<span class="cor-tab" tab-title="Container Usage" tab-target="#usage-counter" tab-init="getUsage()">
@ -24,17 +21,21 @@
<span class="cor-tab" tab-title="Internal Logs and Debugging" tab-target="#debug" tab-init="loadDebugServices()">
<i class="fa fa-bug"></i>
</span>
<span class="cor-tab" tab-title="Registry Settings" tab-target="#setup"
tab-init="loadConfig()">
<i class="fa fa-cog"></i>
</span>
</div> <!-- /cor-tabs -->
<div class="cor-tab-content">
<!-- Setup tab -->
<div id="setup" class="tab-pane active">
<div class="config-setup-tool" is-active="configStatus.ready"></div>
<div id="setup" class="tab-pane">
<div class="config-setup-tool" is-active="configStatus == 'ready'"></div>
</div>
<!-- Debugging tab -->
<div id="debug" class="tab-pane">
<div class="quay-spinner" ng-show="!debugServices"></div>
<div class="cor-loader" ng-show="!debugServices"></div>
<div role="tabpanel" ng-show="debugServices">
<!-- Nav tabs -->
@ -65,7 +66,7 @@
<!-- Usage tab -->
<div id="usage-counter" class="tab-pane">
<div class="quay-spinner" ng-show="systemUsage == null"></div>
<div class="cor-loader" ng-show="systemUsage == null"></div>
<div class="usage-chart" total="systemUsage.allowed" limit="systemUsageLimit"
current="systemUsage.usage" usage-title="Deployed Containers"></div>
@ -88,9 +89,9 @@
For more information: <a href="https://coreos.com/products/enterprise-registry/plans/">See Here</a>.
</div> <!-- /usage-counter tab-->
<!-- Users tab -->
<div id="users" class="tab-pane">
<div class="quay-spinner" ng-show="!users"></div>
<!-- Users tab -->
<div id="users" class="tab-pane active">
<div class="cor-loader" ng-show="!users"></div>
<div class="alert alert-error" ng-show="usersError">
{{ usersError }}
</div>
@ -207,7 +208,7 @@
</table>
</div>
<div class="modal-body" ng-show="creatingUser">
<div class="quay-spinner"></div>
<div class="cor-loader"></div>
</div>
<div class="modal-body" ng-show="!creatingUser && !createdUser">
<div class="form-group">
@ -263,176 +264,4 @@
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div> <!-- /page-content -->
<!-- Modal message dialog -->
<div class="modal fade initial-setup-modal" id="createSuperuserModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title"><span><span class="registry-name"></span> Setup</h4></span>
</div>
<div class="modal-body config-setup-tool-element" ng-show="configStep == 'creating-superuser'">
Creating super user account.... Please Wait
</div>
<form id="superuserForm" name="superuserForm" ng-submit="createSuperUser()">
<div class="modal-body config-setup-tool-element" ng-show="configStep == 'create-superuser'">
<p>A super user account is required to manage the <span class="registry-name"></span>
installation. Please enter details for the new account below.</p>
<div class="form-group">
<label>Username</label>
<input class="form-control" type="text" ng-model="superUser.username"
ng-pattern="/^[a-z0-9_]{4,30}$/" required>
<div class="help-text">Minimum 4 characters in length</div>
</div>
<div class="form-group">
<label>Email address</label>
<input class="form-control" type="email" ng-model="superUser.email" required>
</div>
<div class="form-group">
<label>Password</label>
<input class="form-control" type="password" ng-model="superUser.password"
ng-pattern="/^[^\s]+$/"
ng-minlength="8" required>
<div class="help-text">Minimum 8 characters in length</div>
</div>
<div class="form-group">
<label>Repeat Password</label>
<input class="form-control" type="password" ng-model="superUser.repeatPassword"
match="superUser.password" required>
</div>
</div>
<div class="modal-footer alert alert-warning" ng-show="createSuperuserIssue">
{{ createSuperuserIssue }}
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" ng-disabled="!superuserForm.$valid"
ng-show="configStep == 'create-superuser'">
Create Super User
</button>
<span class="modal-body config-setup-tool-element" ng-show="configStep == 'creating-superuser'">
<span class="quay-spinner"></span>
Creating account...
</span>
</div>
</form>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade initial-setup-modal" id="initializeConfigModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title"><span><span class="registry-name"></span> Setup</h4></span>
</div>
<div class="modal-body config-setup-tool-element valid-database" ng-show="configStep == 'valid-database'">
<div class="verified">
<i class="fa fa-check-circle"></i> Your database has been verified as working.
</div>
<p>
<strong>Please restart the <span class="registry-name"></span> container</strong>, which will automatically generate the database's schema.
</p>
<p>This operation may take a few minutes.</p>
</div>
<div class="modal-body config-setup-tool-element" ng-show="configStep == 'updating-config'">
Updating Configuration.... Please Wait
</div>
<div class="modal-body config-setup-tool-element" ng-show="configStep == 'validating-database'">
Validating Database.... Please Wait
</div>
<div class="modal-body config-setup-tool-element"
ng-show="configStep == 'enter-database' || configStep == 'invalid-database'">
<div class="alert alert-warning" ng-show="configStatus.has_file">
Could not connect to or validate the database configuration found. Please reconfigure to continue.
</div>
<p>
Please enter the connection details for your <strong>empty</strong> database. The schema will be created in the following step.</p>
</p>
<div class="config-parsed-field" binding="databaseUri"
parser="parseDbUri(value)"
serializer="serializeDbUri(fields)">
<table class="config-table">
<tr>
<td class="non-input">Database Type:</td>
<td>
<select ng-model="fields.kind">
<option value="mysql+pymysql">MySQL</option>
<option value="postgresql">Postgres</option>
</select>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Database Server:</td>
<td>
<span class="config-string-field" binding="fields.server"
placeholder="dbserverhost"></span>
<div class="help-text">
The server (and optionally, custom port) where the database lives
</div>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Database Name:</td>
<td>
<span class="config-string-field" binding="fields.database"
placeholder="registry-database"></span>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Username:</td>
<td>
<span class="config-string-field" binding="fields.username"
placeholder="someuser"></span>
<div class="help-text">This user must have <strong>full access</strong> to the database</div>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Password:</td>
<td>
<input class="form-control" type="password" ng-model="fields.password"></span>
</td>
</tr>
</table>
</div>
</div>
<div class="modal-footer alert alert-warning" ng-show="databaseInvalid">
Database Validation Issue: {{ databaseInvalid }}
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" ng-disabled="!databaseUri"
ng-click="validateDatabase()"
ng-show="configStep == 'enter-database' || configStep == 'invalid-database'">
Confirm Database
</button>
<span class="modal-body config-setup-tool-element" ng-show="configStep == 'validating-database'">
<span class="quay-spinner"></span>
Validating Database...
</span>
<span class="modal-body config-setup-tool-element" ng-show="configStep == 'updating-config'">
<span class="quay-spinner"></span>
Updating Configuration...
</span>
<span class="modal-body config-setup-tool-element" ng-show="configStep == 'valid-database'">
<span class="quay-spinner"></span>
Waiting For Updated Container...
</span>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -21,10 +21,7 @@ class TestSuperUserRegistryStatus(ApiTestCase):
def test_registry_status(self):
with ConfigForTesting():
json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertTrue(json['is_testing'])
self.assertTrue(json['valid_db'])
self.assertFalse(json['file_exists'])
self.assertFalse(json['ready'])
self.assertEquals('config-db', json['status'])
class TestSuperUserConfigFile(ApiTestCase):

View file

@ -61,6 +61,12 @@ class BaseProvider(object):
"""
raise NotImplementedError
def requires_restart(self, app_config):
""" If true, the configuration loaded into memory for the app does not match that on disk,
indicating that this container requires a restart.
"""
raise NotImplementedError
class FileConfigProvider(BaseProvider):
""" Implementation of the config provider that reads the data from the file system. """
@ -104,6 +110,16 @@ class FileConfigProvider(BaseProvider):
def save_volume_file(self, filename, flask_file):
flask_file.save(os.path.join(self.config_volume, filename))
def requires_restart(self, app_config):
file_config = self.get_yaml()
if not file_config:
return False
for key in file_config:
if app_config.get(key) != file_config[key]:
return True
return False
class TestConfigProvider(BaseProvider):
""" Implementation of the config provider for testing. Everything is kept in-memory instead on
@ -136,6 +152,9 @@ class TestConfigProvider(BaseProvider):
def save_volume_file(self, filename, flask_file):
self.files[filename] = ''
def requires_restart(self, app_config):
return False
def reset_for_test(self):
self._config['SUPER_USERS'] = ['devtable']
self.files = {}