Add Actuator ApiVersion support and bump version
Add `ApiVersion` enum that can be injected into actuator endpoints if they need to support more than one API revision. Spring MVC, WebFlux and Jersey integrations now detect the API version based on the HTTP accept header. If the request explicitly accepts a `application/vnd.spring-boot.actuator.v` media type then the version is set from the header. If no explicit Spring Boot media type is accepted then the latest `ApiVersion` is assumed. A new v3 API revision has also been introduced to allow upcoming health endpoint format changes. By default all endpoints now consume and can produce v3, v2 and `application/json` media types. See gh-17929pull/18371/head
parent
d83238aaab
commit
deb9d67cef
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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
|
||||||
|
*
|
||||||
|
* https://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.springframework.boot.actuate.endpoint.http;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API versions supported for the actuator HTTP API. This enum may be injected into
|
||||||
|
* actuator endpoints in order to return a response compatible with the requested version.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 2.2.0
|
||||||
|
*/
|
||||||
|
public enum ApiVersion {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version 2 (supported by Spring Boot 2.0+).
|
||||||
|
*/
|
||||||
|
V2,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version 3 (supported by Spring Boot 2.2+).
|
||||||
|
*/
|
||||||
|
V3;
|
||||||
|
|
||||||
|
private static final String MEDIA_TYPE_PREFIX = "application/vnd.spring-boot.actuator.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The latest API version.
|
||||||
|
*/
|
||||||
|
public static final ApiVersion LATEST = ApiVersion.V3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link ApiVersion} to use based on the HTTP request headers. The version
|
||||||
|
* will be deduced based on the {@code Accept} header.
|
||||||
|
* @param headers the HTTP headers
|
||||||
|
* @return the API version to use
|
||||||
|
*/
|
||||||
|
public static ApiVersion fromHttpHeaders(Map<String, List<String>> headers) {
|
||||||
|
ApiVersion version = null;
|
||||||
|
List<String> accepts = headers.get("Accept");
|
||||||
|
if (!CollectionUtils.isEmpty(accepts)) {
|
||||||
|
for (String accept : accepts) {
|
||||||
|
for (String type : MimeTypeUtils.tokenize(accept)) {
|
||||||
|
version = mostRecent(version, forType(type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (version != null) ? version : LATEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApiVersion forType(String type) {
|
||||||
|
if (type.startsWith(MEDIA_TYPE_PREFIX)) {
|
||||||
|
type = type.substring(MEDIA_TYPE_PREFIX.length());
|
||||||
|
int suffixIndex = type.indexOf("+");
|
||||||
|
type = (suffixIndex != -1) ? type.substring(0, suffixIndex) : type;
|
||||||
|
try {
|
||||||
|
return valueOf(type.toUpperCase());
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApiVersion mostRecent(ApiVersion existing, ApiVersion candidate) {
|
||||||
|
int existingOrdinal = (existing != null) ? existing.ordinal() : -1;
|
||||||
|
int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1;
|
||||||
|
return (candidateOrdinal > existingOrdinal) ? candidate : existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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
|
||||||
|
*
|
||||||
|
* https://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.springframework.boot.actuate.endpoint;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link InvocationContext}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class InvocationContextTests {
|
||||||
|
|
||||||
|
private final SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
|
||||||
|
private final Map<String, Object> arguments = Collections.singletonMap("test", "value");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenApiVersionIsNullUsesLatestVersion() {
|
||||||
|
InvocationContext context = new InvocationContext(null, this.securityContext, this.arguments);
|
||||||
|
assertThat(context.getApiVersion()).isEqualTo(ApiVersion.LATEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenSecurityContextIsNullThrowsException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new InvocationContext(null, this.arguments))
|
||||||
|
.withMessage("SecurityContext must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenArgumentsIsNullThrowsException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new InvocationContext(this.securityContext, null))
|
||||||
|
.withMessage("Arguments must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getApiVersionReturnsApiVersion() {
|
||||||
|
InvocationContext context = new InvocationContext(ApiVersion.V2, this.securityContext, this.arguments);
|
||||||
|
assertThat(context.getApiVersion()).isEqualTo(ApiVersion.V2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSecurityContextReturnsSecurityContext() {
|
||||||
|
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||||
|
assertThat(context.getSecurityContext()).isEqualTo(this.securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getArgumentsReturnsArguments() {
|
||||||
|
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||||
|
assertThat(context.getArguments()).isEqualTo(this.arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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
|
||||||
|
*
|
||||||
|
* https://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.springframework.boot.actuate.endpoint.http;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link ApiVersion}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ApiVersionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void latestIsLatestVersion() {
|
||||||
|
ApiVersion[] values = ApiVersion.values();
|
||||||
|
assertThat(ApiVersion.LATEST).isEqualTo(values[values.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromHttpHeadersWhenEmptyReturnsLatest() {
|
||||||
|
ApiVersion version = ApiVersion.fromHttpHeaders(Collections.emptyMap());
|
||||||
|
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromHttpHeadersWhenHasSingleV2HeaderReturnsV2() {
|
||||||
|
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON));
|
||||||
|
assertThat(version).isEqualTo(ApiVersion.V2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromHttpHeadersWhenHasSingleV3HeaderReturnsV3() {
|
||||||
|
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V3_JSON));
|
||||||
|
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() {
|
||||||
|
ApiVersion version = ApiVersion
|
||||||
|
.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON));
|
||||||
|
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() {
|
||||||
|
ApiVersion version = ApiVersion
|
||||||
|
.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON));
|
||||||
|
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromHttpHeadersWhenHasSingleHeaderWithoutJsonReturnsHeader() {
|
||||||
|
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v2"));
|
||||||
|
assertThat(version).isEqualTo(ApiVersion.V2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromHttpHeadersWhenHasUnknownVersionReturnsLatest() {
|
||||||
|
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v200"));
|
||||||
|
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<String>> acceptHeader(String... types) {
|
||||||
|
List<String> value = Arrays.asList(types);
|
||||||
|
return value.isEmpty() ? Collections.emptyMap() : Collections.singletonMap("Accept", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue