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': 'time-machine', 'title': 'Time Machine'}, {'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; }}, {'id': 'bittorrent', 'title': 'BitTorrent downloads', 'condition': function(config) { return config.FEATURE_BITTORRENT; }}, {'id': 'oidc-login', 'title': 'OIDC Login(s)', 'condition': function(config) { return $scope.getOIDCProviders(config).length > 0; }}, ]; $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}, {'name': 'port', 'title': 'S3 Port (optional)', 'placeholder': '443', 'kind': 'text', 'pattern': '^[0-9]+$', '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': 'port', 'title': 'Custom Port (optional)', 'placeholder': '443', 'kind': 'text', 'pattern': '^[0-9]+$', 'optional': true}, {'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.removeOIDCProvider = function(provider) { delete $scope.config[provider]; }; $scope.addOIDCProvider = function() { bootbox.prompt('Enter an ID for the OIDC provider', function(result) { if (!result) { return; } result = result.toUpperCase(); if (!result.match(/^[A-Z0-9]+$/)) { bootbox.alert('Invalid ID for OIDC provider: must be alphanumeric'); return; } if (result == 'GITHUB' || result == 'GOOGLE') { bootbox.alert('Invalid ID for OIDC provider: cannot be a reserved name'); return; } var key = result + '_LOGIN_CONFIG'; if ($scope.config[key]) { bootbox.alert('Invalid ID for OIDC provider: already exists'); return; } $scope.config[key] = {}; }); }; $scope.getOIDCProviderId = function(key) { var index = key.indexOf('_LOGIN_CONFIG'); if (index <= 0) { return null; } return key.substr(0, index); }; $scope.getOIDCProviders = function(config) { var keys = Object.keys(config || {}); return keys.filter(function(key) { if (key == 'GITHUB_LOGIN_CONFIG' || key == 'GOOGLE_LOGIN_CONFIG') { // Has custom UI and config. return false; } return !!$scope.getOIDCProviderId(key); }); }; $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:' + '
' + '' + '
', "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'); $scope.mapped['TLS_SETTING'] = 'none'; if (config['PREFERRED_URL_SCHEME'] == 'https') { if (config['EXTERNAL_TLS_TERMINATION'] === true) { $scope.mapped['TLS_SETTING'] = 'external-tls'; } else { $scope.mapped['TLS_SETTING'] = 'internal-tls'; } } }; var tlsSetter = function(value) { if (value == null || !$scope.config) { return; } switch (value) { case 'none': $scope.config['PREFERRED_URL_SCHEME'] = 'http'; delete $scope.config['EXTERNAL_TLS_TERMINATION']; return; case 'external-tls': $scope.config['PREFERRED_URL_SCHEME'] = 'https'; $scope.config['EXTERNAL_TLS_TERMINATION'] = true; return; case 'internal-tls': $scope.config['PREFERRED_URL_SCHEME'] = 'https'; delete $scope.config['EXTERNAL_TLS_TERMINATION']; return; } }; 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.TLS_SETTING', tlsSetter); $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', 'itemPattern': '@itemPattern' }, 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.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 (!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; }) .directive('configCertificatesField', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/config/config-certificates-field.html', replace: false, transclude: false, restrict: 'C', scope: { }, controller: function($scope, $element, $upload, ApiService, UserService) { $scope.resetUpload = 0; $scope.certsUploading = false; var loadCertificates = function() { $scope.certificatesResource = ApiService.getCustomCertificatesAsResource().get(function(resp) { $scope.certInfo = resp; $scope.certsUploading = false; }); }; UserService.updateUserIn($scope, function(user) { if (!user.anonymous) { loadCertificates(); } }); $scope.handleCertsSelected = function(files, callback) { $scope.certsUploading = true; $upload.upload({ url: '/api/v1/superuser/customcerts/' + files[0].name, method: 'POST', data: {'_csrf_token': window.__token}, file: files[0] }).success(function() { callback(true); $scope.resetUpload++; loadCertificates(); }).error(function(r) { bootbox.alert('Could not upload certificate') callback(false); $scope.resetUpload++; loadCertificates(); }); }; $scope.deleteCert = function(path) { var errorDisplay = ApiService.errorDisplay('Could not delete certificate'); var params = { 'certpath': path }; ApiService.deleteCustomCertificate(null, params).then(loadCertificates, errorDisplay); }; } }; return directiveDefinitionObject; }) .directive('configLicenseField', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/config/config-license-field.html', replace: false, transclude: false, restrict: 'C', scope: { 'isValid': '=?isValid', 'forSetup': '@forSetup' }, controller: function($scope, $element, ApiService, UserService) { $scope.LicenseStates = { none: 'no-license', loading: 'license-loading', valid: 'license-valid', invalid: 'license-error', validating: 'validating-license' }; $scope.state = $scope.forSetup == 'true' ? $scope.LicenseStates.none : $scope.LicenseStates.loading; $scope.showingEditor = $scope.forSetup == 'true'; $scope.requiredBox = ''; $scope.requirementTitles = { 'software.quay': 'Quay Enterprise', 'software.quay.deployments': 'Quay Enterprise Deployments' }; var handleLicenseSuccess = function(resp) { $scope.state = resp['success'] ? $scope.LicenseStates.valid : $scope.LicenseStates.invalid; $scope.requiredBox = resp['success'] ? 'filled' : ''; $scope.showingEditor = !resp['success']; $scope.licenseStatus = resp['status']; $scope.licenseError = null; $scope.isValid = resp['success']; }; var handleLicenseError = function(resp) { $scope.licenseError = ApiService.getErrorMessage(resp); $scope.licenseStatus = null; $scope.state = 'license-error'; $scope.showingEditor = true; $scope.requiredBox = ''; $scope.isValid = false; }; var loadLicense = function() { if ($scope.forSetup == 'true') { $scope.state = $scope.LicenseStates.none; return; } ApiService.getLicense().then(handleLicenseSuccess, handleLicenseError); }; UserService.updateUserIn($scope, function(user) { if (!user.anonymous) { loadLicense(); } }); $scope.showEditor = function($event) { $event.preventDefault(); $event.stopPropagation(); $scope.showingEditor = true; }; $scope.validateAndUpdate = function($event) { $event.preventDefault(); $event.stopPropagation(); $scope.state = $scope.LicenseStates.validating; var data = { 'license': $scope.licenseContents }; if ($scope.forSetup == 'true') { ApiService.suSetAndValidateLicense(data).then(handleLicenseSuccess, handleLicenseError); } else { ApiService.updateLicense(data).then(handleLicenseSuccess, handleLicenseError); } }; } }; return directiveDefinitionObject; });