Get end-to-end configuration setup working, including verification (except for Github, which is in progress)

This commit is contained in:
Joseph Schorr 2015-01-07 16:20:51 -05:00
parent 825455ea6c
commit 63504c87fb
14 changed files with 611 additions and 206 deletions

8
app.py
View file

@ -19,7 +19,7 @@ from util.exceptionlog import Sentry
from util.queuemetrics import QueueMetrics
from util.names import urn_generator
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.buildlogs import BuildLogs
from data.archivedlogs import LogArchive
@ -124,9 +124,9 @@ queue_metrics = QueueMetrics(app)
authentication = UserAuthentication(app)
userevents = UserEventsBuilderModule(app)
github_login = GithubOAuthConfig(app, 'GITHUB_LOGIN_CONFIG')
github_trigger = GithubOAuthConfig(app, 'GITHUB_TRIGGER_CONFIG')
google_login = GoogleOAuthConfig(app, 'GOOGLE_LOGIN_CONFIG')
github_login = GithubOAuthConfig(app.config, 'GITHUB_LOGIN_CONFIG')
github_trigger = GithubOAuthConfig(app.config, 'GITHUB_TRIGGER_CONFIG')
google_login = GoogleOAuthConfig(app.config, 'GOOGLE_LOGIN_CONFIG')
oauth_apps = [github_login, github_trigger, google_login]
tf = app.config['DB_TRANSACTION_FACTORY']

View file

