8000 serve swagger UI in LocalStack (#11509) · localstack/localstack@7c0c409 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7c0c409

Browse files
authored
serve swagger UI in LocalStack (#11509)
1 parent 5271fc0 commit 7c0c409

File tree

8 files changed

+153
-1
lines changed

8 files changed

+153
-1
lines changed

localstack-core/localstack/http/resources/__init__.py

Whitespace-only changes.

localstack-core/localstack/http/resources/swagger/__init__.py

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
3+
from jinja2 import Environment, FileSystemLoader
4+
from rolo import route
5+
6+
from localstack.config import external_service_url
7+
from localstack.http import Response
8+
9+
10+
class SwaggerUIApi:
11+
init_path: str
12+
13+
def __init__(self) -> None:
14+
self.init_path = f"{external_service_url()}/openapi.yaml"
15+
16+
@route("/_localstack/swagger", methods=["GET"])
17+
def server_swagger_ui(self, _request):
18+
oas_path = os.path.join(os.path.dirname(__file__), "templates")
19+
env = Environment(loader=FileSystemLoader(oas_path))
20+
template = env.get_template("index.html")
21+
rendered_template = template.render(swagger_url=self.init_path)
22+
return Response(rendered_template, content_type="text/html")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import werkzeug
2+
import yaml
3+
from rolo.routing import RuleAdapter
4+
5+
from localstack.http.resources.swagger.endpoints import SwaggerUIApi
6+
from localstack.runtime import hooks
7+
from localstack.services.edge import ROUTER
8+
from localstack.services.internal import get_internal_apis
9+
from localstack.utils.openapi import get_localstack_openapi_spec
10+
11+
12+
@hooks.on_infra_start()
13+
def register_swagger_endpoints():
14+
get_internal_apis().add(SwaggerUIApi())
15+
16+
def _serve_openapi_spec(_request):
17+
spec = get_localstack_openapi_spec()
18+
response_body = yaml.dump(spec)
19+
return werkzeug.Response(
20+
response_body, content_type="application/yaml", direct_passthrough=True
21+
)
22+
23+
ROUTER.add(RuleAdapter("/openapi.yaml", _serve_openapi_spec))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<meta name="description" content="SwaggerUI" />
7+
<title>SwaggerUI</title>
8+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
9+
</head>
10+
<body>
11+
<div id="swagger-ui"></div>
12+
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
13+
<script>
14+
window.onload = () => {
15+
window.ui = SwaggerUIBundle({
16+
url: "{{ swagger_url }}",
17+
dom_id: '#swagger-ui',
18+
});
19+
};
20+
</script>
21+
</body>
22+
</html>

localstack-core/localstack/logging/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"urllib3": logging.WARNING,
2424
"werkzeug": logging.WARNING,
2525
"rolo": logging.WARNING,
26+
"parse": logging.WARNING,
2627
"localstack.aws.accounts": logging.INFO,
2728
"localstack.aws.protocol.serializer": logging.INFO,
2829
"localstack.aws.serving.wsgi": logging.WARNING,
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import copy
2+
import logging
3+
import textwrap
4+
from typing import Any
5+
6+
import yaml
7+
from plux import PluginManager
8+
9+
from localstack import version
10+
11+
LOG = logging.getLogger(__name__)
12+
13+
spec_top_info = textwrap.dedent("""
14+
openapi: 3.1.0
15+
info:
16+
contact:
17+
email: info@localstack.cloud
18+
name: LocalStack Support
19+
url: https://www.localstack.cloud/contact
20+
summary: The LocalStack REST API exposes functionality related to diagnostics, health
21+
checks, plugins, initialisation hooks, service introspection, and more.
22+
termsOfService: https://www.localstack.cloud/legal/tos
23+
title: LocalStack REST API
24+
version: 1.0
25+
externalDocs:
26+
description: LocalStack Documentation
27+
url: https://docs.localstack.cloud
28+
servers:
29+
- url: http://{host}:{port}
30+
variables:
31+
port:
32+
default: '4566'
33+
host:
34+
default: 'localhost.localstack.cloud'
35+
""")
36+
37+
38+
def _merge_openapi_specs(specs: list[dict[str, Any]]) -> dict[str, Any]:
39+
"""
40+
Merge a list of OpenAPI specs into a single specification.
41+
:param specs: a list of OpenAPI specs loaded in a dictionary
42+
:return: the dictionary of a merged spec.
43+
"""
44+
merged_spec = {}
45+
for idx, spec in enumerate(specs):
46+
if idx == 0:
47+
merged_spec = copy.deepcopy(spec)
48+
else:
49+
# Merge paths
50+
if "paths" in spec:
51+
merged_spec.setdefault("paths", {}).update(spec.get("paths", {}))
52+
53+
# Merge components
54+
if "components" in spec:
55+
if "components" not in merged_spec:
56+
merged_spec["components"] = {}
57+
for component_type, component_value in spec["components"].items():
58+
if component_type not in merged_spec["components"]:
59+
merged_spec["components"][component_type] = component_value
60+
else:
61+
merged_spec["components"][component_type].update(component_value)
62+
63+
# Update the initial part of the spec, i.e., info and correct LocalStack version
64+
top_content = yaml.safe_load(spec_top_info)
65+
# Set the correct version
66+
top_content["info"]["version"] = version.version
67+
merged_spec.update(top_content)
68+
return merged_spec
69+
70+
71+
def get_localstack_openapi_spec() -> dict[str, Any]:
72+
"""
73+
Collects all the declared OpenAPI specs in LocalStack.
74+
Specs are declared by implementing a OASPlugin.
75+
:return: the entire LocalStack OpenAPI spec in a Python dictionary.
76+
"""
77+
specs = PluginManager("localstack.openapi.spec").load_all()
78+
try:
79+
return _merge_openapi_specs([spec.spec for spec in specs])
80+
except Exception as e:
81+
LOG.debug("An error occurred while trying to merge the collected OpenAPI specs %s", e)
82+
# In case of an error while merging the spec, we return the first collected one.
83+
return specs[0].spec

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ exclude = ["tests*"]
169169
"services/**/*.html",
170170
"services/**/resource_providers/*.schema.json",
171171
"utils/kinesis/java/cloud/localstack/*.*",
172-
"openapi.yaml"
172+
"openapi.yaml",
173+
"http/resources/swagger/templates/index.html"
173174
]
174175

175176
[tool.ruff]

0 commit comments

Comments
 (0)
0