Files
codegenerator/codegenerator/openapi/designate.py
Artem Goncharov fa25f3b8f0 Start building Designate specs
Sadly no schemas, but at least first rough structure.

Change-Id: Id2ed7ecfbf20ca522e173cf11f236d00f75871c5
2024-10-01 20:28:45 +02:00

253 lines
9.6 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import inspect
from multiprocessing import Process
from pathlib import Path
from unittest import mock
import fixtures
from codegenerator.common.schema import SpecSchema
from codegenerator.openapi.base import OpenStackServerSourceBase
from codegenerator.openapi.utils import merge_api_ref_doc
from ruamel.yaml.scalarstring import LiteralScalarString
class DesignateGenerator(OpenStackServerSourceBase):
URL_TAG_MAP = {
"/zones/tasks/transfer_accepts": "zone-ownership-transfers-accepts",
"/zones/tasks/transfer_requests": "zone-ownership-transfers-requests",
"/zones/tasks/imports": "zone-imports",
"/zones/tasks/exports": "zone-exports",
"/zones/{zone_id}/tasks/export": "zone-exports",
"/zones/{zone_id}/tasks": "zone-tasks",
"/zones/{zone_id}/recordsets": "recordsets",
"/zones/{zone_id}/shares": "shared-zones",
"/service_statuses": "service-statuses",
"/tlds": "tld",
"/tsigkeys": "tsigkey",
"/reverse/floatingips": "floatingips",
}
def __init__(self):
self.api_version = "2.1"
self.min_api_version = "2.0"
def _api_ver_major(self, ver):
return ver.ver_major
def _api_ver_minor(self, ver):
return ver.ver_minor
def _api_ver(self, ver):
return (ver.ver_major, ver.ver_minor)
def _build_routes(
self,
mapper,
node,
path="",
sub_map: dict[str, str] = {},
subcontroller_name: str | None = None,
):
# path = f"{path}/{subcontroller_name}"
for part in [x for x in dir(node) if callable(getattr(node, x))]:
# Iterate over functions to find what is exposed on the current
# level
if part == "_route":
continue
obj = getattr(node, part)
_pecan = getattr(obj, "_pecan", None)
exposed = getattr(obj, "exposed", None)
if _pecan and exposed:
argspec = _pecan.get("argspec", None)
args: list[str] = []
if argspec:
args = argspec.args
if "self" in args:
args.remove("self")
# Only whatever is pecan exposed is of interest
conditions = {}
action = None
url = path
resource = None
# Check whether for mandatory params there is path defined
# If there is entry with all parameters we are most likely on
# the subcontroller level, however on the subcontroller there
# might be parent_id and id for which we do not have
# combination yet, thus take parent_id as base url
# for "zone_id" => /zones/{zone_id}/
# for "zone_id zone_transfer_request_id" => /zones/{zone_id}
# while in transfer_request controller
if " ".join(args) in sub_map:
url = sub_map[" ".join(args)]
if subcontroller_name and subcontroller_name not in url:
url += f"/{subcontroller_name}"
elif " ".join(args[0:-1]) in sub_map:
url = sub_map[" ".join(args[0:-1])]
if subcontroller_name and subcontroller_name not in url:
url += f"/{subcontroller_name}"
url += f"/{{{args[-1]}}}"
elif len(args) > 0:
url += f"/{{{args[-1]}}}"
# Identify the action from function name
# https://pecan.readthedocs.io/en/latest/rest.html#url-mapping
if part == "get_one":
conditions["method"] = ["GET"]
action = "show"
if " ".join(args) not in sub_map:
sub_map[" ".join(args)] = url
elif part == "get_all":
conditions["method"] = ["GET"]
action = "list"
elif part in ["post", "post_all"]:
conditions["method"] = ["POST"]
action = "create"
elif part in ["put"]:
conditions["method"] = ["PUT"]
action = "update"
elif part in ["patch_one"]:
conditions["method"] = ["PATCH"]
action = "update"
elif part in ["delete", "delete_one"]:
conditions["method"] = ["DELETE"]
action = "delete"
if action:
# If we identified method as "interesting" register it into
# the routes mapper
mapper.connect(
None,
url,
controller=obj,
action=action,
conditions=conditions,
)
# yield part
if not hasattr(node, "__dict__"):
return
for subcontroller, v in (
node.__dict__.items()
if isinstance(node, type)
else node.__class__.__dict__.items()
):
# Iterate over node attributes for subcontrollers
if subcontroller.startswith("_"):
continue
if subcontroller in ["central_api", "__wrapped__", "SORT_KEYS"]:
# Not underested in those
continue
subpath = f"{path}/{subcontroller}"
self._build_routes(mapper, v, subpath, sub_map, subcontroller)
return
def generate(self, target_dir, args):
proc = Process(target=self._generate, args=[target_dir, args])
proc.start()
proc.join()
if proc.exitcode != 0:
raise RuntimeError("Error generating Designate OpenAPI schema")
def _generate(self, target_dir, args):
from designate.api.v2.controllers import root
from designate.api.v2.controllers import zones
from designate.api.v2.controllers.zones import nameservers
from designate.api.v2.controllers.zones import recordsets
from designate.api.v2.controllers.zones import sharedzones
from designate.api.v2.controllers.zones import tasks
from oslo_config import cfg
# import oslo_messaging as messaging
# from oslo_messaging import conffixture as messaging_conffixture
from pecan import make_app as pecan_make_app
from routes import Mapper
work_dir = Path(target_dir)
work_dir.mkdir(parents=True, exist_ok=True)
impl_path = Path(
work_dir, "openapi_specs", "dns", f"v{self.api_version}.yaml"
)
impl_path.parent.mkdir(parents=True, exist_ok=True)
openapi_spec = self.load_openapi(Path(impl_path))
if not openapi_spec:
openapi_spec = SpecSchema(
info={
"title": "OpenStack DNS API",
"description": LiteralScalarString(
"DNS API provided by Designate service"
),
"version": self.api_version,
},
openapi="3.1.0",
security=[{"ApiKeyAuth": []}],
components={
"securitySchemes": {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-Auth-Token",
}
},
"parameters": {
"x-auth-all-projects": {
"name": "x-auth-all-projects",
"in": "header",
"schema": {"type": "boolean"},
"description": "If enabled this will show results from all projects in Designate",
},
"x-auth-sudo-project-id": {
"name": "x-auth-sudo-project-id",
"in": "header",
"schema": {"type": "string", "format": "uuid"},
"description": "This allows a user to impersonate another project",
},
},
},
)
self._buses = {}
self.useFixture(
fixtures.MonkeyPatch(
"designate.central.rpcapi.CentralAPI", mock.MagicMock()
)
)
self.app = pecan_make_app(root.RootController())
self.root = self.app.application.root
mapper = Mapper()
self._build_routes(mapper, root.RootController, "/v2")
for route in mapper.matchlist:
self._process_route(route, openapi_spec, framework="pecan")
if args.api_ref_src:
merge_api_ref_doc(
openapi_spec, args.api_ref_src, allow_strip_version=False
)
self.dump_openapi(openapi_spec, Path(impl_path), args.validate)
lnk = Path(impl_path.parent, "v2.yaml")
lnk.unlink(missing_ok=True)
lnk.symlink_to(impl_path.name)
return impl_path