@ -9,18 +9,17 @@ from endpoints.api import (ApiResource, nickname, resource, internal_only, show_
from endpoints.common import common_login
from app import app, OVERRIDE_CONFIG_YAML_FILENAME, OVERRIDE_CONFIG_DIRECTORY
from data import model
from data.database import User, validate_database_url
from auth.permissions import SuperUserPermission
from auth.auth_context import get_authenticated_user
from util.configutil import (import_yaml, export_yaml, add_enterprise_config_defaults,
set_config_value)
from data.database import User
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
logger = logging.getLogger(__name__)
CONFIG_FILE_WHITELIST = ['ssl.key', 'ssl.cert']
def database_is_valid():
try:
User.select().limit(1)
@ -131,7 +130,7 @@ class SuperUserConfigFile(ApiResource):
@nickname('scConfigFileExists')
def get(self, filename):
""" 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)
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.
if not os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME) or SuperUserPermission().can():
config = request.get_json()['config']
if service == 'database':
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 {}
return validate_service_for_config(service, config)
abort(403)

View file

@ -231,6 +231,31 @@
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 {
color: #ccc;
margin-bottom: 10px;
@ -338,4 +363,64 @@
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;
}

View file

@ -4907,12 +4907,12 @@ i.slack-icon {
font-size: 18px;
}
.initial-setup-modal .valid-database .verified {
.verified {
font-size: 16px;
margin-bottom: 16px;
}
.initial-setup-modal .valid-database .verified i.fa {
.verified i.fa {
font-size: 26px;
margin-right: 10px;
vertical-align: middle;
@ -4923,8 +4923,4 @@ i.slack-icon {
border: 1px solid #eee;
vertical-align: middle;
padding: 4px;
}
.config-contact-field .form-control {
width: 350px;
}

View file

@ -5,10 +5,10 @@
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
<span ng-switch="kind">
<span ng-switch-when="mailto"><i class="fa fa-envelope"></i></span>
<span ng-switch-when="irc"><i class="fa fa-comment"></i></span>
<span ng-switch-when="tel"><i class="fa fa-phone"></i></span>
<span ng-switch-default><i class="fa fa-ticket"></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>IRC</span>
<span ng-switch-when="tel"><i class="fa fa-phone"></i>Phone</span>
<span ng-switch-default><i class="fa fa-ticket"></i>URL</span>
</span>
<span class="caret"></span>
</button>

View file

@ -44,23 +44,6 @@
</div>
</td>
</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>
</div>
</div>
@ -155,9 +138,6 @@
</td>
</tr>
</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> <!-- /Redis -->
@ -186,122 +166,27 @@
</td>
</tr>
<!-- Storage Path -->
<tr>
<td>Storage Path:</td>
<!-- Fields -->
<tr ng-repeat="field in STORAGE_CONFIG_FIELDS[config.DISTRIBUTED_STORAGE_CONFIG.local[0]]">
<td>{{ field.title }}:</td>
<td>
<span class="config-string-field"
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1].storage_path"
placeholder="Path under the volume or bucket"></span>
</td>
</tr>
<!-- S3 -->
<tr ng-show="config.DISTRIBUTED_STORAGE_CONFIG.local[0] == 'S3Storage'">
<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>
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]"
placeholder="{{ field.placeholder }}"
ng-if="field.kind == 'text'"></span>
<div class="co-checkbox" ng-if="field.kind == 'bool'">
<input id="dsc-{{ field.name }}" type="checkbox"
ng-model="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]">
<label for="dsc-{{ field.name }}">{{ field.placeholder }}</label>
</div>
</td>
</tr>
<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 class="help-text" ng-if="field.help_url">
See <a href="{{ field.help_url }}" target="_blank">Documentation</a> for more information
</div>
</td>
</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>
</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>
@ -375,7 +260,8 @@
<tr>
<td>Password:</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>
</td>
</tr>
@ -384,9 +270,6 @@
</tr>
</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> <!-- /E-mail -->
@ -446,10 +329,6 @@
<td><span class="config-list-field" item-title="RDN" binding="config.LDAP_USER_RDN"></span></td>
</tr>
</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> <!-- /Authentication -->
@ -513,11 +392,6 @@
</td>
</tr>
</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> <!-- /Github Authentication -->
@ -562,17 +436,34 @@
</td>
</tr>
</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> <!-- /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 -->
<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">
<i class="fa fa-github"></i> Github (Enterprise) Build Triggers
</div>
@ -631,12 +522,90 @@
</td>
</tr>
</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> <!-- /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>

View file

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

View file

@ -10,8 +10,137 @@ angular.module("core-config-setup", ['angularFileUpload'])
'isActive': '=isActive'
},
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.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) {
return function(value) {
@ -36,8 +165,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
var current = config;
for (var i = 0; i < parts.length; ++i) {
var part = parts[i];
if (!config[part]) { return null; }
current = config[part];
if (!current[part]) { return null; }
current = current[part];
}
return current;
};
@ -86,7 +215,36 @@ angular.module("core-config-setup", ['angularFileUpload'])
$scope.$watch('mapped.redis.port', redisSetter('port'));
$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.mapped['$hasChanges'] = true;
}, true);
$scope.$watch('isActive', function(value) {
@ -95,6 +253,7 @@ angular.module("core-config-setup", ['angularFileUpload'])
ApiService.scGetConfig().then(function(resp) {
$scope.config = resp['config'];
initializeMappedLogic($scope.config);
$scope.mapped['$hasChanges'] = false;
});
});
}
@ -376,9 +535,7 @@ angular.module("core-config-setup", ['angularFileUpload'])
'binding': '=binding'
},
controller: function($scope, $element) {
$scope.$watch('items', function(items) {
if (!items) { return; }
var padItems = function(items) {
// 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) {
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 (items[items.length - 1].value) {
if (items.length == 0 || items[items.length - 1].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);
$scope.$watch('binding', function(binding) {
$scope.items = [];
$scope.items.push({'value': ''});
if (!binding) { return; }
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;
var updateBinding = function() {
if ($scope.value == null) { return; }
var value = $scope.value || '';
switch ($scope.kind) {

View file

@ -175,6 +175,49 @@ angular.module("core-ui", [])
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() {
var directiveDefinitionObject = {
priority: 4,

View file

@ -11,6 +11,14 @@ STORAGE_DRIVER_CLASSES = {
'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):
def __init__(self, app=None):
@ -23,12 +31,7 @@ class Storage(object):
def init_app(self, app):
storages = {}
for location, storage_params in app.config.get('DISTRIBUTED_STORAGE_CONFIG').items():
driver = storage_params[0]
parameters = storage_params[1]
driver_class = STORAGE_DRIVER_CLASSES.get(driver, FakeStorage)
storage = driver_class(**parameters)
storages[location] = storage
storages[location] = get_storage_driver(storage_params)
preference = app.config.get('DISTRIBUTED_STORAGE_PREFERENCE', None)
if not preference:

0
util/config/__init__.py Normal file
View file

122
util/config/validator.py Normal file
View 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,
}

View file

@ -1,9 +1,9 @@
import urlparse
class OAuthConfig(object):
def __init__(self, app, key_name):
def __init__(self, config, 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):
raise NotImplementedError
@ -23,6 +23,9 @@ class OAuthConfig(object):
def client_secret(self):
return self.config.get('CLIENT_SECRET')
def basic_scope(self):
raise NotImplementedError
def _get_url(self, endpoint, *args):
for arg in args:
endpoint = urlparse.urljoin(endpoint, arg)
@ -31,8 +34,8 @@ class OAuthConfig(object):
class GithubOAuthConfig(OAuthConfig):
def __init__(self, app, key_name):
super(GithubOAuthConfig, self).__init__(app, key_name)
def __init__(self, config, key_name):
super(GithubOAuthConfig, self).__init__(config, key_name)
def service_name(self):
return 'GitHub'
@ -43,6 +46,9 @@ class GithubOAuthConfig(OAuthConfig):
endpoint = endpoint + '/'
return endpoint
def basic_scope(self):
return 'user:email'
def authorize_endpoint(self):
return self._get_url(self._endpoint(), '/login/oauth/authorize') + '?'
@ -73,12 +79,15 @@ class GithubOAuthConfig(OAuthConfig):
class GoogleOAuthConfig(OAuthConfig):
def __init__(self, app, key_name):
super(GoogleOAuthConfig, self).__init__(app, key_name)
def __init__(self, config, key_name):
super(GoogleOAuthConfig, self).__init__(config, key_name)
def service_name(self):
return 'Google'
def basic_scope(self):
return 'openid email'
def authorize_endpoint(self):
return 'https://accounts.google.com/o/oauth2/auth?response_type=code&'