Consume decorated Keystone methods

We started decorating Keystone with schema validation similarly to other
services. Since Keystone uses Flask and thus we do not use base
processing split the processing into separate method and call it.

Change-Id: I4e70b85bb16a40cb3dff0d5ddc6004aabe0d1c62
This commit is contained in:
Artem Goncharov 2024-07-05 20:17:07 +02:00
parent fdf8bda6be
commit d038d9d8d1
No known key found for this signature in database
GPG Key ID: 334C245686980408
3 changed files with 187 additions and 106 deletions

View File

@ -647,88 +647,20 @@ class OpenStackServerSourceBase:
if action_name:
operation_name = action_name
# Unwrap operation decorators to access all properties
f = func
while hasattr(f, "__wrapped__"):
closure = inspect.getclosurevars(f)
closure_locals = closure.nonlocals
min_ver = closure_locals.get("min_version", start_version)
if min_ver and not isinstance(min_ver, str):
min_ver = min_ver.get_string()
max_ver = closure_locals.get("max_version", end_version)
if max_ver and not isinstance(max_ver, str):
max_ver = max_ver.get_string()
if "errors" in closure_locals:
expected_errors = closure_locals["errors"]
if isinstance(expected_errors, list):
expected_errors = [
str(x)
for x in filter(
lambda x: isinstance(x, int), expected_errors
)
]
elif isinstance(expected_errors, int):
expected_errors = [str(expected_errors)]
if "request_body_schema" in closure_locals or hasattr(
f, "_request_body_schema"
):
# Body type is known through method decorator
obj = closure_locals.get(
"request_body_schema",
getattr(f, "_request_body_schema", {}),
)
if obj.get("type") in ["object", "array"]:
# We only allow object and array bodies
# To prevent type name collision keep module name part of the name
typ_name = (
"".join([x.title() for x in path_resource_names])
+ func.__name__.title()
+ (f"_{min_ver.replace('.', '')}" if min_ver else "")
)
comp_schema = openapi_spec.components.schemas.setdefault(
typ_name,
self._sanitize_schema(
copy.deepcopy(obj),
start_version=start_version,
end_version=end_version,
),
)
if min_ver:
if not comp_schema.openstack:
comp_schema.openstack = {}
comp_schema.openstack["min-ver"] = min_ver
if max_ver:
if not comp_schema.openstack:
comp_schema.openstack = {}
comp_schema.openstack["max-ver"] = max_ver
if mode == "action":
if not comp_schema.openstack:
comp_schema.openstack = {}
comp_schema.openstack["action-name"] = action_name
ref_name = f"#/components/schemas/{typ_name}"
body_schemas.append(ref_name)
if "response_body_schema" in closure_locals or hasattr(
f, "_response_body_schema"
):
# Response type is known through method decorator - PERFECT
obj = closure_locals.get(
"response_body_schema",
getattr(f, "_response_body_schema", {}),
)
ser_schema = obj
if "query_params_schema" in closure_locals or hasattr(
f, "_request_query_schema"
):
obj = closure_locals.get(
"query_params_schema",
getattr(f, "_request_query_schema", {}),
)
query_params_versions.append((obj, min_ver, max_ver))
f = f.__wrapped__
(
query_params_versions,
body_schemas,
response_body_schema,
expected_errors,
) = self._process_decorators(
func,
path_resource_names,
openapi_spec,
mode,
start_version,
end_version,
action_name,
)
if hasattr(func, "_wsme_definition"):
fdef = getattr(func, "_wsme_definition")
@ -786,6 +718,8 @@ class OpenStackServerSourceBase:
operation_name,
)
if ser_schema and not response_body_schema:
response_body_schema = ser_schema
responses_spec = operation_spec.responses
for error in expected_errors:
responses_spec.setdefault(str(error), dict(description="Error"))
@ -830,7 +764,7 @@ class OpenStackServerSourceBase:
if not action_name
else f"Response of the {operation_spec.operationId}:{action_name} action"
), # noqa
schema_def=ser_schema,
schema_def=response_body_schema,
action_name=action_name,
)
@ -910,7 +844,8 @@ class OpenStackServerSourceBase:
**copy.deepcopy(spec["items"])
)
else:
raise RuntimeError("Error")
param_attrs["schema"] = TypeSchema(**copy.deepcopy(spec))
param_attrs["description"] = spec.get("description")
if min_ver:
os_ext = param_attrs.setdefault("x-openstack", {})
os_ext["min-ver"] = min_ver
@ -1218,6 +1153,113 @@ class OpenStackServerSourceBase:
response_code = "200"
return [response_code]
def _process_decorators(
self,
func,
path_resource_names: list[str],
openapi_spec,
mode: str,
start_version,
end_version,
action_name: str | None,
):
"""Extract schemas from the decorated method."""
# Unwrap operation decorators to access all properties
expected_errors: list[str] = []
body_schemas: list[str] = []
query_params_versions: list[tuple] = []
response_body_schema: dict | None = None
f = func
while hasattr(f, "__wrapped__"):
closure = inspect.getclosurevars(f)
closure_locals = closure.nonlocals
min_ver = closure_locals.get("min_version", start_version)
if min_ver and not isinstance(min_ver, str):
min_ver = min_ver.get_string()
max_ver = closure_locals.get("max_version", end_version)
if max_ver and not isinstance(max_ver, str):
max_ver = max_ver.get_string()
if "errors" in closure_locals:
expected_errors = closure_locals["errors"]
if isinstance(expected_errors, list):
expected_errors = [
str(x)
for x in filter(
lambda x: isinstance(x, int), expected_errors
)
]
elif isinstance(expected_errors, int):
expected_errors = [str(expected_errors)]
if "request_body_schema" in closure_locals or hasattr(
f, "_request_body_schema"
):
# Body type is known through method decorator
obj = closure_locals.get(
"request_body_schema",
getattr(f, "_request_body_schema", {}),
)
if obj.get("type") in ["object", "array"]:
# We only allow object and array bodies
# To prevent type name collision keep module name part of the name
typ_name = (
"".join([x.title() for x in path_resource_names])
+ func.__name__.title()
+ (f"_{min_ver.replace('.', '')}" if min_ver else "")
)
comp_schema = openapi_spec.components.schemas.setdefault(
typ_name,
self._sanitize_schema(
copy.deepcopy(obj),
start_version=start_version,
end_version=end_version,
),
)
if min_ver:
if not comp_schema.openstack:
comp_schema.openstack = {}
comp_schema.openstack["min-ver"] = min_ver
if max_ver:
if not comp_schema.openstack:
comp_schema.openstack = {}
comp_schema.openstack["max-ver"] = max_ver
if mode == "action":
if not comp_schema.openstack:
comp_schema.openstack = {}
comp_schema.openstack["action-name"] = action_name
ref_name = f"#/components/schemas/{typ_name}"
body_schemas.append(ref_name)
if "response_body_schema" in closure_locals or hasattr(
f, "_response_body_schema"
):
# Response type is known through method decorator - PERFECT
obj = closure_locals.get(
"response_body_schema",
getattr(f, "_response_body_schema", {}),
)
response_body_schema = obj
if "query_params_schema" in closure_locals or hasattr(
f, "_request_query_schema"
):
obj = closure_locals.get(
"query_params_schema",
getattr(f, "_request_query_schema", {}),
)
query_params_versions.append((obj, min_ver, max_ver))
f = f.__wrapped__
return (
query_params_versions,
body_schemas,
response_body_schema,
expected_errors,
)
def _convert_wsme_to_jsonschema(body_spec):
"""Convert WSME type description to JsonSchema"""

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
import copy
import inspect
from multiprocessing import Process
import logging
@ -35,6 +36,7 @@ from codegenerator.openapi.keystone_schemas import role
from codegenerator.openapi.keystone_schemas import service
from codegenerator.openapi.keystone_schemas import user
from codegenerator.openapi.utils import merge_api_ref_doc
from codegenerator.openapi.utils import rst_to_md
class KeystoneGenerator(OpenStackServerSourceBase):
@ -151,10 +153,6 @@ class KeystoneGenerator(OpenStackServerSourceBase):
for route in self.router.iter_rules():
if route.rule.startswith("/static"):
continue
# if not route.rule.startswith("/v3/domains"):
# continue
if "/credentials/OS-EC2" in route.rule:
continue
self._process_route(route, openapi_spec)
@ -358,26 +356,62 @@ class KeystoneGenerator(OpenStackServerSourceBase):
path,
method,
)
doc = inspect.getdoc(func)
if doc and not operation_spec.description:
doc = rst_to_md(doc)
operation_spec.description = LiteralScalarString(doc)
query_params_versions = []
body_schemas = []
expected_errors = ["404"]
response_code = None
start_version = None
end_version = None
deser_schema: dict = {}
ser_schema: dict = {}
(
query_params_versions,
body_schemas,
ser_schema,
expected_errors,
) = self._process_decorators(
func,
path_resource_names,
openapi_spec,
method,
start_version,
end_version,
None,
)
if query_params_versions:
so = sorted(
query_params_versions,
key=lambda d: (
tuple(map(int, d[1].split("."))) if d[1] else (0, 0)
),
)
for data, min_ver, max_ver in so:
self.process_query_parameters(
openapi_spec,
operation_spec,
path_resource_names,
data,
min_ver,
max_ver,
)
if method in ["PUT", "POST", "PATCH"]:
# This is clearly a modification operation but we know nothing about request
schema_name = (
"".join([x.title() for x in path_resource_names])
+ method.title()
+ "Request"
)
(schema_ref, mime_type) = self._get_schema_ref(
self.process_body_parameters(
openapi_spec,
schema_name,
description=f"Request of the {operation_spec.operationId} operation",
operation_spec,
path_resource_names,
body_schemas,
None,
method,
)
if schema_ref:
content = operation_spec.requestBody = {"content": {}}
content["content"][mime_type] = {
"schema": {"$ref": schema_ref}
}
responses_spec = operation_spec.responses
# Errors
for error in ["403", "404"]:
@ -420,6 +454,7 @@ class KeystoneGenerator(OpenStackServerSourceBase):
openapi_spec,
schema_name,
description=f"Response of the {operation_spec.operationId} operation",
schema_def=ser_schema,
)
if schema_ref:
@ -486,7 +521,11 @@ class KeystoneGenerator(OpenStackServerSourceBase):
# Default
(ref, mime_type) = super()._get_schema_ref(
openapi_spec, name, description, action_name=action_name
openapi_spec,
name,
description,
schema_def=schema_def,
action_name=action_name,
)
return (ref, mime_type)

View File

@ -170,7 +170,7 @@ def _get_schema_ref(
TypeSchema(**APPLICATION_CREDENTIAL_CREATE_SCHEMA),
)
ref = f"#/components/schemas/{name}"
elif name in "UsersApplication_CredentialsPostResponse":
elif name == "UsersApplication_CredentialsPostResponse":
openapi_spec.components.schemas.setdefault(
name,
TypeSchema(**APPLICATION_CREDENTIAL_CREATE_RESPONSE_SCHEMA),