From 87ff2fe25e6c4441b0422d8c71ab8cd910bc03b3 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Tue, 20 May 2025 19:08:17 +0200 Subject: [PATCH] YAML version support When exporting OpenAPI in YAML format we are exporting in YAML version 1.2, which is the latest. The problem with that is that there are tools that use the PyYAML [1] python package to read these files, and that package only supports YAML v1.1, which will lead to reading things incorrectly. An example is with booleans. In v1.1 a lot of values are considered as booleans (case insensitive): true, false, 1, 0, off, on, no, yes... But in v1.2 only true and false are considered booleans, so the others don't need to be quoted. As an example we were generating something like this: ``` os_hypervisors_with_servers: in: query name: with_servers schema: type: - boolean - string enum: - true - 'True' - 'TRUE' - 'true' - '1' - ON - On - on - YES - Yes - yes - false - 'False' - 'FALSE' - 'false' - '0' - OFF - Off - off - NO - No - no x-openstack: min-ver: '2.53' ``` Which is incorrectly interpreted by PyYAML like this: ``` enum: - true - 'True' - 'TRUE' - 'true' - '1' - true - true - true - true - true - true - false - 'False' - 'FALSE' - 'false' - '0' - false - false - false - false - false - false ``` ``` To fix this we enable our tool to output the specs in v1.1 with a new parameter `--yaml-version` so that when it's set to 1.1 it will quote all booleans that in v1.1 could be misinterpreted. [1]: https://pypi.org/project/PyYAML/ Change-Id: I7236f6a94ccb2e92a086c16895efa4dc557460c4 --- README.rst | 8 ++++++++ codegenerator/cli.py | 12 ++++++++++++ codegenerator/openapi/barbican.py | 6 +++++- codegenerator/openapi/base.py | 11 ++++++++++- codegenerator/openapi/cinder.py | 6 +++++- codegenerator/openapi/designate.py | 8 +++++++- codegenerator/openapi/glance.py | 4 +++- codegenerator/openapi/ironic.py | 6 +++++- codegenerator/openapi/keystone.py | 8 +++++++- codegenerator/openapi/magnum.py | 1 + codegenerator/openapi/manila.py | 6 +++++- codegenerator/openapi/neutron.py | 22 +++++++++++++++++++--- codegenerator/openapi/nova.py | 8 +++++++- codegenerator/openapi/octavia.py | 6 +++++- codegenerator/openapi/placement.py | 8 +++++++- 15 files changed, 106 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 1ab8e21..4dd5e20 100644 --- a/README.rst +++ b/README.rst @@ -113,6 +113,14 @@ additional ``--api-ref-src`` argument: Your API documentation should now be looking much better. You'll even have documentation available inline. +.. note:: + Beware that the output YAML file follows version 1.2 of the YAML + specification and there are libraries, such as PyYAML, that only support + version 1.1 and will parse the contents incorrectly. + + Option ``--yaml-version 1.1`` can be passed on the call to force the output + to use specification version 1.1. + There are a variety of options available, which you can view with the ``--help`` option. diff --git a/codegenerator/cli.py b/codegenerator/cli.py index 5808516..bdaaaa3 100644 --- a/codegenerator/cli.py +++ b/codegenerator/cli.py @@ -167,6 +167,18 @@ def main(): ], help="Target for which to generate code", ) + parser.add_argument( + "--yaml-version", + choices=["1.1", "1.2"], + default="", + help=( + "Yaml specification version to use for openapi-spec target " + "output. Defaults to latest supported version without the YAML " + "directive, which will be included if version is explicit (eg: " + "%%YAML 1.2). Beware of libraries like PyYaml that only support " + "version 1.1" + ), + ) parser.add_argument( "--work-dir", help="Working directory for the generated code" ) diff --git a/codegenerator/openapi/barbican.py b/codegenerator/openapi/barbican.py index afd3606..025309d 100644 --- a/codegenerator/openapi/barbican.py +++ b/codegenerator/openapi/barbican.py @@ -493,7 +493,11 @@ class BarbicanGenerator(OpenStackServerSourceBase): ) self.dump_openapi( - openapi_spec, Path(impl_path), args.validate, "key-manager" + openapi_spec, + Path(impl_path), + args.validate, + "key-manager", + args.yaml_version, ) lnk = Path(impl_path.parent, "v1.yaml") diff --git a/codegenerator/openapi/base.py b/codegenerator/openapi/base.py index ebed404..0e12cfc 100644 --- a/codegenerator/openapi/base.py +++ b/codegenerator/openapi/base.py @@ -173,12 +173,21 @@ class OpenStackServerSourceBase: if spec: return SpecSchema(**spec) - def dump_openapi(self, spec, path, validate: bool, service_type: str): + def dump_openapi( + self, + spec, + path, + validate: bool, + service_type: str, + yaml_version: str = "", + ): """Dump OpenAPI spec into the file""" if validate: self.validate_spec(spec, service_type) yaml = YAML() yaml.preserve_quotes = True + if yaml_version: + yaml.version = yaml_version yaml.indent(mapping=2, sequence=4, offset=2) with open(path, "w") as fp: yaml.dump( diff --git a/codegenerator/openapi/cinder.py b/codegenerator/openapi/cinder.py index bad289e..2dd5443 100644 --- a/codegenerator/openapi/cinder.py +++ b/codegenerator/openapi/cinder.py @@ -174,7 +174,11 @@ class CinderV3Generator(OpenStackServerSourceBase): merge_api_ref_doc(openapi_spec, args.api_ref_src) self.dump_openapi( - openapi_spec, impl_path, args.validate, "block-storage" + openapi_spec, + impl_path, + args.validate, + "block-storage", + args.yaml_version, ) lnk = Path(impl_path.parent, "v3.yaml") diff --git a/codegenerator/openapi/designate.py b/codegenerator/openapi/designate.py index 1f97a0f..35ed5b2 100644 --- a/codegenerator/openapi/designate.py +++ b/codegenerator/openapi/designate.py @@ -250,7 +250,13 @@ class DesignateGenerator(OpenStackServerSourceBase): openapi_spec, args.api_ref_src, allow_strip_version=False ) - self.dump_openapi(openapi_spec, Path(impl_path), args.validate, "dns") + self.dump_openapi( + openapi_spec, + Path(impl_path), + args.validate, + "dns", + args.yaml_version, + ) lnk = Path(impl_path.parent, "v2.yaml") lnk.unlink(missing_ok=True) diff --git a/codegenerator/openapi/glance.py b/codegenerator/openapi/glance.py index c466db0..eca67d8 100644 --- a/codegenerator/openapi/glance.py +++ b/codegenerator/openapi/glance.py @@ -333,7 +333,9 @@ class GlanceGenerator(OpenStackServerSourceBase): if args.api_ref_src: merge_api_ref_doc(openapi_spec, args.api_ref_src) - self.dump_openapi(openapi_spec, impl_path, args.validate, "image") + self.dump_openapi( + openapi_spec, impl_path, args.validate, "image", args.yaml_version + ) lnk = Path(impl_path.parent, "v2.yaml") lnk.unlink(missing_ok=True) diff --git a/codegenerator/openapi/ironic.py b/codegenerator/openapi/ironic.py index 8649d16..bc55721 100644 --- a/codegenerator/openapi/ironic.py +++ b/codegenerator/openapi/ironic.py @@ -193,7 +193,11 @@ class IronicGenerator(OpenStackServerSourceBase): ) self.dump_openapi( - openapi_spec, Path(impl_path), args.validate, "baremetal" + openapi_spec, + Path(impl_path), + args.validate, + "baremetal", + args.yaml_version, ) lnk = Path(impl_path.parent, "v1.yaml") diff --git a/codegenerator/openapi/keystone.py b/codegenerator/openapi/keystone.py index 866c619..803d850 100644 --- a/codegenerator/openapi/keystone.py +++ b/codegenerator/openapi/keystone.py @@ -160,7 +160,13 @@ class KeystoneGenerator(OpenStackServerSourceBase): openapi_spec, args.api_ref_src, allow_strip_version=False ) - self.dump_openapi(openapi_spec, impl_path, args.validate, "identity") + self.dump_openapi( + openapi_spec, + impl_path, + args.validate, + "identity", + args.yaml_version, + ) lnk = Path(impl_path.parent, "v3.yaml") lnk.unlink(missing_ok=True) diff --git a/codegenerator/openapi/magnum.py b/codegenerator/openapi/magnum.py index 0728b4d..b22c74e 100644 --- a/codegenerator/openapi/magnum.py +++ b/codegenerator/openapi/magnum.py @@ -237,6 +237,7 @@ class MagnumGenerator(OpenStackServerSourceBase): Path(impl_path), args.validate, "container-infrastructure-management", + args.yaml_version, ) lnk = Path(impl_path.parent, "v1.yaml") diff --git a/codegenerator/openapi/manila.py b/codegenerator/openapi/manila.py index 64da8f8..4328081 100644 --- a/codegenerator/openapi/manila.py +++ b/codegenerator/openapi/manila.py @@ -123,7 +123,11 @@ class ManilaGenerator(OpenStackServerSourceBase): ) self.dump_openapi( - openapi_spec, impl_path, args.validate, "shared-file-system" + openapi_spec, + impl_path, + args.validate, + "shared-file-system", + args.yaml_version, ) lnk = Path(impl_path.parent, "v2.yaml") diff --git a/codegenerator/openapi/neutron.py b/codegenerator/openapi/neutron.py index 4a773fa..157397f 100644 --- a/codegenerator/openapi/neutron.py +++ b/codegenerator/openapi/neutron.py @@ -180,7 +180,13 @@ class NeutronGenerator(OpenStackServerSourceBase): # Add base resource routes exposed as a pecan app self._process_base_resource_routes(openapi_spec, processed_routes) - self.dump_openapi(openapi_spec, impl_path, args.validate, "network") + self.dump_openapi( + openapi_spec, + impl_path, + args.validate, + "network", + args.yaml_version, + ) def process_neutron_with_vpnaas(self, work_dir, processed_routes, args): """Setup base Neutron with enabled vpnaas""" @@ -240,7 +246,13 @@ class NeutronGenerator(OpenStackServerSourceBase): (impl_path, openapi_spec) = self._read_spec(work_dir) self._process_router(router, openapi_spec, processed_routes) - self.dump_openapi(openapi_spec, impl_path, args.validate, "network") + self.dump_openapi( + openapi_spec, + impl_path, + args.validate, + "network", + args.yaml_version, + ) def _read_spec(self, work_dir): """Read the spec from file or create an empty one""" @@ -374,7 +386,11 @@ class NeutronGenerator(OpenStackServerSourceBase): ) self.dump_openapi( - openapi_spec, Path(impl_path), args.validate, "network" + openapi_spec, + Path(impl_path), + args.validate, + "network", + args.yaml_version, ) return impl_path diff --git a/codegenerator/openapi/nova.py b/codegenerator/openapi/nova.py index 85b71c7..867116c 100644 --- a/codegenerator/openapi/nova.py +++ b/codegenerator/openapi/nova.py @@ -103,7 +103,13 @@ class NovaGenerator(OpenStackServerSourceBase): doc_url_prefix="/v2.1", ) - self.dump_openapi(openapi_spec, impl_path, args.validate, "compute") + self.dump_openapi( + openapi_spec, + impl_path, + args.validate, + "compute", + args.yaml_version, + ) lnk = Path(impl_path.parent, "v2.yaml") lnk.unlink(missing_ok=True) diff --git a/codegenerator/openapi/octavia.py b/codegenerator/openapi/octavia.py index b959447..5347597 100644 --- a/codegenerator/openapi/octavia.py +++ b/codegenerator/openapi/octavia.py @@ -1093,7 +1093,11 @@ class OctaviaGenerator(OpenStackServerSourceBase): ) self.dump_openapi( - openapi_spec, Path(impl_path), args.validate, "load-balancer" + openapi_spec, + Path(impl_path), + args.validate, + "load-balancer", + args.yaml_version, ) lnk = Path(impl_path.parent, "v2.yaml") diff --git a/codegenerator/openapi/placement.py b/codegenerator/openapi/placement.py index 0e0ca38..ca0727f 100644 --- a/codegenerator/openapi/placement.py +++ b/codegenerator/openapi/placement.py @@ -121,7 +121,13 @@ class PlacementGenerator(OpenStackServerSourceBase): openapi_spec, args.api_ref_src, allow_strip_version=False ) - self.dump_openapi(openapi_spec, impl_path, args.validate, "placement") + self.dump_openapi( + openapi_spec, + impl_path, + args.validate, + "placement", + args.yaml_version, + ) lnk = Path(impl_path.parent, "v1.yaml") lnk.unlink(missing_ok=True)