Skip to content

[7.x] Deprecate creation of dot-prefixed index names except for hidden and system indices (#49959) #51509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference/index-modules.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ indices.
Indicates whether <<query-filter-context, cached filters>> are pre-loaded for
nested queries. Possible values are `true` (default) and `false`.

`index.hidden`::
[[index-hidden]] `index.hidden`::

Indicates whether the index should be hidden by default. Hidden indices are not
returned by default when using a wildcard expression. This behavior is controlled
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/indices/create-index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ PUT /twitter

[[indices-create-api-desc]]
==== {api-description-title}
You can use the create index API to add a new index to an {es} cluster. When
You can use the create index API to add a new index to an {es} cluster. When
creating an index, you can specify the following:

* Settings for the index
Expand All @@ -44,6 +44,7 @@ Index names must meet the following criteria:
- Cannot start with `-`, `_`, `+`
- Cannot be `.` or `..`
- Cannot be longer than 255 bytes (note it is bytes, so multi-byte characters will count towards the 255 limit faster)
- Names starting with `.` are deprecated, except for <<index-hidden,hidden indices>> and internal indices managed by plugins
// end::index-name-reqs[]
--

Expand Down
25 changes: 25 additions & 0 deletions modules/tasks/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

esplugin {
description 'Supports the Tasks API'
classname 'org.elasticsearch.tasksplugin.TasksPlugin'
}

integTest.enabled = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.tasksplugin;

import org.elasticsearch.indices.SystemIndexDescriptor;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.SystemIndexPlugin;

import java.util.Collection;
import java.util.Collections;

import static org.elasticsearch.tasks.TaskResultsService.TASK_INDEX;

/**
* This plugin currently only exists to register `.tasks` as a system index.
*/
public class TasksPlugin extends Plugin implements SystemIndexPlugin {

@Override
public Collection<SystemIndexDescriptor> getSystemIndexDescriptors() {
return Collections.singletonList(new SystemIndexDescriptor(TASK_INDEX, this.getClass().getSimpleName()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.tasksplugin;

import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matchers;

public class TasksPluginTests extends ESTestCase {

public void testDummy() {
// This is a dummy test case to satisfy the conventions
TasksPlugin plugin = new TasksPlugin();
assertThat(plugin.getSystemIndexDescriptors(), Matchers.hasSize(1));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ protected void masterOperation(Task task, final RolloverRequest rolloverRequest,
? rolloverRequest.getNewIndexName()
: generateRolloverIndexName(sourceProvidedName, indexNameExpressionResolver);
final String rolloverIndexName = indexNameExpressionResolver.resolveDateMathExpression(unresolvedName);
MetaDataCreateIndexService.validateIndexName(rolloverIndexName, state); // will fail if the index already exists
final Boolean isHidden = IndexMetaData.INDEX_HIDDEN_SETTING.exists(rolloverRequest.getCreateIndexRequest().settings()) ?
IndexMetaData.INDEX_HIDDEN_SETTING.get(rolloverRequest.getCreateIndexRequest().settings()) : null;
createIndexService.validateIndexName(rolloverIndexName, state, isHidden); // fails if the index already exists
checkNoDuplicatedAliasInIndexTemplate(metaData, rolloverIndexName, rolloverRequest.getAlias(), isHidden);
IndicesStatsRequest statsRequest = new IndicesStatsRequest().indices(rolloverRequest.getAlias())
.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
import org.elasticsearch.indices.IndexCreationException;
import org.elasticsearch.indices.IndicesService;
import org.elasticsearch.indices.InvalidIndexNameException;
import org.elasticsearch.indices.SystemIndexDescriptor;
import org.elasticsearch.indices.cluster.IndicesClusterStateService.AllocatedIndices.IndexRemovalReason;
import org.elasticsearch.threadpool.ThreadPool;

Expand All @@ -81,6 +82,7 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -92,6 +94,7 @@
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static java.util.stream.Collectors.toList;
Expand Down Expand Up @@ -119,19 +122,21 @@ public class MetaDataCreateIndexService {
private final IndexScopedSettings indexScopedSettings;
private final ActiveShardsObserver activeShardsObserver;
private final NamedXContentRegistry xContentRegistry;
private final Collection<SystemIndexDescriptor> systemIndexDescriptors;
private final boolean forbidPrivateIndexSettings;

public MetaDataCreateIndexService(
final Settings settings,
final ClusterService clusterService,
final IndicesService indicesService,
final AllocationService allocationService,
final AliasValidator aliasValidator,
final Environment env,
final IndexScopedSettings indexScopedSettings,
final ThreadPool threadPool,
final NamedXContentRegistry xContentRegistry,
final boolean forbidPrivateIndexSettings) {
final Settings settings,
final ClusterService clusterService,
final IndicesService indicesService,
final AllocationService allocationService,
final AliasValidator aliasValidator,
final Environment env,
final IndexScopedSettings indexScopedSettings,
final ThreadPool threadPool,
final NamedXContentRegistry xContentRegistry,
final Collection<SystemIndexDescriptor> systemIndexDescriptors,
final boolean forbidPrivateIndexSettings) {
this.settings = settings;
this.clusterService = clusterService;
this.indicesService = indicesService;
Expand All @@ -141,17 +146,40 @@ public MetaDataCreateIndexService(
this.indexScopedSettings = indexScopedSettings;
this.activeShardsObserver = new ActiveShardsObserver(clusterService, threadPool);
this.xContentRegistry = xContentRegistry;
this.systemIndexDescriptors = systemIndexDescriptors;
this.forbidPrivateIndexSettings = forbidPrivateIndexSettings;
}

/**
* Validate the name for an index against some static rules and a cluster state.
*/
public static void validateIndexName(String index, ClusterState state) {
public void validateIndexName(String index, ClusterState state, @Nullable Boolean isHidden) {
validateIndexOrAliasName(index, InvalidIndexNameException::new);
if (!index.toLowerCase(Locale.ROOT).equals(index)) {
throw new InvalidIndexNameException(index, "must be lowercase");
}

if (index.charAt(0) == '.') {
List<SystemIndexDescriptor> matchingDescriptors = systemIndexDescriptors.stream()
.filter(descriptor -> descriptor.matchesIndexPattern(index))
.collect(toList());
if (matchingDescriptors.isEmpty() && (isHidden == null || isHidden == Boolean.FALSE)) {
DEPRECATION_LOGGER.deprecated("index name [{}] starts with a dot '.', in the next major version, index names " +
"starting with a dot are reserved for hidden indices and system indices", index);
} else if (matchingDescriptors.size() > 1) {
// This should be prevented by erroring on overlapping patterns at startup time, but is here just in case.
StringBuilder errorMessage = new StringBuilder()
.append("index name [")
.append(index)
.append("] is claimed as a system index by multiple system index patterns: [")
.append(matchingDescriptors.stream()
.map(descriptor -> "pattern: [" + descriptor.getIndexPattern() +
"], description: [" + descriptor.getDescription() + "]").collect(Collectors.joining("; ")));
// Throw AssertionError if assertions are enabled, or a regular exception otherwise:
assert false : errorMessage.toString();
throw new IllegalStateException(errorMessage.toString());
}
}
if (state.routingTable().hasIndex(index)) {
throw new ResourceAlreadyExistsException(state.routingTable().index(index).getIndex());
}
Expand Down Expand Up @@ -693,7 +721,8 @@ private static IndexService validateActiveShardCountAndCreateIndexService(String
}

private void validate(CreateIndexClusterStateUpdateRequest request, ClusterState state) {
validateIndexName(request.index(), state);
boolean isHidden = IndexMetaData.INDEX_HIDDEN_SETTING.get(request.settings());
validateIndexName(request.index(), state, isHidden);
validateIndexSettings(request.index(), request.settings(), forbidPrivateIndexSettings);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.indices;

import org.apache.lucene.util.automaton.Automaton;
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
import org.apache.lucene.util.automaton.Operations;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.regex.Regex;

import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
* Describes a system index. Provides the information required to create and maintain the system index.
*/
public class SystemIndexDescriptor {
private final String indexPattern;
private final String description;
private final CharacterRunAutomaton indexPatternAutomaton;

/**
*
* @param indexPattern The pattern of index names that this descriptor will be used for. Must start with a '.' character.
* @param description The name of the plugin responsible for this system index.
*/
public SystemIndexDescriptor(String indexPattern, String description) {
Objects.requireNonNull(indexPattern, "system index pattern must not be null");
if (indexPattern.length() < 2) {
throw new IllegalArgumentException("system index pattern provided as [" + indexPattern +
"] but must at least 2 characters in length");
}
if (indexPattern.charAt(0) != '.') {
throw new IllegalArgumentException("system index pattern provided as [" + indexPattern +
"] but must start with the character [.]");
}
if (indexPattern.charAt(1) == '*') {
throw new IllegalArgumentException("system index pattern provided as [" + indexPattern +
"] but must not start with the character sequence [.*] to prevent conflicts");
}
this.indexPattern = indexPattern;
this.indexPatternAutomaton = new CharacterRunAutomaton(Regex.simpleMatchToAutomaton(indexPattern));
this.description = description;
}

/**
* @return The pattern of index names that this descriptor will be used for.
*/
public String getIndexPattern() {
return indexPattern;
}

/**
* Checks whether an index name matches the system index name pattern for this descriptor.
* @param index The index name to be checked against the index pattern given at construction time.
* @return True if the name matches the pattern, false otherwise.
*/
public boolean matchesIndexPattern(String index) {
return indexPatternAutomaton.run(index);
}

/**
* @return A short description of the purpose of this system index.
*/
public String getDescription() {
return description;
}

@Override
public String toString() {
return "SystemIndexDescriptor[pattern=[" + indexPattern + "], description=[" + description + "]]";
}

/**
* Given a collection of {@link SystemIndexDescriptor}s and their sources, checks to see if the index patterns of the listed
* descriptors overlap with any of the other patterns. If any do, throws an exception.
*
* @param sourceToDescriptors A map of source (plugin) names to the SystemIndexDescriptors they provide.
* @throws IllegalStateException Thrown if any of the index patterns overlaps with another.
*/
public static void checkForOverlappingPatterns(Map<String, Collection<SystemIndexDescriptor>> sourceToDescriptors) {
List<Tuple<String, SystemIndexDescriptor>> sourceDescriptorPair = sourceToDescriptors.entrySet().stream()
.flatMap(entry -> entry.getValue().stream().map(descriptor -> new Tuple<>(entry.getKey(), descriptor)))
.sorted(Comparator.comparing(d -> d.v1() + ":" + d.v2().getIndexPattern())) // Consistent ordering -> consistent error message
.collect(Collectors.toList());

// This is O(n^2) with the number of system index descriptors, and each check is quadratic with the number of states in the
// automaton, but the absolute number of system index descriptors should be quite small (~10s at most), and the number of states
// per pattern should be low as well. If these assumptions change, this might need to be reworked.
sourceDescriptorPair.forEach(descriptorToCheck -> {
List<Tuple<String, SystemIndexDescriptor>> descriptorsMatchingThisPattern = sourceDescriptorPair.stream()

.filter(d -> descriptorToCheck.v2() != d.v2()) // Exclude the pattern currently being checked
.filter(d -> overlaps(descriptorToCheck.v2(), d.v2()))
.collect(Collectors.toList());
if (descriptorsMatchingThisPattern.isEmpty() == false) {
StringBuilder errorMessage = new StringBuilder();
errorMessage.append("a system index descriptor [")
.append(descriptorToCheck.v2())
.append("] from plugin [")
.append(descriptorToCheck.v1())
.append("] overlaps with other system index descriptors: [")
.append(descriptorsMatchingThisPattern.stream()
.map(descriptor -> descriptor.v2() + " from plugin [" + descriptor.v1() + "]")
.collect(Collectors.joining(", ")));
throw new IllegalStateException(errorMessage.toString());
}
});
}

private static boolean overlaps(SystemIndexDescriptor a1, SystemIndexDescriptor a2) {
Automaton a1Automaton = Regex.simpleMatchToAutomaton(a1.getIndexPattern());
Automaton a2Automaton = Regex.simpleMatchToAutomaton(a2.getIndexPattern());
return Operations.isEmpty(Operations.intersection(a1Automaton, a2Automaton)) == false;
}

// TODO: Index settings and mapping
// TODO: getThreadpool()
// TODO: Upgrade handling (reindex script?)
}
Loading