14b93f72ff
If not specified, then boto will fallback to reading the credentials from IAM if on an EC2 machine. This should be safe as the validator will still ensure the credentials work if not specified. Fixes #1707
1219 lines
40 KiB
JavaScript
1219 lines
40 KiB
JavaScript
angular.module("core-config-setup", ['angularFileUpload'])
|
|
.directive('configSetupTool', function() {
|
|
var directiveDefinitionObject = {
|
|
priority: 1,
|
|
templateUrl: '/static/directives/config/config-setup-tool.html',
|
|
replace: true,
|
|
transclude: true,
|
|
restrict: 'C',
|
|
scope: {
|
|
'isActive': '=isActive',
|
|
'configurationSaved': '&configurationSaved'
|
|
},
|
|
controller: function($rootScope, $scope, $element, $timeout, ApiService) {
|
|
var authPassword = null;
|
|
|
|
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$';
|
|
$scope.GITHOST_REGEX = '^https?://([a-zA-Z0-9]+\.?\/?)+$';
|
|
|
|
$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';
|
|
}, 'password': true},
|
|
|
|
{'id': 'jwt', 'title': 'JWT Authentication', 'condition': function(config) {
|
|
return config.AUTHENTICATION_TYPE == 'JWT';
|
|
}, 'password': true},
|
|
|
|
{'id': 'keystone', 'title': 'Keystone Authentication', 'condition': function(config) {
|
|
return config.AUTHENTICATION_TYPE == 'Keystone';
|
|
}, 'password': true},
|
|
|
|
{'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) {
|
|
return config.FEATURE_ACI_CONVERSION;
|
|
}},
|
|
|
|
{'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;
|
|
}},
|
|
|
|
{'id': 'google-login', 'title': 'Google Authentication', 'condition': function(config) {
|
|
return config.FEATURE_GOOGLE_LOGIN;
|
|
}},
|
|
|
|
{'id': 'github-trigger', 'title': 'GitHub (Enterprise) Build Triggers', 'condition': function(config) {
|
|
return config.FEATURE_GITHUB_BUILD;
|
|
}},
|
|
|
|
{'id': 'bitbucket-trigger', 'title': 'BitBucket Build Triggers', 'condition': function(config) {
|
|
return config.FEATURE_BITBUCKET_BUILD;
|
|
}},
|
|
|
|
{'id': 'gitlab-trigger', 'title': 'GitLab Build Triggers', 'condition': function(config) {
|
|
return config.FEATURE_GITLAB_BUILD;
|
|
}},
|
|
|
|
{'id': 'security-scanner', 'title': 'Quay Security Scanner', 'condition': function(config) {
|
|
return config.FEATURE_SECURITY_SCANNER;
|
|
}}
|
|
];
|
|
|
|
$scope.STORAGE_CONFIG_FIELDS = {
|
|
'LocalStorage': [
|
|
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/some/directory', 'kind': 'text'}
|
|
],
|
|
|
|
'S3Storage': [
|
|
{'name': 's3_bucket', 'title': 'S3 Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
|
|
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'},
|
|
{'name': 's3_access_key', 'title': 'AWS Access Key (optional if using IAM)', 'placeholder': 'accesskeyhere', 'kind': 'text', 'optional': true},
|
|
{'name': 's3_secret_key', 'title': 'AWS Secret Key (optional if using IAM)', 'placeholder': 'secretkeyhere', 'kind': 'text', 'optional': true},
|
|
{'name': 'host', 'title': 'S3 Host (optional)', 'placeholder': 's3.amazonaws.com', 'kind': 'text', 'optional': true}
|
|
],
|
|
|
|
'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'}
|
|
],
|
|
|
|
'SwiftStorage': [
|
|
{'name': 'auth_version', 'title': 'Swift Auth Version', 'kind': 'option', 'values': [1, 2, 3]},
|
|
{'name': 'auth_url', 'title': 'Swift Auth URL', 'placeholder': 'http://swiftdomain/auth/v1.0', 'kind': 'text'},
|
|
{'name': 'swift_container', 'title': 'Swift Container Name', 'placeholder': 'mycontainer', 'kind': 'text',
|
|
'help_text': 'The swift container for all objects. Must already exist inside Swift.'},
|
|
|
|
{'name': 'storage_path', 'title': 'Storage Path', 'placeholder': '/path/inside/container', 'kind': 'text'},
|
|
|
|
{'name': 'swift_user', 'title': 'Username', 'placeholder': 'accesskeyhere', 'kind': 'text',
|
|
'help_text': 'Note: For Swift V1, this is "username:password" (-U on the CLI).'},
|
|
{'name': 'swift_password', 'title': 'Key/Password', 'placeholder': 'secretkeyhere', 'kind': 'text',
|
|
'help_text': 'Note: For Swift V1, this is the API token (-K on the CLI).'},
|
|
|
|
{'name': 'ca_cert_path', 'title': 'CA Cert Filename', 'placeholder': 'conf/stack/swift.cert', 'kind': 'text', 'optional': true},
|
|
|
|
{'name': 'temp_url_key', 'title': 'Temp URL Key (optional)', 'placholder': 'key-here', 'kind': 'text', 'optional': true,
|
|
'help_url': 'https://coreos.com/products/enterprise-registry/docs/latest/swift-temp-url.html',
|
|
'help_text': 'If enabled, will allow for faster pulls directly from Swift.'},
|
|
|
|
{'name': 'os_options', 'title': 'OS Options', 'kind': 'map',
|
|
'keys': ['tenant_id', 'auth_token', 'service_type', 'endpoint_type', 'tenant_name', 'object_storage_url', 'region_name',
|
|
'project_id', 'project_name', 'project_domain_name', 'user_domain_name', 'user_domain_id']}
|
|
]
|
|
};
|
|
|
|
$scope.validateHostname = function(hostname) {
|
|
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
|
|
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
$scope.config = null;
|
|
$scope.mapped = {
|
|
'$hasChanges': false
|
|
};
|
|
|
|
$scope.hasfile = {};
|
|
$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.cancelValidation = function() {
|
|
$('#validateAndSaveModal').modal('hide');
|
|
$scope.validating = null;
|
|
$scope.savingConfiguration = false;
|
|
};
|
|
|
|
$scope.validateService = function(serviceInfo, opt_password) {
|
|
var params = {
|
|
'service': serviceInfo.service.id
|
|
};
|
|
|
|
var data = {
|
|
'config': $scope.config,
|
|
'password': opt_password || ''
|
|
};
|
|
|
|
var errorDisplay = ApiService.errorDisplay(
|
|
'Could not validate configuration. Please report this error.',
|
|
function() {
|
|
authPassword = null;
|
|
});
|
|
|
|
ApiService.scValidateConfig(data, params).then(function(resp) {
|
|
serviceInfo.status = resp.status ? 'success' : 'error';
|
|
serviceInfo.errorMessage = $.trim(resp.reason || '');
|
|
|
|
if (!resp.status) {
|
|
authPassword = null;
|
|
}
|
|
|
|
}, errorDisplay);
|
|
};
|
|
|
|
$scope.checkValidateAndSave = function() {
|
|
if ($scope.configform.$valid) {
|
|
saveStorageConfig();
|
|
$scope.validateAndSave();
|
|
return;
|
|
}
|
|
|
|
var query = $element.find("input.ng-invalid:first");
|
|
|
|
if (query && query.length) {
|
|
query[0].scrollIntoView();
|
|
query.focus();
|
|
}
|
|
};
|
|
|
|
$scope.validateAndSave = function() {
|
|
$scope.validating = $scope.getServices($scope.config);
|
|
|
|
var requirePassword = false;
|
|
for (var i = 0; i < $scope.validating.length; ++i) {
|
|
var serviceInfo = $scope.validating[i];
|
|
if (serviceInfo.service.password) {
|
|
requirePassword = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!requirePassword) {
|
|
$scope.performValidateAndSave();
|
|
return;
|
|
}
|
|
|
|
var box = bootbox.dialog({
|
|
"message": 'Please enter your superuser password to validate your auth configuration:' +
|
|
'<form style="margin-top: 10px" action="javascript:$(\'.btn-continue\').click()">' +
|
|
'<input id="validatePassword" class="form-control" type="password" placeholder="Password">' +
|
|
'</form>',
|
|
"title": 'Enter Password',
|
|
"buttons": {
|
|
"success": {
|
|
"label": "Validate Config",
|
|
"className": "btn-success btn-continue",
|
|
"callback": function() {
|
|
$scope.performValidateAndSave($('#validatePassword').val());
|
|
}
|
|
},
|
|
"close": {
|
|
"label": "Cancel",
|
|
"className": "btn-default",
|
|
"callback": function() {
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
box.bind('shown.bs.modal', function(){
|
|
box.find("input").focus();
|
|
box.find("form").submit(function() {
|
|
if (!$('#validatePassword').val()) { return; }
|
|
box.modal('hide');
|
|
});
|
|
});
|
|
};
|
|
|
|
$scope.performValidateAndSave = function(opt_password) {
|
|
$scope.savingConfiguration = false;
|
|
$scope.validating = $scope.getServices($scope.config);
|
|
|
|
authPassword = opt_password;
|
|
|
|
$('#validateAndSaveModal').modal({
|
|
keyboard: false,
|
|
backdrop: 'static'
|
|
});
|
|
|
|
for (var i = 0; i < $scope.validating.length; ++i) {
|
|
var serviceInfo = $scope.validating[i];
|
|
$scope.validateService(serviceInfo, opt_password);
|
|
}
|
|
};
|
|
|
|
$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,
|
|
'password': authPassword || ''
|
|
};
|
|
|
|
var errorDisplay = ApiService.errorDisplay(
|
|
'Could not save configuration. Please report this error.',
|
|
function() {
|
|
authPassword = null;
|
|
});
|
|
|
|
ApiService.scUpdateConfig(data).then(function(resp) {
|
|
authPassword = null;
|
|
|
|
$scope.savingConfiguration = false;
|
|
$scope.mapped.$hasChanges = false;
|
|
|
|
$('#validateAndSaveModal').modal('hide');
|
|
|
|
$scope.configurationSaved({'config': $scope.config});
|
|
}, errorDisplay);
|
|
};
|
|
|
|
// Convert storage config to an array
|
|
var initializeStorageConfig = function($scope) {
|
|
var config = $scope.config.DISTRIBUTED_STORAGE_CONFIG || {};
|
|
var defaultLocations = $scope.config.DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS || [];
|
|
var preference = $scope.config.DISTRIBUTED_STORAGE_PREFERENCE || [];
|
|
|
|
$scope.serverStorageConfig = angular.copy(config);
|
|
$scope.storageConfig = [];
|
|
|
|
Object.keys(config).forEach(function(location) {
|
|
$scope.storageConfig.push({
|
|
location: location,
|
|
defaultLocation: defaultLocations.indexOf(location) >= 0,
|
|
data: angular.copy(config[location]),
|
|
error: {},
|
|
});
|
|
});
|
|
|
|
if (!$scope.storageConfig.length) {
|
|
$scope.addStorageConfig('default');
|
|
return;
|
|
}
|
|
|
|
// match DISTRIBUTED_STORAGE_PREFERENCE order first, remaining are
|
|
// ordered by unicode point value
|
|
$scope.storageConfig.sort(function(a, b) {
|
|
var indexA = preference.indexOf(a.location);
|
|
var indexB = preference.indexOf(b.location);
|
|
|
|
if (indexA > -1 && indexB > -1) return indexA < indexB ? -1 : 1;
|
|
if (indexA > -1) return -1;
|
|
if (indexB > -1) return 1;
|
|
|
|
return a.location < b.location ? -1 : 1;
|
|
});
|
|
};
|
|
|
|
$scope.allowChangeLocationStorageConfig = function(location) {
|
|
if (!$scope.serverStorageConfig[location]) { return true };
|
|
|
|
// allow user to change location ID if another exists with the same ID
|
|
return $scope.storageConfig.filter(function(sc) {
|
|
return sc.location === location;
|
|
}).length >= 2;
|
|
};
|
|
|
|
$scope.allowRemoveStorageConfig = function(location) {
|
|
return $scope.storageConfig.length > 1 && $scope.allowChangeLocationStorageConfig(location);
|
|
};
|
|
|
|
$scope.canAddStorageConfig = function() {
|
|
return $scope.config &&
|
|
$scope.config.FEATURE_STORAGE_REPLICATION &&
|
|
$scope.storageConfig &&
|
|
(!$scope.storageConfig.length || $scope.storageConfig.length < 10);
|
|
};
|
|
|
|
$scope.addStorageConfig = function(location) {
|
|
var storageType = 'LocalStorage';
|
|
|
|
// Use last storage type by default
|
|
if ($scope.storageConfig.length) {
|
|
storageType = $scope.storageConfig[$scope.storageConfig.length-1].data[0];
|
|
}
|
|
|
|
$scope.storageConfig.push({
|
|
location: location || '',
|
|
defaultLocation: false,
|
|
data: [storageType, {}],
|
|
error: {},
|
|
});
|
|
};
|
|
|
|
$scope.removeStorageConfig = function(sc) {
|
|
$scope.storageConfig.splice($scope.storageConfig.indexOf(sc), 1);
|
|
};
|
|
|
|
var saveStorageConfig = function() {
|
|
var config = {};
|
|
var defaultLocations = [];
|
|
var preference = [];
|
|
|
|
$scope.storageConfig.forEach(function(sc) {
|
|
config[sc.location] = sc.data;
|
|
if (sc.defaultLocation) defaultLocations.push(sc.location);
|
|
preference.push(sc.location);
|
|
});
|
|
|
|
$scope.config.DISTRIBUTED_STORAGE_CONFIG = config;
|
|
$scope.config.DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS = defaultLocations;
|
|
$scope.config.DISTRIBUTED_STORAGE_PREFERENCE = preference;
|
|
};
|
|
|
|
var gitlabSelector = function(key) {
|
|
return function(value) {
|
|
if (!value || !$scope.config) { return; }
|
|
|
|
if (!$scope.config[key]) {
|
|
$scope.config[key] = {};
|
|
}
|
|
|
|
if (value == 'enterprise') {
|
|
if ($scope.config[key]['GITLAB_ENDPOINT'] == 'https://gitlab.com/') {
|
|
$scope.config[key]['GITLAB_ENDPOINT'] = '';
|
|
}
|
|
} else if (value == 'hosted') {
|
|
$scope.config[key]['GITLAB_ENDPOINT'] = 'https://gitlab.com/';
|
|
}
|
|
};
|
|
};
|
|
|
|
var githubSelector = function(key) {
|
|
return function(value) {
|
|
if (!value || !$scope.config) { return; }
|
|
|
|
if (!$scope.config[key]) {
|
|
$scope.config[key] = {};
|
|
}
|
|
|
|
if (value == 'enterprise') {
|
|
if ($scope.config[key]['GITHUB_ENDPOINT'] == 'https://github.com/') {
|
|
$scope.config[key]['GITHUB_ENDPOINT'] = '';
|
|
}
|
|
delete $scope.config[key]['API_ENDPOINT'];
|
|
} else if (value == 'hosted') {
|
|
$scope.config[key]['GITHUB_ENDPOINT'] = 'https://github.com/';
|
|
$scope.config[key]['API_ENDPOINT'] = 'https://api.github.com/';
|
|
}
|
|
};
|
|
};
|
|
|
|
var getKey = function(config, path) {
|
|
if (!config) {
|
|
return null;
|
|
}
|
|
|
|
var parts = path.split('.');
|
|
var current = config;
|
|
for (var i = 0; i < parts.length; ++i) {
|
|
var part = parts[i];
|
|
if (!current[part]) { return null; }
|
|
current = current[part];
|
|
}
|
|
return current;
|
|
};
|
|
|
|
var initializeMappedLogic = function(config) {
|
|
var gle = getKey(config, 'GITHUB_LOGIN_CONFIG.GITHUB_ENDPOINT');
|
|
var gte = getKey(config, 'GITHUB_TRIGGER_CONFIG.GITHUB_ENDPOINT');
|
|
|
|
$scope.mapped['GITHUB_LOGIN_KIND'] = gle == 'https://github.com/' ? 'hosted' : 'enterprise';
|
|
$scope.mapped['GITHUB_TRIGGER_KIND'] = gte == 'https://github.com/' ? 'hosted' : 'enterprise';
|
|
|
|
var glabe = getKey(config, 'GITLAB_TRIGGER_KIND.GITHUB_ENDPOINT');
|
|
$scope.mapped['GITLAB_TRIGGER_KIND'] = glabe == 'https://gitlab.com/' ? 'hosted' : 'enterprise';
|
|
|
|
$scope.mapped['redis'] = {};
|
|
$scope.mapped['redis']['host'] = getKey(config, 'BUILDLOGS_REDIS.host') || getKey(config, 'USER_EVENTS_REDIS.host');
|
|
$scope.mapped['redis']['port'] = getKey(config, 'BUILDLOGS_REDIS.port') || getKey(config, 'USER_EVENTS_REDIS.port');
|
|
$scope.mapped['redis']['password'] = getKey(config, 'BUILDLOGS_REDIS.password') || getKey(config, 'USER_EVENTS_REDIS.password');
|
|
};
|
|
|
|
var redisSetter = function(keyname) {
|
|
return function(value) {
|
|
if (value == null || !$scope.config) { return; }
|
|
|
|
if (!$scope.config['BUILDLOGS_REDIS']) {
|
|
$scope.config['BUILDLOGS_REDIS'] = {};
|
|
}
|
|
|
|
if (!$scope.config['USER_EVENTS_REDIS']) {
|
|
$scope.config['USER_EVENTS_REDIS'] = {};
|
|
}
|
|
|
|
if (!value) {
|
|
delete $scope.config['BUILDLOGS_REDIS'][keyname];
|
|
delete $scope.config['USER_EVENTS_REDIS'][keyname];
|
|
return;
|
|
}
|
|
|
|
$scope.config['BUILDLOGS_REDIS'][keyname] = value;
|
|
$scope.config['USER_EVENTS_REDIS'][keyname] = value;
|
|
};
|
|
};
|
|
|
|
// Add mapped logic.
|
|
$scope.$watch('mapped.GITHUB_LOGIN_KIND', githubSelector('GITHUB_LOGIN_CONFIG'));
|
|
$scope.$watch('mapped.GITHUB_TRIGGER_KIND', githubSelector('GITHUB_TRIGGER_CONFIG'));
|
|
$scope.$watch('mapped.GITLAB_TRIGGER_KIND', gitlabSelector('GITLAB_TRIGGER_KIND'));
|
|
|
|
$scope.$watch('mapped.redis.host', redisSetter('host'));
|
|
$scope.$watch('mapped.redis.port', redisSetter('port'));
|
|
$scope.$watch('mapped.redis.password', redisSetter('password'));
|
|
|
|
// Remove extra extra fields (which are not allowed) from storage config.
|
|
var updateFields = function(sc) {
|
|
var type = sc.data[0];
|
|
var configObject = sc.data[1];
|
|
var allowedFields = $scope.STORAGE_CONFIG_FIELDS[type];
|
|
|
|
// Remove any fields not allowed.
|
|
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];
|
|
}
|
|
}
|
|
|
|
// Set any missing boolean fields to false.
|
|
for (var i = 0; i < allowedFields.length; ++i) {
|
|
if (allowedFields[i].kind == 'bool') {
|
|
configObject[allowedFields[i].name] = configObject[allowedFields[i].name] || false;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Validate and update storage config on update.
|
|
var refreshStorageConfig = function() {
|
|
if (!$scope.config || !$scope.storageConfig) return;
|
|
|
|
var locationCounts = {};
|
|
var errors = [];
|
|
var valid = true;
|
|
|
|
$scope.storageConfig.forEach(function(sc) {
|
|
// remove extra fields from storage config
|
|
updateFields(sc);
|
|
|
|
if (!locationCounts[sc.location]) locationCounts[sc.location] = 0;
|
|
locationCounts[sc.location]++;
|
|
});
|
|
|
|
// validate storage config
|
|
$scope.storageConfig.forEach(function(sc) {
|
|
var error = {};
|
|
|
|
if ($scope.config.FEATURE_STORAGE_REPLICATION && sc.data[0] === 'LocalStorage') {
|
|
error.engine = 'Replication to a locally mounted directory is unsupported as it is only accessible on a single machine.';
|
|
valid = false;
|
|
}
|
|
|
|
if (locationCounts[sc.location] > 1) {
|
|
error.location = 'Location ID must be unique.';
|
|
valid = false;
|
|
}
|
|
|
|
errors.push(error);
|
|
});
|
|
|
|
$scope.storageConfigError = errors;
|
|
$scope.configform.$setValidity('storageConfig', valid);
|
|
};
|
|
|
|
$scope.$watch('config.FEATURE_STORAGE_REPLICATION', function() {
|
|
refreshStorageConfig();
|
|
});
|
|
|
|
$scope.$watch('storageConfig', function() {
|
|
refreshStorageConfig();
|
|
}, true);
|
|
|
|
$scope.$watch('config', function(value) {
|
|
$scope.mapped['$hasChanges'] = true;
|
|
}, true);
|
|
|
|
$scope.$watch('isActive', function(value) {
|
|
if (!value) { return; }
|
|
|
|
ApiService.scGetConfig().then(function(resp) {
|
|
$scope.config = resp['config'] || {};
|
|
initializeMappedLogic($scope.config);
|
|
initializeStorageConfig($scope);
|
|
$scope.mapped['$hasChanges'] = false;
|
|
}, ApiService.errorDisplay('Could not load config'));
|
|
});
|
|
}
|
|
};
|
|
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configParsedField', function ($timeout) {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-parsed-field.html',
|
|
replace: false,
|
|
transclude: true,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding',
|
|
'parser': '&parser',
|
|
'serializer': '&serializer'
|
|
},
|
|
controller: function($scope, $element, $transclude) {
|
|
$scope.childScope = null;
|
|
|
|
$transclude(function(clone, scope) {
|
|
$scope.childScope = scope;
|
|
$scope.childScope['fields'] = {};
|
|
$element.append(clone);
|
|
});
|
|
|
|
$scope.childScope.$watch('fields', function(value) {
|
|
// Note: We need the timeout here because Angular starts the digest of the
|
|
// parent scope AFTER the child scope, which means it can end up one action
|
|
// behind. The timeout ensures that the parent scope will be fully digest-ed
|
|
// and then we update the binding. Yes, this is a hack :-/.
|
|
$timeout(function() {
|
|
$scope.binding = $scope.serializer({'fields': value});
|
|
});
|
|
}, true);
|
|
|
|
$scope.$watch('binding', function(value) {
|
|
var parsed = $scope.parser({'value': value});
|
|
for (var key in parsed) {
|
|
if (parsed.hasOwnProperty(key)) {
|
|
$scope.childScope['fields'][key] = parsed[key];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configVariableField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-variable-field.html',
|
|
replace: false,
|
|
transclude: true,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding'
|
|
},
|
|
controller: function($scope, $element) {
|
|
$scope.sections = {};
|
|
$scope.currentSection = null;
|
|
|
|
$scope.setSection = function(section) {
|
|
$scope.binding = section.value;
|
|
};
|
|
|
|
this.addSection = function(section, element) {
|
|
$scope.sections[section.value] = {
|
|
'title': section.valueTitle,
|
|
'value': section.value,
|
|
'element': element
|
|
};
|
|
|
|
element.hide();
|
|
|
|
if (!$scope.binding) {
|
|
$scope.binding = section.value;
|
|
}
|
|
};
|
|
|
|
$scope.$watch('binding', function(binding) {
|
|
if (!binding) { return; }
|
|
|
|
if ($scope.currentSection) {
|
|
$scope.currentSection.element.hide();
|
|
}
|
|
|
|
if ($scope.sections[binding]) {
|
|
$scope.sections[binding].element.show();
|
|
$scope.currentSection = $scope.sections[binding];
|
|
}
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('variableSection', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-variable-field.html',
|
|
priority: 1,
|
|
require: '^configVariableField',
|
|
replace: false,
|
|
transclude: true,
|
|
restrict: 'C',
|
|
scope: {
|
|
'value': '@value',
|
|
'valueTitle': '@valueTitle'
|
|
},
|
|
controller: function($scope, $element) {
|
|
var parentCtrl = $element.parent().controller('configVariableField');
|
|
parentCtrl.addSection($scope, $element);
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configListField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-list-field.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding',
|
|
'placeholder': '@placeholder',
|
|
'defaultValue': '@defaultValue',
|
|
'itemTitle': '@itemTitle'
|
|
},
|
|
controller: function($scope, $element) {
|
|
$scope.removeItem = function(item) {
|
|
var index = $scope.binding.indexOf(item);
|
|
if (index >= 0) {
|
|
$scope.binding.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
$scope.addItem = function() {
|
|
if (!$scope.newItemName) {
|
|
return;
|
|
}
|
|
|
|
if (!$scope.binding) {
|
|
$scope.binding = [];
|
|
}
|
|
|
|
if ($scope.binding.indexOf($scope.newItemName) >= 0) {
|
|
return;
|
|
}
|
|
|
|
$scope.binding.push($scope.newItemName);
|
|
$scope.newItemName = null;
|
|
};
|
|
|
|
$scope.$watch('binding', function(binding) {
|
|
if (!binding && $scope.defaultValue) {
|
|
$scope.binding = eval($scope.defaultValue);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configFileField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-file-field.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'filename': '@filename',
|
|
'skipCheckFile': '@skipCheckFile',
|
|
'hasFile': '=hasFile'
|
|
},
|
|
controller: function($scope, $element, Restangular, $upload) {
|
|
$scope.hasFile = false;
|
|
|
|
$scope.onFileSelect = function(files) {
|
|
if (files.length < 1) {
|
|
$scope.hasFile = false;
|
|
return;
|
|
}
|
|
|
|
$scope.uploadProgress = 0;
|
|
$scope.upload = $upload.upload({
|
|
url: '/api/v1/superuser/config/file/' + $scope.filename,
|
|
method: 'POST',
|
|
data: {'_csrf_token': window.__token},
|
|
file: files[0],
|
|
}).progress(function(evt) {
|
|
$scope.uploadProgress = parseInt(100.0 * evt.loaded / evt.total);
|
|
if ($scope.uploadProgress == 100) {
|
|
$scope.uploadProgress = null;
|
|
$scope.hasFile = true;
|
|
}
|
|
}).success(function(data, status, headers, config) {
|
|
$scope.uploadProgress = null;
|
|
$scope.hasFile = true;
|
|
});
|
|
};
|
|
|
|
var loadStatus = function(filename) {
|
|
Restangular.one('superuser/config/file/' + filename).get().then(function(resp) {
|
|
$scope.hasFile = resp['exists'];
|
|
});
|
|
};
|
|
|
|
if ($scope.filename && $scope.skipCheckFile != "true") {
|
|
loadStatus($scope.filename);
|
|
}
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configBoolField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-bool-field.html',
|
|
replace: false,
|
|
transclude: true,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding'
|
|
},
|
|
controller: function($scope, $element) {
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configNumericField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-numeric-field.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding',
|
|
'placeholder': '@placeholder',
|
|
'defaultValue': '@defaultValue'
|
|
},
|
|
controller: function($scope, $element) {
|
|
$scope.bindinginternal = 0;
|
|
|
|
$scope.$watch('binding', function(binding) {
|
|
if ($scope.binding == 0 && $scope.defaultValue) {
|
|
$scope.binding = $scope.defaultValue * 1;
|
|
}
|
|
|
|
$scope.bindinginternal = $scope.binding;
|
|
});
|
|
|
|
$scope.$watch('bindinginternal', function(binding) {
|
|
var newValue = $scope.bindinginternal * 1;
|
|
if (isNaN(newValue)) {
|
|
newValue = 0;
|
|
}
|
|
$scope.binding = newValue;
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configContactsField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-contacts-field.html',
|
|
priority: 1,
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding'
|
|
},
|
|
controller: function($scope, $element) {
|
|
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);
|
|
return;
|
|
}
|
|
|
|
// If the last item is non-empty, add a new item.
|
|
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) {
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configContactField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-contact-field.html',
|
|
priority: 1,
|
|
replace: false,
|
|
transclude: true,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding'
|
|
},
|
|
controller: function($scope, $element) {
|
|
$scope.kind = null;
|
|
$scope.value = null;
|
|
|
|
var updateBinding = function() {
|
|
if ($scope.value == null) { return; }
|
|
var value = $scope.value || '';
|
|
|
|
switch ($scope.kind) {
|
|
case 'mailto':
|
|
$scope.binding = 'mailto:' + value;
|
|
return;
|
|
|
|
case 'tel':
|
|
$scope.binding = 'tel:' + value;
|
|
return;
|
|
|
|
case 'irc':
|
|
$scope.binding = 'irc://' + value;
|
|
return;
|
|
|
|
default:
|
|
$scope.binding = value;
|
|
return;
|
|
}
|
|
};
|
|
|
|
$scope.$watch('kind', updateBinding);
|
|
$scope.$watch('value', updateBinding);
|
|
|
|
$scope.$watch('binding', function(value) {
|
|
if (!value) {
|
|
$scope.kind = null;
|
|
$scope.value = null;
|
|
return;
|
|
}
|
|
|
|
var uri = URI(value);
|
|
$scope.kind = uri.scheme();
|
|
|
|
switch ($scope.kind) {
|
|
case 'mailto':
|
|
case 'tel':
|
|
$scope.value = uri.path();
|
|
break;
|
|
|
|
case 'irc':
|
|
$scope.value = value.substr('irc://'.length);
|
|
break;
|
|
|
|
default:
|
|
$scope.kind = 'http';
|
|
$scope.value = value;
|
|
break;
|
|
}
|
|
});
|
|
|
|
$scope.getPlaceholder = function(kind) {
|
|
switch (kind) {
|
|
case 'mailto':
|
|
return 'some@example.com';
|
|
|
|
case 'tel':
|
|
return '555-555-5555';
|
|
|
|
case 'irc':
|
|
return 'myserver:port/somechannel';
|
|
|
|
default:
|
|
return 'http://some/url';
|
|
}
|
|
};
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configMapField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-map-field.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding',
|
|
'keys': '=keys'
|
|
},
|
|
controller: function($scope, $element) {
|
|
$scope.newKey = null;
|
|
$scope.newValue = null;
|
|
|
|
$scope.hasValues = function(binding) {
|
|
return binding && Object.keys(binding).length;
|
|
};
|
|
|
|
$scope.removeKey = function(key) {
|
|
delete $scope.binding[key];
|
|
};
|
|
|
|
$scope.addEntry = function() {
|
|
if (!$scope.newKey || !$scope.newValue) { return; }
|
|
|
|
$scope.binding = $scope.binding || {};
|
|
$scope.binding[$scope.newKey] = $scope.newValue;
|
|
$scope.newKey = null;
|
|
$scope.newValue = null;
|
|
}
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configServiceKeyField', function (ApiService) {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-service-key-field.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'serviceName': '@serviceName',
|
|
},
|
|
controller: function($scope, $element) {
|
|
$scope.foundKeys = [];
|
|
$scope.loading = false;
|
|
$scope.loadError = false;
|
|
$scope.hasValidKey = false;
|
|
$scope.hasValidKeyStr = null;
|
|
|
|
$scope.updateKeys = function() {
|
|
$scope.foundKeys = [];
|
|
$scope.loading = true;
|
|
|
|
ApiService.listServiceKeys().then(function(resp) {
|
|
$scope.loading = false;
|
|
$scope.loadError = false;
|
|
|
|
resp['keys'].forEach(function(key) {
|
|
if (key['service'] == $scope.serviceName) {
|
|
$scope.foundKeys.push(key);
|
|
}
|
|
});
|
|
|
|
$scope.hasValidKey = checkValidKey($scope.foundKeys);
|
|
$scope.hasValidKeyStr = $scope.hasValidKey ? 'true' : '';
|
|
}, function() {
|
|
$scope.loading = false;
|
|
$scope.loadError = true;
|
|
});
|
|
};
|
|
|
|
// Perform initial loading of the keys.
|
|
$scope.updateKeys();
|
|
|
|
$scope.isKeyExpired = function(key) {
|
|
if (key.expiration_date != null) {
|
|
var expiration_date = moment(key.expiration_date);
|
|
return moment().isAfter(expiration_date);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
$scope.showRequestServiceKey = function() {
|
|
$scope.requestKeyInfo = {
|
|
'service': $scope.serviceName
|
|
};
|
|
};
|
|
|
|
$scope.handleKeyCreated = function() {
|
|
$scope.updateKeys();
|
|
};
|
|
|
|
var checkValidKey = function(keys) {
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
if (!key.approval) {
|
|
continue;
|
|
}
|
|
|
|
if ($scope.isKeyExpired(key)) {
|
|
continue;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configStringField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-string-field.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding',
|
|
'placeholder': '@placeholder',
|
|
'pattern': '@pattern',
|
|
'defaultValue': '@defaultValue',
|
|
'validator': '&validator',
|
|
'isOptional': '=isOptional'
|
|
},
|
|
controller: function($scope, $element) {
|
|
var firstSet = true;
|
|
|
|
$scope.patternMap = {};
|
|
|
|
$scope.getRegexp = function(pattern) {
|
|
if (!pattern) {
|
|
pattern = '.*';
|
|
}
|
|
|
|
if ($scope.patternMap[pattern]) {
|
|
return $scope.patternMap[pattern];
|
|
}
|
|
|
|
return $scope.patternMap[pattern] = new RegExp(pattern);
|
|
};
|
|
|
|
$scope.$watch('binding', function(binding) {
|
|
if (firstSet && !binding && $scope.defaultValue) {
|
|
$scope.binding = $scope.defaultValue;
|
|
firstSet = false;
|
|
}
|
|
|
|
$scope.errorMessage = $scope.validator({'value': binding || ''});
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
})
|
|
|
|
.directive('configStringListField', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/config/config-string-list-field.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'binding': '=binding',
|
|
'itemTitle': '@itemTitle',
|
|
'itemDelimiter': '@itemDelimiter',
|
|
'placeholder': '@placeholder',
|
|
'isOptional': '=isOptional'
|
|
},
|
|
controller: function($scope, $element) {
|
|
$scope.$watch('internalBinding', function(value) {
|
|
if (value) {
|
|
$scope.binding = value.split($scope.itemDelimiter);
|
|
}
|
|
});
|
|
|
|
$scope.$watch('binding', function(value) {
|
|
if (value) {
|
|
$scope.internalBinding = value.join($scope.itemDelimiter);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
});
|
|
|