-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: serve swagger UI in LocalStack #8
base: master
Are you sure you want to change the base?
Changes from all commits
1274346
81cc12f
a36cd68
ba5c7e1
35c9613
89eb29e
a264b3c
25c6c9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
html { | ||
box-sizing: border-box; | ||
overflow: -moz-scrollbars-vertical; | ||
overflow-y: scroll; | ||
} | ||
|
||
*, | ||
*:before, | ||
*:after { | ||
box-sizing: inherit; | ||
} | ||
|
||
body { | ||
margin: 0; | ||
background: #fafafa; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<!doctype html> | ||
<html lang="en-US"> | ||
<head> | ||
<title>Swagger UI: OAuth2 Redirect</title> | ||
</head> | ||
<body> | ||
<script> | ||
'use strict'; | ||
function run () { | ||
var oauth2 = window.opener.swaggerUIRedirectOauth2; | ||
var sentState = oauth2.state; | ||
var redirectUrl = oauth2.redirectUrl; | ||
var isValid, qp, arr; | ||
|
||
if (/code|token|error/.test(window.location.hash)) { | ||
qp = window.location.hash.substring(1).replace('?', '&'); | ||
} else { | ||
qp = location.search.substring(1); | ||
} | ||
|
||
arr = qp.split("&"); | ||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';}); | ||
qp = qp ? JSON.parse('{' + arr.join() + '}', | ||
function (key, value) { | ||
return key === "" ? value : decodeURIComponent(value); | ||
} | ||
) : {}; | ||
|
||
isValid = qp.state === sentState; | ||
|
||
if (( | ||
oauth2.auth.schema.get("flow") === "accessCode" || | ||
oauth2.auth.schema.get("flow") === "authorizationCode" || | ||
oauth2.auth.schema.get("flow") === "authorization_code" | ||
) && !oauth2.auth.code) { | ||
if (!isValid) { | ||
oauth2.errCb({ | ||
authId: oauth2.auth.name, | ||
source: "auth", | ||
level: "warning", | ||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server." | ||
}); | ||
} | ||
|
||
if (qp.code) { | ||
delete oauth2.state; | ||
oauth2.auth.code = qp.code; | ||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl}); | ||
} else { | ||
let oauthErrorMsg; | ||
if (qp.error) { | ||
oauthErrorMsg = "["+qp.error+"]: " + | ||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") + | ||
(qp.error_uri ? "More info: "+qp.error_uri : ""); | ||
} | ||
|
||
oauth2.errCb({ | ||
authId: oauth2.auth.name, | ||
source: "auth", | ||
level: "error", | ||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server." | ||
}); | ||
} | ||
} else { | ||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl}); | ||
} | ||
window.close(); | ||
} | ||
|
||
if (document.readyState !== 'loading') { | ||
run(); | ||
} else { | ||
document.addEventListener('DOMContentLoaded', function () { | ||
run(); | ||
}); | ||
} | ||
</script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
window.onload = function() { | ||
//<editor-fold desc="Changeable Configuration Block"> | ||
|
||
// the following lines will be replaced by docker/configurator, when it runs in a docker-container | ||
window.ui = SwaggerUIBundle({ | ||
url: "{{ swagger_url }}", | ||
dom_id: '#swagger-ui', | ||
deepLinking: true, | ||
presets: [ | ||
SwaggerUIBundle.presets.apis, | ||
SwaggerUIStandalonePreset | ||
], | ||
plugins: [ | ||
SwaggerUIBundle.plugins.DownloadUrl | ||
], | ||
layout: "StandaloneLayout" | ||
}); | ||
|
||
//</editor-fold> | ||
}; |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<!-- HTML for static distribution bundle build --> | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<title>Swagger UI</title> | ||
<link rel="stylesheet" type="text/css" href="/static/swagger-ui.css" /> | ||
<link rel="stylesheet" type="text/css" href="/static/index.css" /> | ||
<link rel="icon" type="image/png" href="/static/favicon-32x32.png" sizes="32x32" /> | ||
<link rel="icon" type="image/png" href="/static/favicon-16x16.png" sizes="16x16" /> | ||
</head> | ||
|
||
<body> | ||
<div id="swagger-ui"></div> | ||
<script src="/static/swagger-ui-bundle.js" charset="UTF-8"> </script> | ||
<script src="/static/swagger-ui-standalone-preset.js" charset="UTF-8"> </script> | ||
<script src="/_localstack/swagger-initializer.js" charset="UTF-8"> </script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import copy | ||
import logging | ||
import textwrap | ||
from typing import Any | ||
|
||
import yaml | ||
from plux import PluginManager | ||
|
||
from localstack import version | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
spec_top_info = textwrap.dedent(""" | ||
openapi: 3.1.0 | ||
info: | ||
contact: | ||
email: [email protected] | ||
name: LocalStack Support | ||
url: https://www.localstack.cloud/contact | ||
summary: The LocalStack REST API exposes functionality related to diagnostics, health | ||
checks, plugins, initialisation hooks, service introspection, and more. | ||
termsOfService: https://www.localstack.cloud/legal/tos | ||
title: LocalStack REST API | ||
version: 1.0 | ||
externalDocs: | ||
description: LocalStack Documentation | ||
url: https://docs.localstack.cloud | ||
servers: | ||
- url: http://{host}:{port} | ||
variables: | ||
port: | ||
default: '4566' | ||
host: | ||
default: 'localhost.localstack.cloud' | ||
""") | ||
|
||
|
||
def _merge_openapi_specs(specs: list[dict[str, Any]]) -> dict[str, Any]: | ||
""" | ||
Merge a list of OpenAPI specs into a single specification. | ||
:param specs: a list of OpenAPI specs loaded in a dictionary | ||
:return: the dictionary of a merged spec. | ||
""" | ||
merged_spec = {} | ||
for idx, spec in enumerate(specs): | ||
if idx == 0: | ||
merged_spec = copy.deepcopy(spec) | ||
else: | ||
# Merge paths | ||
if "paths" in spec: | ||
merged_spec.setdefault("paths", {}).update(spec.get("paths", {})) | ||
|
||
# Merge components | ||
if "components" in spec: | ||
if "components" not in merged_spec: | ||
merged_spec["components"] = {} | ||
for component_type, component_value in spec["components"].items(): | ||
if component_type not in merged_spec["components"]: | ||
merged_spec["components"][component_type] = component_value | ||
else: | ||
merged_spec["components"][component_type].update(component_value) | ||
|
||
# Update the initial part of the spec, i.e., info and correct LocalStack version | ||
top_content = yaml.safe_load(spec_top_info) | ||
# Set the correct version | ||
top_content["info"]["version"] = version.version | ||
merged_spec.update(top_content) | ||
return merged_spec | ||
|
||
|
||
def get_localstack_openapi_spec() -> dict[str, Any]: | ||
""" | ||
Collects all the declared OpenAPI specs in LocalStack. | ||
Specs are declared by implementing a OASPlugin. | ||
:return: the entire LocalStack OpenAPI spec in a Python dictionary. | ||
""" | ||
specs = PluginManager("localstack.openapi.spec").load_all() | ||
try: | ||
return _merge_openapi_specs([spec.spec for spec in specs]) | ||
except Exception as e: | ||
LOG.debug("An error occurred while trying to merge the collected OpenAPI specs %s", e) | ||
# In case of an error while merging the spec, we return the first collected one. | ||
return specs[0].spec | ||
Comment on lines
+82
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Returning the first spec on error might lead to unexpected behavior. Consider raising a custom exception instead |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import os | ||
|
||
from jinja2 import Environment, FileSystemLoader | ||
from rolo import route | ||
|
||
from localstack.config import external_service_url | ||
from localstack.http import Response | ||
|
||
|
||
class SwaggerUIApi: | ||
init_path: str | ||
|
||
def __init__(self) -> None: | ||
self.init_path = f"{external_service_url()}/openapi.yaml" | ||
|
||
@route("/_localstack/swagger", methods=["GET"]) | ||
def server_swagger_ui(self, _request): | ||
oas_path = os.path.join(os.path.dirname(__file__), "..", "templates") | ||
env = Environment(loader=FileSystemLoader(oas_path)) | ||
template = env.get_template("index.html") | ||
rendered_template = template.render() | ||
return Response(rendered_template, content_type="text/html") | ||
Comment on lines
+17
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Consider caching the rendered template to improve performance for subsequent requests |
||
|
||
@route("/_localstack/swagger-initializer.js", methods=["GET"]) | ||
def serve_swagger_initializer(self, _request): | ||
oas_path = os.path.join(os.path.dirname(__file__), "..", "static") | ||
env = Environment(loader=FileSystemLoader(oas_path)) | ||
template = env.get_template("swagger-initializer.js") | ||
|
||
rendered_template = template.render(swagger_url=self.init_path) | ||
return Response(rendered_template, content_type="application/javascript") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider adding type hints for the return value in the function signature