1# -*- coding: utf-8 -*- # 2# Copyright 2018 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""JSON schema YAML validator module. 17 18Usage: 19 20 # Get a validator for the JSON schema in the file schema_path. 21 validator = yaml_validator.Validator(schema_path) 22 # Validate parsed YAML data. 23 validator.Validate(parsed_yaml_data) 24""" 25 26from __future__ import absolute_import 27from __future__ import division 28from __future__ import unicode_literals 29 30import io 31import os 32 33from googlecloudsdk.core import exceptions 34from googlecloudsdk.core import yaml 35from googlecloudsdk.core.util import pkg_resources 36 37import jsonschema 38 39 40class Error(exceptions.Error): 41 """Errors for this module.""" 42 43 44class InvalidSchemaError(Error): 45 """JSON schema is invalid.""" 46 47 48class InvalidSchemaVersionError(Error): 49 """JSON schema version is invalid.""" 50 51 52class RefError(Error): 53 """Ref error -- YAML $ref path not found.""" 54 55 56class ValidationError(Error): 57 """Validation error -- YAML data does not match the schema. 58 59 Attributes: 60 message: A user-readable error message describing the validation error. 61 """ 62 63 def __init__(self, error): 64 super(ValidationError, self).__init__(error) 65 self.message = error.message 66 67 68class Validator(object): 69 """JSON schema validator.""" 70 71 def __init__(self, schema_path): 72 """"Initilaizes the schema and validator for schema_path. 73 74 The validator resolves references to all other schemas in the directory 75 of schema_path. 76 77 Yes, it's really this ugly defining a validator with a resolver to 78 pkg_resources resources. 79 80 Raises: 81 IOError: if schema not found in installed resources. 82 files.Error: if schema file not found. 83 SchemaError: if the schema is invalid. 84 85 Args: 86 schema_path: JSON schema file path. 87 88 Returns: 89 The schema to validate and the validator. 90 """ 91 schema_dir = os.path.dirname(schema_path) 92 93 class RefResolver(jsonschema.RefResolver): 94 """$ref: resolver that consults pkg_resources.""" 95 96 @staticmethod 97 def resolve_remote(ref): 98 """pkg_resources $ref override -- schema_dir closure needed here.""" 99 path = os.path.join(schema_dir, ref) 100 data = pkg_resources.GetResourceFromFile(path) 101 try: 102 schema = yaml.load(data) 103 except Exception as e: # pylint: disable=broad-except, avoid crash 104 raise InvalidSchemaError(e) 105 self.ValidateSchemaVersion(schema, path) 106 return schema 107 108 try: 109 schema = yaml.load(pkg_resources.GetResourceFromFile(schema_path)) 110 except Exception as e: # pylint: disable=broad-except, avoid crash 111 raise InvalidSchemaError(e) 112 self.ValidateSchemaVersion(schema, schema_path) 113 resolver = RefResolver.from_schema(schema) 114 self._validator = jsonschema.validators.validator_for(schema)( 115 schema, resolver=resolver) 116 self._validate = self._validator.validate 117 118 def ValidateSchemaVersion(self, schema, path): 119 """Validates the parsed_yaml JSON schema version.""" 120 try: 121 version = schema.get('$schema') 122 except AttributeError: 123 version = None 124 if (not version or 125 not version.startswith('http://json-schema.org/') or 126 not version.endswith('/schema#')): 127 raise InvalidSchemaVersionError( 128 'Schema [{}] version [{}] is invalid. Expected "$schema: ' 129 'http://json-schema.org/*/schema#".'.format(path, version)) 130 131 def Validate(self, parsed_yaml): 132 """Validates parsed_yaml against JSON schema. 133 134 Args: 135 parsed_yaml: YAML to validate 136 137 Raises: 138 ValidationError: if the template doesn't obey the schema. 139 """ 140 try: 141 self._validate(parsed_yaml) 142 except jsonschema.RefResolutionError as e: 143 raise RefError(e) 144 except jsonschema.ValidationError as e: 145 raise ValidationError(e) 146 147 def ValidateWithDetailedError(self, parsed_yaml): 148 """Validates parsed_yaml against JSON schema. 149 150 Provides details of validation failure in the returned error message. 151 Args: 152 parsed_yaml: YAML to validate 153 154 Raises: 155 ValidationError: if the template doesn't obey the schema. 156 """ 157 try: 158 self._validate(parsed_yaml) 159 except jsonschema.RefResolutionError as e: 160 raise RefError(e) 161 except jsonschema.exceptions.ValidationError as ve: 162 msg = io.StringIO() 163 msg.write('ERROR: Schema validation failed: {}\n\n'.format(ve)) 164 165 if ve.cause: 166 additional_exception = 'Root Exception: {}'.format(ve.cause) 167 else: 168 additional_exception = '' 169 170 root_error = ve.context[-1] if ve.context else None 171 if root_error: 172 error_path = ''.join( 173 ('[{}]'.format(elem) for elem in root_error.absolute_path)) 174 else: 175 error_path = '' 176 177 msg.write('Additional Details:\n' 178 'Error Message: {msg}\n\n' 179 'Failing Validation Schema: {schema}\n\n' 180 'Failing Element: {instance}\n\n' 181 'Failing Element Path: {path}\n\n' 182 '{additional_cause}\n'.format( 183 msg=root_error.message if root_error else None, 184 instance=root_error.instance if root_error else None, 185 schema=root_error.schema if root_error else None, 186 path=error_path, 187 additional_cause=additional_exception)) 188 ve.message = msg.getvalue() 189 raise ValidationError(ve) 190 191 def Iterate(self, parsed_yaml): 192 """Validates parsed_yaml against JSON schema and returns all errors. 193 194 Args: 195 parsed_yaml: YAML to validate 196 197 Raises: 198 ValidationError: if the template doesn't obey the schema. 199 200 Returns: 201 A list of all errors, empty if no validation errors. 202 """ 203 return self._validator.iter_errors(parsed_yaml) 204