Get end-to-end configuration setup working, including verification (except for Github, which is in progress)
This commit is contained in:
parent
825455ea6c
commit
63504c87fb
14 changed files with 611 additions and 206 deletions
8
app.py
8
app.py
|
@ -19,7 +19,7 @@ from util.exceptionlog import Sentry
|
||||||
from util.queuemetrics import QueueMetrics
|
from util.queuemetrics import QueueMetrics
|
||||||
from util.names import urn_generator
|
from util.names import urn_generator
|
||||||
from util.oauth import GoogleOAuthConfig, GithubOAuthConfig
|
from util.oauth import GoogleOAuthConfig, GithubOAuthConfig
|
||||||
from util.configutil import import_yaml, generate_secret_key
|
from util.config.configutil import import_yaml, generate_secret_key
|
||||||
from data.billing import Billing
|
from data.billing import Billing
|
||||||
from data.buildlogs import BuildLogs
|
from data.buildlogs import BuildLogs
|
||||||
from data.archivedlogs import LogArchive
|
from data.archivedlogs import LogArchive
|
||||||
|
@ -124,9 +124,9 @@ queue_metrics = QueueMetrics(app)
|
||||||
authentication = UserAuthentication(app)
|
authentication = UserAuthentication(app)
|
||||||
userevents = UserEventsBuilderModule(app)
|
userevents = UserEventsBuilderModule(app)
|
||||||
|
|
||||||
github_login = GithubOAuthConfig(app, 'GITHUB_LOGIN_CONFIG')
|
github_login = GithubOAuthConfig(app.config, 'GITHUB_LOGIN_CONFIG')
|
||||||
github_trigger = GithubOAuthConfig(app, 'GITHUB_TRIGGER_CONFIG')
|
github_trigger = GithubOAuthConfig(app.config, 'GITHUB_TRIGGER_CONFIG')
|
||||||
google_login = GoogleOAuthConfig(app, 'GOOGLE_LOGIN_CONFIG')
|
google_login = GoogleOAuthConfig(app.config, 'GOOGLE_LOGIN_CONFIG')
|
||||||
oauth_apps = [github_login, github_trigger, google_login]
|
oauth_apps = [github_login, github_trigger, google_login]
|
||||||
|
|
||||||
tf = app.config['DB_TRANSACTION_FACTORY']
|
tf = app.config['DB_TRANSACTION_FACTORY']
|
||||||
|
|
|
@ -9,18 +9,17 @@ from endpoints.api import (ApiResource, nickname, resource, internal_only, show_
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
from app import app, OVERRIDE_CONFIG_YAML_FILENAME, OVERRIDE_CONFIG_DIRECTORY
|
from app import app, OVERRIDE_CONFIG_YAML_FILENAME, OVERRIDE_CONFIG_DIRECTORY
|
||||||
from data import model
|
from data import model
|
||||||
from data.database import User, validate_database_url
|
|
||||||
from auth.permissions import SuperUserPermission
|
from auth.permissions import SuperUserPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from util.configutil import (import_yaml, export_yaml, add_enterprise_config_defaults,
|
from data.database import User
|
||||||
set_config_value)
|
from util.config.configutil import (import_yaml, export_yaml, add_enterprise_config_defaults,
|
||||||
|
set_config_value)
|
||||||
|
from util.config.validator import validate_service_for_config, SSL_FILENAMES
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_FILE_WHITELIST = ['ssl.key', 'ssl.cert']
|
|
||||||
|
|
||||||
def database_is_valid():
|
def database_is_valid():
|
||||||
try:
|
try:
|
||||||
User.select().limit(1)
|
User.select().limit(1)
|
||||||
|
@ -131,7 +130,7 @@ class SuperUserConfigFile(ApiResource):
|
||||||
@nickname('scConfigFileExists')
|
@nickname('scConfigFileExists')
|
||||||
def get(self, filename):
|
def get(self, filename):
|
||||||
""" Returns whether the configuration file with the given name exists. """
|
""" Returns whether the configuration file with the given name exists. """
|
||||||
if not filename in CONFIG_FILE_WHITELIST:
|
if not filename in SSL_FILENAMES:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -260,19 +259,6 @@ class SuperUserConfigValidate(ApiResource):
|
||||||
# this is also safe since this method does not access any information not given in the request.
|
# this is also safe since this method does not access any information not given in the request.
|
||||||
if not os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME) or SuperUserPermission().can():
|
if not os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME) or SuperUserPermission().can():
|
||||||
config = request.get_json()['config']
|
config = request.get_json()['config']
|
||||||
if service == 'database':
|
return validate_service_for_config(service, config)
|
||||||
try:
|
|
||||||
validate_database_url(config['DB_URI'])
|
|
||||||
return {
|
|
||||||
'status': True
|
|
||||||
}
|
|
||||||
except Exception as ex:
|
|
||||||
logger.exception('Could not validate database')
|
|
||||||
return {
|
|
||||||
'status': False,
|
|
||||||
'reason': str(ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
|
@ -231,6 +231,31 @@
|
||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-contact-field {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-contact-field .dropdown button {
|
||||||
|
width: 100px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-contact-field .dropdown button .caret {
|
||||||
|
float: right;
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-contact-field .dropdown button i.fa {
|
||||||
|
margin-right: 6px;
|
||||||
|
width: 14px;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-contact-field .form-control {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
.config-list-field-element .empty {
|
.config-list-field-element .empty {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
@ -338,4 +363,64 @@
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.co-floating-bottom-bar {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-floating-bottom-bar.floating {
|
||||||
|
position: fixed;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-setup-tool .service-verification {
|
||||||
|
padding: 20px;
|
||||||
|
background: #343434;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: -14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-setup-tool .service-verification-row {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-setup-tool .service-verification-row .service-title {
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-size: 145%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateAndSaveModal .fa-warning {
|
||||||
|
font-size: 22px;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: rgb(255, 186, 53);
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateAndSaveModal .fa-check-circle {
|
||||||
|
font-size: 22px;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: rgb(53, 186, 53);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-setup-tool .service-verification-error {
|
||||||
|
white-space: pre;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 36px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #797979;
|
||||||
|
background: black;
|
||||||
|
padding: 6px;
|
||||||
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
|
@ -4907,12 +4907,12 @@ i.slack-icon {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.initial-setup-modal .valid-database .verified {
|
.verified {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.initial-setup-modal .valid-database .verified i.fa {
|
.verified i.fa {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -4923,8 +4923,4 @@ i.slack-icon {
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.config-contact-field .form-control {
|
|
||||||
width: 350px;
|
|
||||||
}
|
}
|
|
@ -5,10 +5,10 @@
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
||||||
<span ng-switch="kind">
|
<span ng-switch="kind">
|
||||||
<span ng-switch-when="mailto"><i class="fa fa-envelope"></i></span>
|
<span ng-switch-when="mailto"><i class="fa fa-envelope"></i>E-mail</span>
|
||||||
<span ng-switch-when="irc"><i class="fa fa-comment"></i></span>
|
<span ng-switch-when="irc"><i class="fa fa-comment"></i>IRC</span>
|
||||||
<span ng-switch-when="tel"><i class="fa fa-phone"></i></span>
|
<span ng-switch-when="tel"><i class="fa fa-phone"></i>Phone</span>
|
||||||
<span ng-switch-default><i class="fa fa-ticket"></i></span>
|
<span ng-switch-default><i class="fa fa-ticket"></i>URL</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -44,23 +44,6 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>Build Support:</td>
|
|
||||||
<td colspan="2">
|
|
||||||
<div class="co-checkbox">
|
|
||||||
<input id="ftbs" type="checkbox" ng-model="config.FEATURE_BUILD_SUPPORT">
|
|
||||||
<label for="ftbs">Enable Dockerfile Build</label>
|
|
||||||
</div>
|
|
||||||
<div class="help-text">
|
|
||||||
If enabled, users can submit Dockerfiles to be built and pushed by the Enterprise Registry.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 10px">
|
|
||||||
<strong>Note: Build workers are required for this feature.</strong>
|
|
||||||
See <a href="https://coreos.com/docs/enterprise-registry/build-support/" target="_blank">Adding Build Workers</a> for instructions on how to setup build workers.
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,9 +138,6 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<div class="co-panel-button-bar">
|
|
||||||
<button class="btn btn-default"><i class="fa fa-sign-in"></i> Test Configuration</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /Redis -->
|
</div> <!-- /Redis -->
|
||||||
|
|
||||||
|
@ -186,122 +166,27 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Storage Path -->
|
<!-- Fields -->
|
||||||
<tr>
|
<tr ng-repeat="field in STORAGE_CONFIG_FIELDS[config.DISTRIBUTED_STORAGE_CONFIG.local[0]]">
|
||||||
<td>Storage Path:</td>
|
<td>{{ field.title }}:</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="config-string-field"
|
<span class="config-string-field"
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].storage_path"
|
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]"
|
||||||
placeholder="Path under the volume or bucket"></span>
|
placeholder="{{ field.placeholder }}"
|
||||||
</td>
|
ng-if="field.kind == 'text'"></span>
|
||||||
</tr>
|
<div class="co-checkbox" ng-if="field.kind == 'bool'">
|
||||||
|
<input id="dsc-{{ field.name }}" type="checkbox"
|
||||||
<!-- S3 -->
|
ng-model="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]">
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'S3Storage'">
|
<label for="dsc-{{ field.name }}">{{ field.placeholder }}</label>
|
||||||
<td>Access Key:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].s3_access_key"
|
|
||||||
placeholder="AWS access key"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'S3Storage'">
|
|
||||||
<td>Secret Key:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].s3_secret_key"
|
|
||||||
placeholder="AWS secret key"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'S3Storage'">
|
|
||||||
<td>Bucket Name:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].s3_bucket"
|
|
||||||
placeholder="S3 bucket name"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- GCS -->
|
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'GoogleCloudStorage'">
|
|
||||||
<td>Access Key:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].access_key"
|
|
||||||
placeholder="GCS access key"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'GoogleCloudStorage'">
|
|
||||||
<td>Secret Key:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].secret_key"
|
|
||||||
placeholder="GCS secret key"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'GoogleCloudStorage'">
|
|
||||||
<td>Bucket Name:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].bucket_name"
|
|
||||||
placeholder="GCS bucket name"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- RADOS -->
|
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'RadosGWStorage'">
|
|
||||||
<td>Hostname:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].hostname"
|
|
||||||
placeholder="RADOS Hostname"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'RadosGWStorage'">
|
|
||||||
<td>Is Secure:</td>
|
|
||||||
<td>
|
|
||||||
<div class="co-checkbox">
|
|
||||||
<input id="dsc-secure" type="checkbox" ng-model="config.DISTRIBUTED_STORAGE_CONFIG.local[1].is_secure">
|
|
||||||
<label for="dsc-secure">Requires SSL</label>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="help-text" ng-if="field.help_url">
|
||||||
</tr>
|
See <a href="{{ field.help_url }}" target="_blank">Documentation</a> for more information
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'RadosGWStorage'">
|
|
||||||
<td>Access Key:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].access_key"
|
|
||||||
placeholder="Access key"></span>
|
|
||||||
<div class="help-text">
|
|
||||||
See <a href="http://ceph.com/docs/master/radosgw/admin/" target="_blank">
|
|
||||||
RADOS Documentation
|
|
||||||
</a> for more information
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'RadosGWStorage'">
|
|
||||||
<td>Secret Key:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].secret_key"
|
|
||||||
placeholder="Secret key"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'RadosGWStorage'">
|
|
||||||
<td>Bucket Name:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field"
|
|
||||||
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].bucket_name"
|
|
||||||
placeholder="Bucket name"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="co-panel-button-bar">
|
|
||||||
<button class="btn btn-default"><i class="fa fa-sign-in"></i> Test Configuration</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -375,7 +260,8 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Password:</td>
|
<td>Password:</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="config-string-field" binding="config.MAIL_PASSWORD"
|
<input class="form-control" type="password"
|
||||||
|
ng-model="config.MAIL_PASSWORD"
|
||||||
placeholder="Password for authentication"></span>
|
placeholder="Password for authentication"></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -384,9 +270,6 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
<div class="co-panel-button-bar" ng-show="config.FEATURE_MAILING">
|
|
||||||
<button class="btn btn-default"><i class="fa fa-sign-in"></i> Test Configuration</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /E-mail -->
|
</div> <!-- /E-mail -->
|
||||||
|
|
||||||
|
@ -446,10 +329,6 @@
|
||||||
<td><span class="config-list-field" item-title="RDN" binding="config.LDAP_USER_RDN"></span></td>
|
<td><span class="config-list-field" item-title="RDN" binding="config.LDAP_USER_RDN"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="co-panel-button-bar" ng-show="config.AUTHENTICATION_TYPE == 'LDAP'">
|
|
||||||
<button class="btn btn-default"><i class="fa fa-sign-in"></i> Test Configuration</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /Authentication -->
|
</div> <!-- /Authentication -->
|
||||||
|
|
||||||
|
@ -513,11 +392,6 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<div class="co-panel-button-bar" ng-show="config.FEATURE_GITHUB_LOGIN">
|
|
||||||
<button class="btn btn-default"><i class="fa fa-sign-in"></i> Test Configuration</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /Github Authentication -->
|
</div> <!-- /Github Authentication -->
|
||||||
|
|
||||||
|
@ -562,17 +436,34 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<div class="co-panel-button-bar" ng-show="config.FEATURE_GOOGLE_LOGIN">
|
|
||||||
<button class="btn btn-default"><i class="fa fa-sign-in"></i> Test Configuration</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /Google Authentication -->
|
</div> <!-- /Google Authentication -->
|
||||||
|
|
||||||
|
<!-- Build Support -->
|
||||||
|
<div class="co-panel">
|
||||||
|
<div class="co-panel-heading">
|
||||||
|
<i class="fa fa-tasks"></i> Dockerfile Build Support
|
||||||
|
</div>
|
||||||
|
<div class="co-panel-body">
|
||||||
|
<div class="description">
|
||||||
|
If enabled, users can submit Dockerfiles to be built and pushed by the Enterprise Registry.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="co-checkbox">
|
||||||
|
<input id="ftbs" type="checkbox" ng-model="config.FEATURE_BUILD_SUPPORT">
|
||||||
|
<label for="ftbs">Enable Dockerfile Build</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 10px">
|
||||||
|
<strong>Note: Build workers are required for this feature.</strong>
|
||||||
|
See <a href="https://coreos.com/docs/enterprise-registry/build-support/" target="_blank">Adding Build Workers</a> for instructions on how to setup build workers.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- /Build Support -->
|
||||||
|
|
||||||
|
|
||||||
<!-- Github Trigger -->
|
<!-- Github Trigger -->
|
||||||
<div class="co-panel" ng-show="config.FEATURE_BUILD_SUPPORT">
|
<div class="co-panel" ng-show="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;">
|
||||||
<div class="co-panel-heading">
|
<div class="co-panel-heading">
|
||||||
<i class="fa fa-github"></i> Github (Enterprise) Build Triggers
|
<i class="fa fa-github"></i> Github (Enterprise) Build Triggers
|
||||||
</div>
|
</div>
|
||||||
|
@ -631,12 +522,90 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<div class="co-panel-button-bar" ng-show="config.FEATURE_GITHUB_BUILD">
|
|
||||||
<button class="btn btn-default"><i class="fa fa-sign-in"></i> Test Configuration</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /Github Trigger -->
|
</div> <!-- /Github Trigger -->
|
||||||
|
|
||||||
|
<!-- Save Bar -->
|
||||||
|
<div class="cor-floating-bottom-bar">
|
||||||
|
<button class="btn" ng-class="mapped.$hasChanges ? 'btn-primary' : 'btn-success'"
|
||||||
|
ng-click="validateAndSave()">
|
||||||
|
<i class="fa fa-lg" ng-class="mapped.$hasChanges ? 'fa-dot-circle-o' : 'fa-check-circle'"></i>
|
||||||
|
<span ng-if="mapped.$hasChanges">Save Configuration Changes</span>
|
||||||
|
<span ng-if="!mapped.$hasChanges">Configuration Saved</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal 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... Please Wait
|
||||||
|
</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>
|
||||||
|
</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 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="service-verification">
|
||||||
|
<div class="service-verification-row" ng-repeat="serviceInfo in validating">
|
||||||
|
<span class="quay-spinner" ng-show="serviceInfo.status == 'validating'"></span>
|
||||||
|
<i class="fa fa-lg fa-check-circle" ng-show="serviceInfo.status == 'success'"></i>
|
||||||
|
<i class="fa fa-lg fa-warning" ng-show="serviceInfo.status == 'error'"></i>
|
||||||
|
<span class="service-title">{{ serviceInfo.service.title }}</span>
|
||||||
|
|
||||||
|
<div class="service-verification-error" ng-show="serviceInfo.status == 'error'">{{ serviceInfo.errorMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer" ng-show="!mapped.$hasChanges">
|
||||||
|
<button class="btn btn-default" data-dismiss="modal">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer" ng-show="mapped.$hasChanges">
|
||||||
|
<span ng-show="validating.length == 0">Please Wait...</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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
3
static/directives/cor-floating-bottom-bar.html
Normal file
3
static/directives/cor-floating-bottom-bar.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="co-floating-bottom-bar">
|
||||||
|
<span ng-transclude/>
|
||||||
|
</div>
|
|
@ -10,8 +10,137 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
'isActive': '=isActive'
|
'isActive': '=isActive'
|
||||||
},
|
},
|
||||||
controller: function($rootScope, $scope, $element, $timeout, ApiService) {
|
controller: function($rootScope, $scope, $element, $timeout, ApiService) {
|
||||||
|
$scope.SERVICES = [
|
||||||
|
{'id': 'redis', 'title': 'Redis'},
|
||||||
|
|
||||||
|
{'id': 'registry-storage', 'title': 'Registry Storage'},
|
||||||
|
|
||||||
|
{'id': 'ssl', 'title': 'SSL certificate and key', 'condition': function(config) {
|
||||||
|
return config.PREFERRED_URL_SCHEME == 'https';
|
||||||
|
}},
|
||||||
|
|
||||||
|
{'id': 'ldap', 'title': 'LDAP Authentication', 'condition': function(config) {
|
||||||
|
return config.AUTHENTICATION_TYPE == 'LDAP';
|
||||||
|
}},
|
||||||
|
|
||||||
|
{'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) {
|
||||||
|
return config.FEATURE_MAILING;
|
||||||
|
}},
|
||||||
|
|
||||||
|
{'id': 'github-login', 'title': 'Github (Enterprise) Authentication', 'condition': function(config) {
|
||||||
|
return config.FEATURE_GITHUB_LOGIN;
|
||||||
|
}}
|
||||||
|
];
|
||||||
|
|
||||||
|
$scope.STORAGE_CONFIG_FIELDS = {
|
||||||
|
'LocalStorage': [
|
||||||
|
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/some/directory', 'kind': 'text'}
|
||||||
|
],
|
||||||
|
|
||||||
|
'S3Storage': [
|
||||||
|
{'name': 's3_access_key', 'title': 'AWS Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'},
|
||||||
|
{'name': 's3_secret_key', 'title': 'AWS Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
|
||||||
|
{'name': 's3_bucket', 'title': 'S3 Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
|
||||||
|
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
|
||||||
|
],
|
||||||
|
|
||||||
|
'GoogleCloudStorage': [
|
||||||
|
{'name': 'access_key', 'title': 'Cloud Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'},
|
||||||
|
{'name': 'secret_key', 'title': 'Cloud Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
|
||||||
|
{'name': 'bucket_name', 'title': 'GCS Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
|
||||||
|
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
|
||||||
|
],
|
||||||
|
|
||||||
|
'RadosGWStorage': [
|
||||||
|
{'name': 'hostname', 'title': 'Rados Server Hostname', 'placeholder': 'my.rados.hostname', 'kind': 'text'},
|
||||||
|
{'name': 'is_secure', 'title': 'Is Secure', 'placeholder': 'Require SSL', 'kind': 'bool'},
|
||||||
|
{'name': 'access_key', 'title': 'Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text', 'help_url': 'http://ceph.com/docs/master/radosgw/admin/'},
|
||||||
|
{'name': 'secret_key', 'title': 'Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
|
||||||
|
{'name': 'bucket_name', 'title': 'Bucket Name', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
|
||||||
|
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
$scope.config = null;
|
$scope.config = null;
|
||||||
$scope.mapped = {};
|
$scope.mapped = {
|
||||||
|
'$hasChanges': false
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.validating = null;
|
||||||
|
$scope.savingConfiguration = false;
|
||||||
|
|
||||||
|
$scope.getServices = function(config) {
|
||||||
|
var services = [];
|
||||||
|
if (!config) { return services; }
|
||||||
|
|
||||||
|
for (var i = 0; i < $scope.SERVICES.length; ++i) {
|
||||||
|
var service = $scope.SERVICES[i];
|
||||||
|
if (!service.condition || service.condition(config)) {
|
||||||
|
services.push({
|
||||||
|
'service': service,
|
||||||
|
'status': 'validating'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.validationStatus = function(serviceInfos) {
|
||||||
|
if (!serviceInfos) { return 'validating'; }
|
||||||
|
|
||||||
|
var hasError = false;
|
||||||
|
for (var i = 0; i < serviceInfos.length; ++i) {
|
||||||
|
if (serviceInfos[i].status == 'validating') {
|
||||||
|
return 'validating';
|
||||||
|
}
|
||||||
|
if (serviceInfos[i].status == 'error') {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasError ? 'failed' : 'success';
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.validateService = function(serviceInfo) {
|
||||||
|
var params = {
|
||||||
|
'service': serviceInfo.service.id
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.scValidateConfig({'config': $scope.config}, params).then(function(resp) {
|
||||||
|
serviceInfo.status = resp.status ? 'success' : 'error';
|
||||||
|
serviceInfo.errorMessage = $.trim(resp.reason || '');
|
||||||
|
}, ApiService.errorDisplay('Could not validate configuration. Please report this error.'));
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.validateAndSave = function() {
|
||||||
|
$scope.savingConfiguration = false;
|
||||||
|
$scope.validating = $scope.getServices($scope.config);
|
||||||
|
|
||||||
|
$('#validateAndSaveModal').modal({
|
||||||
|
keyboard: false,
|
||||||
|
backdrop: 'static'
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var i = 0; i < $scope.validating.length; ++i) {
|
||||||
|
var serviceInfo = $scope.validating[i];
|
||||||
|
$scope.validateService(serviceInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveConfiguration = function() {
|
||||||
|
$scope.savingConfiguration = true;
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'config': $scope.config,
|
||||||
|
'hostname': window.location.host
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.scUpdateConfig(data).then(function(resp) {
|
||||||
|
$scope.savingConfiguration = false;
|
||||||
|
$scope.mapped.$hasChanges = false
|
||||||
|
}, ApiService.errorDisplay('Could not save configuration. Please report this error.'));
|
||||||
|
};
|
||||||
|
|
||||||
var githubSelector = function(key) {
|
var githubSelector = function(key) {
|
||||||
return function(value) {
|
return function(value) {
|
||||||
|
@ -36,8 +165,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
var current = config;
|
var current = config;
|
||||||
for (var i = 0; i < parts.length; ++i) {
|
for (var i = 0; i < parts.length; ++i) {
|
||||||
var part = parts[i];
|
var part = parts[i];
|
||||||
if (!config[part]) { return null; }
|
if (!current[part]) { return null; }
|
||||||
current = config[part];
|
current = current[part];
|
||||||
}
|
}
|
||||||
return current;
|
return current;
|
||||||
};
|
};
|
||||||
|
@ -86,7 +215,36 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
$scope.$watch('mapped.redis.port', redisSetter('port'));
|
$scope.$watch('mapped.redis.port', redisSetter('port'));
|
||||||
$scope.$watch('mapped.redis.password', redisSetter('password'));
|
$scope.$watch('mapped.redis.password', redisSetter('password'));
|
||||||
|
|
||||||
|
// Add a watch to remove any fields not allowed by the current storage configuration.
|
||||||
|
// We have to do this otherwise extra fields (which are not allowed) can end up in the
|
||||||
|
// configuration.
|
||||||
|
$scope.$watch('config.DISTRIBUTED_STORAGE_CONFIG.local[0]', function(value) {
|
||||||
|
// Remove any fields not associated with the current kind.
|
||||||
|
if (!value || !$scope.STORAGE_CONFIG_FIELDS[value]
|
||||||
|
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG
|
||||||
|
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG.local
|
||||||
|
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG.local[1]) { return; }
|
||||||
|
|
||||||
|
var allowedFields = $scope.STORAGE_CONFIG_FIELDS[value];
|
||||||
|
var configObject = $scope.config.DISTRIBUTED_STORAGE_CONFIG.local[1];
|
||||||
|
|
||||||
|
for (var fieldName in configObject) {
|
||||||
|
if (!configObject.hasOwnProperty(fieldName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isValidField = $.grep(allowedFields, function(field) {
|
||||||
|
return field.name == fieldName;
|
||||||
|
}).length > 0;
|
||||||
|
|
||||||
|
if (!isValidField) {
|
||||||
|
delete configObject[fieldName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$watch('config', function(value) {
|
$scope.$watch('config', function(value) {
|
||||||
|
$scope.mapped['$hasChanges'] = true;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
$scope.$watch('isActive', function(value) {
|
$scope.$watch('isActive', function(value) {
|
||||||
|
@ -95,6 +253,7 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
ApiService.scGetConfig().then(function(resp) {
|
ApiService.scGetConfig().then(function(resp) {
|
||||||
$scope.config = resp['config'];
|
$scope.config = resp['config'];
|
||||||
initializeMappedLogic($scope.config);
|
initializeMappedLogic($scope.config);
|
||||||
|
$scope.mapped['$hasChanges'] = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -376,9 +535,7 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
'binding': '=binding'
|
'binding': '=binding'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element) {
|
||||||
$scope.$watch('items', function(items) {
|
var padItems = function(items) {
|
||||||
if (!items) { return; }
|
|
||||||
|
|
||||||
// Remove the last item if both it and the second to last items are empty.
|
// Remove the last item if both it and the second to last items are empty.
|
||||||
if (items.length > 1 && !items[items.length - 2].value && !items[items.length - 1].value) {
|
if (items.length > 1 && !items[items.length - 2].value && !items[items.length - 1].value) {
|
||||||
items.splice(items.length - 1, 1);
|
items.splice(items.length - 1, 1);
|
||||||
|
@ -386,14 +543,45 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the last item is non-empty, add a new item.
|
// If the last item is non-empty, add a new item.
|
||||||
if (items[items.length - 1].value) {
|
if (items.length == 0 || items[items.length - 1].value) {
|
||||||
items.push({'value': ''});
|
items.push({'value': ''});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.itemHash = null;
|
||||||
|
$scope.$watch('items', function(items) {
|
||||||
|
if (!items) { return; }
|
||||||
|
padItems(items);
|
||||||
|
|
||||||
|
var itemHash = '';
|
||||||
|
var binding = [];
|
||||||
|
for (var i = 0; i < items.length; ++i) {
|
||||||
|
var item = items[i];
|
||||||
|
if (item.value && (URI(item.value).host() || URI(item.value).path())) {
|
||||||
|
binding.push(item.value);
|
||||||
|
itemHash += item.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.itemHash = itemHash;
|
||||||
|
$scope.binding = binding;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
$scope.$watch('binding', function(binding) {
|
$scope.$watch('binding', function(binding) {
|
||||||
$scope.items = [];
|
if (!binding) { return; }
|
||||||
$scope.items.push({'value': ''});
|
|
||||||
|
var current = binding;
|
||||||
|
var items = [];
|
||||||
|
var itemHash = '';
|
||||||
|
for (var i = 0; i < current.length; ++i) {
|
||||||
|
items.push({'value': current[i]})
|
||||||
|
itemHash += current[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.itemHash != itemHash) {
|
||||||
|
$scope.items = items;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -416,6 +604,7 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
$scope.value = null;
|
$scope.value = null;
|
||||||
|
|
||||||
var updateBinding = function() {
|
var updateBinding = function() {
|
||||||
|
if ($scope.value == null) { return; }
|
||||||
var value = $scope.value || '';
|
var value = $scope.value || '';
|
||||||
|
|
||||||
switch ($scope.kind) {
|
switch ($scope.kind) {
|
||||||
|
|
|
@ -175,6 +175,49 @@ angular.module("core-ui", [])
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.directive('corFloatingBottomBar', function() {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 3,
|
||||||
|
templateUrl: '/static/directives/cor-floating-bottom-bar.html',
|
||||||
|
replace: true,
|
||||||
|
transclude: true,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {},
|
||||||
|
controller: function($rootScope, $scope, $element, $timeout, $interval) {
|
||||||
|
var handler = function() {
|
||||||
|
$element.removeClass('floating');
|
||||||
|
$element.css('width', $element[0].parentNode.clientWidth + 'px');
|
||||||
|
|
||||||
|
var windowHeight = $(window).height();
|
||||||
|
var rect = $element[0].getBoundingClientRect();
|
||||||
|
if (rect.bottom > windowHeight) {
|
||||||
|
$element.addClass('floating');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$(window).on("scroll", handler);
|
||||||
|
$(window).on("resize", handler);
|
||||||
|
|
||||||
|
var previousHeight = $element[0].parentNode.clientHeight;
|
||||||
|
var stop = $interval(function() {
|
||||||
|
var currentHeight = $element[0].parentNode.clientWidth;
|
||||||
|
if (previousHeight != currentHeight) {
|
||||||
|
currentHeight = previousHeight;
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
$(window).off("resize", handler);
|
||||||
|
$(window).off("scroll", handler);
|
||||||
|
$internval.stop(stop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
.directive('corTab', function() {
|
.directive('corTab', function() {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
priority: 4,
|
priority: 4,
|
||||||
|
|
|
@ -11,6 +11,14 @@ STORAGE_DRIVER_CLASSES = {
|
||||||
'RadosGWStorage': RadosGWStorage,
|
'RadosGWStorage': RadosGWStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_storage_driver(storage_params):
|
||||||
|
""" Returns a storage driver class for the given storage configuration
|
||||||
|
(a pair of string name and a dict of parameters). """
|
||||||
|
driver = storage_params[0]
|
||||||
|
parameters = storage_params[1]
|
||||||
|
driver_class = STORAGE_DRIVER_CLASSES.get(driver, FakeStorage)
|
||||||
|
return driver_class(**parameters)
|
||||||
|
|
||||||
|
|
||||||
class Storage(object):
|
class Storage(object):
|
||||||
def __init__(self, app=None):
|
def __init__(self, app=None):
|
||||||
|
@ -23,12 +31,7 @@ class Storage(object):
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
storages = {}
|
storages = {}
|
||||||
for location, storage_params in app.config.get('DISTRIBUTED_STORAGE_CONFIG').items():
|
for location, storage_params in app.config.get('DISTRIBUTED_STORAGE_CONFIG').items():
|
||||||
driver = storage_params[0]
|
storages[location] = get_storage_driver(storage_params)
|
||||||
parameters = storage_params[1]
|
|
||||||
|
|
||||||
driver_class = STORAGE_DRIVER_CLASSES.get(driver, FakeStorage)
|
|
||||||
storage = driver_class(**parameters)
|
|
||||||
storages[location] = storage
|
|
||||||
|
|
||||||
preference = app.config.get('DISTRIBUTED_STORAGE_PREFERENCE', None)
|
preference = app.config.get('DISTRIBUTED_STORAGE_PREFERENCE', None)
|
||||||
if not preference:
|
if not preference:
|
||||||
|
|
0
util/config/__init__.py
Normal file
0
util/config/__init__.py
Normal file
122
util/config/validator.py
Normal file
122
util/config/validator.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import redis
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import ldap
|
||||||
|
|
||||||
|
from data.users import LDAPConnection
|
||||||
|
from flask import Flask
|
||||||
|
from flask.ext.mail import Mail, Message
|
||||||
|
from data.database import validate_database_url, User
|
||||||
|
from storage import get_storage_driver
|
||||||
|
from app import app, OVERRIDE_CONFIG_DIRECTORY
|
||||||
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from util.oauth import GoogleOAuthConfig, GithubOAuthConfig
|
||||||
|
|
||||||
|
SSL_FILENAMES = ['ssl.cert', 'ssl.key']
|
||||||
|
|
||||||
|
def validate_service_for_config(service, config):
|
||||||
|
""" Attempts to validate the configuration for the given service. """
|
||||||
|
if not service in _VALIDATORS:
|
||||||
|
return {
|
||||||
|
'status': False
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
_VALIDATORS[service](config)
|
||||||
|
return {
|
||||||
|
'status': True
|
||||||
|
}
|
||||||
|
except Exception as ex:
|
||||||
|
return {
|
||||||
|
'status': False,
|
||||||
|
'reason': str(ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _validate_database(config):
|
||||||
|
""" Validates connecting to the database. """
|
||||||
|
validate_database_url(config['DB_URI'])
|
||||||
|
|
||||||
|
def _validate_redis(config):
|
||||||
|
""" Validates connecting to redis. """
|
||||||
|
redis_config = config['BUILDLOGS_REDIS']
|
||||||
|
client = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
|
||||||
|
client.ping()
|
||||||
|
|
||||||
|
def _validate_registry_storage(config):
|
||||||
|
""" Validates registry storage. """
|
||||||
|
parameters = config.get('DISTRIBUTED_STORAGE_CONFIG', {}).get('local', ['LocalStorage', {}])
|
||||||
|
try:
|
||||||
|
driver = get_storage_driver(parameters)
|
||||||
|
except TypeError:
|
||||||
|
raise Exception('Missing required storage configuration parameter(s)')
|
||||||
|
|
||||||
|
# Put and remove a temporary file.
|
||||||
|
driver.put_content('_verify', 'testing 123')
|
||||||
|
driver.remove('_verify')
|
||||||
|
|
||||||
|
def _validate_mailing(config):
|
||||||
|
""" Validates sending email. """
|
||||||
|
test_app = Flask("mail-test-app")
|
||||||
|
test_app.config.update(config)
|
||||||
|
test_app.config.update({
|
||||||
|
'MAIL_FAIL_SILENTLY': False,
|
||||||
|
'TESTING': False
|
||||||
|
})
|
||||||
|
|
||||||
|
test_mail = Mail(test_app)
|
||||||
|
test_msg = Message("Test e-mail from %s" % app.config['REGISTRY_TITLE'])
|
||||||
|
test_msg.add_recipient(get_authenticated_user().email)
|
||||||
|
test_mail.send(test_msg)
|
||||||
|
|
||||||
|
def _validate_github_login(config):
|
||||||
|
""" Validates the OAuth credentials and API endpoint for Github Login. """
|
||||||
|
client = app.config['HTTPCLIENT']
|
||||||
|
oauth = GithubOAuthConfig(config, 'GITHUB_LOGIN_CONFIG')
|
||||||
|
endpoint = oauth.authorize_endpoint()
|
||||||
|
# TODO: this
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_ssl(config):
|
||||||
|
""" Validates the SSL configuration (if enabled). """
|
||||||
|
if config.get('PREFERRED_URL_SCHEME', 'http') != 'https':
|
||||||
|
return
|
||||||
|
|
||||||
|
for filename in SSL_FILENAMES:
|
||||||
|
if not os.path.exists(os.path.join(OVERRIDE_CONFIG_DIRECTORY, filename)):
|
||||||
|
raise Exception('Missing required SSL file: %s' % filename)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_ldap(config):
|
||||||
|
""" Validates the LDAP connection. """
|
||||||
|
if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Note: raises ldap.INVALID_CREDENTIALS on failure
|
||||||
|
admin_dn = config.get('LDAP_ADMIN_DN')
|
||||||
|
admin_passwd = config.get('LDAP_ADMIN_PASSWD')
|
||||||
|
|
||||||
|
if not admin_dn:
|
||||||
|
raise Exception('Missing Admin DN for LDAP configuration')
|
||||||
|
|
||||||
|
if not admin_passwd:
|
||||||
|
raise Exception('Missing Admin Password for LDAP configuration')
|
||||||
|
|
||||||
|
ldap_uri = config.get('LDAP_URI', 'ldap://localhost')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with LDAPConnection(ldap_uri, admin_dn, admin_passwd):
|
||||||
|
pass
|
||||||
|
except ldap.LDAPError as ex:
|
||||||
|
values = ex.args[0] if ex.args else {}
|
||||||
|
raise Exception(values.get('desc', 'Unknown error'))
|
||||||
|
|
||||||
|
|
||||||
|
_VALIDATORS = {
|
||||||
|
'database': _validate_database,
|
||||||
|
'redis': _validate_redis,
|
||||||
|
'registry-storage': _validate_registry_storage,
|
||||||
|
'mail': _validate_mailing,
|
||||||
|
'github-login': _validate_github_login,
|
||||||
|
'ssl': _validate_ssl,
|
||||||
|
'ldap': _validate_ldap,
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
class OAuthConfig(object):
|
class OAuthConfig(object):
|
||||||
def __init__(self, app, key_name):
|
def __init__(self, config, key_name):
|
||||||
self.key_name = key_name
|
self.key_name = key_name
|
||||||
self.config = app.config.get(key_name) or {}
|
self.config = config.get(key_name) or {}
|
||||||
|
|
||||||
def service_name(self):
|
def service_name(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -23,6 +23,9 @@ class OAuthConfig(object):
|
||||||
def client_secret(self):
|
def client_secret(self):
|
||||||
return self.config.get('CLIENT_SECRET')
|
return self.config.get('CLIENT_SECRET')
|
||||||
|
|
||||||
|
def basic_scope(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def _get_url(self, endpoint, *args):
|
def _get_url(self, endpoint, *args):
|
||||||
for arg in args:
|
for arg in args:
|
||||||
endpoint = urlparse.urljoin(endpoint, arg)
|
endpoint = urlparse.urljoin(endpoint, arg)
|
||||||
|
@ -31,8 +34,8 @@ class OAuthConfig(object):
|
||||||
|
|
||||||
|
|
||||||
class GithubOAuthConfig(OAuthConfig):
|
class GithubOAuthConfig(OAuthConfig):
|
||||||
def __init__(self, app, key_name):
|
def __init__(self, config, key_name):
|
||||||
super(GithubOAuthConfig, self).__init__(app, key_name)
|
super(GithubOAuthConfig, self).__init__(config, key_name)
|
||||||
|
|
||||||
def service_name(self):
|
def service_name(self):
|
||||||
return 'GitHub'
|
return 'GitHub'
|
||||||
|
@ -43,6 +46,9 @@ class GithubOAuthConfig(OAuthConfig):
|
||||||
endpoint = endpoint + '/'
|
endpoint = endpoint + '/'
|
||||||
return endpoint
|
return endpoint
|
||||||
|
|
||||||
|
def basic_scope(self):
|
||||||
|
return 'user:email'
|
||||||
|
|
||||||
def authorize_endpoint(self):
|
def authorize_endpoint(self):
|
||||||
return self._get_url(self._endpoint(), '/login/oauth/authorize') + '?'
|
return self._get_url(self._endpoint(), '/login/oauth/authorize') + '?'
|
||||||
|
|
||||||
|
@ -73,12 +79,15 @@ class GithubOAuthConfig(OAuthConfig):
|
||||||
|
|
||||||
|
|
||||||
class GoogleOAuthConfig(OAuthConfig):
|
class GoogleOAuthConfig(OAuthConfig):
|
||||||
def __init__(self, app, key_name):
|
def __init__(self, config, key_name):
|
||||||
super(GoogleOAuthConfig, self).__init__(app, key_name)
|
super(GoogleOAuthConfig, self).__init__(config, key_name)
|
||||||
|
|
||||||
def service_name(self):
|
def service_name(self):
|
||||||
return 'Google'
|
return 'Google'
|
||||||
|
|
||||||
|
def basic_scope(self):
|
||||||
|
return 'openid email'
|
||||||
|
|
||||||
def authorize_endpoint(self):
|
def authorize_endpoint(self):
|
||||||
return 'https://accounts.google.com/o/oauth2/auth?response_type=code&'
|
return 'https://accounts.google.com/o/oauth2/auth?response_type=code&'
|
||||||
|
|
||||||
|
|
Reference in a new issue