diff --git a/codegenerator/openapi/designate.py b/codegenerator/openapi/designate.py new file mode 100644 index 0000000..c588ea7 --- /dev/null +++ b/codegenerator/openapi/designate.py @@ -0,0 +1,252 @@ +# 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 diff --git a/codegenerator/openapi_spec.py b/codegenerator/openapi_spec.py index 1ec1e89..49d84a3 100644 --- a/codegenerator/openapi_spec.py +++ b/codegenerator/openapi_spec.py @@ -68,6 +68,11 @@ class OpenApiSchemaGenerator(BaseGenerator): ManilaGenerator().generate(target_dir, args) + def generate_designate(self, target_dir, args): + from codegenerator.openapi.designate import DesignateGenerator + + DesignateGenerator().generate(target_dir, args) + def generate( self, res, target_dir, openapi_spec=None, operation_id=None, args=None ): @@ -80,6 +85,8 @@ class OpenApiSchemaGenerator(BaseGenerator): self.generate_nova(target_dir, args) elif args.service_type in ["block-storage", "volume"]: self.generate_cinder(target_dir, args) + elif args.service_type == "dns": + self.generate_designate(target_dir, args) elif args.service_type == "image": self.generate_glance(target_dir, args) elif args.service_type == "identity": diff --git a/tools/generate_openapi_specs.sh b/tools/generate_openapi_specs.sh index c8c7f0c..a9b7ff9 100755 --- a/tools/generate_openapi_specs.sh +++ b/tools/generate_openapi_specs.sh @@ -34,3 +34,7 @@ fi if [ -z "$1" -o "$1" = "shared-file-system" ]; then openstack-codegenerator --work-dir wrk --target openapi-spec --service-type shared-file-system --api-ref-src ${API_REF_BUILD_ROOT}/manila/api-ref/build/html/index.html --validate fi + +if [ -z "$1" -o "$1" = "dns" ]; then + openstack-codegenerator --work-dir wrk --target openapi-spec --service-type dns --api-ref-src ${API_REF_BUILD_ROOT}/designate/api-ref/build/html/dns-api-v2-index.html --validate +fi diff --git a/zuul.d/openapi.yaml b/zuul.d/openapi.yaml index 0302d6a..5888ef9 100644 --- a/zuul.d/openapi.yaml +++ b/zuul.d/openapi.yaml @@ -91,6 +91,35 @@ project: "opendev.org/openstack/nova" path: "/api-ref/build/html/index.html" +- job: + name: codegenerator-openapi-dns-tips + parent: codegenerator-openapi-tips-base + description: | + Generate OpenAPI spec for Designate + required-projects: + - name: openstack/designate + + vars: + openapi_service: dns + install_additional_projects: + - project: "opendev.org/openstack/designate" + name: "." + +- job: + name: codegenerator-openapi-dns-tips-with-api-ref + parent: codegenerator-openapi-dns-tips + description: | + Generate OpenAPI spec for Designate consuming API-REF + required-projects: + - name: openstack/designate + + pre-run: + - playbooks/openapi/pre-api-ref.yaml + vars: + codegenerator_api_ref: + project: "opendev.org/openstack/designate" + path: "/api-ref/build/html/dns-api-v2-index.html" + - job: name: codegenerator-openapi-identity-tips parent: codegenerator-openapi-tips-base @@ -290,6 +319,8 @@ soft: true - name: codegenerator-openapi-compute-tips-with-api-ref soft: true + - name: codegenerator-openapi-dns-tips-with-api-ref + soft: true - name: codegenerator-openapi-identity-tips-with-api-ref soft: true - name: codegenerator-openapi-image-tips-with-api-ref diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index e84fdfd..6da8431 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -8,6 +8,7 @@ - openstack-tox-py311 - codegenerator-openapi-block-storage-tips-with-api-ref - codegenerator-openapi-compute-tips-with-api-ref + - codegenerator-openapi-dns-tips-with-api-ref - codegenerator-openapi-identity-tips-with-api-ref - codegenerator-openapi-image-tips-with-api-ref - codegenerator-openapi-load-balancing-tips-with-api-ref @@ -23,6 +24,7 @@ - openstack-tox-py311 - codegenerator-openapi-block-storage-tips-with-api-ref - codegenerator-openapi-compute-tips-with-api-ref + - codegenerator-openapi-dns-tips-with-api-ref - codegenerator-openapi-identity-tips-with-api-ref - codegenerator-openapi-image-tips-with-api-ref - codegenerator-openapi-load-balancing-tips-with-api-ref