Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ protected ClusterBlockException checkBlock(RolloverRequest request, ClusterState
.matchOpen(request.indicesOptions().expandWildcardsOpen())
.matchClosed(request.indicesOptions().expandWildcardsClosed())
.build(),
IndicesOptions.GatekeeperOptions.DEFAULT
IndicesOptions.GatekeeperOptions.DEFAULT,
IndicesOptions.ResolutionModeOptions.DEFAULT
);
ResolvedExpression resolvedRolloverTarget = SelectorResolver.parseExpression(request.getRolloverTarget(), request.indicesOptions());
final IndexAbstraction indexAbstraction = projectMetadata.getIndicesLookup().get(resolvedRolloverTarget.resource());
Expand Down Expand Up @@ -255,7 +256,8 @@ protected void masterOperation(
final var statsIndicesOptions = new IndicesOptions(
IndicesOptions.ConcreteTargetOptions.ALLOW_UNAVAILABLE_TARGETS,
IndicesOptions.WildcardOptions.builder().matchClosed(true).allowEmptyExpressions(false).build(),
IndicesOptions.GatekeeperOptions.DEFAULT
IndicesOptions.GatekeeperOptions.DEFAULT,
IndicesOptions.ResolutionModeOptions.DEFAULT
);
// Make sure to recombine any selectors on the stats request
IndicesStatsRequest statsRequest = new IndicesStatsRequest().indices(resolvedRolloverTarget.combined())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
package org.elasticsearch.action.support;

import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.TransportVersions;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.core.Nullable;
Expand Down Expand Up @@ -49,7 +51,8 @@
public record IndicesOptions(
ConcreteTargetOptions concreteTargetOptions,
WildcardOptions wildcardOptions,
GatekeeperOptions gatekeeperOptions
GatekeeperOptions gatekeeperOptions,
ResolutionModeOptions resolutionModeOptions
) implements ToXContentFragment {

public static IndicesOptions.Builder builder() {
Expand Down Expand Up @@ -413,6 +416,42 @@ public static Builder builder(GatekeeperOptions gatekeeperOptions) {
}
}

/**
* The resolution mode options are internal-only options that apply on all indices that have been selected by the other Options. These
* options may contextually change over the lifetime of the request.
* @param crossProject determines that the index expression must be resolved for cross-project requests, defaults to false.
*/
public record ResolutionModeOptions(boolean crossProject) implements ToXContentFragment, Writeable {

public static final ResolutionModeOptions DEFAULT = new ResolutionModeOptions(false);

private static final TransportVersion INDICES_OPTIONS_RESOLUTION_MODE = TransportVersion.fromName(
"indices_options_resolution_mode"
);

private static final String CROSS_PROJECT_NAME = "resolve_cross_project";

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.field(CROSS_PROJECT_NAME, crossProject);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
if (out.getTransportVersion().supports(INDICES_OPTIONS_RESOLUTION_MODE)) {
out.writeBoolean(crossProject);
}
}

public static ResolutionModeOptions readFrom(StreamInput in) throws IOException {
if (in.getTransportVersion().supports(INDICES_OPTIONS_RESOLUTION_MODE)) {
return new ResolutionModeOptions(in.readBoolean());
} else {
return ResolutionModeOptions.DEFAULT;
}
}
}

/**
* This class is maintained for backwards compatibility and performance purposes. We use it for serialisation along with {@link Option}.
*/
Expand Down Expand Up @@ -463,7 +502,8 @@ private enum Option {
public static final IndicesOptions DEFAULT = new IndicesOptions(
ConcreteTargetOptions.ERROR_WHEN_UNAVAILABLE_TARGETS,
WildcardOptions.DEFAULT,
GatekeeperOptions.DEFAULT
GatekeeperOptions.DEFAULT,
ResolutionModeOptions.DEFAULT
);

public static final IndicesOptions STRICT_EXPAND_OPEN = IndicesOptions.builder()
Expand Down Expand Up @@ -857,6 +897,13 @@ public boolean ignoreThrottled() {
return gatekeeperOptions().ignoreThrottled();
}

/**
* @return whether indices will resolve to the cross-project "flat world" expression
*/
public boolean resolveCrossProject() {
return resolutionModeOptions().crossProject();
}

public void writeIndicesOptions(StreamOutput out) throws IOException {
EnumSet<Option> backwardsCompatibleOptions = EnumSet.noneOf(Option.class);
if (allowNoIndices()) {
Expand Down Expand Up @@ -917,6 +964,7 @@ public void writeIndicesOptions(StreamOutput out) throws IOException {
out.writeByte((byte) 0); // ordinal 0 (::data selector)
}
}
out.writeWriteable(resolutionModeOptions);
}

public static IndicesOptions readIndicesOptions(StreamInput in) throws IOException {
Expand Down Expand Up @@ -968,14 +1016,16 @@ public static IndicesOptions readIndicesOptions(StreamInput in) throws IOExcepti
? ConcreteTargetOptions.ALLOW_UNAVAILABLE_TARGETS
: ConcreteTargetOptions.ERROR_WHEN_UNAVAILABLE_TARGETS,
wildcardOptions,
gatekeeperOptions
gatekeeperOptions,
ResolutionModeOptions.readFrom(in)
);
}

public static class Builder {
private ConcreteTargetOptions concreteTargetOptions;
private WildcardOptions wildcardOptions;
private GatekeeperOptions gatekeeperOptions;
private ResolutionModeOptions resolutionModeOptions;

Builder() {
this(DEFAULT);
Expand All @@ -985,6 +1035,7 @@ public static class Builder {
concreteTargetOptions = indicesOptions.concreteTargetOptions;
wildcardOptions = indicesOptions.wildcardOptions;
gatekeeperOptions = indicesOptions.gatekeeperOptions;
resolutionModeOptions = indicesOptions.resolutionModeOptions;
}

public Builder concreteTargetOptions(ConcreteTargetOptions concreteTargetOptions) {
Expand Down Expand Up @@ -1012,8 +1063,13 @@ public Builder gatekeeperOptions(GatekeeperOptions.Builder generalOptions) {
return this;
}

public Builder resolutionModeOptions(ResolutionModeOptions resolutionModeOptions) {
this.resolutionModeOptions = resolutionModeOptions;
return this;
}

public IndicesOptions build() {
return new IndicesOptions(concreteTargetOptions, wildcardOptions, gatekeeperOptions);
return new IndicesOptions(concreteTargetOptions, wildcardOptions, gatekeeperOptions, resolutionModeOptions);
}
}

Expand Down Expand Up @@ -1115,7 +1171,8 @@ public static IndicesOptions fromOptions(
return new IndicesOptions(
ignoreUnavailable ? ConcreteTargetOptions.ALLOW_UNAVAILABLE_TARGETS : ConcreteTargetOptions.ERROR_WHEN_UNAVAILABLE_TARGETS,
wildcards,
gatekeeperOptions
gatekeeperOptions,
ResolutionModeOptions.DEFAULT
);
}

Expand Down Expand Up @@ -1157,7 +1214,8 @@ public static boolean isIndicesOptions(String name) {
|| GatekeeperOptions.IGNORE_THROTTLED.equals(name)
|| "ignoreThrottled".equals(name)
|| WildcardOptions.ALLOW_NO_INDICES.equals(name)
|| "allowNoIndices".equals(name);
|| "allowNoIndices".equals(name)
|| ResolutionModeOptions.CROSS_PROJECT_NAME.equals(name);
Copy link
Member

Choose a reason for hiding this comment

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

I think this is probably unnecessary. This method is used in a single place for error reporting on RestoreSnapshotRequest which is user facing. We don't expect it to be set on such request and hence no need to check here. I think it's might be the reason why we don't have everything here.

PS: It might be helpful to make each option explicitly declare whether it's internal or not so that their usage is easier to reason with. Not suggesting it for this PR. Just writing it down as an idea.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could - it exists to prevent xcontent parsing from erroring here: https://github.com/elastic/elasticsearch/blob/main/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java#L582

We'd change it so that we don't write any new xcontent if the field is false, then we check if the field is present before parsing it here: https://github.com/elastic/elasticsearch/blob/main/server/src/main/java/org/elasticsearch/action/support/IndicesOptions.java#L1136

Does that sound good? I can push the change as a separate commit so we can see what it looks like and revert it if we don't like it

Copy link
Member

Choose a reason for hiding this comment

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

We probably don't want to expose this new option to REST layer at all since it is internally only. For example, the parsing and toXContent methods of GatekeeperOptions do not involve any of the allowXxx options since they are internal, i.e. not configurable by end-users as described in its javadoc. Along this line of reasoning, I think we can just have no-op parsring and toXContent methods here to avoid inadverntly exposing it to end-users. It means RestoreSnapshotRequest will throw an exception if user specifies the new option and that is desirable. Overall I think it is easier to expose it later if necessary while more difficult to hide if already exposed.

PS: There is some fuzziness on how it should work for background tasks such as ML datafeed. I think the previous discussion seems to prefer inject the indicesOption when necessary, similar to how it is injected at the REST layer for ResolveIndexRequest. In another word, we don't plan to rely on parsing it from the job's configs.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we do need a way to capture it in the job config though, because logs will resolve differently over time. We would likely want it such that each time the ML datafeed (or Transform) runs, it resolves logs based on the current state of the system. The alternative is resolving once when the job is created, logs -> p1:logs, -p2:logs and then a future p3:logs would be omitted from the job.

Copy link
Member Author

Choose a reason for hiding this comment

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

I assume it's easier to add toXContent methods later than it is to remove toXContent methods, so we can start with them being no-op and go from there?

Copy link
Member

Choose a reason for hiding this comment

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

I think we do need a way to capture it in the job config though, because logs will resolve differently over time.

This is the fuziness part for me. An alternative to capturing is to inject it when the job runs similar to how we plan to inject it for RestResolveIndexAction. I am not sure exactly how that would look like yet but feels theorectically possible.

I assume it's easier to add toXContent methods later than it is to remove toXContent methods

Yep exactly my thought as well.

}

public static IndicesOptions fromParameters(
Expand Down Expand Up @@ -1202,13 +1260,15 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params par
concreteTargetOptions.toXContent(builder, params);
wildcardOptions.toXContent(builder, params);
gatekeeperOptions.toXContent(builder, params);
resolutionModeOptions.toXContent(builder, params);
return builder;
}

private static final ParseField EXPAND_WILDCARDS_FIELD = new ParseField(WildcardOptions.EXPAND_WILDCARDS);
private static final ParseField IGNORE_UNAVAILABLE_FIELD = new ParseField(ConcreteTargetOptions.IGNORE_UNAVAILABLE);
private static final ParseField IGNORE_THROTTLED_FIELD = new ParseField(GatekeeperOptions.IGNORE_THROTTLED).withAllDeprecated();
private static final ParseField ALLOW_NO_INDICES_FIELD = new ParseField(WildcardOptions.ALLOW_NO_INDICES);
private static final ParseField RESOLVE_CROSS_PROJECT = new ParseField(ResolutionModeOptions.CROSS_PROJECT_NAME);

public static IndicesOptions fromXContent(XContentParser parser) throws IOException {
return fromXContent(parser, null);
Expand All @@ -1221,6 +1281,7 @@ public static IndicesOptions fromXContent(XContentParser parser, @Nullable Indic
.ignoreThrottled(defaults != null && defaults.gatekeeperOptions().ignoreThrottled());
Boolean allowNoIndices = defaults == null ? null : defaults.allowNoIndices();
Boolean ignoreUnavailable = defaults == null ? null : defaults.ignoreUnavailable();
boolean resolveCrossProject = defaults == null ? ResolutionModeOptions.DEFAULT.crossProject() : defaults.resolveCrossProject();
Token token = parser.currentToken() == Token.START_OBJECT ? parser.currentToken() : parser.nextToken();
String currentFieldName = null;
if (token != Token.START_OBJECT) {
Expand Down Expand Up @@ -1268,6 +1329,8 @@ public static IndicesOptions fromXContent(XContentParser parser, @Nullable Indic
allowNoIndices = parser.booleanValue();
} else if (IGNORE_THROTTLED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
generalOptions.ignoreThrottled(parser.booleanValue());
} else if (RESOLVE_CROSS_PROJECT.match(currentFieldName, parser.getDeprecationHandler())) {
resolveCrossProject = parser.booleanValue();
} else {
throw new ElasticsearchParseException(
"could not read indices options. Unexpected index option [" + currentFieldName + "]"
Expand Down Expand Up @@ -1298,6 +1361,7 @@ public static IndicesOptions fromXContent(XContentParser parser, @Nullable Indic
.concreteTargetOptions(new ConcreteTargetOptions(ignoreUnavailable))
.wildcardOptions(wildcards)
.gatekeeperOptions(generalOptions)
.resolutionModeOptions(new ResolutionModeOptions(resolveCrossProject))
.build();
}

Expand Down Expand Up @@ -1459,6 +1523,8 @@ public String toString() {
+ allowSelectors()
+ ", include_failure_indices="
+ includeFailureIndices()
+ ", resolve_cross_project="
+ resolveCrossProject()
+ ']';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9188000
2 changes: 1 addition & 1 deletion server/src/main/resources/transport/upper_bounds/9.3.csv
Original file line number Diff line number Diff line change
@@ -1 +1 @@
resolved_index_expressions,9187000
indices_options_resolution_mode,9188000
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import org.elasticsearch.action.support.IndicesOptions.ConcreteTargetOptions;
import org.elasticsearch.action.support.IndicesOptions.GatekeeperOptions;
import org.elasticsearch.action.support.IndicesOptions.ResolutionModeOptions;
import org.elasticsearch.action.support.IndicesOptions.WildcardOptions;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
Expand Down Expand Up @@ -57,6 +58,7 @@ public void testSerialization() throws Exception {
.allowClosedIndices(randomBoolean())
.allowSelectors(randomBoolean())
)
.resolutionModeOptions(new ResolutionModeOptions(randomBoolean()))
.build();

BytesStreamOutput output = new BytesStreamOutput();
Expand Down Expand Up @@ -102,6 +104,7 @@ public void testFromOptions() {
assertThat(indicesOptions.forbidClosedIndices(), equalTo(forbidClosedIndices));
assertEquals(ignoreAliases, indicesOptions.ignoreAliases());
assertEquals(ignoreThrottled, indicesOptions.ignoreThrottled());
assertEquals(indicesOptions.resolveCrossProject(), ResolutionModeOptions.DEFAULT.crossProject());
}

public void testFromOptionsWithDefaultOptions() {
Expand Down Expand Up @@ -138,6 +141,7 @@ public void testFromOptionsWithDefaultOptions() {
assertEquals(defaultOptions.allowAliasesToMultipleIndices(), indicesOptions.allowAliasesToMultipleIndices());
assertEquals(defaultOptions.forbidClosedIndices(), indicesOptions.forbidClosedIndices());
assertEquals(defaultOptions.ignoreAliases(), indicesOptions.ignoreAliases());
assertEquals(defaultOptions.resolveCrossProject(), indicesOptions.resolveCrossProject());
}

public void testFromParameters() {
Expand Down Expand Up @@ -201,6 +205,7 @@ public void testFromParameters() {
assertEquals(defaultOptions.allowAliasesToMultipleIndices(), updatedOptions.allowAliasesToMultipleIndices());
assertEquals(defaultOptions.forbidClosedIndices(), updatedOptions.forbidClosedIndices());
assertEquals(defaultOptions.ignoreAliases(), updatedOptions.ignoreAliases());
assertEquals(defaultOptions.resolveCrossProject(), updatedOptions.resolveCrossProject());
}

public void testEqualityAndHashCode() {
Expand Down Expand Up @@ -330,6 +335,7 @@ public void testFromMap() {
assertEquals(ignoreUnavailable == null ? defaults.ignoreUnavailable() : ignoreUnavailable, fromMap.ignoreUnavailable());
assertEquals(allowNoIndices == null ? defaults.allowNoIndices() : allowNoIndices, fromMap.allowNoIndices());
assertEquals(ignoreThrottled == null ? defaults.ignoreThrottled() : ignoreThrottled, fromMap.ignoreThrottled());
assertEquals(fromMap.resolveCrossProject(), ResolutionModeOptions.DEFAULT.crossProject());
}

public void testToXContent() throws IOException {
Expand All @@ -348,8 +354,14 @@ public void testToXContent() throws IOException {
randomBoolean(),
randomBoolean()
);
ResolutionModeOptions resolutionModeOptions = new ResolutionModeOptions(randomBoolean());

IndicesOptions indicesOptions = new IndicesOptions(concreteTargetOptions, wildcardOptions, gatekeeperOptions);
IndicesOptions indicesOptions = new IndicesOptions(
concreteTargetOptions,
wildcardOptions,
gatekeeperOptions,
resolutionModeOptions
);

XContentType type = randomFrom(XContentType.values());
BytesReference xContentBytes = toXContentBytes(indicesOptions, type);
Expand All @@ -364,6 +376,7 @@ public void testToXContent() throws IOException {
assertThat(map.get("ignore_unavailable"), equalTo(concreteTargetOptions.allowUnavailableTargets()));
assertThat(map.get("allow_no_indices"), equalTo(wildcardOptions.allowEmptyExpressions()));
assertThat(map.get("ignore_throttled"), equalTo(gatekeeperOptions.ignoreThrottled()));
assertThat(map.get("resolve_cross_project"), equalTo(resolutionModeOptions.crossProject()));
}

public void testFromXContent() throws IOException {
Expand All @@ -375,10 +388,12 @@ public void testFromXContent() throws IOException {
randomBoolean()
);
ConcreteTargetOptions concreteTargetOptions = new ConcreteTargetOptions(randomBoolean());
ResolutionModeOptions resolutionModeOptions = new ResolutionModeOptions(randomBoolean());

IndicesOptions indicesOptions = IndicesOptions.builder()
.concreteTargetOptions(concreteTargetOptions)
.wildcardOptions(wildcardOptions)
.resolutionModeOptions(resolutionModeOptions)
.build();

XContentType type = randomFrom(XContentType.values());
Expand All @@ -397,6 +412,7 @@ public void testFromXContent() throws IOException {
assertEquals(indicesOptions.ignoreUnavailable(), fromXContentOptions.ignoreUnavailable());
assertEquals(indicesOptions.allowNoIndices(), fromXContentOptions.allowNoIndices());
assertEquals(indicesOptions.ignoreThrottled(), fromXContentOptions.ignoreThrottled());
assertEquals(indicesOptions.resolveCrossProject(), fromXContentOptions.resolveCrossProject());
}

public void testFromXContentWithWildcardSpecialValues() throws IOException {
Expand All @@ -423,6 +439,7 @@ public void testFromXContentWithWildcardSpecialValues() throws IOException {
assertTrue(fromXContentOptions.expandWildcardsClosed());
assertTrue(fromXContentOptions.expandWildcardsHidden());
assertTrue(fromXContentOptions.expandWildcardsOpen());
assertFalse(fromXContentOptions.resolveCrossProject());

try (XContentBuilder builder = XContentFactory.contentBuilder(type)) {
builder.startObject();
Expand All @@ -441,6 +458,7 @@ public void testFromXContentWithWildcardSpecialValues() throws IOException {
assertFalse(fromXContentOptions.expandWildcardsClosed());
assertFalse(fromXContentOptions.expandWildcardsHidden());
assertFalse(fromXContentOptions.expandWildcardsOpen());
assertFalse(fromXContentOptions.resolveCrossProject());
}

public void testFromXContentWithDefaults() throws Exception {
Expand Down Expand Up @@ -494,6 +512,7 @@ public void testFromXContentWithDefaults() throws Exception {
}
assertEquals(ignoreUnavailable, fromXContentOptions.ignoreUnavailable());
assertEquals(expectedWildcardStates, fromXContentOptions.wildcardOptions());
assertFalse(fromXContentOptions.resolveCrossProject());
}

private BytesReference toXContentBytes(IndicesOptions indicesOptions, XContentType type) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2371,7 +2371,8 @@ public void testIgnoreThrottled() {
new IndicesOptions(
IndicesOptions.ConcreteTargetOptions.ERROR_WHEN_UNAVAILABLE_TARGETS,
IndicesOptions.WildcardOptions.DEFAULT,
IndicesOptions.GatekeeperOptions.builder().ignoreThrottled(true).build()
IndicesOptions.GatekeeperOptions.builder().ignoreThrottled(true).build(),
IndicesOptions.ResolutionModeOptions.DEFAULT
),
"ind*",
"test-index"
Expand Down
Loading