1 # ============LICENSE_START=======================================================
3 # ================================================================================
4 # Copyright (c) 2017-2020 AT&T Intellectual Property. All rights reserved.
5 # ================================================================================
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 # ============LICENSE_END=========================================================
19 # ECOMP is a trademark and service mark of AT&T Intellectual Property.
21 # -*- coding: utf-8 -*-
26 from functools import partial, reduce
29 from jsonschema import validate, ValidationError
32 from dcae_cli.util import reraise_with_msg, fetch_file_from_web
33 from dcae_cli.util import config as cli_config
34 from dcae_cli.util.exc import DcaeException
35 from dcae_cli.util.logger import get_logger
38 log = get_logger('Schema')
40 # UPDATE: This message applies to the component spec which has been moved on a
43 # WARNING: The below has a "oneOf" for service provides, that will validate as long as any of them are chosen.
44 # However, this is wrong because what we really want is something like:
45 # if component_type == docker
47 # elif component_type == cdap
49 # The unlikely but problematic case is the cdap developer gets a hold of the docker documentation, uses that, it validates, and blows up at cdap runtime
52 # TODO: The next step here is to decide how to manage the links to the schemas. Either:
54 # a) Manage the links in the dcae-cli tool here and thus need to ask if this
55 # belongs in the config to point to some remote server or even point to local
57 # UPDATE: This item has been mostly completed where at least the path is configurable now.
59 # b) Read the links to the schemas from the spec - self-describing jsons. Is
65 class FetchSchemaError(RuntimeError):
69 def __init__(self, path):
76 with open(self.path, 'r') as f:
77 self.ret = json.loads(f.read())
79 except Exception as e:
80 raise FetchSchemaError("Unexpected error from fetching schema", e)
82 component_schema = _Schema('schemas/compspec.json')
83 dataformat_schema = _Schema('schemas/dataformat.json')
86 '''Returns a dict from a dict or json string'''
87 if isinstance(obj, str):
88 return json.loads(obj)
92 def _validate(schema, spec):
93 '''Validate the given spec
95 Fetch the schema and then validate. Upon a error from fetching or validation,
96 a DcaeException is raised.
100 fetch_schema_func: function that takes schema_path -> dict representation of schema
101 throws a FetchSchemaError upon any failure
102 schema_path: string - path to schema
103 spec: dict or string representation of JSON of schema instance
107 Nothing, silence is golden
110 validate(_safe_dict(spec), schema.get())
111 except ValidationError as e:
112 reraise_with_msg(e, as_dcae=True)
113 except FetchSchemaError as e:
114 reraise_with_msg(e, as_dcae=True)
116 def apply_defaults(properties_definition, properties):
117 """Utility method to enforce expected defaults
119 This method is used to enforce properties that are *expected* to have at least
120 the default if not set by a user. Expected properties are not required but
121 have a default set. jsonschema does not provide this.
125 properties_definition: dict of the schema definition of the properties to use
126 for verifying and applying defaults
127 properties: dict of the target properties to verify and apply defaults to
131 dict - a new version of properties that has the expected default values
133 # Recursively process all inner objects. Look for more properties and not match
135 for k,v in six.iteritems(properties_definition):
136 if "properties" in v:
137 properties[k] = apply_defaults(v["properties"], properties.get(k, {}))
140 defaults = [ (k, v["default"]) for k, v in properties_definition.items() if "default" in v ]
142 def apply_default(accumulator, default):
144 if k not in accumulator:
145 # Not doing data type checking and any casting. Assuming that this
146 # should have been taken care of in validation
150 return reduce(apply_default, defaults, properties)
152 def apply_defaults_docker_config(config):
153 """Apply expected defaults to Docker config
156 config: Docker config dict
159 Updated Docker config dict
161 # Apply health check defaults
162 healthcheck_type = config["healthcheck"]["type"]
163 component_spec = component_schema.get()
165 if healthcheck_type in ["http", "https"]:
166 apply_defaults_func = partial(apply_defaults,
167 component_spec["definitions"]["docker_healthcheck_http"]["properties"])
168 elif healthcheck_type in ["script"]:
169 apply_defaults_func = partial(apply_defaults,
170 component_spec["definitions"]["docker_healthcheck_script"]["properties"])
172 # You should never get here
173 apply_defaults_func = lambda x: x
175 config["healthcheck"] = apply_defaults_func(config["healthcheck"])
179 def validate_component(spec):
180 _validate(component_schema, spec)
182 # REVIEW: Could not determine how to do this nicely in json schema. This is
183 # not ideal. We want json schema to be the "it" for validation.
184 ctype = component_type = spec["self"]["component_type"]
187 invalid = [s for s in spec["streams"].get("subscribes", []) \
188 if s["type"] in ["data_router", "data router"]]
190 raise DcaeException("Cdap component as data router subscriber is not supported.")
192 def validate_format(spec):
193 path = cli_config.get_path_data_format()
194 _validate(dataformat_schema, spec)