diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 50d61efe1..b970b0f32 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -19,6 +19,8 @@ angular.module("core-config-setup", ['angularFileUpload']) $scope.GITHOST_REGEX = '^https?://([a-zA-Z0-9]+\.?\/?)+$'; $scope.SERVICES = [ + {'id': 'license', 'title': 'License'}, + {'id': 'redis', 'title': 'Redis'}, {'id': 'registry-storage', 'title': 'Registry Storage'}, diff --git a/util/config/validator.py b/util/config/validator.py index 90582ccd1..c53dc428c 100644 --- a/util/config/validator.py +++ b/util/config/validator.py @@ -3,6 +3,7 @@ import logging from auth.auth_context import get_authenticated_user from data.users import LDAP_CERT_FILENAME +from util.config.validators.validate_license import LicenseValidator from util.config.validators.validate_database import DatabaseValidator from util.config.validators.validate_redis import RedisValidator from util.config.validators.validate_storage import StorageValidator @@ -40,6 +41,7 @@ CONFIG_FILENAMES = (SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES + ACI_CERT_ EXTRA_CA_DIRECTORY = 'extra_ca_certs' VALIDATORS = { + LicenseValidator.name: LicenseValidator.validate, DatabaseValidator.name: DatabaseValidator.validate, RedisValidator.name: RedisValidator.validate, StorageValidator.name: StorageValidator.validate, diff --git a/util/config/validators/test/test_validate_license.py b/util/config/validators/test/test_validate_license.py new file mode 100644 index 000000000..bf1908742 --- /dev/null +++ b/util/config/validators/test/test_validate_license.py @@ -0,0 +1,48 @@ +import pytest + +from mock import patch + +from util.config.validators import ConfigValidationException +from util.config.validators.validate_license import LicenseValidator +from util.morecollections import AttrDict +from util.license import License, QUAY_DEPLOYMENTS_ENTITLEMENT, QUAY_ENTITLEMENT + +from test.fixtures import * + +@pytest.mark.parametrize('deployments, allowed_deployments', [ + (1, 1), + (3, 3), + (3, 2), + (3, 1), +]) +def test_too_many_storage_engines(deployments, allowed_deployments, app): + def get_license(): + decoded = { + 'expirationDate': '2157-12-1', + 'subscriptions': { + 'someSubscription': { + 'serviceEnd': '2157-12-1', + 'durationPeriod': 'yearly', + 'entitlements': { + QUAY_ENTITLEMENT: 1, + QUAY_DEPLOYMENTS_ENTITLEMENT: allowed_deployments, + }, + }, + }, + } + return License(decoded) + + storage_configs = [(str(i), {}) for i in range(0, deployments)] + + with patch('app.config_provider.get_license', get_license): + validator = LicenseValidator() + + if allowed_deployments < deployments: + with pytest.raises(ConfigValidationException): + validator.validate({ + 'DISTRIBUTED_STORAGE_CONFIG': storage_configs, + }, None, None) + else: + validator.validate({ + 'DISTRIBUTED_STORAGE_CONFIG': storage_configs, + }, None, None) diff --git a/util/config/validators/validate_license.py b/util/config/validators/validate_license.py new file mode 100644 index 000000000..ebb13e9e6 --- /dev/null +++ b/util/config/validators/validate_license.py @@ -0,0 +1,19 @@ +from app import config_provider +from util.config.validators import BaseValidator, ConfigValidationException +from util.license import LicenseDecodeError, EntitlementStatus + +class LicenseValidator(BaseValidator): + name = "license" + + @classmethod + def validate(cls, config, user, user_password): + try: + decoded_license = config_provider.get_license() + except LicenseDecodeError as le: + raise ConfigValidationException('Could not decode license: %s' % le.message) + + results = decoded_license.validate(config) + all_met = all(result.is_met() for result in results) + if not all_met: + reason = [result.description() for result in results if not result.is_met()] + raise ConfigValidationException('License does not match configuration: %s' % reason) diff --git a/util/license.py b/util/license.py index decd6bdbb..734a1ab78 100644 --- a/util/license.py +++ b/util/license.py @@ -204,6 +204,10 @@ class EntitlementValidationResult(object): entitlement=repr(self.entitlement), )) + def description(self): + msg = '%s requires %s: has status %s' + return msg % (self.requirement.name, self.requirement.count, self.get_status()) + def as_dict(self, for_private=False): def req_view(): return {