Skip to content
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

WIP: Openapi #1417

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ enforced in the UI, and is now enforced in the REST API. The check is not perfor

icon:check[] Core: When a project was deleted, its associated version purge jobs were not. This has been fixed.

icon:check[] Rest: New OpenAPI secure endpoints `/openapi.yaml` and `/openapi.json` have been added.

[[v1.8.6]]
== 1.8.6 (06.07.2022)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
Expand Down Expand Up @@ -98,8 +99,11 @@ public class InternalEndpointRouteImpl implements InternalEndpointRoute {

private Boolean mutating;

private boolean insecure = false;

/**
* Create a new endpoint wrapper using the provided router to create the wrapped route instance.
* Create a new endpoint wrapper using the provided router to create the wrapped
* route instance.
*
* @param router
* @param localConfigApi
Expand Down Expand Up @@ -181,7 +185,8 @@ public InternalEndpointRoute validate() {
log.error("Endpoint {" + getRamlPath() + "} has no example response.");
throw new RuntimeException("Endpoint {" + getRamlPath() + "} has no example responses.");
}
if ((consumes.contains(APPLICATION_JSON) || consumes.contains(APPLICATION_JSON_UTF8)) && exampleRequestMap == null) {
if ((consumes.contains(APPLICATION_JSON) || consumes.contains(APPLICATION_JSON_UTF8))
&& exampleRequestMap == null) {
log.error("Endpoint {" + getPath() + "} has no example request.");
throw new RuntimeException("Endpoint has no example request.");
}
Expand All @@ -194,7 +199,8 @@ public InternalEndpointRoute validate() {
List<String> segments = getNamedSegments();
for (String segment : segments) {
if (!getUriParameters().containsKey(segment)) {
throw new RuntimeException("Missing URI description for path {" + getRamlPath() + "} segment {" + segment + "}");
throw new RuntimeException(
"Missing URI description for path {" + getRamlPath() + "} segment {" + segment + "}");
}
}
return this;
Expand Down Expand Up @@ -288,7 +294,8 @@ public String getDisplayName() {
}

@Override
public InternalEndpointRoute exampleResponse(HttpResponseStatus status, String description, String headerName, String example, String headerDescription) {
public InternalEndpointRoute exampleResponse(HttpResponseStatus status, String description, String headerName,
String example, String headerDescription) {
Response response = new Response();
response.setDescription(description);
exampleResponses.put(status.code(), response);
Expand Down Expand Up @@ -324,7 +331,11 @@ public InternalEndpointRoute exampleResponse(HttpResponseStatus status, Object m
map.put("application/json", mimeType);
} else {
mimeType.setExample(model.toString());
map.put("text/plain", mimeType);
if (model.getClass().getSimpleName().toLowerCase().startsWith("json")) {
map.put("application/json", mimeType);
} else {
map.put("text/plain", mimeType);
}
}

exampleResponses.put(status.code(), response);
Expand Down Expand Up @@ -516,9 +527,7 @@ public Set<MeshEvent> getEvents() {
@Override
public boolean isMutating() {
return Optional.ofNullable(mutating)
.orElse(Optional.ofNullable(getMethod())
.map(mutatingMethods::contains)
.orElse(false));
.orElse(Optional.ofNullable(getMethod()).map(mutatingMethods::contains).orElse(false));
}

@Override
Expand All @@ -532,6 +541,27 @@ public Route getRoute() {
return route;
}

@Override
public boolean isInsecure() {
return insecure;
}

@Override
public InternalEndpointRoute setInsecure(boolean insecure) {
this.insecure = insecure;
return this;
}

@Override
public Set<String> getProduces() {
return Collections.unmodifiableSet(produces);
}

@Override
public Set<String> getConsumes() {
return Collections.unmodifiableSet(consumes);
}

private class ReadOnlyHandler implements PlatformHandler {

private final LocalConfigApi localConfigApi;
Expand Down
16 changes: 15 additions & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,22 @@
<groupId>com.gentics.mesh</groupId>
<artifactId>mesh-service-aws-s3-storage</artifactId>
</dependency>
</dependencies>

<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-core</artifactId>
<version>2.2.20</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static io.vertx.core.http.HttpMethod.DELETE;
import static io.vertx.core.http.HttpMethod.GET;
import static io.vertx.core.http.HttpMethod.POST;
import static io.vertx.core.http.HttpMethod.PUT;

import com.gentics.mesh.auth.MeshAuthChainImpl;
import com.gentics.mesh.context.InternalActionContext;
Expand Down Expand Up @@ -77,5 +78,21 @@ protected void addRolePermissionHandler(String uuidParameterName, String uuidPar
String uuid = rc.request().getParam(uuidParameterName);
crudHandler.handleRevokePermissions(ac, uuid);
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP standard has no directions over DELETE methods with body, so the client generator fails on the method above, as not following the standard.

InternalEndpointRoute revokePermissionsEndpointStandard = createRoute();
revokePermissionsEndpointStandard.path(path);
revokePermissionsEndpointStandard.addUriParameter(uuidParameterName, "Uuid of the " + typeDescription, uuidParameterExample);
revokePermissionsEndpointStandard.method(PUT);
revokePermissionsEndpointStandard.description("Revoke permissions on the " + typeDescription + " from multiple roles.");
revokePermissionsEndpointStandard.consumes(APPLICATION_JSON);
revokePermissionsEndpointStandard.produces(APPLICATION_JSON);
revokePermissionsEndpointStandard.exampleRequest(roleExamples.getObjectPermissionRevokeRequest(includePublishPermissions));
revokePermissionsEndpointStandard.exampleResponse(OK, roleExamples.getObjectPermissionResponse(includePublishPermissions), "Updated permissions.");
revokePermissionsEndpointStandard.events(ROLE_PERMISSIONS_CHANGED);
revokePermissionsEndpointStandard.blockingHandler(rc -> {
InternalActionContext ac = wrap(rc);
String uuid = rc.request().getParam(uuidParameterName);
crudHandler.handleRevokePermissions(ac, uuid);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import static com.gentics.mesh.example.ExampleUuids.JOB_UUID;
import static com.gentics.mesh.example.ExampleUuids.PLUGIN_1_ID;
import static com.gentics.mesh.http.HttpConstants.APPLICATION_JSON;
import static com.gentics.mesh.http.HttpConstants.APPLICATION_OCTET_STREAM;
import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.vertx.core.http.HttpMethod.DELETE;
import static io.vertx.core.http.HttpMethod.GET;
Expand Down Expand Up @@ -359,6 +361,7 @@ private void addJobHandler() {
deleteJob.method(DELETE);
deleteJob.description("Deletes the job. Note that it is only possible to delete failed jobs");
deleteJob.addUriParameter("jobUuid", "Uuid of the job.", JOB_UUID);
deleteJob.exampleResponse(NO_CONTENT, "Job has been deleted.");
deleteJob.blockingHandler(rc -> {
InternalActionContext ac = wrap(rc);
String uuid = ac.getParameter("jobUuid");
Expand All @@ -369,6 +372,7 @@ private void addJobHandler() {
processJob.path("/jobs/:jobUuid/process");
processJob.method(POST);
processJob.description("Process the job. Failed jobs will be automatically reset and put in queued state.");
processJob.exampleResponse(OK, "Job has been queued for processing.");
processJob.addUriParameter("jobUuid", "Uuid of the job.", JOB_UUID);
processJob.blockingHandler(rc -> {
InternalActionContext ac = wrap(rc);
Expand All @@ -380,6 +384,7 @@ private void addJobHandler() {
resetJob.path("/jobs/:jobUuid/error");
resetJob.method(DELETE);
resetJob.description("Deletes error state from the job. This will make it possible to execute the job once again.");
resetJob.exampleResponse(NO_CONTENT, "Job has been reset.");
resetJob.addUriParameter("jobUuid", "Uuid of the job.", JOB_UUID);
resetJob.blockingHandler(rc -> {
InternalActionContext ac = wrap(rc);
Expand All @@ -392,6 +397,8 @@ private void addDebugInfoHandler() {
InternalEndpointRoute route = createRoute();
route.path("/debuginfo");
route.method(GET);
route.produces(APPLICATION_OCTET_STREAM);
route.exampleResponse(OK, "ZIP file");
route.description("Downloads a zip file of various [debug information](/docs/administration-guide/#debuginfo) files.");
route.addQueryParameter("include", "Information to include. See the [documentation](/docs/administration-guide/#debuginfo) for possible values.", "-backup,consistencyCheck");
route.handler(rc -> debugInfoHandler.handle(rc));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.gentics.mesh.context.InternalActionContext;
import com.gentics.mesh.core.data.user.HibUser;
import com.gentics.mesh.core.db.Database;
import com.gentics.mesh.core.db.cluster.ClusterManager;
import com.gentics.mesh.core.endpoint.admin.consistency.ConsistencyCheckHandler;
import com.gentics.mesh.core.endpoint.handler.AbstractHandler;
import com.gentics.mesh.core.rest.MeshServerInfoModel;
Expand All @@ -30,6 +31,7 @@
import com.gentics.mesh.distributed.coordinator.Coordinator;
import com.gentics.mesh.distributed.coordinator.MasterServer;
import com.gentics.mesh.etc.config.MeshOptions;
import com.gentics.mesh.generator.OpenAPIv3Generator;
import com.gentics.mesh.generator.RAMLGenerator;
import com.gentics.mesh.parameter.BackupParameters;
import com.gentics.mesh.router.RouterStorageImpl;
Expand Down Expand Up @@ -72,10 +74,12 @@ public abstract class AdminHandler extends AbstractHandler {

private final CacheRegistry cacheRegistry;

protected final ClusterManager clusterManager;

protected AdminHandler(Vertx vertx, Database db, RouterStorageImpl routerStorage, BootstrapInitializer boot, SearchProvider searchProvider,
HandlerUtilities utils,
MeshOptions options, RouterStorageRegistryImpl routerStorageRegistry, Coordinator coordinator, WriteLock writeLock,
ConsistencyCheckHandler consistencyCheckHandler, CacheRegistry cacheRegistry) {
ConsistencyCheckHandler consistencyCheckHandler, CacheRegistry cacheRegistry, ClusterManager clusterManager) {
this.vertx = vertx;
this.db = db;
this.routerStorage = routerStorage;
Expand All @@ -88,6 +92,7 @@ protected AdminHandler(Vertx vertx, Database db, RouterStorageImpl routerStorage
this.writeLock = writeLock;
this.consistencyCheckHandler = consistencyCheckHandler;
this.cacheRegistry = cacheRegistry;
this.clusterManager = clusterManager;
}

/**
Expand Down Expand Up @@ -224,6 +229,11 @@ public MeshServerInfoModel getMeshServerInfoModel(InternalActionContext ac) {
return info;
}

public void handleOpenAPIv3(InternalActionContext ac, String format) {
OpenAPIv3Generator generator = new OpenAPIv3Generator(clusterManager.getHazelcast());
generator.generate(ac, format);
}

/**
* Generate and return the RAML of the server.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import com.gentics.mesh.rest.InternalEndpointRoute;
import com.gentics.mesh.router.route.AbstractInternalEndpoint;

import io.netty.handler.codec.http.HttpResponseStatus;

/**
* Endpoint definition for health / readiness checks
*/
Expand Down Expand Up @@ -40,25 +42,31 @@ public String getDescription() {

private void addLive() {
InternalEndpointRoute deployEndpoint = createRoute();
deployEndpoint.setInsecure(true);
deployEndpoint.path("/live");
deployEndpoint.method(GET);
deployEndpoint.description("Returns an empty response with status code 200 if Gentics Mesh is alive.");
deployEndpoint.exampleResponse(HttpResponseStatus.OK, "Mesh is alive.");
deployEndpoint.handler(rc -> monitoringCrudHandler.handleLive(rc));
}

private void addReady() {
InternalEndpointRoute deployEndpoint = createRoute();
deployEndpoint.path("/ready");
deployEndpoint.setInsecure(true);
deployEndpoint.method(GET);
deployEndpoint.description("Returns an empty response with status code 200 if Gentics Mesh is ready. Responds with 503 otherwise.");
deployEndpoint.exampleResponse(HttpResponseStatus.OK, "Mesh is ready.");
deployEndpoint.handler(rc -> monitoringCrudHandler.handleReady(rc));
}

private void addWritable() {
InternalEndpointRoute deployEndpoint = createRoute();
deployEndpoint.path("/writable");
deployEndpoint.setInsecure(true);
deployEndpoint.method(GET);
deployEndpoint.description("Returns an empty response with status code 200 if Gentics Mesh is writable. Responds with 503 otherwise.");
deployEndpoint.exampleResponse(HttpResponseStatus.OK, "Mesh is writable.");
deployEndpoint.handler(rc -> monitoringCrudHandler.handleWritable(rc));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,35 @@ public void registerEndPoints() {
adminHandler.handleRAML(ac);
}, false);

secure("/openapi.yaml");
plyhun marked this conversation as resolved.
Show resolved Hide resolved
InternalEndpointRoute openapiYml = createRoute();
openapiYml.path("/openapi.yaml");
openapiYml.method(GET);
openapiYml.description("Endpoint which provides a OpenAPIv3 YAML document for all registed endpoints.");
openapiYml.displayName("OpenAPI YAML specification");
openapiYml.exampleResponse(OK, "Not yet specified");
openapiYml.produces(APPLICATION_YAML);
openapiYml.blockingHandler(rc -> {
InternalActionContext ac = wrap(rc);
adminHandler.handleOpenAPIv3(ac, "yaml");
}, false);

secure("/openapi.json");
InternalEndpointRoute openapiJson = createRoute();
openapiJson.path("/openapi.json");
openapiJson.method(GET);
openapiJson.description("Endpoint which provides a OpenAPIv3 JSON document for all registed endpoints.");
openapiJson.displayName("OpenAPI JSON specification");
openapiJson.exampleResponse(OK, "Not yet specified");
openapiJson.produces(APPLICATION_JSON);
openapiJson.blockingHandler(rc -> {
InternalActionContext ac = wrap(rc);
adminHandler.handleOpenAPIv3(ac, "json");
}, false);

secure("/");
InternalEndpointRoute infoEndpoint = createRoute();
infoEndpoint.setInsecure(true);
infoEndpoint.path("/");
infoEndpoint.description("Endpoint which returns version information");
infoEndpoint.displayName("Version Information");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public void registerEndPoints() {
loginEndpoint.path("/login");
loginEndpoint.method(POST);
loginEndpoint.setMutating(false);
loginEndpoint.setInsecure(true);
loginEndpoint.consumes(APPLICATION_JSON);
loginEndpoint.produces(APPLICATION_JSON);
loginEndpoint.description("Login via this dedicated login endpoint.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private void addEventBusHandler() {
endpoint.setRAMLPath("/");
endpoint.description("This endpoint provides a sockjs compliant websocket which can be used to interface with the vert.x eventbus.");

if (!isRamlGeneratorContext()) {
if (!isSpecGeneratorContext()) {
SockJSHandlerOptions sockJSoptions = new SockJSHandlerOptions().setHeartbeatInterval(2000);
SockJSHandler handler = SockJSHandler.create(vertx, sockJSoptions);
SockJSBridgeOptions bridgeOptions = new SockJSBridgeOptions();
Expand Down Expand Up @@ -87,10 +87,10 @@ private void addEventBusHandler() {
}

/**
* Returns whether the method is called from during the documentation generation context.
* Returns whether the method is called from during the specification generation context.
* @return
*/
private boolean isRamlGeneratorContext() {
private boolean isSpecGeneratorContext() {
return localRouter == null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static com.gentics.mesh.example.ExampleUuids.TAG_RED_UUID;
import static com.gentics.mesh.example.ExampleUuids.UUID_1;
import static com.gentics.mesh.http.HttpConstants.APPLICATION_JSON;
import static com.gentics.mesh.http.HttpConstants.APPLICATION_OCTET_STREAM;
import static io.netty.handler.codec.http.HttpResponseStatus.CONFLICT;
import static io.netty.handler.codec.http.HttpResponseStatus.CREATED;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
Expand Down Expand Up @@ -221,6 +222,8 @@ private void addBinaryHandlers() {
fieldGet.addQueryParameters(ImageManipulationParametersImpl.class);
fieldGet.addQueryParameters(VersioningParametersImpl.class);
fieldGet.method(GET);
fieldGet.produces(APPLICATION_OCTET_STREAM);
fieldGet.exampleResponse(OK, "Binary data");
fieldGet.description(
"Download the binary field with the given name. You can use image query parameters for crop and resize if the binary data represents an image.");
fieldGet.blockingHandler(rc -> {
Expand Down
Loading