Correct warning header to be compliant

The warning header used by Elasticsearch for delivering deprecation
warnings has a specific format (RFC 7234, section 5.5). The format
specifies that the warning header should be of the form

    warn-code warn-agent warn-text [warn-date]

Here, the warn-code is a three-digit code which communicates various
meanings. The warn-agent is a string used to identify the source of the
warning (either a host:port combination, or some other identifier). The
warn-text is quoted string which conveys the semantic meaning of the
warning. The warn-date is an optional quoted date that can be in a few
different formats.

This commit corrects the warning header within Elasticsearch to follow
this specification. We use the warn-code 299 which means a
"miscellaneous persistent warning." For the warn-agent, we use the
version of Elasticsearch that produced the warning. The warn-text is
unchanged from what we deliver today, but is wrapped in quotes as
specified (this is important as a problem that exists today is that
multiple warnings can not be split by comma to obtain the individual
warnings as the warnings might themselves contain commas). For the
warn-date, we use the RFC 1123 format.

Relates #23275
This commit is contained in:
Jason Tedor 2017-02-27 12:14:21 -05:00 committed by GitHub
parent 2fb0466f66
commit 577e6a5e14
11 changed files with 376 additions and 167 deletions

View File

@ -21,13 +21,21 @@ package org.elasticsearch.common.logging;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.common.SuppressLoggerChecks;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Iterator;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A logger that logs deprecation notices.
@ -36,14 +44,6 @@ public class DeprecationLogger {
private final Logger logger;
/**
* The "Warning" Header comes from RFC-7234. As the RFC describes, it's generally used for caching purposes, but it can be
* used for <em>any</em> warning.
*
* https://tools.ietf.org/html/rfc7234#section-5.5
*/
public static final String WARNING_HEADER = "Warning";
/**
* This is set once by the {@code Node} constructor, but it uses {@link CopyOnWriteArraySet} to ensure that tests can run in parallel.
* <p>
@ -112,6 +112,57 @@ public class DeprecationLogger {
deprecated(THREAD_CONTEXT, msg, params);
}
/*
* RFC7234 specifies the warning format as warn-code <space> warn-agent <space> "warn-text" [<space> "warn-date"]. Here, warn-code is a
* three-digit number with various standard warn codes specified. The warn code 299 is apt for our purposes as it represents a
* miscellaneous persistent warning (can be presented to a human, or logged, and must not be removed by a cache). The warn-agent is an
* arbitrary token; here we use the Elasticsearch version and build hash. The warn text must be quoted. The warn-date is an optional
* quoted field that can be in a variety of specified date formats; here we use RFC 1123 format.
*/
private static final String WARNING_FORMAT =
String.format(
Locale.ROOT,
"299 Elasticsearch-%s%s-%s ",
Version.CURRENT.toString(),
Build.CURRENT.isSnapshot() ? "-SNAPSHOT" : "",
Build.CURRENT.shortHash()) +
"\"%s\" \"%s\"";
private static final ZoneId GMT = ZoneId.of("GMT");
/**
* Regular expression to test if a string matches the RFC7234 specification for warning headers. This pattern assumes that the warn code
* is always 299. Further, this pattern assumes that the warn agent represents a version of Elasticsearch including the build hash.
*/
public static Pattern WARNING_HEADER_PATTERN = Pattern.compile(
"299 " + // warn code
"Elasticsearch-\\d+\\.\\d+\\.\\d+(?:-(?:alpha|beta|rc)\\d+)?(?:-SNAPSHOT)?-(?:[a-f0-9]{7}|Unknown) " + // warn agent
"\"((?:\t| |!|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x80-\\xff]|\\\\|\\\\\")*)\" " + // quoted warning value, captured
// quoted RFC 1123 date format
"\"" + // opening quote
"(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), " + // weekday
"\\d{2} " + // 2-digit day
"(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) " + // month
"\\d{4} " + // 4-digit year
"\\d{2}:\\d{2}:\\d{2} " + // (two-digit hour):(two-digit minute):(two-digit second)
"GMT" + // GMT
"\""); // closing quote
/**
* Extracts the warning value from the value of a warning header that is formatted according to RFC 7234. That is, given a string
* {@code 299 Elasticsearch-6.0.0 "warning value" "Sat, 25 Feb 2017 10:27:43 GMT"}, the return value of this method would be {@code
* warning value}.
*
* @param s the value of a warning header formatted according to RFC 7234.
* @return the extracted warning value
*/
public static String extractWarningValueFromWarningHeader(final String s) {
final Matcher matcher = WARNING_HEADER_PATTERN.matcher(s);
final boolean matches = matcher.matches();
assert matches;
return matcher.group(1);
}
/**
* Logs a deprecated message to the deprecation log, as well as to the local {@link ThreadContext}.
*
@ -120,16 +171,19 @@ public class DeprecationLogger {
* @param params The parameters used to fill in the message, if any exist.
*/
@SuppressLoggerChecks(reason = "safely delegates to logger")
void deprecated(Set<ThreadContext> threadContexts, String message, Object... params) {
Iterator<ThreadContext> iterator = threadContexts.iterator();
void deprecated(final Set<ThreadContext> threadContexts, final String message, final Object... params) {
final Iterator<ThreadContext> iterator = threadContexts.iterator();
if (iterator.hasNext()) {
final String formattedMessage = LoggerMessageFormat.format(message, params);
final String warningHeaderValue = formatWarning(formattedMessage);
assert WARNING_HEADER_PATTERN.matcher(warningHeaderValue).matches();
assert extractWarningValueFromWarningHeader(warningHeaderValue).equals(escape(formattedMessage));
while (iterator.hasNext()) {
try {
iterator.next().addResponseHeader(WARNING_HEADER, formattedMessage);
} catch (IllegalStateException e) {
final ThreadContext next = iterator.next();
next.addResponseHeader("Warning", warningHeaderValue, DeprecationLogger::extractWarningValueFromWarningHeader);
} catch (final IllegalStateException e) {
// ignored; it should be removed shortly
}
}
@ -139,4 +193,25 @@ public class DeprecationLogger {
}
}
/**
* Format a warning string in the proper warning format by prepending a warn code, warn agent, wrapping the warning string in quotes,
* and appending the RFC 1123 date.
*
* @param s the warning string to format
* @return a warning value formatted according to RFC 7234
*/
public static String formatWarning(final String s) {
return String.format(Locale.ROOT, WARNING_FORMAT, escape(s), DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(GMT)));
}
/**
* Escape backslashes and quotes in the specified string.
*
* @param s the string to escape
* @return the escaped string
*/
public static String escape(String s) {
return s.replaceAll("(\\\\|\")", "\\\\$1");
}
}

View File

@ -34,7 +34,9 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -257,12 +259,25 @@ public final class ThreadContext implements Closeable, Writeable {
}
/**
* Add the <em>unique</em> response {@code value} for the specified {@code key}.
* <p>
* Any duplicate {@code value} is ignored.
* Add the {@code value} for the specified {@code key} Any duplicate {@code value} is ignored.
*
* @param key the header name
* @param value the header value
*/
public void addResponseHeader(String key, String value) {
threadLocal.set(threadLocal.get().putResponse(key, value));
public void addResponseHeader(final String key, final String value) {
addResponseHeader(key, value, v -> v);
}
/**
* Add the {@code value} for the specified {@code key} with the specified {@code uniqueValue} used for de-duplication. Any duplicate
* {@code value} after applying {@code uniqueValue} is ignored.
*
* @param key the header name
* @param value the header value
* @param uniqueValue the function that produces de-duplication values
*/
public void addResponseHeader(final String key, final String value, final Function<String, String> uniqueValue) {
threadLocal.set(threadLocal.get().putResponse(key, value, uniqueValue));
}
/**
@ -396,14 +411,16 @@ public final class ThreadContext implements Closeable, Writeable {
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders);
}
private ThreadContextStruct putResponse(String key, String value) {
private ThreadContextStruct putResponse(final String key, final String value, final Function<String, String> uniqueValue) {
assert value != null;
final Map<String, List<String>> newResponseHeaders = new HashMap<>(this.responseHeaders);
final List<String> existingValues = newResponseHeaders.get(key);
if (existingValues != null) {
if (existingValues.contains(value)) {
final Set<String> existingUniqueValues = existingValues.stream().map(uniqueValue).collect(Collectors.toSet());
assert existingValues.size() == existingUniqueValues.size();
if (existingUniqueValues.contains(uniqueValue.apply(value))) {
return this;
}

View File

@ -18,9 +18,11 @@
*/
package org.elasticsearch.common.logging;
import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.hamcrest.RegexMatcher;
import java.io.IOException;
import java.util.Collections;
@ -28,17 +30,22 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.IntStream;
import static org.hamcrest.Matchers.not;
import static org.elasticsearch.common.logging.DeprecationLogger.WARNING_HEADER_PATTERN;
import static org.elasticsearch.test.hamcrest.RegexMatcher.matches;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
/**
* Tests {@link DeprecationLogger}
*/
public class DeprecationLoggerTests extends ESTestCase {
private static final RegexMatcher warningValueMatcher = matches(WARNING_HEADER_PATTERN.pattern());
private final DeprecationLogger logger = new DeprecationLogger(Loggers.getLogger(getClass()));
@Override
@ -48,43 +55,42 @@ public class DeprecationLoggerTests extends ESTestCase {
}
public void testAddsHeaderWithThreadContext() throws IOException {
String msg = "A simple message [{}]";
String param = randomAsciiOfLengthBetween(1, 5);
String formatted = LoggerMessageFormat.format(msg, (Object)param);
try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) {
Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
final Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
logger.deprecated(threadContexts, msg, param);
final String param = randomAsciiOfLengthBetween(1, 5);
logger.deprecated(threadContexts, "A simple message [{}]", param);
Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
assertEquals(1, responseHeaders.size());
assertEquals(formatted, responseHeaders.get(DeprecationLogger.WARNING_HEADER).get(0));
assertThat(responseHeaders.size(), equalTo(1));
final List<String> responses = responseHeaders.get("Warning");
assertThat(responses, hasSize(1));
assertThat(responses.get(0), warningValueMatcher);
assertThat(responses.get(0), containsString("\"A simple message [" + param + "]\""));
}
}
public void testAddsCombinedHeaderWithThreadContext() throws IOException {
String msg = "A simple message [{}]";
String param = randomAsciiOfLengthBetween(1, 5);
String formatted = LoggerMessageFormat.format(msg, (Object)param);
String formatted2 = randomAsciiOfLengthBetween(1, 10);
try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) {
Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
final Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
logger.deprecated(threadContexts, msg, param);
logger.deprecated(threadContexts, formatted2);
final String param = randomAsciiOfLengthBetween(1, 5);
logger.deprecated(threadContexts, "A simple message [{}]", param);
final String second = randomAsciiOfLengthBetween(1, 10);
logger.deprecated(threadContexts, second);
Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
assertEquals(1, responseHeaders.size());
List<String> responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER);
final List<String> responses = responseHeaders.get("Warning");
assertEquals(2, responses.size());
assertEquals(formatted, responses.get(0));
assertEquals(formatted2, responses.get(1));
assertThat(responses.get(0), warningValueMatcher);
assertThat(responses.get(0), containsString("\"A simple message [" + param + "]\""));
assertThat(responses.get(1), warningValueMatcher);
assertThat(responses.get(1), containsString("\"" + second + "\""));
}
}
@ -93,28 +99,30 @@ public class DeprecationLoggerTests extends ESTestCase {
final String unexpected = "testCannotRemoveThreadContext";
try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) {
// NOTE: by adding it to the logger, we allow any concurrent test to write to it (from their own threads)
DeprecationLogger.setThreadContext(threadContext);
logger.deprecated(expected);
Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
List<String> responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER);
{
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
final List<String> responses = responseHeaders.get("Warning");
// ensure it works (note: concurrent tests may be adding to it, but in different threads, so it should have no impact)
assertThat(responses, hasSize(atLeast(1)));
assertThat(responses, hasItem(equalTo(expected)));
assertThat(responses, hasSize(1));
assertThat(responses.get(0), warningValueMatcher);
assertThat(responses.get(0), containsString(expected));
}
DeprecationLogger.removeThreadContext(threadContext);
logger.deprecated(unexpected);
responseHeaders = threadContext.getResponseHeaders();
responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER);
{
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
final List<String> responses = responseHeaders.get("Warning");
assertThat(responses, hasSize(atLeast(1)));
assertThat(responses, hasItem(expected));
assertThat(responses, not(hasItem(unexpected)));
assertThat(responses, hasSize(1));
assertThat(responses.get(0), warningValueMatcher);
assertThat(responses.get(0), containsString(expected));
assertThat(responses.get(0), not(containsString(unexpected)));
}
}
}
@ -158,4 +166,28 @@ public class DeprecationLoggerTests extends ESTestCase {
}
}
public void testWarningValueFromWarningHeader() throws InterruptedException {
final String s = randomAsciiOfLength(16);
final String first = DeprecationLogger.formatWarning(s);
assertThat(DeprecationLogger.extractWarningValueFromWarningHeader(first), equalTo(s));
}
public void testEscape() {
assertThat(DeprecationLogger.escape("\\"), equalTo("\\\\"));
assertThat(DeprecationLogger.escape("\""), equalTo("\\\""));
assertThat(DeprecationLogger.escape("\\\""), equalTo("\\\\\\\""));
assertThat(DeprecationLogger.escape("\"foo\\bar\""),equalTo("\\\"foo\\\\bar\\\""));
// test that characters other than '\' and '"' are left unchanged
String chars = "\t !" + range(0x23, 0x5b + 1) + range(0x5d, 0x73 + 1) + range(0x80, 0xff + 1);
final String s = new CodepointSetGenerator(chars.toCharArray()).ofCodePointsLength(random(), 16, 16);
assertThat(DeprecationLogger.escape(s), equalTo(s));
}
private String range(int lowerInclusive, int upperInclusive) {
return IntStream
.range(lowerInclusive, upperInclusive + 1)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
}

