Skip to content

Commit

Permalink
GEODE-6608: Add Swagger UI to Management REST API (apache#3431)
Browse files Browse the repository at this point in the history
Authored-by: Jens Deppe <[email protected]>
  • Loading branch information
jdeppe-pivotal authored Apr 9, 2019
1 parent 2402cac commit 232dc0f
Show file tree
Hide file tree
Showing 19 changed files with 263 additions and 16 deletions.
5 changes: 5 additions & 0 deletions boms/geode-all-bom/src/test/resources/expected-pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.20</version>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ class DependencyConstraints implements Plugin<Project> {
api(group: 'org.postgresql', name: 'postgresql', version: '42.2.2')
api(group: 'org.skyscreamer', name: 'jsonassert', version: '1.5.0')
api(group: 'org.slf4j', name: 'slf4j-api', version: get('slf4j-api.version'))
api(group: 'io.swagger', name: 'swagger-annotations', version: '1.5.20')
api(group: 'org.springframework.hateoas', name: 'spring-hateoas', version: '0.25.0.RELEASE')
api(group: 'org.springframework.ldap', name: 'spring-ldap-core', version: '2.3.2.RELEASE')
api(group: 'org.springframework.shell', name: 'spring-shell', version: '1.2.0.RELEASE')
Expand Down
12 changes: 9 additions & 3 deletions geode-assembly/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ dependencies {
integrationTestCompile('org.apache.httpcomponents:httpclient')
integrationTestCompile('javax.annotation:javax.annotation-api')

integrationTestRuntime('io.swagger:swagger-annotations')


distributedTestCompile(project(':geode-core'))
distributedTestCompile(project(':geode-dunit')){
Expand All @@ -220,15 +222,19 @@ dependencies {
exclude group: 'org.apache.tomcat'
}
distributedTestRuntime('org.codehaus.cargo:cargo-core-uberjar')
distributedTestRuntime('io.swagger:swagger-annotations')


acceptanceTestCompile(project(':geode-core'))
// geodeArchives is a direct reflection of what is contained in geode-dependencies.jar. To that
// end only add _test_ dependencies to acceptanceTestCompile/Runtime. All other product
// dependencies should be a part of geodeArchives and should not need to be added as individual
// dependencies here.
acceptanceTestCompile(configurations.geodeArchives)
acceptanceTestCompile(project(':geode-dunit')) {
exclude module: 'geode-core'
}
acceptanceTestCompile(project(':geode-assembly:geode-assembly-test'))
acceptanceTestCompile('org.apache.httpcomponents:httpclient')


// This is used by 'gradle within gradle' tests. No need to bump this version; but if you do,
// don't have it be the same version as the outer gradle version.
acceptanceTestCompile('org.gradle:gradle-tooling-api:5.1.1')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ private String getJarOrClassesForModule(String module) {
String classPath = Arrays.stream(System.getProperty("java.class.path")
.split(File.pathSeparator))
.filter(x -> x.contains(module)
&& (x.endsWith("/classes") || x.endsWith("/resources") || x.endsWith(".jar")))
&& (x.endsWith("/classes") || x.endsWith("/classes/java/main")
|| x.endsWith("/resources") || x.endsWith(".jar")))
.collect(Collectors.joining(File.pathSeparator));

assertThat(classPath).as("no classes found for module: " + module)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.management.internal.rest;


import static org.apache.geode.test.junit.rules.HttpResponseAssert.assertResponse;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import com.fasterxml.jackson.databind.JsonNode;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.test.junit.categories.RestAPITest;
import org.apache.geode.test.junit.categories.SecurityTest;
import org.apache.geode.test.junit.rules.GeodeHttpClientRule;
import org.apache.geode.test.junit.rules.LocatorStarterRule;
import org.apache.geode.test.junit.rules.RequiresGeodeHome;

@Category({SecurityTest.class, RestAPITest.class})
public class SwaggerManagementVerificationIntegrationTest {

@ClassRule
public static LocatorStarterRule locatorStarter = new LocatorStarterRule()
.withHttpService()
.withAutoStart();

@Rule
public GeodeHttpClientRule client = new GeodeHttpClientRule(locatorStarter::getHttpPort);

@Rule
public RequiresGeodeHome requiresGeodeHome = new RequiresGeodeHome();

@Test
public void isSwaggerRunning() throws Exception {
// Check the UI
assertResponse(client.get("/geode-management/swagger-ui.html")).hasStatusCode(200);

// Check the JSON
JsonNode json =
assertResponse(client.get("/geode-management/v2/api-docs")).hasStatusCode(200)
.getJsonObject();
assertThat(json.get("swagger").asText(), is("2.0"));

JsonNode info = json.get("info");
assertThat(info.get("description").asText(),
is("REST API and interface to manage Geode components."));
assertThat(info.get("title").asText(),
is("Apache Geode Management REST API"));

JsonNode license = info.get("license");
assertThat(license.get("name").asText(), is("Apache License, version 2.0"));
assertThat(license.get("url").asText(), is("http://www.apache.org/licenses/"));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,7 @@ lib/spring-core-4.3.20.RELEASE.jar
lib/spring-expression-4.3.20.RELEASE.jar
lib/spring-shell-1.2.0.RELEASE.jar
lib/spring-web-4.3.20.RELEASE.jar
lib/swagger-annotations-1.5.20.jar
tools/ClientProtocol/geode-protobuf-messages-definitions-0.0.0.zip
tools/Extensions/geode-web-0.0.0.war
tools/Extensions/geode-web-api-0.0.0.war
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ slf4j-api-1.7.25.jar
snappy-0.4.jar
spring-core-4.3.20.RELEASE.jar
spring-shell-1.2.0.RELEASE.jar
swagger-annotations-1.5.20.jar
4 changes: 4 additions & 0 deletions geode-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ dependencies {
runtimeOnly('org.apache.logging.log4j:log4j-jul') {
ext.optional = true
}
runtimeOnly('io.swagger:swagger-annotations') {
ext.optional = true
}

//Jetty is used by the http service, which is used for the developer and management rest APIs
implementation('org.eclipse.jetty:jetty-webapp') {
ext.optional = true
Expand Down
6 changes: 6 additions & 0 deletions geode-core/src/test/resources/expected-pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -304,5 +304,11 @@
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
</project>
6 changes: 5 additions & 1 deletion geode-management/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ dependencies {
compile('org.springframework:spring-web')
compile('javax.xml.bind:jaxb-api')
compile('javax.xml.bind:jaxb-api')
compile('org.apache.httpcomponents:httpclient')
compile('org.apache.httpcomponents:httpclient')

compileOnly(project(':geode-common')) {
exclude module: 'junit'
}
compileOnly('io.springfox:springfox-swagger2') {
exclude module: 'slf4j-api'
exclude module: "jackson-annotations"
}

testCompile(project(':geode-common'))
testCompile(project(':geode-junit')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import javax.xml.bind.annotation.XmlType;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModelProperty;

import org.apache.geode.annotations.Experimental;
import org.apache.geode.management.api.RestfulEndpoint;
Expand All @@ -47,10 +48,13 @@ public class BasicRegionConfig implements CacheElement, RestfulEndpoint {

public static final String REGION_CONFIG_ENDPOINT = "/regions";

@ApiModelProperty(hidden = true)
@XmlElement(name = "region-attributes", namespace = "http://geode.apache.org/schema/cache")
protected RegionAttributesType regionAttributes;

@XmlAttribute(name = "name", required = true)
protected String name;

@XmlAttribute(name = "refid")
protected String type;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ public interface RestfulEndpoint {
* @return e.g. /regions
*/
@JsonIgnore
// @ApiModelProperty(hidden = true)
String getEndpoint();
}
16 changes: 12 additions & 4 deletions geode-web-management/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,30 @@ dependencies {
compileOnly(project(':geode-core'))

compileOnly('javax.servlet:javax.servlet-api')
// jackson-annotations must be accessed from the geode classloader and not the webapp
compileOnly('com.fasterxml.jackson.core:jackson-annotations')

compile('org.apache.commons:commons-lang3')
compile('commons-fileupload:commons-fileupload') {
exclude module: 'commons-io'
}
compileOnly('com.fasterxml.jackson.core:jackson-annotations')
compileOnly('com.fasterxml.jackson.core:jackson-core')
compileOnly('com.fasterxml.jackson.core:jackson-databind')
compile('com.fasterxml.jackson.core:jackson-core')
compile('com.fasterxml.jackson.core:jackson-databind') {
exclude module: 'jackson-annotations'
}

compileOnly('com.fasterxml.jackson.module:jackson-module-scala_2.10')
compileOnly('io.swagger:swagger-annotations')

compile('io.springfox:springfox-swagger2') {
exclude module: 'slf4j-api'
exclude module: "jackson-annotations"
exclude module: 'jackson-annotations'
exclude module: 'swagger-annotations'
}
compile('io.springfox:springfox-swagger-ui') {
exclude module: 'slf4j-api'
}

compile('org.springframework:spring-beans')
compile('org.springframework.security:spring-security-core')
compile('org.springframework.security:spring-security-web')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,19 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
// performs the actual request before logging
filterChain.doFilter(wrappedRequest, wrappedResponse);

// log after the request has been made and ContentCachingRequestWrapper has cached the request
// payload.
// Log after the request has been made and ContentCachingRequestWrapper has cached the request
// payload. We don't want to log any swagger requests though.
if (!(request.getRequestURI().contains("swagger")
|| request.getRequestURI().contains("api-docs"))) {
logRequest(request, wrappedRequest);
logResponse(response, wrappedResponse);
}

// IMPORTANT: copy content of response back into original response
wrappedResponse.copyBodyToResponse();
}

private void logRequest(HttpServletRequest request, ContentCachingRequestWrapper wrappedRequest) {
String requestPattern = "Management Request: %s[url=%s]; user=%s; payload=%s";
String requestUrl = request.getRequestURI();
if (request.getQueryString() != null) {
Expand All @@ -63,21 +74,22 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
wrappedRequest.getCharacterEncoding());
logger.info(String.format(requestPattern, request.getMethod(), requestUrl,
request.getRemoteUser(), payload));
}

private void logResponse(HttpServletResponse response,
ContentCachingResponseWrapper wrappedResponse) {
// construct the response message
String responsePattern = "Management Response: Status=%s; response=%s";
payload = getContentAsString(wrappedResponse.getContentAsByteArray(),
String payload = getContentAsString(wrappedResponse.getContentAsByteArray(),
wrappedResponse.getCharacterEncoding());
payload = payload.replaceAll(System.lineSeparator(), "");
logger.info(String.format(responsePattern, response.getStatus(), payload));

// IMPORTANT: copy content of response back into original response
wrappedResponse.copyBodyToResponse();
}

private String getContentAsString(byte[] buf, String encoding) {
if (buf == null || buf.length == 0)
if (buf == null || buf.length == 0) {
return "";
}
int length = Math.min(buf.length, MAX_PAYLOAD_LENGTH);
try {
return new String(buf, 0, length, encoding);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import static org.apache.geode.cache.configuration.BasicRegionConfig.REGION_CONFIG_ENDPOINT;
import static org.apache.geode.management.internal.rest.controllers.AbstractManagementController.MANAGEMENT_API_VERSION;

import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
Expand All @@ -34,6 +37,12 @@
@RequestMapping(MANAGEMENT_API_VERSION)
public class RegionManagementController extends AbstractManagementController {

@ApiOperation(value = "create regions")
@ApiResponses({@ApiResponse(code = 200, message = "OK."),
@ApiResponse(code = 401, message = "Invalid Username or Password."),
@ApiResponse(code = 403, message = "Insufficient privileges for operation."),
@ApiResponse(code = 409, message = "Region already exist."),
@ApiResponse(code = 500, message = "GemFire throws an error or exception.")})
@PreAuthorize("@securityService.authorize('DATA', 'MANAGE')")
@RequestMapping(method = RequestMethod.POST, value = REGION_CONFIG_ENDPOINT)
public ResponseEntity<ClusterManagementResult> createRegion(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.management.internal.rest.swagger;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;


@PropertySource({"classpath:swagger-management.properties"})
@Configuration
@EnableSwagger2
@SuppressWarnings("unused")
public class SwaggerConfig {

@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}

/**
* API Info as it appears on the Swagger-UI page
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Apache Geode Management REST API")
.description(
"REST API and interface to manage Geode components.")
.version("1.0")
.termsOfServiceUrl("http://www.apache.org/licenses/")
.license("Apache License, version 2.0")
.licenseUrl("http://www.apache.org/licenses/")
.contact(new Contact("the Apache Geode Community",
"http://geode.apache.org",
"[email protected]"))
.build();
}
}
Loading

0 comments on commit 232dc0f

Please sign in to comment.