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
This commit is contained in:
Gorka Eguileor
2025-05-20 19:08:17 +02:00
parent 9872d0f590
commit 87ff2fe25e
15 changed files with 106 additions and 14 deletions

View File

@@ -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.

View File

@@ -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"
)

View File

@@ -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")

View File

@@ -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(

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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")

View File

@@ -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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)