-
Notifications
You must be signed in to change notification settings - Fork 24.9k
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
CORE: Validate Type for String Settings #33503
Conversation
* Special handling for `String` in the generic Setting class because we parse raw values String values for all types: * If we parse out a `String` setting validate that it came from a scalar raw value * Closes elastic#33135
Pinging @elastic/es-core-infra |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of special casing this inside Setting, I think the root problem is Settings.toString
used by Settings.get
serializes whatever the value is to a String no matter the type. The same Map/List logic could be moved to toString to not allow any implicit conversion of object/array to String?
Actually I wonder if toString should only work on Numeric objects? Maybe that or the value already being a String are ok, and anything else should be an error? |
@rjernst the problem is that we simply always expect a string here for the parsers: e.g. all list type settings will use this (and similar) : public static <T> Setting<List<T>> listSetting(String key, Function<Settings, List<String>> defaultStringValue,
Function<String, T> singleValueParser, Property... properties) {
if (defaultStringValue.apply(Settings.EMPTY) == null) {
throw new IllegalArgumentException("default value function must not return null");
}
Function<String, List<T>> parser = (s) ->
parseableStringToList(s).stream().map(singleValueParser).collect(Collectors.toList());
return new ListSetting<>(key, defaultStringValue, parser, properties);
} => If we parse an actual list out of a YAML then we currently have to serialize it to => This is really dirty :( This is the best fix I came up with that doesn't require a big change though. I could think about a bigger but cleaner change if we're willing to live with more noise here? |
... though any change that gives us the type inside of the setting get method and before parsing would literally mean changing 100% of |
Ok, I understand the problem. Though, I think this hack is just leading us further down a rabbit hole of the current problem, which is that we assume all values are strings. We did already change the concrete type for lists to be a List instead of String (see #26878), but the concrete Setting parser functions all take String. Perhaps they should take Object? Then getRaw can return object instead of String, and each setting parser can decide whether to call toString() on the object (or have a helper method since it is the common case, and that helper method can error when the type is not convertible to String like with Map or List). |
Just that would probably solve the issue best I think (with the slightly annoying caveat that we'd maybe have to worry about mutability of that => I think either we change all the parsers (big change but nicer :) ... apart from the mutability thing) or we pass the return type class to |
@jasontedor adjusted PR to reflect what we discussed just now :) Take a look if this is what you were looking for, from my end it's a fine solution to #33135. |
I discussed an approach with @original-brownbear where we identity which settings are list settings; we think this will give us a path forward here and also let us fix upgrading of list settings. We can discuss longer-term fixes in the future, but we need an approach to unblock some in-flight work for now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a couple of comments.
} | ||
} else if (isList) { | ||
throw new IllegalArgumentException( | ||
"Expected list type value for setting [" + setting + "] but found [" + value.getClass() + ']' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that will fail compilation? 😇
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compiles just fine for me? :) What am I missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, never mind, I misread. Sorry for that. 😄
*/ | ||
public String get(String setting, String defaultValue, boolean isList) { | ||
Object value = settings.get(setting); | ||
if(value != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: if(
-> if (
(add space)
@jasontedor Missing space added :) |
With this change and: diff --git a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java
index e25d954aa4f..aee85b6ff57 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java
@@ -54,7 +54,7 @@ public abstract class AbstractScopedSettings extends AbstractComponent {
private final List<SettingUpdater<?>> settingUpdaters = new CopyOnWriteArrayList<>();
private final Map<String, Setting<?>> complexMatchers;
private final Map<String, Setting<?>> keySettings;
- private final Map<Setting<?>, Function<Map.Entry<String, String>, Map.Entry<String, String>>> settingUpgraders;
+ private final Map<Setting<?>, SettingUpgrader<?>> settingUpgraders;
private final Setting.Property scope;
private static final Pattern KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])*[-\\w]+$");
private static final Pattern GROUP_KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])+$");
@@ -70,12 +70,8 @@ public abstract class AbstractScopedSettings extends AbstractComponent {
this.settingUpgraders =
Collections.unmodifiableMap(
- settingUpgraders
- .stream()
- .collect(
- Collectors.toMap(
- SettingUpgrader::getSetting,
- u -> e -> new AbstractMap.SimpleEntry<>(u.getKey(e.getKey()), u.getValue(e.getValue())))));
+ settingUpgraders.stream().collect(Collectors.toMap(SettingUpgrader::getSetting, Function.identity())));
+
this.scope = scope;
Map<String, Setting<?>> complexMatchers = new HashMap<>();
@@ -786,15 +782,24 @@ public abstract class AbstractScopedSettings extends AbstractComponent {
boolean changed = false; // track if any settings were upgraded
for (final String key : settings.keySet()) {
final Setting<?> setting = getRaw(key);
- final Function<Map.Entry<String, String>, Map.Entry<String, String>> upgrader = settingUpgraders.get(setting);
+ final SettingUpgrader<?> upgrader = settingUpgraders.get(setting);
if (upgrader == null) {
// the setting does not have an upgrader, copy the setting
builder.copy(key, settings);
} else {
// the setting has an upgrader, so mark that we have changed a setting and apply the upgrade logic
changed = true;
- final Map.Entry<String, String> upgrade = upgrader.apply(new Entry(key, settings));
- builder.put(upgrade.getKey(), upgrade.getValue());
+ if (setting.isListSetting()) {
+ final List<String> value = settings.getAsList(key);
+ final String upgradedKey = upgrader.getKey(key);
+ final List<String> upgradedValue = upgrader.getListValue(value);
+ builder.putList(upgradedKey, upgradedValue);
+ } else {
+ final String value = settings.get(key);
+ final String upgradedKey = upgrader.getKey(key);
+ final String upgradedValue = upgrader.getValue(value);
+ builder.put(upgradedKey, upgradedValue);
+ }
}
}
// we only return a new instance if there was an upgrade
diff --git a/server/src/main/java/org/elasticsearch/common/settings/SettingUpgrader.java b/server/src/main/java/org/elasticsearch/common/settings/SettingUpgrader.java
index 91f2bead300..bc41b554905 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/SettingUpgrader.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/SettingUpgrader.java
@@ -19,6 +19,8 @@
package org.elasticsearch.common.settings;
+import java.util.List;
+
/**
* Represents the logic to upgrade a setting.
*
@@ -51,4 +53,8 @@ public interface SettingUpgrader<T> {
return value;
}
+ default List<String> getListValue(final List<String> value) {
+ return value;
+ }
+
}
diff --git a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java
index 0ee1d2e9c4a..6766316fafd 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java
@@ -47,6 +47,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -1171,4 +1172,47 @@ public class ScopedSettingsTests extends ESTestCase {
}
}
+ public void testUpgradeListSetting() {
+ final Setting<List<String>> oldSetting =
+ Setting.listSetting("foo.old", Collections.emptyList(), Function.identity(), Property.NodeScope);
+ final Setting<List<String>> newSetting =
+ Setting.listSetting("foo.new", Collections.emptyList(), Function.identity(), Property.NodeScope);
+
+ final AbstractScopedSettings service =
+ new ClusterSettings(
+ Settings.EMPTY,
+ new HashSet<>(Arrays.asList(oldSetting, newSetting)),
+ Collections.singleton(new SettingUpgrader<List<String>>() {
+
+ @Override
+ public Setting<List<String>> getSetting() {
+ return oldSetting;
+ }
+
+ @Override
+ public String getKey(final String key) {
+ return "foo.new";
+ }
+
+ @Override
+ public List<String> getListValue(final List<String> value) {
+ return value.stream().map(s -> "new." + s).collect(Collectors.toList());
+ }
+ }));
+
+ final int length = randomIntBetween(0, 16);
+ final List<String> values = length == 0 ? Collections.emptyList() : new ArrayList<>(length);
+ for (int i = 0; i < length; i++) {
+ values.add(randomAlphaOfLength(8));
+ }
+
+ final Settings settings = Settings.builder().putList("foo.old", values).build();
+ final Settings upgradedSettings = service.upgradeSettings(settings);
+ assertFalse(oldSetting.exists(upgradedSettings));
+ assertTrue(newSetting.exists(upgradedSettings));
+ assertThat(
+ newSetting.get(upgradedSettings),
+ equalTo(oldSetting.get(settings).stream().map(s -> "new." + s).collect(Collectors.toList())));
+ }
+
} I can also get the upgrade settings case to work too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am good with this change, but let us see what @rjernst thinks too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks much better. Two minor suggestions.
* Returns the setting value associated with the setting key. If it does not exists, | ||
* returns the default value provided. | ||
*/ | ||
public String get(String setting, String defaultValue, boolean isList) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this be package private?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea I think so will adjust in a bit :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
@@ -453,7 +458,7 @@ public final String getRaw(final Settings settings) { | |||
* @return the raw string representation of the setting value | |||
*/ | |||
String innerGetRaw(final Settings settings) { | |||
return settings.get(getKey(), defaultValue.apply(settings)); | |||
return settings.get(getKey(), defaultValue.apply(settings), isListSetting()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having an isListSetting, since this is already package private anyways, can this just check instanceof ListSetting
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yea it could :) -> will change to that :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@original-brownbear @rjernst See my diff, we need to expose Setting#isListSetting
in AbstractScopedSettings
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its implementation though can be final
and be an instance of check against ListSetting
.
Adjusted as discussed, will merge once green :) |
Thanks @original-brownbear. |
@original-brownbear I merged this so I can open a PR on top of it. Can you handle backporting this to 6.x? |
When we see a settings value, it could be a list. Yet this should only happen if the underlying setting type is a list setting type. This commit adds validation that when we get a setting value that is a list, that the setting that we are getting is a list setting. And similarly, if we get a value for a list setting, the underlying value should be a list.
@original-brownbear I ended up backporting this in e084a0a. |
* master: Add full cluster restart base class (elastic#33577) Validate list values for settings (elastic#33503) Copy and validatie soft-deletes setting on resize (elastic#33517) Test: Fix package name SQL: Fix result column names for arithmetic functions (elastic#33500) Upgrade to latest Lucene snapshot (elastic#33505) Enable not wiping cluster settings after REST test (elastic#33575) MINOR: Remove Dead Code in SearchScript (elastic#33569) [Test] Remove duplicate method in TestShardRouting (elastic#32815) mute test on windows Update beats template to include apm-server metrics (elastic#33286) Fix typos (elastic#33499) [CCR] Delay auto follow license check (elastic#33557) [CCR] Add create_follow_index privilege (elastic#33559) Strengthen FilterRoutingTests (elastic#33149) Correctly handle PKCS#11 tokens for system keystore (elastic#33460) Remove some duplicate request conversion methods. (elastic#33538)
@jasontedor thanks! |
* master: (91 commits) Preserve cluster settings on full restart tests (elastic#33590) Use IndexWriter.getFlushingBytes() rather than tracking it ourselves (elastic#33582) Fix upgrading of list settings (elastic#33589) Add read-only Engine (elastic#33563) HLRC: Add ML get categories API (elastic#33465) SQL: Adds MONTHNAME, DAYNAME and QUARTER functions (elastic#33411) Add predicate_token_filter (elastic#33431) Fix Replace function. Adds more tests to all string functions. (elastic#33478) [ML] Rename input_fields to column_names in file structure (elastic#33568) Add full cluster restart base class (elastic#33577) Validate list values for settings (elastic#33503) Copy and validatie soft-deletes setting on resize (elastic#33517) Test: Fix package name SQL: Fix result column names for arithmetic functions (elastic#33500) Upgrade to latest Lucene snapshot (elastic#33505) Enable not wiping cluster settings after REST test (elastic#33575) MINOR: Remove Dead Code in SearchScript (elastic#33569) [Test] Remove duplicate method in TestShardRouting (elastic#32815) mute test on windows Update beats template to include apm-server metrics (elastic#33286) ...
* Reverts setting type validation introduced in elastic#33503
* Reverts setting type validation introduced in #33503
* Reverts setting type validation introduced in elastic#33503
String
in the generic Setting class (I think this is justified though a little dirty because we parse raw values String values for all types ... I don't see how else we can do this without changing the APIs either):String
type setting, validate that it came from a scalar raw value