From 7a5d4463f834e644ddfeb587bf541b531a85333e Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Wed, 23 Apr 2025 15:08:52 +0100 Subject: [PATCH 01/15] New per-project only settings can be defined and used by components --- .../org/elasticsearch/TransportVersions.java | 1 + .../cluster/metadata/Metadata.java | 3 +- .../cluster/metadata/ProjectMetadata.java | 74 ++- .../cluster/service/ClusterService.java | 40 +- .../settings/ProjectScopedSettings.java | 18 + .../common/settings/Setting.java | 7 +- .../common/settings/Settings.java | 1 + .../common/settings/SettingsModule.java | 15 + .../common/settings/SettingsUpdater.java | 52 +- .../elasticsearch/node/NodeConstruction.java | 1 + .../settings/ProjectSettingsUpdaterTests.java | 459 ++++++++++++++++++ 11 files changed, 646 insertions(+), 25 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/settings/ProjectScopedSettings.java create mode 100644 server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 40e233388fdab..0cd412218aff2 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -225,6 +225,7 @@ static TransportVersion def(int id) { public static final TransportVersion COMPRESS_DELAYABLE_WRITEABLE = def(9_059_0_00); public static final TransportVersion SYNONYMS_REFRESH_PARAM = def(9_060_0_00); public static final TransportVersion DOC_FIELDS_AS_LIST = def(9_061_0_00); + public static final TransportVersion PROJECT_METADATA_SETTINGS = def(9_062_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 3155604088fae..bb782fdb4a3f0 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -950,7 +950,8 @@ private MetadataDiff(StreamInput in) throws IOException { RESERVED_DIFF_VALUE_READER ); - singleProject = new ProjectMetadata.ProjectMetadataDiff(indices, templates, projectCustoms, DiffableUtils.emptyDiff()); + singleProject = new ProjectMetadata.ProjectMetadataDiff(indices, templates, projectCustoms, + DiffableUtils.emptyDiff(), Settings.EMPTY_DIFF); multiProject = null; } else { fromNodeBeforeMultiProjectsSupport = false; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java index 81acc8e52b565..5c9645d04eecc 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java @@ -102,6 +102,8 @@ public class ProjectMetadata implements Iterable, Diffable indicesLookup; private final Map mappingsByHash; + private final Settings settings; + private final IndexVersion oldestIndexVersion; @SuppressWarnings("this-escape") @@ -122,6 +124,7 @@ private ProjectMetadata( String[] visibleClosedIndices, SortedMap indicesLookup, Map mappingsByHash, + Settings settings, IndexVersion oldestIndexVersion ) { this.id = id; @@ -140,6 +143,7 @@ private ProjectMetadata( this.visibleClosedIndices = visibleClosedIndices; this.indicesLookup = indicesLookup; this.mappingsByHash = mappingsByHash; + this.settings = settings; this.oldestIndexVersion = oldestIndexVersion; assert assertConsistent(); } @@ -221,8 +225,8 @@ public ProjectMetadata withLifecycleState(Index index, LifecycleExecutionState l visibleClosedIndices, indicesLookup, mappingsByHash, - oldestIndexVersion - ); + settings, + oldestIndexVersion); } public ProjectMetadata withIndexSettingsUpdates(Map updates) { @@ -254,8 +258,8 @@ public ProjectMetadata withIndexSettingsUpdates(Map updates) { visibleClosedIndices, indicesLookup, mappingsByHash, - oldestIndexVersion - ); + settings, + oldestIndexVersion); } /** @@ -288,8 +292,8 @@ public ProjectMetadata withAllocationAndTermUpdatesOnly(Map> aliasesAfterAddingIndex(IndexMetadata index, Map aliases) { @@ -727,6 +731,10 @@ public Map templates() { return templates; } + public Settings settings() { + return settings; + } + /** * Checks whether the provided index is a data stream. */ @@ -1130,6 +1138,7 @@ public static class Builder { private final ImmutableOpenMap.Builder templates; private final ImmutableOpenMap.Builder customs; private final ImmutableOpenMap.Builder reservedStateMetadata; + private Settings settings = Settings.EMPTY; private SortedMap previousIndicesLookup; @@ -1147,6 +1156,7 @@ public static class Builder { this.templates = ImmutableOpenMap.builder(projectMetadata.templates); this.customs = ImmutableOpenMap.builder(projectMetadata.customs); this.reservedStateMetadata = ImmutableOpenMap.builder(projectMetadata.reservedStateMetadata); + this.settings = projectMetadata.settings; this.previousIndicesLookup = projectMetadata.indicesLookup; this.mappingsByHash = new HashMap<>(projectMetadata.mappingsByHash); this.checkForUnusedMappings = false; @@ -1525,6 +1535,15 @@ public Builder removeReservedState(ReservedStateMetadata metadata) { return this; } + public Settings settings() { + return this.settings; + } + + public ProjectMetadata.Builder settings(Settings settings) { + this.settings = settings; + return this; + } + public Builder indexGraveyard(final IndexGraveyard indexGraveyard) { return putCustom(IndexGraveyard.TYPE, indexGraveyard); } @@ -1684,8 +1703,8 @@ public ProjectMetadata build(boolean skipNameCollisionChecks) { visibleClosedIndicesArray, indicesLookup, Collections.unmodifiableMap(mappingsByHash), - IndexVersion.fromId(oldestIndexVersionId) - ); + settings, + IndexVersion.fromId(oldestIndexVersionId)); } static void ensureNoNameCollisions( @@ -2110,6 +2129,9 @@ public static ProjectMetadata fromXContent(XContentParser parser) throws IOExcep projectBuilder.put(IndexTemplateMetadata.Builder.fromXContent(parser, parser.currentName())); } } + case "settings" -> { + projectBuilder.settings(Settings.fromXContent(parser)); + } default -> Metadata.Builder.parseCustomObject( parser, currentFieldName, @@ -2157,6 +2179,9 @@ public Iterator toXContentChunked(ToXContent.Params p) { customs, multiProject ? ChunkedToXContentHelper.object("reserved_state", reservedStateMetadata().values().iterator()) + : Collections.emptyIterator(), + multiProject + ? ChunkedToXContentHelper.object("settings", Iterators.single(settings)) : Collections.emptyIterator() ); } @@ -2189,6 +2214,10 @@ public static ProjectMetadata readFrom(StreamInput in) throws IOException { builder.put(ReservedStateMetadata.readFrom(in)); } + if (in.getTransportVersion().onOrAfter(TransportVersions.PROJECT_METADATA_SETTINGS)) { + builder.settings(Settings.readSettingsFromStream(in)); + } + return builder.build(); } @@ -2229,6 +2258,10 @@ public void writeTo(StreamOutput out) throws IOException { } VersionedNamedWriteable.writeVersionedWriteables(out, filteredCustoms); out.writeCollection(reservedStateMetadata.values()); + + if (out.getTransportVersion().onOrAfter(TransportVersions.PROJECT_METADATA_SETTINGS)) { + settings.writeTo(out); + } } // this needs to be package accessible for bwc serialization in Metadata.java @@ -2248,6 +2281,7 @@ static class ProjectMetadataDiff implements Diff { String, ReservedStateMetadata, ImmutableOpenMap> reservedStateMetadata; + private final Diff settingsDiff; private ProjectMetadataDiff(ProjectMetadata before, ProjectMetadata after) { if (before == after) { @@ -2255,6 +2289,7 @@ private ProjectMetadataDiff(ProjectMetadata before, ProjectMetadata after) { templates = DiffableUtils.emptyDiff(); customs = DiffableUtils.emptyDiff(); reservedStateMetadata = DiffableUtils.emptyDiff(); + settingsDiff = Settings.EMPTY_DIFF; } else { indices = DiffableUtils.diff(before.indices, after.indices, DiffableUtils.getStringKeySerializer()); templates = DiffableUtils.diff(before.templates, after.templates, DiffableUtils.getStringKeySerializer()); @@ -2269,6 +2304,7 @@ private ProjectMetadataDiff(ProjectMetadata before, ProjectMetadata after) { after.reservedStateMetadata, DiffableUtils.getStringKeySerializer() ); + settingsDiff = after.settings.diff(before.settings); } } @@ -2276,12 +2312,14 @@ private ProjectMetadataDiff(ProjectMetadata before, ProjectMetadata after) { DiffableUtils.MapDiff> indices, DiffableUtils.MapDiff> templates, DiffableUtils.MapDiff> customs, - DiffableUtils.MapDiff> reservedStateMetadata + DiffableUtils.MapDiff> reservedStateMetadata, + Diff settingsDiff ) { this.indices = indices; this.templates = templates; this.customs = customs; this.reservedStateMetadata = reservedStateMetadata; + this.settingsDiff = settingsDiff; } ProjectMetadataDiff(StreamInput in) throws IOException { @@ -2293,6 +2331,11 @@ private ProjectMetadataDiff(ProjectMetadata before, ProjectMetadata after) { DiffableUtils.getStringKeySerializer(), RESERVED_DIFF_VALUE_READER ); + if (in.getTransportVersion().onOrAfter(TransportVersions.PROJECT_METADATA_SETTINGS)) { + settingsDiff = Settings.readSettingsDiffFromStream(in); + } else { + settingsDiff = Settings.EMPTY_DIFF; + } } Diff> indices() { @@ -2317,11 +2360,15 @@ public void writeTo(StreamOutput out) throws IOException { templates.writeTo(out); customs.writeTo(out); reservedStateMetadata.writeTo(out); + if (out.getTransportVersion().onOrAfter(TransportVersions.PROJECT_METADATA_SETTINGS)) { + settingsDiff.writeTo(out); + } } @Override public ProjectMetadata apply(ProjectMetadata part) { - if (indices.isEmpty() && templates.isEmpty() && customs.isEmpty() && reservedStateMetadata.isEmpty()) { + if (indices.isEmpty() && templates.isEmpty() && customs.isEmpty() + && reservedStateMetadata.isEmpty() && settingsDiff == Settings.EMPTY_DIFF) { // nothing to do return part; } @@ -2336,13 +2383,14 @@ public ProjectMetadata apply(ProjectMetadata part) { && builder.dataStreamMetadata() == part.custom(DataStreamMetadata.TYPE, DataStreamMetadata.EMPTY)) { builder.previousIndicesLookup = part.indicesLookup; } + builder.settings = settingsDiff.apply(builder.settings); return builder.build(true); } ProjectMetadataDiff withCustoms( DiffableUtils.MapDiff> customs ) { - return new ProjectMetadataDiff(indices, templates, customs, reservedStateMetadata); + return new ProjectMetadataDiff(indices, templates, customs, reservedStateMetadata, settingsDiff); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/service/ClusterService.java b/server/src/main/java/org/elasticsearch/cluster/service/ClusterService.java index 1a9af6a76fc6f..9fa6066b91ceb 100644 --- a/server/src/main/java/org/elasticsearch/cluster/service/ClusterService.java +++ b/server/src/main/java/org/elasticsearch/cluster/service/ClusterService.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.Priority; import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.ProjectScopedSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; @@ -31,6 +32,8 @@ import org.elasticsearch.tasks.TaskManager; import org.elasticsearch.threadpool.ThreadPool; +import java.util.Collections; + public class ClusterService extends AbstractLifecycleComponent { private final MasterService masterService; @@ -52,26 +55,57 @@ public class ClusterService extends AbstractLifecycleComponent { private final ClusterSettings clusterSettings; + private final ProjectScopedSettings projectScopedSettings; + private final String nodeName; - public ClusterService(Settings settings, ClusterSettings clusterSettings, ThreadPool threadPool, TaskManager taskManager) { + public ClusterService(Settings settings, ClusterSettings clusterSettings, + ThreadPool threadPool, TaskManager taskManager) { this( settings, clusterSettings, + new ProjectScopedSettings(settings, Collections.emptySet()), new MasterService(settings, clusterSettings, threadPool, taskManager), new ClusterApplierService(Node.NODE_NAME_SETTING.get(settings), settings, clusterSettings, threadPool) ); } + public ClusterService(Settings settings, ClusterSettings clusterSettings, ProjectScopedSettings projectScopedSettings, + ThreadPool threadPool, TaskManager taskManager) { + this( + settings, + clusterSettings, + projectScopedSettings, new MasterService(settings, clusterSettings, threadPool, taskManager), + new ClusterApplierService(Node.NODE_NAME_SETTING.get(settings), settings, clusterSettings, threadPool) + ); + } + public ClusterService( Settings settings, ClusterSettings clusterSettings, MasterService masterService, ClusterApplierService clusterApplierService + ) { + this( + settings, + clusterSettings, + new ProjectScopedSettings(settings, Collections.emptySet()), + masterService, + clusterApplierService + ); + } + + public ClusterService( + Settings settings, + ClusterSettings clusterSettings, + ProjectScopedSettings projectScopedSettings, + MasterService masterService, + ClusterApplierService clusterApplierService ) { this.settings = settings; this.nodeName = Node.NODE_NAME_SETTING.get(settings); this.masterService = masterService; + this.projectScopedSettings = projectScopedSettings; this.operationRouting = new OperationRouting(settings, clusterSettings); this.clusterSettings = clusterSettings; this.clusterName = ClusterName.CLUSTER_NAME_SETTING.get(settings); @@ -201,6 +235,10 @@ public ClusterSettings getClusterSettings() { return clusterSettings; } + public ProjectScopedSettings getProjectScopedSettings() { + return projectScopedSettings; + } + /** * The node's settings. */ diff --git a/server/src/main/java/org/elasticsearch/common/settings/ProjectScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ProjectScopedSettings.java new file mode 100644 index 0000000000000..d638349a3d345 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/settings/ProjectScopedSettings.java @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.common.settings; + +import java.util.Set; + +public class ProjectScopedSettings extends AbstractScopedSettings { + public ProjectScopedSettings(Settings settings, Set> settingsSet) { + super(settings, settingsSet, Setting.Property.ProjectScope); + } +} diff --git a/server/src/main/java/org/elasticsearch/common/settings/Setting.java b/server/src/main/java/org/elasticsearch/common/settings/Setting.java index 264ea182d0a8d..1ccaeee626fc9 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Setting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Setting.java @@ -179,7 +179,12 @@ public enum Property { * All other settings will be rejected when used on a PUT request * and filtered out on a GET */ - ServerlessPublic + ServerlessPublic, + + /** + * Project-level file-level setting. Not an index setting. + */ + ProjectScope } private final Key key; diff --git a/server/src/main/java/org/elasticsearch/common/settings/Settings.java b/server/src/main/java/org/elasticsearch/common/settings/Settings.java index 4ceee12c3bc5f..c0b5e96ce868d 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Settings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Settings.java @@ -79,6 +79,7 @@ public final class Settings implements ToXContentFragment, Writeable, Diffable { public static final Settings EMPTY = new Settings(Map.of(), null); + public static final Diff EMPTY_DIFF = new SettingsDiff(DiffableUtils.emptyDiff()); public static final String FLAT_SETTINGS_PARAM = "flat_settings"; diff --git a/server/src/main/java/org/elasticsearch/common/settings/SettingsModule.java b/server/src/main/java/org/elasticsearch/common/settings/SettingsModule.java index 2e372a269e449..c9d1a0bc25778 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SettingsModule.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SettingsModule.java @@ -37,10 +37,12 @@ public class SettingsModule implements Module { private final Settings settings; private final Set settingsFilterPattern = new HashSet<>(); private final Map> nodeSettings = new HashMap<>(); + private final Map> projectSettings = new HashMap<>(); private final Map> indexSettings = new HashMap<>(); private final Set> consistentSettings = new HashSet<>(); private final IndexScopedSettings indexScopedSettings; private final ClusterSettings clusterSettings; + private final ProjectScopedSettings projectScopedSettings; private final SettingsFilter settingsFilter; public SettingsModule(Settings settings, Setting... additionalSettings) { @@ -80,6 +82,7 @@ public SettingsModule(Settings settings, List> additionalSettings, Li } this.indexScopedSettings = new IndexScopedSettings(settings, new HashSet<>(this.indexSettings.values())); this.clusterSettings = new ClusterSettings(settings, new HashSet<>(this.nodeSettings.values())); + this.projectScopedSettings = new ProjectScopedSettings(settings, new HashSet<>(this.projectSettings.values())); Settings indexSettings = settings.filter((s) -> s.startsWith("index.") && clusterSettings.get(s) == null); if (indexSettings.isEmpty() == false) { try { @@ -140,6 +143,7 @@ public void configure(Binder binder) { binder.bind(SettingsFilter.class).toInstance(settingsFilter); binder.bind(ClusterSettings.class).toInstance(clusterSettings); binder.bind(IndexScopedSettings.class).toInstance(indexScopedSettings); + binder.bind(ProjectScopedSettings.class).toInstance(projectScopedSettings); } /** @@ -176,8 +180,15 @@ private void registerSetting(Setting setting) { } } nodeSettings.put(setting.getKey(), setting); + + if (setting.getProperties().contains(Setting.Property.ProjectScope)) { + projectSettings.put(setting.getKey(), setting); + } } if (setting.hasIndexScope()) { + if (setting.getProperties().contains(Setting.Property.ProjectScope)) { + throw new IllegalStateException("setting [" + setting.getKey() + "] cannot be both project and index scoped"); + } Setting existingSetting = indexSettings.get(setting.getKey()); if (existingSetting != null) { throw new IllegalArgumentException("Cannot register setting [" + setting.getKey() + "] twice"); @@ -225,6 +236,10 @@ public ClusterSettings getClusterSettings() { return clusterSettings; } + public ProjectScopedSettings getProjectScopedSettings() { + return projectScopedSettings; + } + public Set> getConsistentSettings() { return consistentSettings; } diff --git a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java index 67609da7d6d48..95bafafbc568a 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java @@ -14,6 +14,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.core.Tuple; import java.util.Map; @@ -28,10 +29,10 @@ public final class SettingsUpdater { final Settings.Builder transientUpdates = Settings.builder(); final Settings.Builder persistentUpdates = Settings.builder(); - private final ClusterSettings clusterSettings; + private final AbstractScopedSettings scopedSettings; - public SettingsUpdater(ClusterSettings clusterSettings) { - this.clusterSettings = clusterSettings; + public SettingsUpdater(AbstractScopedSettings scopedSettings) { + this.scopedSettings = scopedSettings; } public synchronized Settings getTransientUpdates() { @@ -70,7 +71,7 @@ public synchronized ClusterState updateSettings( final Settings knownAndValidTransientSettings = partitionedTransientSettings.v1(); final Settings unknownOrInvalidTransientSettings = partitionedTransientSettings.v2(); final Settings.Builder transientSettings = Settings.builder().put(knownAndValidTransientSettings); - changed |= clusterSettings.updateDynamicSettings(transientToApply, transientSettings, transientUpdates, "transient"); + changed |= scopedSettings.updateDynamicSettings(transientToApply, transientSettings, transientUpdates, "transient"); final Tuple partitionedPersistentSettings = partitionKnownAndValidSettings( currentState.metadata().persistentSettings(), @@ -80,7 +81,7 @@ public synchronized ClusterState updateSettings( final Settings knownAndValidPersistentSettings = partitionedPersistentSettings.v1(); final Settings unknownOrInvalidPersistentSettings = partitionedPersistentSettings.v2(); final Settings.Builder persistentSettings = Settings.builder().put(knownAndValidPersistentSettings); - changed |= clusterSettings.updateDynamicSettings(persistentToApply, persistentSettings, persistentUpdates, "persistent"); + changed |= scopedSettings.updateDynamicSettings(persistentToApply, persistentSettings, persistentUpdates, "persistent"); final ClusterState clusterState; if (changed) { @@ -88,8 +89,8 @@ public synchronized ClusterState updateSettings( Settings persistentFinalSettings = persistentSettings.build(); // both transient and persistent settings must be consistent by itself we can't allow dependencies to be // in either of them otherwise a full cluster restart will break the settings validation - clusterSettings.validate(transientFinalSettings, true); - clusterSettings.validate(persistentFinalSettings, true); + scopedSettings.validate(transientFinalSettings, true); + scopedSettings.validate(persistentFinalSettings, true); Metadata.Builder metadata = Metadata.builder(currentState.metadata()) .transientSettings(Settings.builder().put(transientFinalSettings).put(unknownOrInvalidTransientSettings).build()) @@ -120,11 +121,44 @@ public synchronized ClusterState updateSettings( * logging, but will not actually apply them. */ final Settings settings = clusterState.metadata().settings(); - clusterSettings.validateUpdate(settings); + scopedSettings.validateUpdate(settings); return clusterState; } + public synchronized ProjectMetadata updateProjectSettings( + final ProjectMetadata projectMetadata, + final Settings settingsToApply, + final Logger logger + ) { + final Tuple partitionedSettings = partitionKnownAndValidSettings( + projectMetadata.settings(), + "project", + logger + ); + final Settings knownAndValidPersistentSettings = partitionedSettings.v1(); + final Settings unknownOrInvalidSettings = partitionedSettings.v2(); + Settings.Builder builder = Settings.builder().put(knownAndValidPersistentSettings); + + // TODO: apply only dynamic? + boolean changed = scopedSettings.updateSettings(settingsToApply, builder, persistentUpdates, + "project[" + projectMetadata.id() + "]"); + if (changed == false) { + return projectMetadata; + } + + Settings finalSettings = builder.build(); + // validate that settings and their values are correct + scopedSettings.validate(finalSettings, true); + + ProjectMetadata.Builder result = ProjectMetadata.builder(projectMetadata) + .settings(Settings.builder().put(finalSettings).put(unknownOrInvalidSettings).build()); + // validate that SettingsUpdaters can be applied without errors + scopedSettings.validateUpdate(result.settings()); + + return result.build(); + } + /** * Partitions the settings into those that are known and valid versus those that are unknown or invalid. The resulting tuple contains * the known and valid settings in the first component and the unknown or invalid settings in the second component. Note that archived @@ -142,7 +176,7 @@ private Tuple partitionKnownAndValidSettings( ) { final Settings existingArchivedSettings = settings.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX)); final Settings settingsExcludingExistingArchivedSettings = settings.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX) == false); - final Settings settingsWithUnknownOrInvalidArchived = clusterSettings.archiveUnknownOrInvalidSettings( + final Settings settingsWithUnknownOrInvalidArchived = scopedSettings.archiveUnknownOrInvalidSettings( settingsExcludingExistingArchivedSettings, e -> logUnknownSetting(settingsType, e, logger), (e, ex) -> logInvalidSetting(settingsType, e, ex, logger) diff --git a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java index 7b7d2b257c424..d2371dc3cea4f 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -1306,6 +1306,7 @@ private ClusterService createClusterService(SettingsModule settingsModule, Threa ClusterService clusterService = new ClusterService( settingsModule.getSettings(), settingsModule.getClusterSettings(), + settingsModule.getProjectScopedSettings(), threadPool, taskManager ); diff --git a/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java b/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java new file mode 100644 index 0000000000000..6a1eab31a5fe4 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java @@ -0,0 +1,459 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +package org.elasticsearch.common.settings; + +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.ProjectId; +import org.elasticsearch.cluster.metadata.ProjectMetadata; +import org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static org.elasticsearch.common.settings.AbstractScopedSettings.ARCHIVED_SETTINGS_PREFIX; +import static org.hamcrest.Matchers.either; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; + +public class ProjectSettingsUpdaterTests extends ESTestCase { + + private static final Setting SETTING_A = Setting.floatSetting( + "project.setting_a", + 0.55f, + 0.0f, + Property.Dynamic, + Property.NodeScope, + Property.ProjectScope + ); + private static final Setting SETTING_B = Setting.floatSetting( + "project.setting_b", + 0.1f, + 0.0f, + Property.Dynamic, + Property.NodeScope, + Property.ProjectScope + ); + + private static ProjectMetadata projectWithSettings(Settings settings) { + ProjectId projectId = randomUniqueProjectId(); + return ProjectMetadata.builder(projectId) + .settings(settings) + .build(); + } + + public void testUpdateSetting() { + AtomicReference valueA = new AtomicReference<>(); + AtomicReference valueB = new AtomicReference<>(); + ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, Set.of(SETTING_A, SETTING_B)); + projectScopedSettings.addSettingsUpdateConsumer(SETTING_A, valueA::set); + projectScopedSettings.addSettingsUpdateConsumer(SETTING_B, valueB::set); + SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); + ProjectMetadata projectMetadata = projectWithSettings(Settings.builder() + .put(SETTING_A.getKey(), 1.5) + .put(SETTING_B.getKey(), 2.5) + .build()); + ProjectMetadata updatedProjectMetadata = updater.updateProjectSettings( + projectMetadata, + Settings.builder().put(SETTING_A.getKey(), 0.5).build(), + logger + ); + assertNotSame(updatedProjectMetadata, projectMetadata); + assertEquals(SETTING_A.get(updatedProjectMetadata.settings()), 0.4, 0.1); + assertEquals(SETTING_B.get(updatedProjectMetadata.settings()), 2.5, 0.1); + + updatedProjectMetadata = updater.updateProjectSettings( + projectMetadata, + Settings.builder().putNull("project.*").build(), + logger + ); + assertEquals(SETTING_A.get(updatedProjectMetadata.settings()), 0.55, 0.1); + assertEquals(SETTING_B.get(updatedProjectMetadata.settings()), 0.1, 0.1); + + assertNull("updater only does a dryRun", valueA.get()); + assertNull("updater only does a dryRun", valueB.get()); + } + + public void testAllOrNothing() { + AtomicReference valueA = new AtomicReference<>(); + AtomicReference valueB = new AtomicReference<>(); + ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, Set.of(SETTING_A, SETTING_B)); + projectScopedSettings.addSettingsUpdateConsumer(SETTING_A, valueA::set); + projectScopedSettings.addSettingsUpdateConsumer(SETTING_B, valueB::set); + SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); + ProjectMetadata projectMetadata = projectWithSettings(Settings.builder() + .put(SETTING_A.getKey(), 1.5) + .put(SETTING_B.getKey(), 2.5) + .build()); + + try { + updater.updateProjectSettings( + projectMetadata, + Settings.builder() + .put(SETTING_A.getKey(), "not a float") + .put(SETTING_B.getKey(), 1.0f) + .build(), + logger + ); + fail("all or nothing"); + } catch (IllegalArgumentException ex) { + logger.info("", ex); + assertEquals("Failed to parse value [not a float] for setting [project.setting_a]", ex.getMessage()); + } + assertNull("updater only does a dryRun", valueA.get()); + assertNull("updater only does a dryRun", valueB.get()); + } + + public void testDeprecationLogging() { + Setting deprecatedSetting = Setting.simpleString( + "deprecated.setting", + Property.Dynamic, + Property.NodeScope, + Property.ProjectScope, + Property.DeprecatedWarning + ); + final Settings settings = Settings.builder().put("deprecated.setting", "foo").build(); + ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, Set.of(deprecatedSetting, SETTING_A)); + projectScopedSettings.addSettingsUpdateConsumer(deprecatedSetting, s -> {}); + final SettingsUpdater settingsUpdater = new SettingsUpdater(projectScopedSettings); + ProjectMetadata projectMetadata = projectWithSettings(settings); + + final Settings toApplyDebug = Settings.builder().put(SETTING_A.getKey(), 1.0f).build(); + final ProjectMetadata afterDebug = settingsUpdater.updateProjectSettings(projectMetadata, toApplyDebug, logger); + assertSettingDeprecationsAndWarnings(new Setting[] { deprecatedSetting }); + + final Settings toApplyUnset = Settings.builder().putNull(SETTING_A.getKey()).build(); + final ProjectMetadata afterUnset = settingsUpdater.updateProjectSettings(afterDebug, toApplyUnset, logger); + assertSettingDeprecationsAndWarnings(new Setting[] { deprecatedSetting }); + + // we also check that if no settings are changed, deprecation logging still occurs + settingsUpdater.updateProjectSettings(afterUnset, toApplyUnset, logger); + assertSettingDeprecationsAndWarnings(new Setting[] { deprecatedSetting }); + } + + public void testUpdateWithUnknownAndSettings() { + // we will randomly apply some new dynamic persistent and transient settings + final int numberOfDynamicSettings = randomIntBetween(1, 8); + final List> dynamicSettings = new ArrayList<>(numberOfDynamicSettings); + for (int i = 0; i < numberOfDynamicSettings; i++) { + final Setting dynamicSetting = Setting.simpleString("dynamic.setting" + i, + Property.Dynamic, Property.NodeScope, Property.ProjectScope); + dynamicSettings.add(dynamicSetting); + } + + // these are invalid settings that exist as either persistent or transient settings + final int numberOfInvalidSettings = randomIntBetween(0, 7); + final List> invalidSettings = invalidSettings(numberOfInvalidSettings); + + // these are unknown settings that exist as either persistent or transient settings + final int numberOfUnknownSettings = randomIntBetween(0, 7); + final List> unknownSettings = unknownSettings(numberOfUnknownSettings); + + final Settings.Builder existingSettings = Settings.builder(); + + for (final Setting dynamicSetting : dynamicSettings) { + if (randomBoolean()) { + existingSettings.put(dynamicSetting.getKey(), "existing_value"); + } + } + + for (final Setting invalidSetting : invalidSettings) { + existingSettings.put(invalidSetting.getKey(), "value"); + } + + for (final Setting unknownSetting : unknownSettings) { + existingSettings.put(unknownSetting.getKey(), "value"); + } + + // register all the known settings (note that we do not register the unknown settings) + final Set> knownSettings = Stream.concat( + Stream.of(SETTING_A, SETTING_B), + Stream.concat(dynamicSettings.stream(), invalidSettings.stream()) + ).collect(Collectors.toSet()); + final ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, knownSettings); + for (final Setting dynamicSetting : dynamicSettings) { + projectScopedSettings.addSettingsUpdateConsumer(dynamicSetting, s -> {}); + } + final SettingsUpdater settingsUpdater = new SettingsUpdater(projectScopedSettings); + ProjectMetadata projectMetadata = projectWithSettings(existingSettings.build()); + + // prepare the dynamic settings update + final Settings.Builder toApply = Settings.builder(); + for (final Setting dynamicSetting : dynamicSettings) { + if (randomBoolean()) { + toApply.put(dynamicSetting.getKey(), "new_value"); + } + } + + final ProjectMetadata afterUpdate = settingsUpdater.updateProjectSettings( + projectMetadata, + toApply.build(), + logger + ); + + // the invalid settings should be archived and not present in non-archived form + for (final Setting invalidSetting : invalidSettings) { + assertThat( + afterUpdate.settings().keySet(), + hasItem(ARCHIVED_SETTINGS_PREFIX + invalidSetting.getKey()) + ); + assertThat(afterUpdate.settings().keySet(), not(hasItem(invalidSetting.getKey()))); + } + + // the unknown settings should be archived and not present in non-archived form + for (final Setting unknownSetting : unknownSettings) { + assertThat( + afterUpdate.settings().keySet(), + hasItem(ARCHIVED_SETTINGS_PREFIX + unknownSetting.getKey()) + ); + assertThat(afterUpdate.settings().keySet(), not(hasItem(unknownSetting.getKey()))); + } + + // the dynamic settings should be applied + for (final Setting dynamicSetting : dynamicSettings) { + if (toApply.keys().contains(dynamicSetting.getKey())) { + assertThat(afterUpdate.settings().keySet(), hasItem(dynamicSetting.getKey())); + assertThat(afterUpdate.settings().get(dynamicSetting.getKey()), equalTo("new_value")); + } else { + if (existingSettings.keys().contains(dynamicSetting.getKey())) { + assertThat(afterUpdate.settings().keySet(), hasItem(dynamicSetting.getKey())); + assertThat( + afterUpdate.settings().get(dynamicSetting.getKey()), + equalTo("existing_value") + ); + } else { + assertThat(afterUpdate.settings().keySet(), not(hasItem(dynamicSetting.getKey()))); + } + } + } + } + + public void testRemovingArchivedSettingsDoesNotRemoveNonArchivedInvalidOrUnknownSettings() { + // these are settings that are archived in the cluster state as either persistent or transient settings + final int numberOfArchivedSettings = randomIntBetween(1, 8); + final List> archivedSettings = new ArrayList<>(numberOfArchivedSettings); + for (int i = 0; i < numberOfArchivedSettings; i++) { + final Setting archivedSetting = Setting.simpleString("setting", Property.NodeScope, Property.ProjectScope); + archivedSettings.add(archivedSetting); + } + + // these are invalid settings that exist as either persistent or transient settings + final int numberOfInvalidSettings = randomIntBetween(0, 7); + final List> invalidSettings = invalidSettings(numberOfInvalidSettings); + + // these are unknown settings that exist as either persistent or transient settings + final int numberOfUnknownSettings = randomIntBetween(0, 7); + final List> unknownSettings = unknownSettings(numberOfUnknownSettings); + + final Settings.Builder existingSettings = Settings.builder(); + + for (final Setting archivedSetting : archivedSettings) { + existingSettings.put(ARCHIVED_SETTINGS_PREFIX + archivedSetting.getKey(), "value"); + } + + for (final Setting invalidSetting : invalidSettings) { + existingSettings.put(invalidSetting.getKey(), "value"); + } + + for (final Setting unknownSetting : unknownSettings) { + existingSettings.put(unknownSetting.getKey(), "value"); + } + + // register all the known settings (not that we do not register the unknown settings) + final Set> knownSettings = Stream.concat( + Stream.of(SETTING_A, SETTING_B), + Stream.concat(archivedSettings.stream(), invalidSettings.stream()) + ).collect(Collectors.toSet()); + final ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, knownSettings); + final SettingsUpdater settingsUpdater = new SettingsUpdater(projectScopedSettings); + final ProjectMetadata projectMetadata = projectWithSettings(existingSettings.build()); + + final Settings.Builder toApply = Settings.builder().put("archived.*", (String) null); + + final ProjectMetadata afterUpdate = settingsUpdater.updateProjectSettings( + projectMetadata, + toApply.build(), + logger + ); + + // existing archived settings are removed + for (final Setting archivedSetting : archivedSettings) { + assertThat( + afterUpdate.settings().keySet(), + not(hasItem(ARCHIVED_SETTINGS_PREFIX + archivedSetting.getKey())) + ); + } + + // the invalid settings should be archived and not present in non-archived form + for (final Setting invalidSetting : invalidSettings) { + assertThat( + afterUpdate.settings().keySet(), + hasItem(ARCHIVED_SETTINGS_PREFIX + invalidSetting.getKey()) + ); + assertThat(afterUpdate.settings().keySet(), not(hasItem(invalidSetting.getKey()))); + } + + // the unknown settings should be archived and not present in non-archived form + for (final Setting unknownSetting : unknownSettings) { + assertThat( + afterUpdate.settings().keySet(), + hasItem(ARCHIVED_SETTINGS_PREFIX + unknownSetting.getKey()) + ); + assertThat(afterUpdate.settings().keySet(), not(hasItem(unknownSetting.getKey()))); + } + } + + private static List> unknownSettings(int numberOfUnknownSettings) { + final List> unknownSettings = new ArrayList<>(numberOfUnknownSettings); + for (int i = 0; i < numberOfUnknownSettings; i++) { + unknownSettings.add(Setting.simpleString("unknown.setting" + i, Property.NodeScope, Property.ProjectScope)); + } + return unknownSettings; + } + + private static List> invalidSettings(int numberOfInvalidSettings) { + final List> invalidSettings = new ArrayList<>(numberOfInvalidSettings); + for (int i = 0; i < numberOfInvalidSettings; i++) { + invalidSettings.add(randomBoolean() ? invalidInIsolationSetting(i) : invalidWithDependenciesSetting(i)); + } + return invalidSettings; + } + + private static Setting invalidInIsolationSetting(int index) { + return Setting.simpleString("invalid.setting" + index, new Setting.Validator<>() { + + @Override + public void validate(final String value) { + throw new IllegalArgumentException("Invalid in isolation setting"); + } + + }, Property.NodeScope, Property.ProjectScope); + } + + private static Setting invalidWithDependenciesSetting(int index) { + return Setting.simpleString("invalid.setting" + index, new Setting.Validator<>() { + + @Override + public void validate(final String value) {} + + @Override + public void validate(final String value, final Map, Object> settings) { + throw new IllegalArgumentException("Invalid with dependencies setting"); + } + + }, Property.NodeScope, Property.ProjectScope); + } + + private static class FooLowSettingValidator implements Setting.Validator { + + @Override + public void validate(final Integer value) {} + + @Override + public void validate(final Integer low, final Map, Object> settings) { + if (settings.containsKey(SETTING_FOO_HIGH) && low > (int) settings.get(SETTING_FOO_HIGH)) { + throw new IllegalArgumentException("[low]=" + low + " is higher than [high]=" + settings.get(SETTING_FOO_HIGH)); + } + } + + @Override + public Iterator> settings() { + final List> settings = List.of(SETTING_FOO_HIGH); + return settings.iterator(); + } + + } + + private static class FooHighSettingValidator implements Setting.Validator { + + @Override + public void validate(final Integer value) { + + } + + @Override + public void validate(final Integer high, final Map, Object> settings) { + if (settings.containsKey(SETTING_FOO_LOW) && high < (int) settings.get(SETTING_FOO_LOW)) { + throw new IllegalArgumentException("[high]=" + high + " is lower than [low]=" + settings.get(SETTING_FOO_LOW)); + } + } + + @Override + public Iterator> settings() { + final List> settings = List.of(SETTING_FOO_LOW); + return settings.iterator(); + } + + } + + private static final Setting SETTING_FOO_LOW = new Setting<>( + "foo.low", + "10", + Integer::valueOf, + new FooLowSettingValidator(), + Property.Dynamic, + Property.NodeScope, + Property.ProjectScope + ); + private static final Setting SETTING_FOO_HIGH = new Setting<>( + "foo.high", + "100", + Integer::valueOf, + new FooHighSettingValidator(), + Property.Dynamic, + Property.NodeScope, + Property.ProjectScope + ); + + public void testUpdateOfValidationDependentSettings() { + final ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, + new HashSet<>(asList(SETTING_FOO_LOW, SETTING_FOO_HIGH))); + final SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); + ProjectMetadata projectMetadata = projectWithSettings(Settings.EMPTY); + + projectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().put(SETTING_FOO_LOW.getKey(), 20).build(), logger); + assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("20")); + + projectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().put(SETTING_FOO_HIGH.getKey(), 40).build(), logger); + assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("20")); + assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("40")); + + projectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().put(SETTING_FOO_LOW.getKey(), 5).build(), logger); + assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("5")); + assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("40")); + + projectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().put(SETTING_FOO_HIGH.getKey(), 8).build(), logger); + assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("5")); + assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("8")); + + final ProjectMetadata finalProjectMetadata = projectMetadata; + Exception exception = expectThrows( + IllegalArgumentException.class, + () -> updater.updateProjectSettings(finalProjectMetadata, Settings.builder().put(SETTING_FOO_HIGH.getKey(), 2).build(), logger) + ); + + assertThat( + exception.getMessage(), + either(equalTo("[high]=2 is lower than [low]=5")).or(equalTo("[low]=5 is higher than [high]=2")) + ); + } + +} From 6eedbb4201ac1fb65755395434cfe625150ff99b Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Wed, 23 Apr 2025 15:11:54 +0100 Subject: [PATCH 02/15] Update docs/changelog/127252.yaml --- docs/changelog/127252.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/127252.yaml diff --git a/docs/changelog/127252.yaml b/docs/changelog/127252.yaml new file mode 100644 index 0000000000000..c0d48adc8b57b --- /dev/null +++ b/docs/changelog/127252.yaml @@ -0,0 +1,5 @@ +pr: 127252 +summary: New per-project only settings can be defined and used by components +area: Infra/Settings +type: feature +issues: [] From 0c452194f88b101ecf84e5c101caa99a1c0a9bdc Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 23 Apr 2025 14:23:05 +0000 Subject: [PATCH 03/15] [CI] Auto commit changes from spotless --- .../cluster/metadata/Metadata.java | 9 +- .../cluster/metadata/ProjectMetadata.java | 26 ++-- .../cluster/service/ClusterService.java | 23 ++-- .../common/settings/SettingsUpdater.java | 14 +-- .../settings/ProjectSettingsUpdaterTests.java | 113 ++++++++---------- 5 files changed, 88 insertions(+), 97 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index bb782fdb4a3f0..03e1aba2a8bb7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -950,8 +950,13 @@ private MetadataDiff(StreamInput in) throws IOException { RESERVED_DIFF_VALUE_READER ); - singleProject = new ProjectMetadata.ProjectMetadataDiff(indices, templates, projectCustoms, - DiffableUtils.emptyDiff(), Settings.EMPTY_DIFF); + singleProject = new ProjectMetadata.ProjectMetadataDiff( + indices, + templates, + projectCustoms, + DiffableUtils.emptyDiff(), + Settings.EMPTY_DIFF + ); multiProject = null; } else { fromNodeBeforeMultiProjectsSupport = false; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java index 5c9645d04eecc..f0fb2b5a86acd 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java @@ -226,7 +226,8 @@ public ProjectMetadata withLifecycleState(Index index, LifecycleExecutionState l indicesLookup, mappingsByHash, settings, - oldestIndexVersion); + oldestIndexVersion + ); } public ProjectMetadata withIndexSettingsUpdates(Map updates) { @@ -259,7 +260,8 @@ public ProjectMetadata withIndexSettingsUpdates(Map updates) { indicesLookup, mappingsByHash, settings, - oldestIndexVersion); + oldestIndexVersion + ); } /** @@ -293,7 +295,8 @@ public ProjectMetadata withAllocationAndTermUpdatesOnly(Map> aliasesAfterAddingIndex(IndexMetadata index, Map aliases) { @@ -1704,7 +1708,8 @@ public ProjectMetadata build(boolean skipNameCollisionChecks) { indicesLookup, Collections.unmodifiableMap(mappingsByHash), settings, - IndexVersion.fromId(oldestIndexVersionId)); + IndexVersion.fromId(oldestIndexVersionId) + ); } static void ensureNoNameCollisions( @@ -2180,9 +2185,7 @@ public Iterator toXContentChunked(ToXContent.Params p) { multiProject ? ChunkedToXContentHelper.object("reserved_state", reservedStateMetadata().values().iterator()) : Collections.emptyIterator(), - multiProject - ? ChunkedToXContentHelper.object("settings", Iterators.single(settings)) - : Collections.emptyIterator() + multiProject ? ChunkedToXContentHelper.object("settings", Iterators.single(settings)) : Collections.emptyIterator() ); } @@ -2367,8 +2370,11 @@ public void writeTo(StreamOutput out) throws IOException { @Override public ProjectMetadata apply(ProjectMetadata part) { - if (indices.isEmpty() && templates.isEmpty() && customs.isEmpty() - && reservedStateMetadata.isEmpty() && settingsDiff == Settings.EMPTY_DIFF) { + if (indices.isEmpty() + && templates.isEmpty() + && customs.isEmpty() + && reservedStateMetadata.isEmpty() + && settingsDiff == Settings.EMPTY_DIFF) { // nothing to do return part; } diff --git a/server/src/main/java/org/elasticsearch/cluster/service/ClusterService.java b/server/src/main/java/org/elasticsearch/cluster/service/ClusterService.java index 9fa6066b91ceb..5caf340036156 100644 --- a/server/src/main/java/org/elasticsearch/cluster/service/ClusterService.java +++ b/server/src/main/java/org/elasticsearch/cluster/service/ClusterService.java @@ -59,8 +59,7 @@ public class ClusterService extends AbstractLifecycleComponent { private final String nodeName; - public ClusterService(Settings settings, ClusterSettings clusterSettings, - ThreadPool threadPool, TaskManager taskManager) { + public ClusterService(Settings settings, ClusterSettings clusterSettings, ThreadPool threadPool, TaskManager taskManager) { this( settings, clusterSettings, @@ -70,12 +69,18 @@ public ClusterService(Settings settings, ClusterSettings clusterSettings, ); } - public ClusterService(Settings settings, ClusterSettings clusterSettings, ProjectScopedSettings projectScopedSettings, - ThreadPool threadPool, TaskManager taskManager) { + public ClusterService( + Settings settings, + ClusterSettings clusterSettings, + ProjectScopedSettings projectScopedSettings, + ThreadPool threadPool, + TaskManager taskManager + ) { this( settings, clusterSettings, - projectScopedSettings, new MasterService(settings, clusterSettings, threadPool, taskManager), + projectScopedSettings, + new MasterService(settings, clusterSettings, threadPool, taskManager), new ClusterApplierService(Node.NODE_NAME_SETTING.get(settings), settings, clusterSettings, threadPool) ); } @@ -86,13 +91,7 @@ public ClusterService( MasterService masterService, ClusterApplierService clusterApplierService ) { - this( - settings, - clusterSettings, - new ProjectScopedSettings(settings, Collections.emptySet()), - masterService, - clusterApplierService - ); + this(settings, clusterSettings, new ProjectScopedSettings(settings, Collections.emptySet()), masterService, clusterApplierService); } public ClusterService( diff --git a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java index 95bafafbc568a..e069edf65c04b 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java @@ -131,18 +131,18 @@ public synchronized ProjectMetadata updateProjectSettings( final Settings settingsToApply, final Logger logger ) { - final Tuple partitionedSettings = partitionKnownAndValidSettings( - projectMetadata.settings(), - "project", - logger - ); + final Tuple partitionedSettings = partitionKnownAndValidSettings(projectMetadata.settings(), "project", logger); final Settings knownAndValidPersistentSettings = partitionedSettings.v1(); final Settings unknownOrInvalidSettings = partitionedSettings.v2(); Settings.Builder builder = Settings.builder().put(knownAndValidPersistentSettings); // TODO: apply only dynamic? - boolean changed = scopedSettings.updateSettings(settingsToApply, builder, persistentUpdates, - "project[" + projectMetadata.id() + "]"); + boolean changed = scopedSettings.updateSettings( + settingsToApply, + builder, + persistentUpdates, + "project[" + projectMetadata.id() + "]" + ); if (changed == false) { return projectMetadata; } diff --git a/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java b/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java index 6a1eab31a5fe4..8f59e59cd0cd5 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java @@ -8,12 +8,8 @@ */ package org.elasticsearch.common.settings; -import org.elasticsearch.cluster.ClusterName; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.ProjectId; import org.elasticsearch.cluster.metadata.ProjectMetadata; -import org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.test.ESTestCase; @@ -55,9 +51,7 @@ public class ProjectSettingsUpdaterTests extends ESTestCase { private static ProjectMetadata projectWithSettings(Settings settings) { ProjectId projectId = randomUniqueProjectId(); - return ProjectMetadata.builder(projectId) - .settings(settings) - .build(); + return ProjectMetadata.builder(projectId).settings(settings).build(); } public void testUpdateSetting() { @@ -67,10 +61,9 @@ public void testUpdateSetting() { projectScopedSettings.addSettingsUpdateConsumer(SETTING_A, valueA::set); projectScopedSettings.addSettingsUpdateConsumer(SETTING_B, valueB::set); SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); - ProjectMetadata projectMetadata = projectWithSettings(Settings.builder() - .put(SETTING_A.getKey(), 1.5) - .put(SETTING_B.getKey(), 2.5) - .build()); + ProjectMetadata projectMetadata = projectWithSettings( + Settings.builder().put(SETTING_A.getKey(), 1.5).put(SETTING_B.getKey(), 2.5).build() + ); ProjectMetadata updatedProjectMetadata = updater.updateProjectSettings( projectMetadata, Settings.builder().put(SETTING_A.getKey(), 0.5).build(), @@ -80,11 +73,7 @@ public void testUpdateSetting() { assertEquals(SETTING_A.get(updatedProjectMetadata.settings()), 0.4, 0.1); assertEquals(SETTING_B.get(updatedProjectMetadata.settings()), 2.5, 0.1); - updatedProjectMetadata = updater.updateProjectSettings( - projectMetadata, - Settings.builder().putNull("project.*").build(), - logger - ); + updatedProjectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().putNull("project.*").build(), logger); assertEquals(SETTING_A.get(updatedProjectMetadata.settings()), 0.55, 0.1); assertEquals(SETTING_B.get(updatedProjectMetadata.settings()), 0.1, 0.1); @@ -99,18 +88,14 @@ public void testAllOrNothing() { projectScopedSettings.addSettingsUpdateConsumer(SETTING_A, valueA::set); projectScopedSettings.addSettingsUpdateConsumer(SETTING_B, valueB::set); SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); - ProjectMetadata projectMetadata = projectWithSettings(Settings.builder() - .put(SETTING_A.getKey(), 1.5) - .put(SETTING_B.getKey(), 2.5) - .build()); + ProjectMetadata projectMetadata = projectWithSettings( + Settings.builder().put(SETTING_A.getKey(), 1.5).put(SETTING_B.getKey(), 2.5).build() + ); try { updater.updateProjectSettings( projectMetadata, - Settings.builder() - .put(SETTING_A.getKey(), "not a float") - .put(SETTING_B.getKey(), 1.0f) - .build(), + Settings.builder().put(SETTING_A.getKey(), "not a float").put(SETTING_B.getKey(), 1.0f).build(), logger ); fail("all or nothing"); @@ -154,8 +139,12 @@ public void testUpdateWithUnknownAndSettings() { final int numberOfDynamicSettings = randomIntBetween(1, 8); final List> dynamicSettings = new ArrayList<>(numberOfDynamicSettings); for (int i = 0; i < numberOfDynamicSettings; i++) { - final Setting dynamicSetting = Setting.simpleString("dynamic.setting" + i, - Property.Dynamic, Property.NodeScope, Property.ProjectScope); + final Setting dynamicSetting = Setting.simpleString( + "dynamic.setting" + i, + Property.Dynamic, + Property.NodeScope, + Property.ProjectScope + ); dynamicSettings.add(dynamicSetting); } @@ -203,27 +192,17 @@ public void testUpdateWithUnknownAndSettings() { } } - final ProjectMetadata afterUpdate = settingsUpdater.updateProjectSettings( - projectMetadata, - toApply.build(), - logger - ); + final ProjectMetadata afterUpdate = settingsUpdater.updateProjectSettings(projectMetadata, toApply.build(), logger); // the invalid settings should be archived and not present in non-archived form for (final Setting invalidSetting : invalidSettings) { - assertThat( - afterUpdate.settings().keySet(), - hasItem(ARCHIVED_SETTINGS_PREFIX + invalidSetting.getKey()) - ); + assertThat(afterUpdate.settings().keySet(), hasItem(ARCHIVED_SETTINGS_PREFIX + invalidSetting.getKey())); assertThat(afterUpdate.settings().keySet(), not(hasItem(invalidSetting.getKey()))); } // the unknown settings should be archived and not present in non-archived form for (final Setting unknownSetting : unknownSettings) { - assertThat( - afterUpdate.settings().keySet(), - hasItem(ARCHIVED_SETTINGS_PREFIX + unknownSetting.getKey()) - ); + assertThat(afterUpdate.settings().keySet(), hasItem(ARCHIVED_SETTINGS_PREFIX + unknownSetting.getKey())); assertThat(afterUpdate.settings().keySet(), not(hasItem(unknownSetting.getKey()))); } @@ -235,10 +214,7 @@ public void testUpdateWithUnknownAndSettings() { } else { if (existingSettings.keys().contains(dynamicSetting.getKey())) { assertThat(afterUpdate.settings().keySet(), hasItem(dynamicSetting.getKey())); - assertThat( - afterUpdate.settings().get(dynamicSetting.getKey()), - equalTo("existing_value") - ); + assertThat(afterUpdate.settings().get(dynamicSetting.getKey()), equalTo("existing_value")); } else { assertThat(afterUpdate.settings().keySet(), not(hasItem(dynamicSetting.getKey()))); } @@ -288,35 +264,22 @@ public void testRemovingArchivedSettingsDoesNotRemoveNonArchivedInvalidOrUnknown final Settings.Builder toApply = Settings.builder().put("archived.*", (String) null); - final ProjectMetadata afterUpdate = settingsUpdater.updateProjectSettings( - projectMetadata, - toApply.build(), - logger - ); + final ProjectMetadata afterUpdate = settingsUpdater.updateProjectSettings(projectMetadata, toApply.build(), logger); // existing archived settings are removed for (final Setting archivedSetting : archivedSettings) { - assertThat( - afterUpdate.settings().keySet(), - not(hasItem(ARCHIVED_SETTINGS_PREFIX + archivedSetting.getKey())) - ); + assertThat(afterUpdate.settings().keySet(), not(hasItem(ARCHIVED_SETTINGS_PREFIX + archivedSetting.getKey()))); } // the invalid settings should be archived and not present in non-archived form for (final Setting invalidSetting : invalidSettings) { - assertThat( - afterUpdate.settings().keySet(), - hasItem(ARCHIVED_SETTINGS_PREFIX + invalidSetting.getKey()) - ); + assertThat(afterUpdate.settings().keySet(), hasItem(ARCHIVED_SETTINGS_PREFIX + invalidSetting.getKey())); assertThat(afterUpdate.settings().keySet(), not(hasItem(invalidSetting.getKey()))); } // the unknown settings should be archived and not present in non-archived form for (final Setting unknownSetting : unknownSettings) { - assertThat( - afterUpdate.settings().keySet(), - hasItem(ARCHIVED_SETTINGS_PREFIX + unknownSetting.getKey()) - ); + assertThat(afterUpdate.settings().keySet(), hasItem(ARCHIVED_SETTINGS_PREFIX + unknownSetting.getKey())); assertThat(afterUpdate.settings().keySet(), not(hasItem(unknownSetting.getKey()))); } } @@ -424,23 +387,41 @@ public Iterator> settings() { ); public void testUpdateOfValidationDependentSettings() { - final ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, - new HashSet<>(asList(SETTING_FOO_LOW, SETTING_FOO_HIGH))); + final ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings( + Settings.EMPTY, + new HashSet<>(asList(SETTING_FOO_LOW, SETTING_FOO_HIGH)) + ); final SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); ProjectMetadata projectMetadata = projectWithSettings(Settings.EMPTY); - projectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().put(SETTING_FOO_LOW.getKey(), 20).build(), logger); + projectMetadata = updater.updateProjectSettings( + projectMetadata, + Settings.builder().put(SETTING_FOO_LOW.getKey(), 20).build(), + logger + ); assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("20")); - projectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().put(SETTING_FOO_HIGH.getKey(), 40).build(), logger); + projectMetadata = updater.updateProjectSettings( + projectMetadata, + Settings.builder().put(SETTING_FOO_HIGH.getKey(), 40).build(), + logger + ); assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("20")); assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("40")); - projectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().put(SETTING_FOO_LOW.getKey(), 5).build(), logger); + projectMetadata = updater.updateProjectSettings( + projectMetadata, + Settings.builder().put(SETTING_FOO_LOW.getKey(), 5).build(), + logger + ); assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("5")); assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("40")); - projectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().put(SETTING_FOO_HIGH.getKey(), 8).build(), logger); + projectMetadata = updater.updateProjectSettings( + projectMetadata, + Settings.builder().put(SETTING_FOO_HIGH.getKey(), 8).build(), + logger + ); assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("5")); assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("8")); From 20732ae6bfa5fc7442b4b71a684aa709e4e33d05 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Wed, 23 Apr 2025 17:35:34 +0100 Subject: [PATCH 04/15] Fix tests --- .../cluster/metadata/ProjectMetadata.java | 10 +- .../cluster/ClusterStateTests.java | 3 + .../metadata/ProjectMetadataTests.java | 370 ++++++++++++++++-- .../metadata/ToAndFromJsonMetadataTests.java | 1 + 4 files changed, 341 insertions(+), 43 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java index f0fb2b5a86acd..a450788293e21 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java @@ -2182,10 +2182,16 @@ public Iterator toXContentChunked(ToXContent.Params p) { ), indices, customs, + multiProject ? + Iterators.single((builder, params) -> { + builder.startObject("settings"); + settings.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("flat_settings", "true"))); + return builder.endObject(); + }) + : Collections.emptyIterator(), multiProject ? ChunkedToXContentHelper.object("reserved_state", reservedStateMetadata().values().iterator()) - : Collections.emptyIterator(), - multiProject ? ChunkedToXContentHelper.object("settings", Iterators.single(settings)) : Collections.emptyIterator() + : Collections.emptyIterator() ); } diff --git a/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java b/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java index 93e0ac6a9567c..e8a9621dabd82 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java @@ -499,6 +499,7 @@ public void testToXContentWithMultipleProjects() throws IOException { } }, "index-graveyard": { "tombstones": [] }, + "settings": {}, "reserved_state": {} }, { @@ -557,6 +558,7 @@ public void testToXContentWithMultipleProjects() throws IOException { } }, "index-graveyard": { "tombstones": [] }, + "settings": {}, "reserved_state": {} }, { @@ -564,6 +566,7 @@ public void testToXContentWithMultipleProjects() throws IOException { "templates": {}, "indices": {}, "index-graveyard": { "tombstones": [] }, + "settings": {}, "reserved_state": {} } ], diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java index 3533ebfd7399c..b1497d70a3da3 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java @@ -12,6 +12,8 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.ingest.IngestMetadata; @@ -34,52 +36,285 @@ import static org.hamcrest.Matchers.sameInstance; public class ProjectMetadataTests extends ESTestCase { + private static final Setting PROJECT_SETTING = Setting.intSetting( + "project.setting.value", + 0, + Setting.Property.Dynamic, + Setting.Property.NodeScope, + Setting.Property.ProjectScope + ); public void testToXContent() throws IOException { - final ProjectId projectId = randomUniqueProjectId(); - final ProjectMetadata.Builder builder = ProjectMetadata.builder(projectId); - for (int i = 1; i <= 3; i++) { - builder.put( - IndexMetadata.builder(Strings.format("index-%02d", i)) - .settings( - indexSettings(IndexVersion.current(), i, i % 2).put( - IndexMetadata.SETTING_INDEX_UUID, - Strings.format("i%x%04d", (i * 1000 << 16), i) - ) - ) - .putAlias(AliasMetadata.builder(Strings.format("alias.%d", i)).build()) - .build(), - false - ); - } - builder.indexTemplates( - Map.of("template", ComposableIndexTemplate.builder().indexPatterns(List.of("index-*")).priority(10L).build()) - ); + final ProjectMetadata projectMetadata = prepareProjectMetadata(); - final String dataStreamName = "logs-ultron"; - final IndexMetadata backingIndex1 = DataStreamTestHelper.createBackingIndex(dataStreamName, 1, 1725000000000L) - .settings( - indexSettings(IndexVersion.current(), 1, 2).put("index.hidden", true) - .put(IndexMetadata.SETTING_INDEX_UUID, Strings.format("d%x", 0x1000001)) - ) - .build(); - final IndexMetadata backingIndex2 = DataStreamTestHelper.createBackingIndex(dataStreamName, 2, 1725025000000L) - .settings( - indexSettings(IndexVersion.current(), 3, 1).put("index.hidden", true) - .put(IndexMetadata.SETTING_INDEX_UUID, Strings.format("d%x", 0x2000002)) + ToXContent.Params params = EMPTY_PARAMS; + AbstractChunkedSerializingTestCase.assertChunkCount(projectMetadata, p -> expectedChunkCount(params, p)); + + final BytesArray expected = new BytesArray( + Strings.format( + """ + { + "templates": {}, + "indices": { + "index-01": { + "version": 1, + "mapping_version": 1, + "settings_version": 1, + "aliases_version": 1, + "routing_num_shards": 1, + "state": "open", + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "1", + "uuid": "i3e800000001", + "version": { + "created": "%s" + } + } + }, + "mappings": {}, + "aliases": [ + "alias.1" + ], + "primary_terms": { + "0": 0 + }, + "in_sync_allocations": { + "0": [] + }, + "rollover_info": {}, + "mappings_updated_version": %s, + "system": false, + "timestamp_range": { + "shards": [] + }, + "event_ingested_range": { + "shards": [] + } + }, + "index-02": { + "version": 1, + "mapping_version": 1, + "settings_version": 1, + "aliases_version": 1, + "routing_num_shards": 2, + "state": "open", + "settings": { + "index": { + "number_of_shards": "2", + "number_of_replicas": "0", + "uuid": "i7d000000002", + "version": { + "created": "%s" + } + } + }, + "mappings": {}, + "aliases": [ + "alias.2" + ], + "primary_terms": { + "0": 0, + "1": 0 + }, + "in_sync_allocations": { + "1": [], + "0": [] + }, + "rollover_info": {}, + "mappings_updated_version": %s, + "system": false, + "timestamp_range": { + "shards": [] + }, + "event_ingested_range": { + "shards": [] + } + }, + "index-03": { + "version": 1, + "mapping_version": 1, + "settings_version": 1, + "aliases_version": 1, + "routing_num_shards": 3, + "state": "open", + "settings": { + "index": { + "number_of_shards": "3", + "number_of_replicas": "1", + "uuid": "ibb800000003", + "version": { + "created": "%s" + } + } + }, + "mappings": {}, + "aliases": [ + "alias.3" + ], + "primary_terms": { + "0": 0, + "1": 0, + "2": 0 + }, + "in_sync_allocations": { + "2": [], + "1": [], + "0": [] + }, + "rollover_info": {}, + "mappings_updated_version": %s, + "system": false, + "timestamp_range": { + "shards": [] + }, + "event_ingested_range": { + "shards": [] + } + }, + ".ds-logs-ultron-2024.08.30-000001": { + "version": 1, + "mapping_version": 1, + "settings_version": 1, + "aliases_version": 1, + "routing_num_shards": 1, + "state": "open", + "settings": { + "index": { + "hidden": "true", + "number_of_shards": "1", + "number_of_replicas": "2", + "uuid": "d1000001", + "version": { + "created": "%s" + } + } + }, + "mappings": {}, + "aliases": [], + "primary_terms": { + "0": 0 + }, + "in_sync_allocations": { + "0": [] + }, + "rollover_info": {}, + "mappings_updated_version": %s, + "system": false, + "timestamp_range": { + "shards": [] + }, + "event_ingested_range": { + "shards": [] + } + }, + ".ds-logs-ultron-2024.08.30-000002": { + "version": 1, + "mapping_version": 1, + "settings_version": 1, + "aliases_version": 1, + "routing_num_shards": 3, + "state": "open", + "settings": { + "index": { + "hidden": "true", + "number_of_shards": "3", + "number_of_replicas": "1", + "uuid": "d2000002", + "version": { + "created": "%s" + } + } + }, + "mappings": {}, + "aliases": [], + "primary_terms": { + "0": 0, + "1": 0, + "2": 0 + }, + "in_sync_allocations": { + "0": [], + "1": [], + "2": [] + }, + "rollover_info": {}, + "mappings_updated_version": %s, + "system": false, + "timestamp_range": { + "shards": [] + }, + "event_ingested_range": { + "shards": [] + } + } + }, + "index_template": { + "index_template": { + "template": { + "index_patterns": [ + "index-*" + ], + "composed_of": [], + "priority": 10 + } + } + }, + "index-graveyard": { + "tombstones": [] + }, + "data_stream": { + "data_stream": { + "logs-ultron": { + "name": "logs-ultron", + "timestamp_field": { + "name": "@timestamp" + }, + "indices": [ + { + "index_name": ".ds-logs-ultron-2024.08.30-000001", + "index_uuid": "d1000001" + }, + { + "index_name": ".ds-logs-ultron-2024.08.30-000002", + "index_uuid": "d2000002" + } + ], + "generation": 2, + "hidden": false, + "replicated": false, + "system": false, + "allow_custom_routing": false, + "failure_rollover_on_write": false, + "rollover_on_write": false + } + }, + "data_stream_aliases": {} + } + } + """, + IndexVersion.current(), + IndexVersion.current(), + IndexVersion.current(), + IndexVersion.current(), + IndexVersion.current(), + IndexVersion.current(), + IndexVersion.current(), + IndexVersion.current(), + IndexVersion.current(), + IndexVersion.current() ) - .build(); - DataStream dataStream = DataStreamTestHelper.newInstance( - dataStreamName, - List.of(backingIndex1.getIndex(), backingIndex2.getIndex()) ); - builder.put(backingIndex1, false); - builder.put(backingIndex2, false); - builder.put(dataStream); + final BytesReference actual = XContentHelper.toXContent(projectMetadata, XContentType.JSON, randomBoolean()); + assertToXContentEquivalent(expected, actual, XContentType.JSON); + } - final ProjectMetadata projectMetadata = builder.build(); + public void testToXContentMultiProject() throws IOException { + final ProjectMetadata projectMetadata = prepareProjectMetadata(); - AbstractChunkedSerializingTestCase.assertChunkCount(projectMetadata, p -> expectedChunkCount(EMPTY_PARAMS, p)); + ToXContent.Params params = new ToXContent.MapParams(Map.of("multi-project", "true")); + AbstractChunkedSerializingTestCase.assertChunkCount(projectMetadata, params, p -> expectedChunkCount(params, p)); final BytesArray expected = new BytesArray( Strings.format( @@ -322,7 +557,11 @@ public void testToXContent() throws IOException { } }, "data_stream_aliases": {} - } + }, + "settings": { + "project.setting.value": "43" + }, + "reserved_state": {} } """, IndexVersion.current(), @@ -337,10 +576,57 @@ public void testToXContent() throws IOException { IndexVersion.current() ) ); - final BytesReference actual = XContentHelper.toXContent(projectMetadata, XContentType.JSON, randomBoolean()); + final BytesReference actual = XContentHelper.toXContent(projectMetadata, XContentType.JSON, params, randomBoolean()); assertToXContentEquivalent(expected, actual, XContentType.JSON); } + private static ProjectMetadata prepareProjectMetadata() { + final ProjectId projectId = randomUniqueProjectId(); + final ProjectMetadata.Builder builder = ProjectMetadata.builder(projectId); + for (int i = 1; i <= 3; i++) { + builder.put( + IndexMetadata.builder(Strings.format("index-%02d", i)) + .settings( + indexSettings(IndexVersion.current(), i, i % 2).put( + IndexMetadata.SETTING_INDEX_UUID, + Strings.format("i%x%04d", (i * 1000 << 16), i) + ) + ) + .putAlias(AliasMetadata.builder(Strings.format("alias.%d", i)).build()) + .build(), + false + ); + } + builder.indexTemplates( + Map.of("template", ComposableIndexTemplate.builder().indexPatterns(List.of("index-*")).priority(10L).build()) + ); + + final String dataStreamName = "logs-ultron"; + final IndexMetadata backingIndex1 = DataStreamTestHelper.createBackingIndex(dataStreamName, 1, 1725000000000L) + .settings( + indexSettings(IndexVersion.current(), 1, 2).put("index.hidden", true) + .put(IndexMetadata.SETTING_INDEX_UUID, Strings.format("d%x", 0x1000001)) + ) + .build(); + final IndexMetadata backingIndex2 = DataStreamTestHelper.createBackingIndex(dataStreamName, 2, 1725025000000L) + .settings( + indexSettings(IndexVersion.current(), 3, 1).put("index.hidden", true) + .put(IndexMetadata.SETTING_INDEX_UUID, Strings.format("d%x", 0x2000002)) + ) + .build(); + DataStream dataStream = DataStreamTestHelper.newInstance( + dataStreamName, + List.of(backingIndex1.getIndex(), backingIndex2.getIndex()) + ); + builder.put(backingIndex1, false); + builder.put(backingIndex2, false); + builder.put(dataStream); + builder.settings(Settings.builder().put(PROJECT_SETTING.getKey(), 43).build()); + + final ProjectMetadata projectMetadata = builder.build(); + return projectMetadata; + } + static int expectedChunkCount(ToXContent.Params params, ProjectMetadata project) { final var context = Metadata.XContentContext.from(params); @@ -382,6 +668,8 @@ static int expectedChunkCount(ToXContent.Params params, ProjectMetadata project) if (params.paramAsBoolean("multi-project", false)) { // 2 chunks for wrapping reserved state + 1 chunk for each item chunkCount += 2 + project.reservedStateMetadata().size(); + // 1 chunk for settings and 3 chunks for each setting + chunkCount += 1; } return Math.toIntExact(chunkCount); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java index 4f58baf472421..48e1a9b8cb19f 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java @@ -281,6 +281,7 @@ public void testToXContentGateway_MultiProject() throws IOException { "index-graveyard" : { "tombstones" : [ ] }, + "settings" : { }, "reserved_state" : { } } ], From 671ae1648f7a790e05c6bf2d95f84c65477b774b Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Wed, 23 Apr 2025 17:37:59 +0100 Subject: [PATCH 05/15] Remove comments --- .../java/org/elasticsearch/common/settings/SettingsUpdater.java | 1 - .../elasticsearch/cluster/metadata/ProjectMetadataTests.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java index e069edf65c04b..04beb2b482663 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java @@ -136,7 +136,6 @@ public synchronized ProjectMetadata updateProjectSettings( final Settings unknownOrInvalidSettings = partitionedSettings.v2(); Settings.Builder builder = Settings.builder().put(knownAndValidPersistentSettings); - // TODO: apply only dynamic? boolean changed = scopedSettings.updateSettings( settingsToApply, builder, diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java index b1497d70a3da3..51a75348fe08a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java @@ -668,7 +668,7 @@ static int expectedChunkCount(ToXContent.Params params, ProjectMetadata project) if (params.paramAsBoolean("multi-project", false)) { // 2 chunks for wrapping reserved state + 1 chunk for each item chunkCount += 2 + project.reservedStateMetadata().size(); - // 1 chunk for settings and 3 chunks for each setting + // 1 chunk for settings chunkCount += 1; } From efebd320ba9e4d8740ec57041bb0c304efad623b Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 23 Apr 2025 16:54:04 +0000 Subject: [PATCH 06/15] [CI] Auto commit changes from spotless --- .../cluster/metadata/ProjectMetadata.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java index a450788293e21..24bed44944eb4 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java @@ -2182,13 +2182,11 @@ public Iterator toXContentChunked(ToXContent.Params p) { ), indices, customs, - multiProject ? - Iterators.single((builder, params) -> { - builder.startObject("settings"); - settings.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("flat_settings", "true"))); - return builder.endObject(); - }) - : Collections.emptyIterator(), + multiProject ? Iterators.single((builder, params) -> { + builder.startObject("settings"); + settings.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("flat_settings", "true"))); + return builder.endObject(); + }) : Collections.emptyIterator(), multiProject ? ChunkedToXContentHelper.object("reserved_state", reservedStateMetadata().values().iterator()) : Collections.emptyIterator() From 1d42716a1d42e6fe0182e32cfc282a4015e998ba Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Wed, 23 Apr 2025 19:49:01 +0100 Subject: [PATCH 07/15] Update docs/changelog/127280.yaml --- docs/changelog/127280.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/127280.yaml diff --git a/docs/changelog/127280.yaml b/docs/changelog/127280.yaml new file mode 100644 index 0000000000000..c353b43591bfb --- /dev/null +++ b/docs/changelog/127280.yaml @@ -0,0 +1,5 @@ +pr: 127280 +summary: New per-project only settings can be defined and used by components +area: Infra/Settings +type: feature +issues: [] From 0d60756e6f838013773e69f2a99161a0c033edcf Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Wed, 23 Apr 2025 19:50:53 +0100 Subject: [PATCH 08/15] Delete old changelog file --- docs/changelog/127252.yaml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 docs/changelog/127252.yaml diff --git a/docs/changelog/127252.yaml b/docs/changelog/127252.yaml deleted file mode 100644 index c0d48adc8b57b..0000000000000 --- a/docs/changelog/127252.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 127252 -summary: New per-project only settings can be defined and used by components -area: Infra/Settings -type: feature -issues: [] From 94b9fcd4127933811427fa0d05b72b167733c49d Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Wed, 23 Apr 2025 20:07:26 +0100 Subject: [PATCH 09/15] Address review comments --- .../org/elasticsearch/cluster/metadata/ProjectMetadata.java | 6 +----- .../org/elasticsearch/common/settings/SettingsUpdater.java | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java index 24bed44944eb4..9087f2d2ff878 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java @@ -1539,11 +1539,7 @@ public Builder removeReservedState(ReservedStateMetadata metadata) { return this; } - public Settings settings() { - return this.settings; - } - - public ProjectMetadata.Builder settings(Settings settings) { + public Builder settings(Settings settings) { this.settings = settings; return this; } diff --git a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java index 04beb2b482663..591155cddfc46 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java @@ -150,10 +150,10 @@ public synchronized ProjectMetadata updateProjectSettings( // validate that settings and their values are correct scopedSettings.validate(finalSettings, true); - ProjectMetadata.Builder result = ProjectMetadata.builder(projectMetadata) - .settings(Settings.builder().put(finalSettings).put(unknownOrInvalidSettings).build()); + Settings resultSettings = Settings.builder().put(finalSettings).put(unknownOrInvalidSettings).build(); + ProjectMetadata.Builder result = ProjectMetadata.builder(projectMetadata).settings(resultSettings); // validate that SettingsUpdaters can be applied without errors - scopedSettings.validateUpdate(result.settings()); + scopedSettings.validateUpdate(resultSettings); return result.build(); } From a8a31c36ebc7e1a103a66cede9f58e74d4b6e0a4 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 24 Apr 2025 16:04:30 +0100 Subject: [PATCH 10/15] Move ProjectSettingsUpdater to serverless --- .../common/settings/BaseSettingsUpdater.java | 79 ++++ .../common/settings/SettingsUpdater.java | 98 +--- .../settings/ProjectSettingsUpdaterTests.java | 440 ------------------ 3 files changed, 82 insertions(+), 535 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java delete mode 100644 server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java diff --git a/server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java b/server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java new file mode 100644 index 0000000000000..9d1a83ea49440 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.common.settings; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.util.Supplier; +import org.elasticsearch.core.Tuple; + +import java.util.Map; + +import static org.elasticsearch.common.settings.AbstractScopedSettings.ARCHIVED_SETTINGS_PREFIX; + +public class BaseSettingsUpdater { + protected final AbstractScopedSettings scopedSettings; + + public BaseSettingsUpdater(AbstractScopedSettings scopedSettings) { + this.scopedSettings = scopedSettings; + } + + private static void logUnknownSetting(final String settingType, final Map.Entry e, final Logger logger) { + logger.warn("ignoring existing unknown {} setting: [{}] with value [{}]; archiving", settingType, e.getKey(), e.getValue()); + } + + private static void logInvalidSetting( + final String settingType, + final Map.Entry e, + final IllegalArgumentException ex, + final Logger logger + ) { + logger.warn( + (Supplier) () -> "ignoring existing invalid " + + settingType + + " setting: [" + + e.getKey() + + "] with value [" + + e.getValue() + + "]; archiving", + ex + ); + } + + /** + * Partitions the settings into those that are known and valid versus those that are unknown or invalid. The resulting tuple contains + * the known and valid settings in the first component and the unknown or invalid settings in the second component. Note that archived + * settings contained in the settings to partition are included in the first component. + * + * @param settings the settings to partition + * @param settingsType a string to identify the settings (for logging) + * @param logger a logger to sending warnings to + * @return the partitioned settings + */ + protected final Tuple partitionKnownAndValidSettings( + final Settings settings, + final String settingsType, + final Logger logger + ) { + final Settings existingArchivedSettings = settings.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX)); + final Settings settingsExcludingExistingArchivedSettings = settings.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX) == false); + final Settings settingsWithUnknownOrInvalidArchived = scopedSettings.archiveUnknownOrInvalidSettings( + settingsExcludingExistingArchivedSettings, + e -> BaseSettingsUpdater.logUnknownSetting(settingsType, e, logger), + (e, ex) -> BaseSettingsUpdater.logInvalidSetting(settingsType, e, ex, logger) + ); + return Tuple.tuple( + Settings.builder() + .put(settingsWithUnknownOrInvalidArchived.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX) == false)) + .put(existingArchivedSettings) + .build(), + settingsWithUnknownOrInvalidArchived.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX)) + ); + } +} diff --git a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java index 591155cddfc46..07d19c4ddf372 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SettingsUpdater.java @@ -10,29 +10,23 @@ package org.elasticsearch.common.settings; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.metadata.Metadata; -import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.core.Tuple; -import java.util.Map; - import static org.elasticsearch.cluster.ClusterState.builder; -import static org.elasticsearch.common.settings.AbstractScopedSettings.ARCHIVED_SETTINGS_PREFIX; /** * Updates transient and persistent cluster state settings if there are any changes * due to the update. */ -public final class SettingsUpdater { +public final class SettingsUpdater extends BaseSettingsUpdater { final Settings.Builder transientUpdates = Settings.builder(); final Settings.Builder persistentUpdates = Settings.builder(); - private final AbstractScopedSettings scopedSettings; - public SettingsUpdater(AbstractScopedSettings scopedSettings) { - this.scopedSettings = scopedSettings; + public SettingsUpdater(ClusterSettings scopedSettings) { + super(scopedSettings); } public synchronized Settings getTransientUpdates() { @@ -125,90 +119,4 @@ public synchronized ClusterState updateSettings( return clusterState; } - - public synchronized ProjectMetadata updateProjectSettings( - final ProjectMetadata projectMetadata, - final Settings settingsToApply, - final Logger logger - ) { - final Tuple partitionedSettings = partitionKnownAndValidSettings(projectMetadata.settings(), "project", logger); - final Settings knownAndValidPersistentSettings = partitionedSettings.v1(); - final Settings unknownOrInvalidSettings = partitionedSettings.v2(); - Settings.Builder builder = Settings.builder().put(knownAndValidPersistentSettings); - - boolean changed = scopedSettings.updateSettings( - settingsToApply, - builder, - persistentUpdates, - "project[" + projectMetadata.id() + "]" - ); - if (changed == false) { - return projectMetadata; - } - - Settings finalSettings = builder.build(); - // validate that settings and their values are correct - scopedSettings.validate(finalSettings, true); - - Settings resultSettings = Settings.builder().put(finalSettings).put(unknownOrInvalidSettings).build(); - ProjectMetadata.Builder result = ProjectMetadata.builder(projectMetadata).settings(resultSettings); - // validate that SettingsUpdaters can be applied without errors - scopedSettings.validateUpdate(resultSettings); - - return result.build(); - } - - /** - * Partitions the settings into those that are known and valid versus those that are unknown or invalid. The resulting tuple contains - * the known and valid settings in the first component and the unknown or invalid settings in the second component. Note that archived - * settings contained in the settings to partition are included in the first component. - * - * @param settings the settings to partition - * @param settingsType a string to identify the settings (for logging) - * @param logger a logger to sending warnings to - * @return the partitioned settings - */ - private Tuple partitionKnownAndValidSettings( - final Settings settings, - final String settingsType, - final Logger logger - ) { - final Settings existingArchivedSettings = settings.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX)); - final Settings settingsExcludingExistingArchivedSettings = settings.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX) == false); - final Settings settingsWithUnknownOrInvalidArchived = scopedSettings.archiveUnknownOrInvalidSettings( - settingsExcludingExistingArchivedSettings, - e -> logUnknownSetting(settingsType, e, logger), - (e, ex) -> logInvalidSetting(settingsType, e, ex, logger) - ); - return Tuple.tuple( - Settings.builder() - .put(settingsWithUnknownOrInvalidArchived.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX) == false)) - .put(existingArchivedSettings) - .build(), - settingsWithUnknownOrInvalidArchived.filter(k -> k.startsWith(ARCHIVED_SETTINGS_PREFIX)) - ); - } - - private static void logUnknownSetting(final String settingType, final Map.Entry e, final Logger logger) { - logger.warn("ignoring existing unknown {} setting: [{}] with value [{}]; archiving", settingType, e.getKey(), e.getValue()); - } - - private static void logInvalidSetting( - final String settingType, - final Map.Entry e, - final IllegalArgumentException ex, - final Logger logger - ) { - logger.warn( - (Supplier) () -> "ignoring existing invalid " - + settingType - + " setting: [" - + e.getKey() - + "] with value [" - + e.getValue() - + "]; archiving", - ex - ); - } - } diff --git a/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java b/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java deleted file mode 100644 index 8f59e59cd0cd5..0000000000000 --- a/server/src/test/java/org/elasticsearch/common/settings/ProjectSettingsUpdaterTests.java +++ /dev/null @@ -1,440 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ -package org.elasticsearch.common.settings; - -import org.elasticsearch.cluster.metadata.ProjectId; -import org.elasticsearch.cluster.metadata.ProjectMetadata; -import org.elasticsearch.common.settings.Setting.Property; -import org.elasticsearch.test.ESTestCase; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.Arrays.asList; -import static org.elasticsearch.common.settings.AbstractScopedSettings.ARCHIVED_SETTINGS_PREFIX; -import static org.hamcrest.Matchers.either; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.not; - -public class ProjectSettingsUpdaterTests extends ESTestCase { - - private static final Setting SETTING_A = Setting.floatSetting( - "project.setting_a", - 0.55f, - 0.0f, - Property.Dynamic, - Property.NodeScope, - Property.ProjectScope - ); - private static final Setting SETTING_B = Setting.floatSetting( - "project.setting_b", - 0.1f, - 0.0f, - Property.Dynamic, - Property.NodeScope, - Property.ProjectScope - ); - - private static ProjectMetadata projectWithSettings(Settings settings) { - ProjectId projectId = randomUniqueProjectId(); - return ProjectMetadata.builder(projectId).settings(settings).build(); - } - - public void testUpdateSetting() { - AtomicReference valueA = new AtomicReference<>(); - AtomicReference valueB = new AtomicReference<>(); - ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, Set.of(SETTING_A, SETTING_B)); - projectScopedSettings.addSettingsUpdateConsumer(SETTING_A, valueA::set); - projectScopedSettings.addSettingsUpdateConsumer(SETTING_B, valueB::set); - SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); - ProjectMetadata projectMetadata = projectWithSettings( - Settings.builder().put(SETTING_A.getKey(), 1.5).put(SETTING_B.getKey(), 2.5).build() - ); - ProjectMetadata updatedProjectMetadata = updater.updateProjectSettings( - projectMetadata, - Settings.builder().put(SETTING_A.getKey(), 0.5).build(), - logger - ); - assertNotSame(updatedProjectMetadata, projectMetadata); - assertEquals(SETTING_A.get(updatedProjectMetadata.settings()), 0.4, 0.1); - assertEquals(SETTING_B.get(updatedProjectMetadata.settings()), 2.5, 0.1); - - updatedProjectMetadata = updater.updateProjectSettings(projectMetadata, Settings.builder().putNull("project.*").build(), logger); - assertEquals(SETTING_A.get(updatedProjectMetadata.settings()), 0.55, 0.1); - assertEquals(SETTING_B.get(updatedProjectMetadata.settings()), 0.1, 0.1); - - assertNull("updater only does a dryRun", valueA.get()); - assertNull("updater only does a dryRun", valueB.get()); - } - - public void testAllOrNothing() { - AtomicReference valueA = new AtomicReference<>(); - AtomicReference valueB = new AtomicReference<>(); - ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, Set.of(SETTING_A, SETTING_B)); - projectScopedSettings.addSettingsUpdateConsumer(SETTING_A, valueA::set); - projectScopedSettings.addSettingsUpdateConsumer(SETTING_B, valueB::set); - SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); - ProjectMetadata projectMetadata = projectWithSettings( - Settings.builder().put(SETTING_A.getKey(), 1.5).put(SETTING_B.getKey(), 2.5).build() - ); - - try { - updater.updateProjectSettings( - projectMetadata, - Settings.builder().put(SETTING_A.getKey(), "not a float").put(SETTING_B.getKey(), 1.0f).build(), - logger - ); - fail("all or nothing"); - } catch (IllegalArgumentException ex) { - logger.info("", ex); - assertEquals("Failed to parse value [not a float] for setting [project.setting_a]", ex.getMessage()); - } - assertNull("updater only does a dryRun", valueA.get()); - assertNull("updater only does a dryRun", valueB.get()); - } - - public void testDeprecationLogging() { - Setting deprecatedSetting = Setting.simpleString( - "deprecated.setting", - Property.Dynamic, - Property.NodeScope, - Property.ProjectScope, - Property.DeprecatedWarning - ); - final Settings settings = Settings.builder().put("deprecated.setting", "foo").build(); - ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, Set.of(deprecatedSetting, SETTING_A)); - projectScopedSettings.addSettingsUpdateConsumer(deprecatedSetting, s -> {}); - final SettingsUpdater settingsUpdater = new SettingsUpdater(projectScopedSettings); - ProjectMetadata projectMetadata = projectWithSettings(settings); - - final Settings toApplyDebug = Settings.builder().put(SETTING_A.getKey(), 1.0f).build(); - final ProjectMetadata afterDebug = settingsUpdater.updateProjectSettings(projectMetadata, toApplyDebug, logger); - assertSettingDeprecationsAndWarnings(new Setting[] { deprecatedSetting }); - - final Settings toApplyUnset = Settings.builder().putNull(SETTING_A.getKey()).build(); - final ProjectMetadata afterUnset = settingsUpdater.updateProjectSettings(afterDebug, toApplyUnset, logger); - assertSettingDeprecationsAndWarnings(new Setting[] { deprecatedSetting }); - - // we also check that if no settings are changed, deprecation logging still occurs - settingsUpdater.updateProjectSettings(afterUnset, toApplyUnset, logger); - assertSettingDeprecationsAndWarnings(new Setting[] { deprecatedSetting }); - } - - public void testUpdateWithUnknownAndSettings() { - // we will randomly apply some new dynamic persistent and transient settings - final int numberOfDynamicSettings = randomIntBetween(1, 8); - final List> dynamicSettings = new ArrayList<>(numberOfDynamicSettings); - for (int i = 0; i < numberOfDynamicSettings; i++) { - final Setting dynamicSetting = Setting.simpleString( - "dynamic.setting" + i, - Property.Dynamic, - Property.NodeScope, - Property.ProjectScope - ); - dynamicSettings.add(dynamicSetting); - } - - // these are invalid settings that exist as either persistent or transient settings - final int numberOfInvalidSettings = randomIntBetween(0, 7); - final List> invalidSettings = invalidSettings(numberOfInvalidSettings); - - // these are unknown settings that exist as either persistent or transient settings - final int numberOfUnknownSettings = randomIntBetween(0, 7); - final List> unknownSettings = unknownSettings(numberOfUnknownSettings); - - final Settings.Builder existingSettings = Settings.builder(); - - for (final Setting dynamicSetting : dynamicSettings) { - if (randomBoolean()) { - existingSettings.put(dynamicSetting.getKey(), "existing_value"); - } - } - - for (final Setting invalidSetting : invalidSettings) { - existingSettings.put(invalidSetting.getKey(), "value"); - } - - for (final Setting unknownSetting : unknownSettings) { - existingSettings.put(unknownSetting.getKey(), "value"); - } - - // register all the known settings (note that we do not register the unknown settings) - final Set> knownSettings = Stream.concat( - Stream.of(SETTING_A, SETTING_B), - Stream.concat(dynamicSettings.stream(), invalidSettings.stream()) - ).collect(Collectors.toSet()); - final ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, knownSettings); - for (final Setting dynamicSetting : dynamicSettings) { - projectScopedSettings.addSettingsUpdateConsumer(dynamicSetting, s -> {}); - } - final SettingsUpdater settingsUpdater = new SettingsUpdater(projectScopedSettings); - ProjectMetadata projectMetadata = projectWithSettings(existingSettings.build()); - - // prepare the dynamic settings update - final Settings.Builder toApply = Settings.builder(); - for (final Setting dynamicSetting : dynamicSettings) { - if (randomBoolean()) { - toApply.put(dynamicSetting.getKey(), "new_value"); - } - } - - final ProjectMetadata afterUpdate = settingsUpdater.updateProjectSettings(projectMetadata, toApply.build(), logger); - - // the invalid settings should be archived and not present in non-archived form - for (final Setting invalidSetting : invalidSettings) { - assertThat(afterUpdate.settings().keySet(), hasItem(ARCHIVED_SETTINGS_PREFIX + invalidSetting.getKey())); - assertThat(afterUpdate.settings().keySet(), not(hasItem(invalidSetting.getKey()))); - } - - // the unknown settings should be archived and not present in non-archived form - for (final Setting unknownSetting : unknownSettings) { - assertThat(afterUpdate.settings().keySet(), hasItem(ARCHIVED_SETTINGS_PREFIX + unknownSetting.getKey())); - assertThat(afterUpdate.settings().keySet(), not(hasItem(unknownSetting.getKey()))); - } - - // the dynamic settings should be applied - for (final Setting dynamicSetting : dynamicSettings) { - if (toApply.keys().contains(dynamicSetting.getKey())) { - assertThat(afterUpdate.settings().keySet(), hasItem(dynamicSetting.getKey())); - assertThat(afterUpdate.settings().get(dynamicSetting.getKey()), equalTo("new_value")); - } else { - if (existingSettings.keys().contains(dynamicSetting.getKey())) { - assertThat(afterUpdate.settings().keySet(), hasItem(dynamicSetting.getKey())); - assertThat(afterUpdate.settings().get(dynamicSetting.getKey()), equalTo("existing_value")); - } else { - assertThat(afterUpdate.settings().keySet(), not(hasItem(dynamicSetting.getKey()))); - } - } - } - } - - public void testRemovingArchivedSettingsDoesNotRemoveNonArchivedInvalidOrUnknownSettings() { - // these are settings that are archived in the cluster state as either persistent or transient settings - final int numberOfArchivedSettings = randomIntBetween(1, 8); - final List> archivedSettings = new ArrayList<>(numberOfArchivedSettings); - for (int i = 0; i < numberOfArchivedSettings; i++) { - final Setting archivedSetting = Setting.simpleString("setting", Property.NodeScope, Property.ProjectScope); - archivedSettings.add(archivedSetting); - } - - // these are invalid settings that exist as either persistent or transient settings - final int numberOfInvalidSettings = randomIntBetween(0, 7); - final List> invalidSettings = invalidSettings(numberOfInvalidSettings); - - // these are unknown settings that exist as either persistent or transient settings - final int numberOfUnknownSettings = randomIntBetween(0, 7); - final List> unknownSettings = unknownSettings(numberOfUnknownSettings); - - final Settings.Builder existingSettings = Settings.builder(); - - for (final Setting archivedSetting : archivedSettings) { - existingSettings.put(ARCHIVED_SETTINGS_PREFIX + archivedSetting.getKey(), "value"); - } - - for (final Setting invalidSetting : invalidSettings) { - existingSettings.put(invalidSetting.getKey(), "value"); - } - - for (final Setting unknownSetting : unknownSettings) { - existingSettings.put(unknownSetting.getKey(), "value"); - } - - // register all the known settings (not that we do not register the unknown settings) - final Set> knownSettings = Stream.concat( - Stream.of(SETTING_A, SETTING_B), - Stream.concat(archivedSettings.stream(), invalidSettings.stream()) - ).collect(Collectors.toSet()); - final ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings(Settings.EMPTY, knownSettings); - final SettingsUpdater settingsUpdater = new SettingsUpdater(projectScopedSettings); - final ProjectMetadata projectMetadata = projectWithSettings(existingSettings.build()); - - final Settings.Builder toApply = Settings.builder().put("archived.*", (String) null); - - final ProjectMetadata afterUpdate = settingsUpdater.updateProjectSettings(projectMetadata, toApply.build(), logger); - - // existing archived settings are removed - for (final Setting archivedSetting : archivedSettings) { - assertThat(afterUpdate.settings().keySet(), not(hasItem(ARCHIVED_SETTINGS_PREFIX + archivedSetting.getKey()))); - } - - // the invalid settings should be archived and not present in non-archived form - for (final Setting invalidSetting : invalidSettings) { - assertThat(afterUpdate.settings().keySet(), hasItem(ARCHIVED_SETTINGS_PREFIX + invalidSetting.getKey())); - assertThat(afterUpdate.settings().keySet(), not(hasItem(invalidSetting.getKey()))); - } - - // the unknown settings should be archived and not present in non-archived form - for (final Setting unknownSetting : unknownSettings) { - assertThat(afterUpdate.settings().keySet(), hasItem(ARCHIVED_SETTINGS_PREFIX + unknownSetting.getKey())); - assertThat(afterUpdate.settings().keySet(), not(hasItem(unknownSetting.getKey()))); - } - } - - private static List> unknownSettings(int numberOfUnknownSettings) { - final List> unknownSettings = new ArrayList<>(numberOfUnknownSettings); - for (int i = 0; i < numberOfUnknownSettings; i++) { - unknownSettings.add(Setting.simpleString("unknown.setting" + i, Property.NodeScope, Property.ProjectScope)); - } - return unknownSettings; - } - - private static List> invalidSettings(int numberOfInvalidSettings) { - final List> invalidSettings = new ArrayList<>(numberOfInvalidSettings); - for (int i = 0; i < numberOfInvalidSettings; i++) { - invalidSettings.add(randomBoolean() ? invalidInIsolationSetting(i) : invalidWithDependenciesSetting(i)); - } - return invalidSettings; - } - - private static Setting invalidInIsolationSetting(int index) { - return Setting.simpleString("invalid.setting" + index, new Setting.Validator<>() { - - @Override - public void validate(final String value) { - throw new IllegalArgumentException("Invalid in isolation setting"); - } - - }, Property.NodeScope, Property.ProjectScope); - } - - private static Setting invalidWithDependenciesSetting(int index) { - return Setting.simpleString("invalid.setting" + index, new Setting.Validator<>() { - - @Override - public void validate(final String value) {} - - @Override - public void validate(final String value, final Map, Object> settings) { - throw new IllegalArgumentException("Invalid with dependencies setting"); - } - - }, Property.NodeScope, Property.ProjectScope); - } - - private static class FooLowSettingValidator implements Setting.Validator { - - @Override - public void validate(final Integer value) {} - - @Override - public void validate(final Integer low, final Map, Object> settings) { - if (settings.containsKey(SETTING_FOO_HIGH) && low > (int) settings.get(SETTING_FOO_HIGH)) { - throw new IllegalArgumentException("[low]=" + low + " is higher than [high]=" + settings.get(SETTING_FOO_HIGH)); - } - } - - @Override - public Iterator> settings() { - final List> settings = List.of(SETTING_FOO_HIGH); - return settings.iterator(); - } - - } - - private static class FooHighSettingValidator implements Setting.Validator { - - @Override - public void validate(final Integer value) { - - } - - @Override - public void validate(final Integer high, final Map, Object> settings) { - if (settings.containsKey(SETTING_FOO_LOW) && high < (int) settings.get(SETTING_FOO_LOW)) { - throw new IllegalArgumentException("[high]=" + high + " is lower than [low]=" + settings.get(SETTING_FOO_LOW)); - } - } - - @Override - public Iterator> settings() { - final List> settings = List.of(SETTING_FOO_LOW); - return settings.iterator(); - } - - } - - private static final Setting SETTING_FOO_LOW = new Setting<>( - "foo.low", - "10", - Integer::valueOf, - new FooLowSettingValidator(), - Property.Dynamic, - Property.NodeScope, - Property.ProjectScope - ); - private static final Setting SETTING_FOO_HIGH = new Setting<>( - "foo.high", - "100", - Integer::valueOf, - new FooHighSettingValidator(), - Property.Dynamic, - Property.NodeScope, - Property.ProjectScope - ); - - public void testUpdateOfValidationDependentSettings() { - final ProjectScopedSettings projectScopedSettings = new ProjectScopedSettings( - Settings.EMPTY, - new HashSet<>(asList(SETTING_FOO_LOW, SETTING_FOO_HIGH)) - ); - final SettingsUpdater updater = new SettingsUpdater(projectScopedSettings); - ProjectMetadata projectMetadata = projectWithSettings(Settings.EMPTY); - - projectMetadata = updater.updateProjectSettings( - projectMetadata, - Settings.builder().put(SETTING_FOO_LOW.getKey(), 20).build(), - logger - ); - assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("20")); - - projectMetadata = updater.updateProjectSettings( - projectMetadata, - Settings.builder().put(SETTING_FOO_HIGH.getKey(), 40).build(), - logger - ); - assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("20")); - assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("40")); - - projectMetadata = updater.updateProjectSettings( - projectMetadata, - Settings.builder().put(SETTING_FOO_LOW.getKey(), 5).build(), - logger - ); - assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("5")); - assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("40")); - - projectMetadata = updater.updateProjectSettings( - projectMetadata, - Settings.builder().put(SETTING_FOO_HIGH.getKey(), 8).build(), - logger - ); - assertThat(projectMetadata.settings().get(SETTING_FOO_LOW.getKey()), equalTo("5")); - assertThat(projectMetadata.settings().get(SETTING_FOO_HIGH.getKey()), equalTo("8")); - - final ProjectMetadata finalProjectMetadata = projectMetadata; - Exception exception = expectThrows( - IllegalArgumentException.class, - () -> updater.updateProjectSettings(finalProjectMetadata, Settings.builder().put(SETTING_FOO_HIGH.getKey(), 2).build(), logger) - ); - - assertThat( - exception.getMessage(), - either(equalTo("[high]=2 is lower than [low]=5")).or(equalTo("[low]=5 is higher than [high]=2")) - ); - } - -} From 1d6ae73f787aff0c15ce681ebb13a0e9cf5bcbf5 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 24 Apr 2025 16:14:38 +0100 Subject: [PATCH 11/15] Delete docs/changelog/127280.yaml --- docs/changelog/127280.yaml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 docs/changelog/127280.yaml diff --git a/docs/changelog/127280.yaml b/docs/changelog/127280.yaml deleted file mode 100644 index c353b43591bfb..0000000000000 --- a/docs/changelog/127280.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 127280 -summary: New per-project only settings can be defined and used by components -area: Infra/Settings -type: feature -issues: [] From e040c9d010995206c555bb29551649adb9e85ad7 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 24 Apr 2025 19:22:16 +0100 Subject: [PATCH 12/15] Fix tests --- .../org/elasticsearch/cluster/metadata/ProjectMetadataTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java index 4854c45f9540a..5fd534da2ddae 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ProjectMetadataTests.java @@ -553,6 +553,7 @@ public void testToXContentMultiProject() throws IOException { "replicated": false, "system": false, "allow_custom_routing": false, + "settings" : { }, "failure_rollover_on_write": false, "rollover_on_write": false } From 0234193e57d51f60721213e763d2128b1d4d715f Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Mon, 28 Apr 2025 17:03:48 +0100 Subject: [PATCH 13/15] Fix a bug in the cluster state application --- .../org/elasticsearch/cluster/metadata/ProjectMetadata.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java index 9087f2d2ff878..5584b05f1c43a 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java @@ -1109,6 +1109,9 @@ public boolean isIndexManagedByILM(IndexMetadata indexMetadata) { } static boolean isStateEquals(ProjectMetadata project1, ProjectMetadata project2) { + if (project1.settings().equals(project2.settings()) == false) { + return false; + } if (project1.templates().equals(project2.templates()) == false) { return false; } @@ -2389,7 +2392,7 @@ public ProjectMetadata apply(ProjectMetadata part) { && builder.dataStreamMetadata() == part.custom(DataStreamMetadata.TYPE, DataStreamMetadata.EMPTY)) { builder.previousIndicesLookup = part.indicesLookup; } - builder.settings = settingsDiff.apply(builder.settings); + builder.settings = settingsDiff.apply(part.settings); return builder.build(true); } From 90f3c757e4de94059d8606941345a5adbb124514 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Mon, 28 Apr 2025 17:27:35 +0100 Subject: [PATCH 14/15] Make abstract --- .../org/elasticsearch/common/settings/BaseSettingsUpdater.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java b/server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java index 9d1a83ea49440..22ae72019182b 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java +++ b/server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java @@ -17,7 +17,7 @@ import static org.elasticsearch.common.settings.AbstractScopedSettings.ARCHIVED_SETTINGS_PREFIX; -public class BaseSettingsUpdater { +public abstract class BaseSettingsUpdater { protected final AbstractScopedSettings scopedSettings; public BaseSettingsUpdater(AbstractScopedSettings scopedSettings) { From dfc13a32095244bea1ed8cb1f916f8aa14bacfca Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Tue, 29 Apr 2025 18:15:13 +0100 Subject: [PATCH 15/15] Make ingest.geoip.downloader.enabled project-scoped --- .../ingest/geoip/GeoIpDownloaderTaskExecutor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java index aac86d4f8bce4..39fec22dc1bdc 100644 --- a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java +++ b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java @@ -71,7 +71,8 @@ public final class GeoIpDownloaderTaskExecutor extends PersistentTasksExecutor POLL_INTERVAL_SETTING = Setting.timeSetting( "ingest.geoip.downloader.poll.interval",