From 6394b665f0a42a0dac03c789c1d0a7a774825e0b Mon Sep 17 00:00:00 2001 From: frne Date: Mon, 12 Aug 2024 11:29:07 +0200 Subject: [PATCH] Fix SmallRye Health OpenAPI definitions The quarkus-smallrye-health extension exposes an openapi definition for the health endpoints it provides. This fixes the exposed schema to actually match the JSON structure returned by the endpoints. Additionally, the filter implementation (io.quarkus.smallrye.health.deployment.HealthOpenAPIFilter) has been refactored to use a more fluid and readable schema definition. --- .../deployment/HealthOpenAPIFilter.java | 291 +++++++----------- .../test/DeprecatedHealthOpenAPITest.java | 12 +- .../health/test/HealthOpenAPITest.java | 11 +- 3 files changed, 136 insertions(+), 178 deletions(-) diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java index feb37ea27dc5f..62d7fd5232743 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java @@ -1,8 +1,6 @@ package io.quarkus.smallrye.health.deployment; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -12,9 +10,7 @@ import org.eclipse.microprofile.openapi.models.PathItem; import org.eclipse.microprofile.openapi.models.Paths; import org.eclipse.microprofile.openapi.models.media.Content; -import org.eclipse.microprofile.openapi.models.media.MediaType; import org.eclipse.microprofile.openapi.models.media.Schema; -import org.eclipse.microprofile.openapi.models.responses.APIResponse; import org.eclipse.microprofile.openapi.models.responses.APIResponses; import io.smallrye.openapi.api.models.ComponentsImpl; @@ -31,9 +27,42 @@ * Create OpenAPI entries (if configured) */ public class HealthOpenAPIFilter implements OASFilter { + private static final List MICROPROFILE_HEALTH_TAG = Collections.singletonList("MicroProfile Health"); - private static final String SCHEMA_HEALTH_RESPONSE = "HealthCheckResponse"; - private static final String SCHEMA_HEALTH_STATUS = "HealthCheckStatus"; + private static final String HEALTH_RESPONSE_SCHEMA_NAME = "HealthResponse"; + private static final String HEALTH_CHECK_SCHEMA_NAME = "HealthCheck"; + + private static final Schema healthResponseSchemaDefinition = new SchemaImpl(HEALTH_RESPONSE_SCHEMA_NAME) + .type(Schema.SchemaType.OBJECT) + .properties(Map.ofEntries( + + Map.entry("status", + new SchemaImpl() + .type(Schema.SchemaType.STRING) + .enumeration(List.of("UP", "DOWN"))), + + Map.entry("checks", + new SchemaImpl() + .type(Schema.SchemaType.ARRAY) + .items(new SchemaImpl().ref("#/components/schemas/" + HEALTH_CHECK_SCHEMA_NAME))))); + + private static final Schema healthCheckSchemaDefinition = new SchemaImpl(HEALTH_CHECK_SCHEMA_NAME) + .type(Schema.SchemaType.OBJECT) + .properties(Map.ofEntries( + + Map.entry("name", + new SchemaImpl() + .type(Schema.SchemaType.STRING)), + + Map.entry("status", + new SchemaImpl() + .type(Schema.SchemaType.STRING) + .enumeration(List.of("UP", "DOWN"))), + + Map.entry("data", + new SchemaImpl() + .type(Schema.SchemaType.OBJECT) + .nullable(Boolean.TRUE)))); private final String rootPath; private final String livenessPath; @@ -52,192 +81,102 @@ public void filterOpenAPI(OpenAPI openAPI) { if (openAPI.getComponents() == null) { openAPI.setComponents(new ComponentsImpl()); } - openAPI.getComponents().addSchema(SCHEMA_HEALTH_RESPONSE, createHealthCheckResponse()); - openAPI.getComponents().addSchema(SCHEMA_HEALTH_STATUS, createHealthCheckStatus()); + openAPI.getComponents().addSchema(HEALTH_RESPONSE_SCHEMA_NAME, healthResponseSchemaDefinition); + openAPI.getComponents().addSchema(HEALTH_CHECK_SCHEMA_NAME, healthCheckSchemaDefinition); if (openAPI.getPaths() == null) { openAPI.setPaths(new PathsImpl()); } - Paths paths = openAPI.getPaths(); + + final Paths paths = openAPI.getPaths(); // Health - paths.addPathItem(rootPath, createHealthPathItem()); + paths.addPathItem( + rootPath, + createHealthEndpoint( + "MicroProfile Health Endpoint", + "MicroProfile Health provides a way for your application to distribute " + + "information about its healthiness state to state whether or not it is able to " + + "function properly", + "Check the health of the application", + "microprofile_health_root", + "An aggregated view of the Liveness, Readiness and Startup of this application")); // Liveness - paths.addPathItem(livenessPath, createLivenessPathItem()); + paths.addPathItem( + livenessPath, + createHealthEndpoint( + "MicroProfile Health - Liveness Endpoint", + "Liveness checks are utilized to tell whether the application should be " + + "restarted", + "Check the liveness of the application", + "microprofile_health_liveness", + "The Liveness check of this application")); // Readiness - paths.addPathItem(readinessPath, createReadinessPathItem()); + paths.addPathItem( + readinessPath, + createHealthEndpoint( + "MicroProfile Health - Readiness Endpoint", + "Readiness checks are used to tell whether the application is able to " + + "process requests", + "Check the readiness of the application", + "microprofile_health_readiness", + "The Readiness check of this application")); // Startup - paths.addPathItem(startupPath, createStartupPathItem()); - } - - private PathItem createHealthPathItem() { - PathItem pathItem = new PathItemImpl(); - pathItem.setDescription("MicroProfile Health Endpoint"); - pathItem.setSummary( - "MicroProfile Health provides a way for your application to distribute information about its healthiness state to state whether or not it is able to function properly"); - pathItem.setGET(createHealthOperation()); - return pathItem; - } - - private PathItem createLivenessPathItem() { - PathItem pathItem = new PathItemImpl(); - pathItem.setDescription("MicroProfile Health - Liveness Endpoint"); - pathItem.setSummary( - "Liveness checks are utilized to tell whether the application should be restarted"); - pathItem.setGET(createLivenessOperation()); - return pathItem; - } - - private PathItem createReadinessPathItem() { - PathItem pathItem = new PathItemImpl(); - pathItem.setDescription("MicroProfile Health - Readiness Endpoint"); - pathItem.setSummary( - "Readiness checks are used to tell whether the application is able to process requests"); - pathItem.setGET(createReadinessOperation()); - return pathItem; - } - - private PathItem createStartupPathItem() { - PathItem pathItem = new PathItemImpl(); - pathItem.setDescription("MicroProfile Health - Startup Endpoint"); - pathItem.setSummary( - "Startup checks are an used to tell when the application has started"); - pathItem.setGET(createStartupOperation()); - return pathItem; - } - - private Operation createHealthOperation() { - Operation operation = new OperationImpl(); - operation.setDescription("Check the health of the application"); - operation.setOperationId("microprofile_health_root"); - operation.setTags(MICROPROFILE_HEALTH_TAG); - operation.setSummary("An aggregated view of the Liveness, Readiness and Startup of this application"); - operation.setResponses(createAPIResponses()); - return operation; - } - - private Operation createLivenessOperation() { - Operation operation = new OperationImpl(); - operation.setDescription("Check the liveness of the application"); - operation.setOperationId("microprofile_health_liveness"); - operation.setTags(MICROPROFILE_HEALTH_TAG); - operation.setSummary("The Liveness check of this application"); - operation.setResponses(createAPIResponses()); - return operation; - } - - private Operation createReadinessOperation() { - Operation operation = new OperationImpl(); - operation.setDescription("Check the readiness of the application"); - operation.setOperationId("microprofile_health_readiness"); - operation.setTags(MICROPROFILE_HEALTH_TAG); - operation.setSummary("The Readiness check of this application"); - operation.setResponses(createAPIResponses()); - return operation; - } - - private Operation createStartupOperation() { - Operation operation = new OperationImpl(); - operation.setDescription("Check the startup of the application"); - operation.setOperationId("microprofile_health_startup"); - operation.setTags(MICROPROFILE_HEALTH_TAG); - operation.setSummary("The Startup check of this application"); - operation.setResponses(createAPIResponses()); - return operation; - } - - private APIResponses createAPIResponses() { - APIResponses responses = new APIResponsesImpl(); - responses.addAPIResponse("200", createAPIResponse("OK")); - responses.addAPIResponse("503", createAPIResponse("Service Unavailable")); - responses.addAPIResponse("500", createAPIResponse("Internal Server Error")); - return responses; - } - - private APIResponse createAPIResponse(String description) { - APIResponse response = new APIResponseImpl(); - response.setDescription(description); - response.setContent(createContent()); - return response; - } - - private Content createContent() { - Content content = new ContentImpl(); - content.addMediaType("application/json", createMediaType()); - return content; - } - - private MediaType createMediaType() { - MediaType mediaType = new MediaTypeImpl(); - mediaType.setSchema(new SchemaImpl().ref("#/components/schemas/" + SCHEMA_HEALTH_RESPONSE)); - return mediaType; + paths.addPathItem( + startupPath, + createHealthEndpoint( + "MicroProfile Health - Startup Endpoint", + "Startup checks are an used to tell when the application has started", + "Check the startup of the application", + "microprofile_health_startup", + "The Startup check of this application")); } /** - * HealthCheckResponse: - * type: object - * properties: - * data: - * type: object - * nullable: true - * name: - * type: string - * status: - * $ref: '#/components/schemas/HealthCheckStatus' + * Creates a {@link PathItem} containing the endpoint definition and GET {@link Operation} for health endpoints. * - * @return Schema representing HealthCheckResponse + * @param endpointDescription The description for the endpoint definition + * @param endpointSummary The summary for the endpoint definition + * @param operationDescription The description for the operation definition + * @param operationId The operation-id for the operation definition + * @param operationSummary The summary for the operation definition */ - private Schema createHealthCheckResponse() { - Schema schema = new SchemaImpl(SCHEMA_HEALTH_RESPONSE); - schema.setType(Schema.SchemaType.OBJECT); - schema.setProperties(createProperties()); - return schema; - } - - private Map createProperties() { - Map map = new HashMap<>(); - map.put("data", createData()); - map.put("name", createName()); - map.put("status", new SchemaImpl().ref("#/components/schemas/" + SCHEMA_HEALTH_STATUS)); - return map; - } - - private Schema createData() { - Schema schema = new SchemaImpl("data"); - schema.setType(Schema.SchemaType.OBJECT); - schema.setNullable(Boolean.TRUE); - return schema; - } - - private Schema createName() { - Schema schema = new SchemaImpl("name"); - schema.setType(Schema.SchemaType.STRING); - return schema; - } - - /** - * HealthCheckStatus: - * enum: - * - DOWN - * - UP - * type: string - * - * @return Schema representing Status - */ - private Schema createHealthCheckStatus() { - Schema schema = new SchemaImpl(SCHEMA_HEALTH_STATUS); - schema.setEnumeration(createStateEnumValues()); - schema.setType(Schema.SchemaType.STRING); - return schema; - } - - private List createStateEnumValues() { - List values = new ArrayList<>(); - values.add("DOWN"); - values.add("UP"); - return values; + private PathItem createHealthEndpoint( + String endpointDescription, + String endpointSummary, + String operationDescription, + String operationId, + String operationSummary) { + final Content content = new ContentImpl() + .addMediaType( + "application/json", + new MediaTypeImpl() + .schema(new SchemaImpl().ref("#/components/schemas/" + HEALTH_RESPONSE_SCHEMA_NAME))); + + final APIResponses responses = new APIResponsesImpl() + .addAPIResponse( + "200", + new APIResponseImpl().description("OK").content(content)) + .addAPIResponse( + "503", + new APIResponseImpl().description("Service Unavailable").content(content)) + .addAPIResponse( + "500", + new APIResponseImpl().description("Internal Server Error").content(content)); + + final Operation getOperation = new OperationImpl() + .operationId(operationId) + .description(operationDescription) + .tags(MICROPROFILE_HEALTH_TAG) + .summary(operationSummary) + .responses(responses); + + return new PathItemImpl() + .description(endpointDescription) + .summary(endpointSummary) + .GET(getOperation); } } diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/DeprecatedHealthOpenAPITest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/DeprecatedHealthOpenAPITest.java index f7c85bd175e98..e9e26dc46e743 100644 --- a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/DeprecatedHealthOpenAPITest.java +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/DeprecatedHealthOpenAPITest.java @@ -29,11 +29,21 @@ void testOpenApiPathAccessResource() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") + .body("paths", Matchers.hasKey("/q/health/ready")) .body("paths", Matchers.hasKey("/q/health/live")) .body("paths", Matchers.hasKey("/q/health/started")) .body("paths", Matchers.hasKey("/q/health")) - .body("components.schemas.HealthCheckResponse.type", Matchers.equalTo("object")); + + .body("components.schemas.HealthResponse.type", Matchers.equalTo("object")) + .body("components.schemas.HealthResponse.properties.status.type", Matchers.equalTo("string")) + .body("components.schemas.HealthResponse.properties.checks.type", Matchers.equalTo("array")) + + .body("components.schemas.HealthCheck.type", Matchers.equalTo("object")) + .body("components.schemas.HealthCheck.properties.status.type", Matchers.equalTo("string")) + .body("components.schemas.HealthCheck.properties.name.type", Matchers.equalTo("string")) + .body("components.schemas.HealthCheck.properties.data.type", Matchers.equalTo("object")) + .body("components.schemas.HealthCheck.properties.data.nullable", Matchers.is(true)); } diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java index 96697dc0e9c5e..fe4e1f28f9ee9 100644 --- a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java @@ -32,7 +32,16 @@ void testOpenApiPathAccessResource() { .body("paths", Matchers.hasKey("/q/health/live")) .body("paths", Matchers.hasKey("/q/health/started")) .body("paths", Matchers.hasKey("/q/health")) - .body("components.schemas.HealthCheckResponse.type", Matchers.equalTo("object")); + + .body("components.schemas.HealthResponse.type", Matchers.equalTo("object")) + .body("components.schemas.HealthResponse.properties.status.type", Matchers.equalTo("string")) + .body("components.schemas.HealthResponse.properties.checks.type", Matchers.equalTo("array")) + + .body("components.schemas.HealthCheck.type", Matchers.equalTo("object")) + .body("components.schemas.HealthCheck.properties.status.type", Matchers.equalTo("string")) + .body("components.schemas.HealthCheck.properties.name.type", Matchers.equalTo("string")) + .body("components.schemas.HealthCheck.properties.data.type", Matchers.equalTo("object")) + .body("components.schemas.HealthCheck.properties.data.nullable", Matchers.is(true)); }