View File

@ -19,6 +19,7 @@
package org.elasticsearch.common.util.concurrent;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ESTestCase;
@ -178,6 +179,14 @@ public class ThreadContextTests extends ESTestCase {
threadContext.addResponseHeader("foo", "bar");
}
final String value = DeprecationLogger.formatWarning("qux");
threadContext.addResponseHeader("baz", value, DeprecationLogger::extractWarningValueFromWarningHeader);
// pretend that another thread created the same response at a different time
if (randomBoolean()) {
final String duplicateValue = DeprecationLogger.formatWarning("qux");
threadContext.addResponseHeader("baz", duplicateValue, DeprecationLogger::extractWarningValueFromWarningHeader);
}
threadContext.addResponseHeader("Warning", "One is the loneliest number");
threadContext.addResponseHeader("Warning", "Two can be as bad as one");
if (expectThird) {
@ -186,11 +195,14 @@ public class ThreadContextTests extends ESTestCase {
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
final List<String> foo = responseHeaders.get("foo");
final List<String> baz = responseHeaders.get("baz");
final List<String> warnings = responseHeaders.get("Warning");
final int expectedWarnings = expectThird ? 3 : 2;
assertThat(foo, hasSize(1));
assertThat(baz, hasSize(1));
assertEquals("bar", foo.get(0));
assertEquals(value, baz.get(0));
assertThat(warnings, hasSize(expectedWarnings));
assertThat(warnings, hasItem(equalTo("One is the loneliest number")));
assertThat(warnings, hasItem(equalTo("Two can be as bad as one")));

View File

@ -102,9 +102,9 @@ public class AzureDiscoveryPlugin extends Plugin implements DiscoveryPlugin {
// setting existed. This check looks for the legacy setting, and sets hosts provider if set
String discoveryType = DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings);
if (discoveryType.equals(AZURE)) {
deprecationLogger.deprecated("Using " + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() +
" setting to set hosts provider is deprecated. " +
"Set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + AZURE + "\" instead");
deprecationLogger.deprecated("using [" + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() +
"] to set hosts provider is deprecated; " +
"set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + AZURE + "\" instead");
if (DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.exists(settings) == false) {
return Settings.builder().put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), AZURE).build();
}

View File

@ -165,9 +165,9 @@ public class Ec2DiscoveryPlugin extends Plugin implements DiscoveryPlugin, Close
// setting existed. This check looks for the legacy setting, and sets hosts provider if set
String discoveryType = DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings);
if (discoveryType.equals(EC2)) {
deprecationLogger.deprecated("Using " + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() +
" setting to set hosts provider is deprecated. " +
"Set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + EC2 + "\" instead");
deprecationLogger.deprecated("using [" + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() +
"] setting to set hosts provider is deprecated; " +
"set [" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + EC2 + "] instead");
if (DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.exists(settings) == false) {
builder.put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), EC2).build();
}

