From 192967afe93e3fd9dea345e0f29dc9b3d95aa5aa Mon Sep 17 00:00:00 2001 From: Huy Huynh Date: Fri, 15 Mar 2019 13:29:40 -0700 Subject: [PATCH] Support INGRESS_AVAILABLE and add tests (#329) --- docs/schema.md | 16 +++-- marketplace/deployer_util/config_helper.py | 31 +++++++-- .../deployer_util/config_helper_test.py | 67 ++++++++++++++++++- marketplace/deployer_util/expand_config.py | 52 +++++++------- .../deployer_util/expand_config_test.py | 28 ++++++++ marketplace/deployer_util/provision.py | 7 ++ .../deployer_envsubst_base/full/schema.yaml | 8 +++ 7 files changed, 173 insertions(+), 36 deletions(-) diff --git a/docs/schema.md b/docs/schema.md index 776c0558..b770e862 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -260,7 +260,8 @@ It defines how this object will be handled. Each type has a different set of pro - `STORAGE_CLASS`: The name of a pre-provisioned k8s `StorageClass`. If it does not exist, one is created. - `STRING`: A string that needs special handling. - `APPLICATION_UID`: The uuid of the created `Application` object. -- `ISTIO_ENABLED`: Indicates whether Istio is enabled for the deployement. +- `ISTIO_ENABLED`: Indicates whether Istio is enabled for the deployment. +- `INGRESS_AVAILABLE`: Indicates whether the cluster is detected to have Ingress support. --- @@ -410,7 +411,11 @@ In the example above, manifests can reference to the password as `explicitPasswo This boolean property receives a True value if the environment is detected to have Istio enabled, and False otherwise. The deployer and template can take this signal to adapt the deployment accordingly. -If `ISTIO_ENABLED` is not present, it is assumed Istio is NOT enabled for the deployment. +--- + +### type: INGRESS_AVAILABLE + +This boolean property receives a True value if the cluster is detected to have Ingress controller. The deployer and template can take this signal to adapt the deployment accordingly. --- @@ -475,9 +480,12 @@ x-google-marketplace: type: OPTIONAL | REQUIRED | UNSUPPORTED ``` +If this is not specified, the UI will warn the user when the app is deployed into an istio enabled environment. + +Also see `ISTIO_ENABLED` property type. + #### Supported types + - `OPTIONAL`: The app works with Istio but does not require it. - `REQUIRED`: The app requires Istio to work properly. - `UNSUPPORTED`: The app does not support Istio. - -If the compatibility with Istio is unknown, the property is omitted completely. diff --git a/marketplace/deployer_util/config_helper.py b/marketplace/deployer_util/config_helper.py index c8fad12b..c9c89082 100644 --- a/marketplace/deployer_util/config_helper.py +++ b/marketplace/deployer_util/config_helper.py @@ -36,6 +36,7 @@ XTYPE_STRING = 'STRING' XTYPE_APPLICATION_UID = 'APPLICATION_UID' XTYPE_ISTIO_ENABLED = 'ISTIO_ENABLED' +XTYPE_INGRESS_AVAILABLE = 'INGRESS_AVAILABLE' WIDGET_TYPES = ['help'] @@ -464,7 +465,6 @@ def __init__(self, name, dictionary, required): self._service_account = None self._storage_class = None self._string = None - self._istio_enabled = None if not NAME_RE.match(name): raise InvalidSchema('Invalid property name: {}'.format(name)) @@ -488,16 +488,20 @@ def __init__(self, name, dictionary, required): if self._x: xt = _must_get(self._x, 'type', 'Property {} has {} without a type'.format(name, XGOOGLE)) - if xt in (XTYPE_NAME, XTYPE_NAMESPACE, XTYPE_DEPLOYER_IMAGE, - XTYPE_ISTIO_ENABLED): - pass + if xt in (XTYPE_NAME, XTYPE_NAMESPACE, XTYPE_DEPLOYER_IMAGE): + _property_must_have_type(self, str) + elif xt in (XTYPE_ISTIO_ENABLED, XTYPE_INGRESS_AVAILABLE): + _property_must_have_type(self, bool) elif xt == XTYPE_APPLICATION_UID: + _property_must_have_type(self, str) d = self._x.get('applicationUid', {}) self._application_uid = SchemaXApplicationUid(d) elif xt == XTYPE_IMAGE: + _property_must_have_type(self, str) d = self._x.get('image', {}) self._image = SchemaXImage(d) elif xt == XTYPE_PASSWORD: + _property_must_have_type(self, str) d = self._x.get('generatedPassword', {}) spec = { 'length': d.get('length', 10), @@ -506,15 +510,19 @@ def __init__(self, name, dictionary, required): } self._password = SchemaXPassword(**spec) elif xt == XTYPE_SERVICE_ACCOUNT: + _property_must_have_type(self, str) d = self._x.get('serviceAccount', {}) self._service_account = SchemaXServiceAccount(d) elif xt == XTYPE_STORAGE_CLASS: + _property_must_have_type(self, str) d = self._x.get('storageClass', {}) self._storage_class = SchemaXStorageClass(d) elif xt == XTYPE_STRING: + _property_must_have_type(self, str) d = self._x.get('string', {}) self._string = SchemaXString(d) elif xt == XTYPE_REPORTING_SECRET: + _property_must_have_type(self, str) d = self._x.get('reportingSecret', {}) self._reporting_secret = SchemaXReportingSecret(d) else: @@ -758,7 +766,20 @@ def _must_get_and_apply(dictionary, key, apply_fn, error_msg): def _must_contain(value, valid_list, error_msg): - """Validates that value is one of valid_list. Raises InvalidSchema if valid_list does not contain the value.""" + """Validates that value in valid_list, or raises InvalidSchema.""" if value not in valid_list: raise InvalidSchema("{}. Must be one of {}".format(error_msg, ', '.join(_ISTIO_TYPES))) + + +def _property_must_have_type(prop, expected_type): + if prop.type != expected_type: + readable_type = { + str: 'string', + bool: 'boolean', + int: 'integer', + float: 'float', + }.get(expected_type, expected_type.__name__) + raise InvalidSchema( + '{} x-google-marketplace type property must be of type {}'.format( + prop.xtype, readable_type)) diff --git a/marketplace/deployer_util/config_helper_test.py b/marketplace/deployer_util/config_helper_test.py index d42e3a0a..493ef306 100644 --- a/marketplace/deployer_util/config_helper_test.py +++ b/marketplace/deployer_util/config_helper_test.py @@ -66,6 +66,10 @@ type: boolean x-google-marketplace: type: ISTIO_ENABLED + ingressAvailable: + type: boolean + x-google-marketplace: + type: INGRESS_AVAILABLE required: - propertyString - propertyPassword @@ -115,7 +119,7 @@ def test_types_and_defaults(self): 'propertyIntegerWithDefault', 'propertyNumber', 'propertyNumberWithDefault', 'propertyBoolean', 'propertyBooleanWithDefault', 'propertyImage', 'propertyDeployerImage', - 'propertyPassword', 'applicationUid', 'istioEnabled' + 'propertyPassword', 'applicationUid', 'istioEnabled', 'ingressAvailable' }, set(schema.properties)) self.assertEqual(str, schema.properties['propertyString'].type) self.assertIsNone(schema.properties['propertyString'].default) @@ -152,6 +156,11 @@ def test_types_and_defaults(self): schema.properties['propertyPassword'].xtype) self.assertEqual('My arbitrary description', schema.form[0]['description']) + self.assertEqual(bool, schema.properties['istioEnabled'].type) + self.assertEqual('ISTIO_ENABLED', schema.properties['istioEnabled'].xtype) + self.assertEqual(bool, schema.properties['ingressAvailable'].type) + self.assertEqual('INGRESS_AVAILABLE', + schema.properties['ingressAvailable'].xtype) def test_invalid_names(self): self.assertRaises( @@ -168,6 +177,62 @@ def test_valid_names(self): type: string """) + def test_invalid_property_types(self): + self.assertRaisesRegexp( + config_helper.InvalidSchema, + r'.*must be of type string$', lambda: config_helper.Schema.load_yaml(""" + properties: + u: + type: integer + x-google-marketplace: + type: NAME + """)) + self.assertRaisesRegexp( + config_helper.InvalidSchema, + r'.*must be of type string$', lambda: config_helper.Schema.load_yaml(""" + properties: + u: + type: number + x-google-marketplace: + type: NAMESPACE + """)) + self.assertRaisesRegexp( + config_helper.InvalidSchema, + r'.*must be of type string$', lambda: config_helper.Schema.load_yaml(""" + properties: + u: + type: int + x-google-marketplace: + type: DEPLOYER_IMAGE + """)) + self.assertRaisesRegexp( + config_helper.InvalidSchema, + r'.*must be of type string$', lambda: config_helper.Schema.load_yaml(""" + properties: + u: + type: boolean + x-google-marketplace: + type: APPLICATION_UID + """)) + self.assertRaisesRegexp( + config_helper.InvalidSchema, r'.*must be of type boolean$', lambda: + config_helper.Schema.load_yaml(""" + properties: + u: + type: string + x-google-marketplace: + type: ISTIO_ENABLED + """)) + self.assertRaisesRegexp( + config_helper.InvalidSchema, r'.*must be of type boolean$', lambda: + config_helper.Schema.load_yaml(""" + properties: + u: + type: string + x-google-marketplace: + type: INGRESS_AVAILABLE + """)) + def test_required(self): schema = config_helper.Schema.load_yaml(SCHEMA) self.assertTrue(schema.properties['propertyString'].required) diff --git a/marketplace/deployer_util/expand_config.py b/marketplace/deployer_util/expand_config.py index 9890534c..1ddd2598 100755 --- a/marketplace/deployer_util/expand_config.py +++ b/marketplace/deployer_util/expand_config.py @@ -20,6 +20,7 @@ import yaml +import config_helper import schema_values_common from password import GeneratePassword @@ -69,32 +70,31 @@ def expand(values_dict, schema, app_uid=''): for k, prop in schema.properties.iteritems(): v = values_dict.get(k, None) - if v is None and prop.password: - if prop.type != str: - raise InvalidProperty( - 'Property {} is expected to be of type string'.format(k)) - result[k] = generate_password(prop.password) - continue - - if v is None and prop.application_uid: - result[k] = app_uid or '' - generate_properties_for_appuid(prop, app_uid, generated) - continue - - if v is None and prop.default is not None: - v = prop.default - - if v is not None and prop.image: - if not isinstance(v, str): - raise InvalidProperty('Invalid value for IMAGE property {}: {}'.format( - k, v)) - generate_properties_for_image(prop, v, generated) - - if v is not None and prop.string: - if not isinstance(v, str): - raise InvalidProperty('Invalid value for STRING property {}: {}'.format( - k, v)) - generate_properties_for_string(prop, v, generated) + if v is None: + if prop.password: + v = generate_password(prop.password) + elif prop.application_uid: + v = app_uid or '' + generate_properties_for_appuid(prop, app_uid, generated) + elif prop.xtype == config_helper.XTYPE_ISTIO_ENABLED: + # For backward compatibility. + v = False + elif prop.xtype == config_helper.XTYPE_INGRESS_AVAILABLE: + # For backward compatibility. + v = True + elif prop.default is not None: + v = prop.default + else: # if v is None + if prop.image: + if not isinstance(v, str): + raise InvalidProperty( + 'Invalid value for IMAGE property {}: {}'.format(k, v)) + generate_properties_for_image(prop, v, generated) + if prop.string: + if not isinstance(v, str): + raise InvalidProperty( + 'Invalid value for STRING property {}: {}'.format(k, v)) + generate_properties_for_string(prop, v, generated) if v is not None: result[k] = v diff --git a/marketplace/deployer_util/expand_config_test.py b/marketplace/deployer_util/expand_config_test.py index 9322dc8d..220c307a 100644 --- a/marketplace/deployer_util/expand_config_test.py +++ b/marketplace/deployer_util/expand_config_test.py @@ -146,6 +146,34 @@ def test_application_uid(self): 'application.create': True }, result) + def test_istio_enabled_backward_compatibility(self): + schema = config_helper.Schema.load_yaml(""" + applicationApiVersion: v1beta1 + properties: + istioEnabled: + type: boolean + x-google-marketplace: + type: ISTIO_ENABLED + """) + result = expand_config.expand({}, schema) + self.assertEqual({'istioEnabled': False}, result) + result = expand_config.expand({'istioEnabled': True}, schema) + self.assertEqual({'istioEnabled': True}, result) + + def test_ingress_available_backward_compatibility(self): + schema = config_helper.Schema.load_yaml(""" + applicationApiVersion: v1beta1 + properties: + ingressAvail: + type: boolean + x-google-marketplace: + type: INGRESS_AVAILABLE + """) + result = expand_config.expand({}, schema) + self.assertEqual({'ingressAvail': True}, result) + result = expand_config.expand({'ingressAvail': False}, schema) + self.assertEqual({'ingressAvail': False}, result) + def test_write_values(self): schema = config_helper.Schema.load_yaml(""" applicationApiVersion: v1beta1 diff --git a/marketplace/deployer_util/provision.py b/marketplace/deployer_util/provision.py index 67a86b6e..012119d4 100755 --- a/marketplace/deployer_util/provision.py +++ b/marketplace/deployer_util/provision.py @@ -20,6 +20,7 @@ import yaml +import config_helper import schema_values_common import storage @@ -83,6 +84,12 @@ def process(schema, values, deployer_image, deployer_entrypoint): schema, prop, app_name=app_name, namespace=namespace) props[prop.name] = value manifests += sc_manifests + elif prop.xtype == config_helper.XTYPE_ISTIO_ENABLED: + # TODO: Really populate this value. + props[prop.name] = False + elif prop.xtype == config_helper.XTYPE_INGRESS_AVAILABLE: + # TODO: Really populate this value. + props[prop.name] = True # Merge input and provisioned properties. app_params = dict(list(values.iteritems()) + list(props.iteritems())) diff --git a/tests/marketplace/deployer_envsubst_base/full/schema.yaml b/tests/marketplace/deployer_envsubst_base/full/schema.yaml index 9c885e97..b895fee5 100644 --- a/tests/marketplace/deployer_envsubst_base/full/schema.yaml +++ b/tests/marketplace/deployer_envsubst_base/full/schema.yaml @@ -60,6 +60,14 @@ properties: type: string x-google-marketplace: type: REPORTING_SECRET + istioEnabled: + type: boolean + x-google-marketplace: + type: ISTIO_ENABLED + ingressAvailable: + type: boolean + x-google-marketplace: + type: INGRESS_AVAILABLE required: - name - namespace