Implement new step-by-step setup
This commit is contained in:
parent
28d319ad26
commit
c8229b9c8a
20 changed files with 1393 additions and 599 deletions
|
@ -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
20
data/runmigration.py
Normal 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()
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 -->
|
||||
|
|
5
static/directives/cor-loader-inline.html
Normal file
5
static/directives/cor-loader-inline.html
Normal 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>
|
5
static/directives/cor-loader.html
Normal file
5
static/directives/cor-loader.html
Normal 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>
|
3
static/directives/cor-step-bar.html
Normal file
3
static/directives/cor-step-bar.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="co-step-bar">
|
||||
<span class="transclude" ng-transclude/>
|
||||
</div>
|
6
static/directives/cor-step.html
Normal file
6
static/directives/cor-step.html
Normal 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>
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
281
static/js/controllers/setup.js
Normal file
281
static/js/controllers/setup.js
Normal 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();
|
||||
}
|
205
static/js/controllers/superuser.js
Normal file
205
static/js/controllers/superuser.js
Normal 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();
|
||||
}
|
|
@ -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.'));
|
||||
};
|
||||
|
||||
|
|
|
@ -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
289
static/partials/setup.html
Normal 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 -->
|
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
Reference in a new issue