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", diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index a435f61414c5d..261b13bac1e27 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -229,6 +229,7 @@ static TransportVersion def(int id) { public static final TransportVersion RANDOM_SAMPLER_QUERY_BUILDER = def(9_063_0_00); public static final TransportVersion SETTINGS_IN_DATA_STREAMS = def(9_064_0_00); public static final TransportVersion INTRODUCE_FAILURES_LIFECYCLE = def(9_065_0_00); + public static final TransportVersion PROJECT_METADATA_SETTINGS = def(9_066_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..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,7 +950,13 @@ 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..5584b05f1c43a 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,6 +225,7 @@ public ProjectMetadata withLifecycleState(Index index, LifecycleExecutionState l visibleClosedIndices, indicesLookup, mappingsByHash, + settings, oldestIndexVersion ); } @@ -254,6 +259,7 @@ public ProjectMetadata withIndexSettingsUpdates(Map updates) { visibleClosedIndices, indicesLookup, mappingsByHash, + settings, oldestIndexVersion ); } @@ -288,6 +294,7 @@ public ProjectMetadata withAllocationAndTermUpdatesOnly(Map templates() { return templates; } + public Settings settings() { + return settings; + } + /** * Checks whether the provided index is a data stream. */ @@ -1097,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; } @@ -1130,6 +1145,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 +1163,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 +1542,11 @@ public Builder removeReservedState(ReservedStateMetadata metadata) { return this; } + public Builder settings(Settings settings) { + this.settings = settings; + return this; + } + public Builder indexGraveyard(final IndexGraveyard indexGraveyard) { return putCustom(IndexGraveyard.TYPE, indexGraveyard); } @@ -1684,6 +1706,7 @@ public ProjectMetadata build(boolean skipNameCollisionChecks) { visibleClosedIndicesArray, indicesLookup, Collections.unmodifiableMap(mappingsByHash), + settings, IndexVersion.fromId(oldestIndexVersionId) ); } @@ -2110,6 +2133,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, @@ -2155,6 +2181,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 ? ChunkedToXContentHelper.object("reserved_state", reservedStateMetadata().values().iterator()) : Collections.emptyIterator() @@ -2189,6 +2220,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 +2264,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 +2287,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 +2295,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 +2310,7 @@ private ProjectMetadataDiff(ProjectMetadata before, ProjectMetadata after) { after.reservedStateMetadata, DiffableUtils.getStringKeySerializer() ); + settingsDiff = after.settings.diff(before.settings); } } @@ -2276,12 +2318,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 +2337,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 +2366,18 @@ 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 +2392,14 @@ public ProjectMetadata apply(ProjectMetadata part) { && builder.dataStreamMetadata() == part.custom(DataStreamMetadata.TYPE, DataStreamMetadata.EMPTY)) { builder.previousIndicesLookup = part.indicesLookup; } + builder.settings = settingsDiff.apply(part.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..5caf340036156 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,12 +55,31 @@ 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) { 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) ); @@ -68,10 +90,21 @@ public ClusterService( 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 +234,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/BaseSettingsUpdater.java b/server/src/main/java/org/elasticsearch/common/settings/BaseSettingsUpdater.java new file mode 100644 index 0000000000000..22ae72019182b --- /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 abstract 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/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 635537d62b2a7..102c78b5834d0 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..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,28 +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.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 ClusterSettings clusterSettings; - public SettingsUpdater(ClusterSettings clusterSettings) { - this.clusterSettings = clusterSettings; + public SettingsUpdater(ClusterSettings scopedSettings) { + super(scopedSettings); } public synchronized Settings getTransientUpdates() { @@ -70,7 +65,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 +75,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 +83,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,62 +115,8 @@ 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; } - - /** - * 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 = clusterSettings.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/main/java/org/elasticsearch/node/NodeConstruction.java b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java index 95f88ef40b9dd..284fbd845b922 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -1314,6 +1314,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/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 0a925dd0eb542..5fd534da2ddae 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,286 @@ 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, + "settings" : { }, + "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( @@ -323,7 +559,11 @@ public void testToXContent() throws IOException { } }, "data_stream_aliases": {} - } + }, + "settings": { + "project.setting.value": "43" + }, + "reserved_state": {} } """, IndexVersion.current(), @@ -338,10 +578,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); @@ -383,6 +670,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 + 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" : { } } ],