Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the added lines in this class are solely from moving the tests over from MetadataIndexTemplateServiceTests.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,10 @@
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.ComponentTemplate;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
import org.elasticsearch.cluster.metadata.Template;
import org.elasticsearch.cluster.project.ProjectResolver;
import org.elasticsearch.cluster.project.ProjectStateRegistry;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.EsExecutors;
import org.elasticsearch.core.FixForMultiProject;
import org.elasticsearch.injection.guice.Inject;
Expand All @@ -39,7 +35,6 @@
public class TransportPutComponentTemplateAction extends AcknowledgedTransportMasterNodeAction<PutComponentTemplateAction.Request> {

private final MetadataIndexTemplateService indexTemplateService;
private final IndexScopedSettings indexScopedSettings;
private final ProjectResolver projectResolver;

@Inject
Expand All @@ -49,7 +44,6 @@ public TransportPutComponentTemplateAction(
ThreadPool threadPool,
MetadataIndexTemplateService indexTemplateService,
ActionFilters actionFilters,
IndexScopedSettings indexScopedSettings,
ProjectResolver projectResolver
) {
super(
Expand All @@ -62,7 +56,6 @@ public TransportPutComponentTemplateAction(
EsExecutors.DIRECT_EXECUTOR_SERVICE
);
this.indexTemplateService = indexTemplateService;
this.indexScopedSettings = indexScopedSettings;
this.projectResolver = projectResolver;
}

Expand All @@ -71,46 +64,36 @@ protected ClusterBlockException checkBlock(PutComponentTemplateAction.Request re
return state.blocks().globalBlockedException(projectResolver.getProjectId(), ClusterBlockLevel.METADATA_WRITE);
}

public static ComponentTemplate normalizeComponentTemplate(
ComponentTemplate componentTemplate,
IndexScopedSettings indexScopedSettings
) {
Template template = componentTemplate.template();
// Normalize the index settings if necessary
if (template.settings() != null) {
Settings.Builder builder = Settings.builder().put(template.settings()).normalizePrefix(IndexMetadata.INDEX_SETTING_PREFIX);
Settings settings = builder.build();
indexScopedSettings.validate(settings, true);
template = Template.builder(template).settings(settings).build();
componentTemplate = new ComponentTemplate(
template,
componentTemplate.version(),
componentTemplate.metadata(),
componentTemplate.deprecated(),
componentTemplate.createdDateMillis().orElse(null),
componentTemplate.modifiedDateMillis().orElse(null)
);
}

return componentTemplate;
}

@Override
protected void masterOperation(
Task task,
final PutComponentTemplateAction.Request request,
final ClusterState state,
final ActionListener<AcknowledgedResponse> listener
) {
ComponentTemplate componentTemplate = normalizeComponentTemplate(request.componentTemplate(), indexScopedSettings);
var projectId = projectResolver.getProjectId();
) throws Exception {
final var project = projectResolver.getProjectMetadata(state);
final ComponentTemplate componentTemplate = indexTemplateService.normalizeComponentTemplate(request.componentTemplate());
final ComponentTemplate existingTemplate = project.componentTemplates().get(request.name());
if (existingTemplate != null) {
if (request.create()) {
listener.onFailure(new IllegalArgumentException("component template [" + request.name() + "] already exists"));
return;
}
// We have an early return here in case the component template already exists and is identical in content. We still need to do
// this check in the cluster state update task in case the cluster state changed since this check.
if (componentTemplate.contentEquals(existingTemplate)) {
listener.onResponse(AcknowledgedResponse.TRUE);
return;
}
}

indexTemplateService.putComponentTemplate(
request.cause(),
request.create(),
request.name(),
request.masterNodeTimeout(),
componentTemplate,
projectId,
project.id(),
listener
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
import org.elasticsearch.cluster.metadata.ProjectId;
import org.elasticsearch.cluster.metadata.ProjectMetadata;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
import org.elasticsearch.reservedstate.ReservedProjectStateHandler;
Expand Down Expand Up @@ -56,14 +55,9 @@ public class ReservedComposableIndexTemplateAction
public static final String COMPOSABLE_PREFIX = "composable_index_template:";

private final MetadataIndexTemplateService indexTemplateService;
private final IndexScopedSettings indexScopedSettings;

public ReservedComposableIndexTemplateAction(
MetadataIndexTemplateService indexTemplateService,
IndexScopedSettings indexScopedSettings
) {
public ReservedComposableIndexTemplateAction(MetadataIndexTemplateService indexTemplateService) {
this.indexTemplateService = indexTemplateService;
this.indexScopedSettings = indexScopedSettings;
}

@Override
Expand Down Expand Up @@ -154,11 +148,7 @@ public TransformState transform(ProjectId projectId, ComponentsAndComposables so

// 1. create or update component templates (composable templates depend on them)
for (var request : components) {
ComponentTemplate template = TransportPutComponentTemplateAction.normalizeComponentTemplate(
request.componentTemplate(),
indexScopedSettings
);

ComponentTemplate template = indexTemplateService.normalizeComponentTemplate(request.componentTemplate());
project = indexTemplateService.addComponentTemplate(project, false, request.name(), template);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,23 @@ public boolean equals(Object obj) {
return false;
}
ComponentTemplate other = (ComponentTemplate) obj;
return contentEquals(other)
&& Objects.equals(createdDateMillis, other.createdDateMillis)
&& Objects.equals(modifiedDateMillis, other.modifiedDateMillis);
}

/**
* Check whether the content of this component template is equal to another component template. Can be used to determine if a template
* already exists.
*/
public boolean contentEquals(ComponentTemplate other) {
if (other == null) {
return false;
}
return Objects.equals(template, other.template)
&& Objects.equals(version, other.version)
&& Objects.equals(metadata, other.metadata)
&& Objects.equals(deprecated, other.deprecated)
&& Objects.equals(createdDateMillis, other.createdDateMillis)
&& Objects.equals(modifiedDateMillis, other.modifiedDateMillis);
&& Objects.equals(deprecated, other.deprecated);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,25 +293,26 @@ public ProjectMetadata execute(ProjectMetadata currentProject) throws Exception
);
}

// Public visible for testing
/**
* Add the given component template to the project. If {@code create} is true, we will fail if there exists a component template with
* the same name. If a component template with the same name exists, but the content is identical, no change will be made.
* This method will perform all necessary validation but assumes that the component template has already been normalized (see
* {@link #normalizeComponentTemplate(ComponentTemplate)}.
*/
public ProjectMetadata addComponentTemplate(
final ProjectMetadata project,
final boolean create,
final String name,
final ComponentTemplate template
) throws Exception {
final ComponentTemplate existing = project.componentTemplates().get(name);
if (create && existing != null) {
throw new IllegalArgumentException("component template [" + name + "] already exists");
}

CompressedXContent mappings = template.template().mappings();
CompressedXContent wrappedMappings = wrapMappingsIfNecessary(mappings, xContentRegistry);

// We may need to normalize index settings, so do that also
Settings finalSettings = template.template().settings();
if (finalSettings != null) {
finalSettings = Settings.builder().put(finalSettings).normalizePrefix(IndexMetadata.INDEX_SETTING_PREFIX).build();
) throws IOException {
final ComponentTemplate existingTemplate = project.componentTemplates().get(name);
if (existingTemplate != null) {
if (create) {
throw new IllegalArgumentException("component template [" + name + "] already exists");
}
if (template.contentEquals(existingTemplate)) {
return project;
}
}

// Collect all the composable (index) templates that use this component template, we'll use
Expand All @@ -324,9 +325,9 @@ public ProjectMetadata addComponentTemplate(
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

// if we're updating a component template, let's check if it's part of any V2 template that will yield the CT update invalid
if (create == false && finalSettings != null) {
if (create == false && template.template().settings() != null) {
// if the CT is specifying the `index.hidden` setting it cannot be part of any global template
if (IndexMetadata.INDEX_HIDDEN_SETTING.exists(finalSettings)) {
if (IndexMetadata.INDEX_HIDDEN_SETTING.exists(template.template().settings())) {
List<String> globalTemplatesThatUseThisComponent = new ArrayList<>();
for (Map.Entry<String, ComposableIndexTemplate> entry : templatesUsingComponent.entrySet()) {
ComposableIndexTemplate templateV2 = entry.getValue();
Expand All @@ -350,47 +351,27 @@ public ProjectMetadata addComponentTemplate(
}
}

final Template finalTemplate = Template.builder(template.template()).settings(finalSettings).mappings(wrappedMappings).build();
final long now = instantSource.instant().toEpochMilli();
final ComponentTemplate finalComponentTemplate;
if (existing == null) {
finalComponentTemplate = new ComponentTemplate(
finalTemplate,
template.version(),
template.metadata(),
template.deprecated(),
now,
now
);
} else {
final ComponentTemplate templateToCompareToExisting = new ComponentTemplate(
finalTemplate,
template.version(),
template.metadata(),
template.deprecated(),
existing.createdDateMillis().orElse(null),
existing.modifiedDateMillis().orElse(null)
);
if (templateToCompareToExisting.equals(existing)) {
return project;
}
finalComponentTemplate = new ComponentTemplate(
finalTemplate,
template.version(),
template.metadata(),
template.deprecated(),
existing.createdDateMillis().orElse(null),
now
);
}
final Long now = instantSource.instant().toEpochMilli();
final Long createdDateMillis = existingTemplate == null ? now : existingTemplate.createdDateMillis().orElse(null);
final ComponentTemplate finalComponentTemplate = new ComponentTemplate(
template.template(),
template.version(),
template.metadata(),
template.deprecated(),
createdDateMillis,
now
);

validateTemplate(finalSettings, wrappedMappings, indicesService);
// These two validation checks are only scoped to the component template itself (and don't depend on any other entities in the
// cluster state) and could thus be done in the transport action. However, since we're parsing mappings here, we shouldn't be doing
// it directly on the transport thread. Instead, we should fork to a different threadpool (management/generic).
validateTemplate(finalComponentTemplate.template().settings(), finalComponentTemplate.template().mappings(), indicesService);
validate(name, finalComponentTemplate.template(), List.of(), null);
Comment on lines +366 to 370
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I experimented with moving these two validations to the transport action, but since the Distributed Coordination team advised running them on the management or generic threadpool, I decided to leave it as future work. In terms of overall processing, we wouldn't save much. The only/main win would be that we avoid adding a cluster state update task in case the validation fails.

Also worth noting is that we potentially do more (heavier) mappings validation in validateIndexTemplateV2 below. Although #135457 helps reduce that in a lot of cases.


ProjectMetadata projectWithComponentTemplateAdded = ProjectMetadata.builder(project).put(name, finalComponentTemplate).build();
// Validate all composable index templates that use this component template
if (templatesUsingComponent.isEmpty() == false) {
Exception validationFailure = null;
IllegalArgumentException validationFailure = null;
for (Map.Entry<String, ComposableIndexTemplate> entry : templatesUsingComponent.entrySet()) {
final String composableTemplateName = entry.getKey();
final ComposableIndexTemplate composableTemplate = entry.getValue();
Expand Down Expand Up @@ -424,10 +405,42 @@ public ProjectMetadata addComponentTemplate(
.addWarningHeaderIfDataRetentionNotEffective(globalRetentionSettings.get(false), false);
}

logger.info("{} component template [{}]", existing == null ? "adding" : "updating", name);
logger.info("{} component template [{}]", existingTemplate == null ? "adding" : "updating", name);
return projectWithComponentTemplateAdded;
}

/**
* Normalize the given component template by trying to normalize settings and wrapping mappings if necessary. Returns the same instance
* if nothing needs to be done.
*/
public ComponentTemplate normalizeComponentTemplate(final ComponentTemplate componentTemplate) throws IOException {
Template template = componentTemplate.template();
// Normalize the index settings if necessary
Settings prefixedSettings = null;
if (template.settings() != null) {
prefixedSettings = template.settings().maybeNormalizePrefix(IndexMetadata.INDEX_SETTING_PREFIX);
}
// TODO: theoretically, we could avoid parsing the mappings once by combining this wrapping with the mapping validation later on,
// but that refactoring will be non-trivial as we currently don't seem to have methods available to merge already-parsed mappings;
// we only allow merging mappings from CompressedXContent.
CompressedXContent wrappedMappings = MetadataIndexTemplateService.wrapMappingsIfNecessary(template.mappings(), xContentRegistry);

// No need to build a new component template if we didn't change anything.
// We can check for reference equality since `maybeNormalizePrefix` and `wrapMappingsIfNecessary` return the same instance if
// nothing needs to be done.
if (prefixedSettings == template.settings() && wrappedMappings == template.mappings()) {
return componentTemplate;
}
return new ComponentTemplate(
Template.builder(template).settings(prefixedSettings).mappings(wrappedMappings).build(),
componentTemplate.version(),
componentTemplate.metadata(),
componentTemplate.deprecated(),
componentTemplate.createdDateMillis().orElse(null),
componentTemplate.modifiedDateMillis().orElse(null)
);
}

/**
* Mappings in templates don't have to include <code>_doc</code>, so update the mappings to include this single type if necessary
*
Expand Down Expand Up @@ -2009,7 +2022,7 @@ private static void validateCompositeTemplate(
}

public static void validateTemplate(Settings validateSettings, CompressedXContent mappings, IndicesService indicesService)
throws Exception {
throws IOException {
// Hard to validate settings if they're non-existent, so used empty ones if none were provided
Settings settings = validateSettings;
if (settings == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,19 @@ public Settings merge(Settings newSettings) {
return builder.build();
}

/**
* Checks if all settings start with the specified prefix and renames any that do not. Returns the current instance if nothing needs
* to be done. See {@link Builder#normalizePrefix(String)} for more info.
*/
public Settings maybeNormalizePrefix(String prefix) {
for (String key : settings.keySet()) {
if (key.startsWith(prefix) == false && key.endsWith("*") == false) {
return builder().put(this).normalizePrefix(prefix).build();
}
}
return this;
}

/**
* A builder allowing to put different settings and then {@link #build()} an immutable
* settings implementation. Use {@link Settings#builder()} in order to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,7 @@ public Map<String, String> queryFields() {
clusterService,
rerouteService,
buildReservedClusterStateHandlers(reservedStateHandlerProviders, settingsModule),
buildReservedProjectStateHandlers(reservedStateHandlerProviders, settingsModule, metadataIndexTemplateService),
buildReservedProjectStateHandlers(reservedStateHandlerProviders, metadataIndexTemplateService),
pluginsService.loadSingletonServiceProvider(RestExtension.class, RestExtension::allowAll),
incrementalBulkService,
projectResolver
Expand Down Expand Up @@ -1687,12 +1687,11 @@ private List<ReservedClusterStateHandler<?>> buildReservedClusterStateHandlers(

private List<ReservedProjectStateHandler<?>> buildReservedProjectStateHandlers(
List<? extends ReservedStateHandlerProvider> handlers,
SettingsModule settingsModule,
MetadataIndexTemplateService templateService
) {
List<ReservedProjectStateHandler<?>> reservedStateHandlers = new ArrayList<>();

reservedStateHandlers.add(new ReservedComposableIndexTemplateAction(templateService, settingsModule.getIndexScopedSettings()));
reservedStateHandlers.add(new ReservedComposableIndexTemplateAction(templateService));

// add all reserved state handlers from plugins
handlers.forEach(h -> reservedStateHandlers.addAll(h.projectHandlers()));
Expand Down
Loading