View File

@ -23,7 +23,6 @@ import com.amazonaws.ClientConfiguration;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import org.elasticsearch.common.settings.MockSecureSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.repositories.s3.S3Repository;
@ -65,8 +64,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(AwsS3Service.SECRET_SETTING.getKey(), "aws_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "aws_key", "aws_secret");
assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(AwsS3Service.KEY_SETTING, AwsS3Service.SECRET_SETTING);
}
public void testAWSCredentialsWithElasticsearchS3SettingsBackcompat() {
@ -75,8 +73,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey(), "s3_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "s3_key", "s3_secret");
assertWarnings("[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(AwsS3Service.CLOUD_S3.KEY_SETTING, AwsS3Service.CLOUD_S3.SECRET_SETTING);
}
public void testAWSCredentialsWithElasticsearchAwsAndS3SettingsBackcompat() {
@ -87,10 +84,11 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey(), "s3_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "s3_key", "s3_secret");
assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(
AwsS3Service.KEY_SETTING,
AwsS3Service.SECRET_SETTING,
AwsS3Service.CLOUD_S3.KEY_SETTING,
AwsS3Service.CLOUD_S3.SECRET_SETTING);
}
public void testAWSCredentialsWithElasticsearchRepositoriesSettingsBackcompat() {
@ -99,8 +97,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret");
assertWarnings("[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(S3Repository.Repositories.KEY_SETTING, S3Repository.Repositories.SECRET_SETTING);
}
public void testAWSCredentialsWithElasticsearchAwsAndRepositoriesSettingsBackcompat() {
@ -111,10 +108,11 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret");
assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated",
"[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(
AwsS3Service.KEY_SETTING,
AwsS3Service.SECRET_SETTING,
S3Repository.Repositories.KEY_SETTING,
S3Repository.Repositories.SECRET_SETTING);
}
public void testAWSCredentialsWithElasticsearchAwsAndS3AndRepositoriesSettingsBackcompat() {
@ -127,12 +125,13 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret");
assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated",
"[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(
AwsS3Service.KEY_SETTING,
AwsS3Service.SECRET_SETTING,
AwsS3Service.CLOUD_S3.KEY_SETTING,
AwsS3Service.CLOUD_S3.SECRET_SETTING,
S3Repository.Repositories.KEY_SETTING,
S3Repository.Repositories.SECRET_SETTING);
}
public void testAWSCredentialsWithElasticsearchRepositoriesSettingsAndRepositorySettingsBackcompat() {
@ -142,8 +141,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret");
assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING);
}
public void testAWSCredentialsWithElasticsearchAwsAndRepositoriesSettingsAndRepositorySettingsBackcompat() {
@ -155,8 +153,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret");
assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING);
}
public void testAWSCredentialsWithElasticsearchAwsAndS3AndRepositoriesSettingsAndRepositorySettingsBackcompat() {
@ -170,8 +167,7 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret")
.build();
launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret");
assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING);
}
protected void launchAWSCredentialsWithElasticsearchSettingsTest(Settings singleRepositorySettings, Settings settings,
@ -215,13 +211,14 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.build();
launchAWSConfigurationTest(settings, Settings.EMPTY, Protocol.HTTP, "aws_proxy_host", 8080, "aws_proxy_username",
"aws_proxy_password", "AWS3SignerType", 3, false, 10000);
assertWarnings("[" + AwsS3Service.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.PROTOCOL_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.PROXY_HOST_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.PROXY_PORT_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.SIGNER_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.READ_TIMEOUT.getKey() + "] setting was deprecated");
assertSettingDeprecations(
AwsS3Service.PROXY_USERNAME_SETTING,
AwsS3Service.PROXY_PASSWORD_SETTING,
AwsS3Service.PROTOCOL_SETTING,
AwsS3Service.PROXY_HOST_SETTING,
AwsS3Service.PROXY_PORT_SETTING,
AwsS3Service.SIGNER_SETTING,
AwsS3Service.READ_TIMEOUT);
}
public void testAWSConfigurationWithAwsAndS3SettingsBackcompat() {
@ -243,20 +240,21 @@ public class AwsS3ServiceImplTests extends ESTestCase {
.build();
launchAWSConfigurationTest(settings, Settings.EMPTY, Protocol.HTTPS, "s3_proxy_host", 8081, "s3_proxy_username",
"s3_proxy_password", "NoOpSignerType", 3, false, 10000);
assertWarnings("[" + AwsS3Service.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.PROTOCOL_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.PROXY_HOST_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.PROXY_PORT_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.SIGNER_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.READ_TIMEOUT.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.PROTOCOL_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.PROXY_HOST_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.PROXY_PORT_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.SIGNER_SETTING.getKey() + "] setting was deprecated",
"[" + AwsS3Service.CLOUD_S3.READ_TIMEOUT.getKey() + "] setting was deprecated");
assertSettingDeprecations(
AwsS3Service.PROXY_USERNAME_SETTING,
AwsS3Service.PROXY_PASSWORD_SETTING,
AwsS3Service.PROTOCOL_SETTING,
AwsS3Service.PROXY_HOST_SETTING,
AwsS3Service.PROXY_PORT_SETTING,
AwsS3Service.SIGNER_SETTING,
AwsS3Service.READ_TIMEOUT,
AwsS3Service.CLOUD_S3.PROXY_USERNAME_SETTING,
AwsS3Service.CLOUD_S3.PROXY_PASSWORD_SETTING,
AwsS3Service.CLOUD_S3.PROTOCOL_SETTING,
AwsS3Service.CLOUD_S3.PROXY_HOST_SETTING,
AwsS3Service.CLOUD_S3.PROXY_PORT_SETTING,
AwsS3Service.CLOUD_S3.SIGNER_SETTING,
AwsS3Service.CLOUD_S3.READ_TIMEOUT);
}
public void testGlobalMaxRetries() {
@ -338,13 +336,13 @@ public class AwsS3ServiceImplTests extends ESTestCase {
public void testEndpointSettingBackcompat() {
assertEndpoint(generateRepositorySettings("repository_key", "repository_secret", "repository.endpoint", null),
Settings.EMPTY, "repository.endpoint");
assertWarnings("[" + S3Repository.Repository.ENDPOINT_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(S3Repository.Repository.ENDPOINT_SETTING);
Settings settings = Settings.builder()
.put(S3Repository.Repositories.ENDPOINT_SETTING.getKey(), "repositories.endpoint")
.build();
assertEndpoint(generateRepositorySettings("repository_key", "repository_secret", null, null), settings,
"repositories.endpoint");
assertWarnings("[" + S3Repository.Repositories.ENDPOINT_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(S3Repository.Repositories.ENDPOINT_SETTING);
}
private void assertEndpoint(Settings repositorySettings, Settings settings,

View File

@ -19,8 +19,6 @@
package org.elasticsearch.repositories.s3;
import java.io.IOException;
import com.amazonaws.services.s3.AbstractAmazonS3;
import com.amazonaws.services.s3.AmazonS3;
import org.elasticsearch.cloud.aws.AwsS3Service;
@ -35,6 +33,8 @@ import org.elasticsearch.repositories.RepositoryException;
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matchers;
import java.io.IOException;
import static org.elasticsearch.repositories.s3.S3Repository.Repositories;
import static org.elasticsearch.repositories.s3.S3Repository.Repository;
import static org.elasticsearch.repositories.s3.S3Repository.getValue;
@ -78,8 +78,7 @@ public class S3RepositoryTests extends ESTestCase {
getValue(Settings.EMPTY, globalSettings, Repository.KEY_SETTING, Repositories.KEY_SETTING));
assertEquals(new SecureString("".toCharArray()),
getValue(Settings.EMPTY, Settings.EMPTY, Repository.KEY_SETTING, Repositories.KEY_SETTING));
assertWarnings("[" + Repository.KEY_SETTING.getKey() + "] setting was deprecated",
"[" + Repositories.KEY_SETTING.getKey() + "] setting was deprecated");
assertSettingDeprecations(Repository.KEY_SETTING, Repositories.KEY_SETTING);
}
public void testInvalidChunkBufferSizeSettings() throws IOException {

View File

@ -59,6 +59,7 @@ import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.util.MockBigArrays;
@ -134,10 +135,8 @@ import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.elasticsearch.common.util.CollectionUtils.arrayAsArrayList;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
/**
* Base testcase for randomized unit testing with Elasticsearch
@ -297,21 +296,33 @@ public abstract class ESTestCase extends LuceneTestCase {
//Check that there are no unaccounted warning headers. These should be checked with {@link #assertWarnings(String...)} in the
//appropriate test
try {
final List<String> warnings = threadContext.getResponseHeaders().get(DeprecationLogger.WARNING_HEADER);
final List<String> warnings = threadContext.getResponseHeaders().get("Warning");
assertNull("unexpected warning headers", warnings);
} finally {
resetDeprecationLogger();
}
}
protected final void assertSettingDeprecations(Setting... settings) {
assertWarnings(
Arrays
.stream(settings)
.map(Setting::getKey)
.map(k -> "[" + k + "] setting was deprecated in Elasticsearch and will be removed in a future release! " +
"See the breaking changes documentation for the next major version.")
.toArray(String[]::new));
}
protected final void assertWarnings(String... expectedWarnings) {
if (enableWarningsCheck() == false) {
throw new IllegalStateException("unable to check warning headers if the test is not set to do so");
}
try {
final List<String> actualWarnings = threadContext.getResponseHeaders().get(DeprecationLogger.WARNING_HEADER);
final List<String> actualWarnings = threadContext.getResponseHeaders().get("Warning");
final Set<String> actualWarningValues =
actualWarnings.stream().map(DeprecationLogger::extractWarningValueFromWarningHeader).collect(Collectors.toSet());
for (String msg : expectedWarnings) {
assertThat(actualWarnings, hasItem(containsString(msg)));
assertTrue(actualWarningValues.contains(DeprecationLogger.escape(msg)));
}
assertEquals("Expected " + expectedWarnings.length + " warnings but found " + actualWarnings.size() + "\nExpected: "
+ Arrays.asList(expectedWarnings) + "\nActual: " + actualWarnings,

View File

@ -16,12 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.test.rest.yaml.section;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentLocation;
@ -39,10 +41,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static org.elasticsearch.common.collect.Tuple.tuple;
import static org.elasticsearch.common.logging.DeprecationLogger.WARNING_HEADER_PATTERN;
import static org.elasticsearch.test.hamcrest.RegexMatcher.matches;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
@ -247,33 +252,48 @@ public class DoSection implements ExecutableSection {
/**
* Check that the response contains only the warning headers that we expect.
*/
void checkWarningHeaders(List<String> warningHeaders) {
StringBuilder failureMessage = null;
void checkWarningHeaders(final List<String> warningHeaders) {
final List<String> unexpected = new ArrayList<>();
final List<String> unmatched = new ArrayList<>();
final List<String> missing = new ArrayList<>();
// LinkedHashSet so that missing expected warnings come back in a predictable order which is nice for testing
Set<String> expected = new LinkedHashSet<>(expectedWarningHeaders);
for (String header : warningHeaders) {
if (expected.remove(header)) {
// Was expected, all good.
continue;
}
if (failureMessage == null) {
failureMessage = new StringBuilder("got unexpected warning headers [");
}
failureMessage.append('\n').append(header);
}
if (false == expected.isEmpty()) {
if (failureMessage == null) {
failureMessage = new StringBuilder();
final Set<String> expected =
new LinkedHashSet<>(expectedWarningHeaders.stream().map(DeprecationLogger::escape).collect(Collectors.toList()));
for (final String header : warningHeaders) {
final Matcher matcher = WARNING_HEADER_PATTERN.matcher(header);
final boolean matches = matcher.matches();
if (matches) {
final String message = matcher.group(1);
if (expected.remove(message) == false) {
unexpected.add(header);
}
} else {
failureMessage.append("\n] ");
}
failureMessage.append("didn't get expected warning headers [");
for (String header : expected) {
failureMessage.append('\n').append(header);
unmatched.add(header);
}
}
if (failureMessage != null) {
fail(failureMessage + "\n]");
if (expected.isEmpty() == false) {
for (final String header : expected) {
missing.add(header);
}
}
if (unexpected.isEmpty() == false || unmatched.isEmpty() == false || missing.isEmpty() == false) {
final StringBuilder failureMessage = new StringBuilder();
appendBadHeaders(failureMessage, unexpected, "got unexpected warning header" + (unexpected.size() > 1 ? "s" : ""));
appendBadHeaders(failureMessage, unmatched, "got unmatched warning header" + (unmatched.size() > 1 ? "s" : ""));
appendBadHeaders(failureMessage, missing, "did not get expected warning header" + (missing.size() > 1 ? "s" : ""));
fail(failureMessage.toString());
}
}
private void appendBadHeaders(final StringBuilder sb, final List<String> headers, final String message) {
if (headers.isEmpty() == false) {
sb.append(message).append(" [\n");
for (final String header : headers) {
sb.append("\t").append(header).append("\n");
}
sb.append("]\n");
}
}

View File

@ -19,6 +19,9 @@
package org.elasticsearch.test.rest.yaml.section;
import org.elasticsearch.Version;
import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentLocation;
import org.elasticsearch.common.xcontent.XContentParser;
@ -26,6 +29,7 @@ import org.elasticsearch.common.xcontent.yaml.YamlXContent;
import org.hamcrest.MatcherAssert;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Map;
@ -37,39 +41,80 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class DoSectionTests extends AbstractClientYamlTestFragmentParserTestCase {
public void testWarningHeaders() throws IOException {
DoSection section = new DoSection(new XContentLocation(1, 1));
{
final DoSection section = new DoSection(new XContentLocation(1, 1));
// No warning headers doesn't throw an exception
section.checkWarningHeaders(emptyList());
// No warning headers doesn't throw an exception
section.checkWarningHeaders(emptyList());
}
final String testHeader = DeprecationLogger.formatWarning("test");
final String anotherHeader = DeprecationLogger.formatWarning("another");
final String someMoreHeader = DeprecationLogger.formatWarning("some more");
final String catHeader = DeprecationLogger.formatWarning("cat");
// Any warning headers fail
AssertionError e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(singletonList("test")));
assertEquals("got unexpected warning headers [\ntest\n]", e.getMessage());
e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "another", "some more")));
assertEquals("got unexpected warning headers [\ntest\nanother\nsome more\n]", e.getMessage());
{
final DoSection section = new DoSection(new XContentLocation(1, 1));
final AssertionError one = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(singletonList(testHeader)));
assertEquals("got unexpected warning header [\n\t" + testHeader + "\n]\n", one.getMessage());
final AssertionError multiple =
expectThrows(
AssertionError.class,
() -> section.checkWarningHeaders(Arrays.asList(testHeader, anotherHeader, someMoreHeader)));
assertEquals(
"got unexpected warning headers [\n\t" +
testHeader + "\n\t" +
anotherHeader + "\n\t" +
someMoreHeader + "\n]\n",
multiple.getMessage());
}
// But not when we expect them
section.setExpectedWarningHeaders(singletonList("test"));
section.checkWarningHeaders(singletonList("test"));
section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
section.checkWarningHeaders(Arrays.asList("test", "another", "some more"));
{
final DoSection section = new DoSection(new XContentLocation(1, 1));
section.setExpectedWarningHeaders(singletonList("test"));
section.checkWarningHeaders(singletonList(testHeader));
}
{
final DoSection section = new DoSection(new XContentLocation(1, 1));
section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
section.checkWarningHeaders(Arrays.asList(testHeader, anotherHeader, someMoreHeader));
}
// But if you don't get some that you did expect, that is an error
section.setExpectedWarningHeaders(singletonList("test"));
e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList()));
assertEquals("didn't get expected warning headers [\ntest\n]", e.getMessage());
section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList()));
assertEquals("didn't get expected warning headers [\ntest\nanother\nsome more\n]", e.getMessage());
e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "some more")));
assertEquals("didn't get expected warning headers [\nanother\n]", e.getMessage());
{
final DoSection section = new DoSection(new XContentLocation(1, 1));
section.setExpectedWarningHeaders(singletonList("test"));
final AssertionError e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList()));
assertEquals("did not get expected warning header [\n\ttest\n]\n", e.getMessage());
}
{
final DoSection section = new DoSection(new XContentLocation(1, 1));
section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
final AssertionError multiple = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList()));
assertEquals("did not get expected warning headers [\n\ttest\n\tanother\n\tsome more\n]\n", multiple.getMessage());
final AssertionError one =
expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList(testHeader, someMoreHeader)));
assertEquals("did not get expected warning header [\n\tanother\n]\n", one.getMessage());
}
// It is also an error if you get some warning you want and some you don't want
section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "cat")));
assertEquals("got unexpected warning headers [\ncat\n] didn't get expected warning headers [\nanother\nsome more\n]",
e.getMessage());
{
final DoSection section = new DoSection(new XContentLocation(1, 1));
section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more"));
final AssertionError e =
expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList(testHeader, catHeader)));
assertEquals("got unexpected warning header [\n\t" +
catHeader + "\n]\n" +
"did not get expected warning headers [\n\tanother\n\tsome more\n]\n",
e.getMessage());
}
}
public void testParseDoSectionNoBody() throws Exception {