From 5ce082cd0aa02995df7cee95d3d6594bb3e33884 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Mon, 20 Aug 2018 08:32:22 -0700 Subject: [PATCH] copy LifecyclePolicy to protocol.xpack (#32915) This is the final PR for copying over the necessary components for clients to parse/render LifecyclePolicy. Changes include: - move of named-x-content server objects away from client - move validation into the client copy of LifecyclePolicy - move LifecycleAction into an interface with `getName` --- .../xpack/core/XPackClientPlugin.java | 15 +- .../core/indexlifecycle/LifecyclePolicy.java | 14 +- .../xpack/core/indexlifecycle/Phase.java | 10 +- .../TimeseriesLifecycleType.java | 19 +- .../xpack/indexlifecycle/IndexLifecycle.java | 27 +- .../xpack/indexlifecycle/AllocateAction.java | 7 +- .../xpack/indexlifecycle/DeleteAction.java | 7 +- .../indexlifecycle/ForceMergeAction.java | 7 +- .../xpack/indexlifecycle/LifecycleAction.java | 9 +- .../xpack/indexlifecycle/LifecyclePolicy.java | 145 +++++++++++ .../protocol/xpack/indexlifecycle/Phase.java | 143 +++++++++++ .../xpack/indexlifecycle/ReadOnlyAction.java | 7 +- .../xpack/indexlifecycle/RolloverAction.java | 7 +- .../xpack/indexlifecycle/ShrinkAction.java | 7 +- .../indexlifecycle/AllocateActionTests.java | 4 + .../indexlifecycle/ForceMergeActionTests.java | 4 + .../indexlifecycle/LifecyclePolicyTests.java | 239 ++++++++++++++++++ .../xpack/indexlifecycle/PhaseTests.java | 76 ++++++ .../indexlifecycle/RolloverActionTests.java | 4 + .../indexlifecycle/ShrinkActionTests.java | 4 + 20 files changed, 711 insertions(+), 44 deletions(-) create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecyclePolicy.java create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/Phase.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecyclePolicyTests.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/PhaseTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index 094cc7e6bef..69fb38d9a17 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java @@ -440,20 +440,7 @@ public class XPackClientPlugin extends Plugin implements ActionPlugin, NetworkPl RollupJobStatus::fromXContent), new NamedXContentRegistry.Entry(PersistentTaskState.class, new ParseField(RollupJobStatus.NAME), RollupJobStatus::fromXContent), - // ILM - Custom Metadata - new NamedXContentRegistry.Entry(MetaData.Custom.class, new ParseField(IndexLifecycleMetadata.TYPE), - parser -> IndexLifecycleMetadata.PARSER.parse(parser, null)), - // ILM - Lifecycle Types - new NamedXContentRegistry.Entry(LifecycleType.class, new ParseField(TimeseriesLifecycleType.TYPE), - (p, c) -> TimeseriesLifecycleType.INSTANCE), - // ILM - Lifecycle Actions - new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(AllocateAction.NAME), AllocateAction::parse), - new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ForceMergeAction.NAME), ForceMergeAction::parse), - new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ReadOnlyAction.NAME), ReadOnlyAction::parse), - new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RolloverAction.NAME), RolloverAction::parse), - new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse), - new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(DeleteAction.NAME), DeleteAction::parse), - // ILM - Client - Lifecycle Actions + // ILM new NamedXContentRegistry.Entry(org.elasticsearch.protocol.xpack.indexlifecycle.LifecycleAction.class, new ParseField(org.elasticsearch.protocol.xpack.indexlifecycle.AllocateAction.NAME), org.elasticsearch.protocol.xpack.indexlifecycle.AllocateAction::parse), diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicy.java index e7b9301f47a..6e01d0ff0f3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicy.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicy.java @@ -34,10 +34,9 @@ import java.util.stream.Collectors; /** * Represents the lifecycle of an index from creation to deletion. A * {@link LifecyclePolicy} is made up of a set of {@link Phase}s which it will - * move through. Soon we will constrain the phases using some kinda of lifecycle - * type which will allow only particular {@link Phase}s to be defined, will - * dictate the order in which the {@link Phase}s are executed and will define - * which {@link LifecycleAction}s are allowed in each phase. + * move through. Policies are constrained by a {@link LifecycleType} which governs which + * {@link Phase}s and {@link LifecycleAction}s are allowed to be defined and in which order + * they are executed. */ public class LifecyclePolicy extends AbstractDiffable implements ToXContentObject, Diffable { @@ -58,9 +57,9 @@ public class LifecyclePolicy extends AbstractDiffable }, PHASES_FIELD); } - protected final String name; - protected final LifecycleType type; - protected final Map phases; + private final String name; + private final LifecycleType type; + private final Map phases; /** * @param name @@ -97,6 +96,7 @@ public class LifecyclePolicy extends AbstractDiffable this.type = type; this.type.validate(phases.values()); } + public static LifecyclePolicy parse(XContentParser parser, String name) { return PARSER.apply(parser, name); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/Phase.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/Phase.java index c2acc441b63..648d3ffc2cf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/Phase.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/Phase.java @@ -51,9 +51,9 @@ public class Phase implements ToXContentObject, Writeable { return PARSER.apply(parser, name); } - private String name; - private Map actions; - private TimeValue after; + private final String name; + private final Map actions; + private final TimeValue after; /** * @param name @@ -133,12 +133,12 @@ public class Phase implements ToXContentObject, Writeable { builder.endObject(); return builder; } - + @Override public int hashCode() { return Objects.hash(name, after, actions); } - + @Override public boolean equals(Object obj) { if (obj == null) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleType.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleType.java index 8ace07e3a9a..8181f791fd5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleType.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleType.java @@ -15,7 +15,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -46,6 +45,14 @@ public class TimeseriesLifecycleType implements LifecycleType { static final Set VALID_DELETE_ACTIONS = Sets.newHashSet(ORDERED_VALID_DELETE_ACTIONS); private static final Phase EMPTY_WARM_PHASE = new Phase("warm", TimeValue.ZERO, Collections.singletonMap("readonly", ReadOnlyAction.INSTANCE)); + private static Map> ALLOWED_ACTIONS = new HashMap<>(); + + static { + ALLOWED_ACTIONS.put("hot", VALID_HOT_ACTIONS); + ALLOWED_ACTIONS.put("warm", VALID_WARM_ACTIONS); + ALLOWED_ACTIONS.put("cold", VALID_COLD_ACTIONS); + ALLOWED_ACTIONS.put("delete", VALID_DELETE_ACTIONS); + } private TimeseriesLifecycleType() { } @@ -182,18 +189,12 @@ public class TimeseriesLifecycleType implements LifecycleType { @Override public void validate(Collection phases) { - Set allowedPhases = new HashSet<>(VALID_PHASES); - Map> allowedActions = new HashMap<>(allowedPhases.size()); - allowedActions.put("hot", VALID_HOT_ACTIONS); - allowedActions.put("warm", VALID_WARM_ACTIONS); - allowedActions.put("cold", VALID_COLD_ACTIONS); - allowedActions.put("delete", VALID_DELETE_ACTIONS); phases.forEach(phase -> { - if (allowedPhases.contains(phase.getName()) == false) { + if (ALLOWED_ACTIONS.containsKey(phase.getName()) == false) { throw new IllegalArgumentException("Timeseries lifecycle does not support phase [" + phase.getName() + "]"); } phase.getActions().forEach((actionName, action) -> { - if (allowedActions.get(phase.getName()).contains(actionName) == false) { + if (ALLOWED_ACTIONS.get(phase.getName()).contains(actionName) == false) { throw new IllegalArgumentException("invalid action [" + actionName + "] " + "defined in phase [" + phase.getName() +"]"); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycle.java index e5c57436190..99b86c52e63 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycle.java @@ -10,8 +10,10 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry; @@ -32,8 +34,17 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.indexlifecycle.AllocateAction; +import org.elasticsearch.xpack.core.indexlifecycle.DeleteAction; +import org.elasticsearch.xpack.core.indexlifecycle.ForceMergeAction; +import org.elasticsearch.xpack.core.indexlifecycle.IndexLifecycleMetadata; +import org.elasticsearch.xpack.core.indexlifecycle.LifecycleAction; import org.elasticsearch.xpack.core.indexlifecycle.LifecycleSettings; +import org.elasticsearch.xpack.core.indexlifecycle.LifecycleType; +import org.elasticsearch.xpack.core.indexlifecycle.ReadOnlyAction; import org.elasticsearch.xpack.core.indexlifecycle.RolloverAction; +import org.elasticsearch.xpack.core.indexlifecycle.ShrinkAction; +import org.elasticsearch.xpack.core.indexlifecycle.TimeseriesLifecycleType; import org.elasticsearch.xpack.core.indexlifecycle.action.DeleteLifecycleAction; import org.elasticsearch.xpack.core.indexlifecycle.action.ExplainLifecycleAction; import org.elasticsearch.xpack.core.indexlifecycle.action.GetLifecycleAction; @@ -146,7 +157,21 @@ public class IndexLifecycle extends Plugin implements ActionPlugin { @Override public List getNamedXContent() { - return Arrays.asList(); + return Arrays.asList( + // Custom Metadata + new NamedXContentRegistry.Entry(MetaData.Custom.class, new ParseField(IndexLifecycleMetadata.TYPE), + parser -> IndexLifecycleMetadata.PARSER.parse(parser, null)), + // Lifecycle Types + new NamedXContentRegistry.Entry(LifecycleType.class, new ParseField(TimeseriesLifecycleType.TYPE), + (p, c) -> TimeseriesLifecycleType.INSTANCE), + // Lifecycle Actions + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(AllocateAction.NAME), AllocateAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ForceMergeAction.NAME), ForceMergeAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ReadOnlyAction.NAME), ReadOnlyAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RolloverAction.NAME), RolloverAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(DeleteAction.NAME), DeleteAction::parse) + ); } @Override diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/AllocateAction.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/AllocateAction.java index 7bbd67fa414..5c8dae1d97f 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/AllocateAction.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/AllocateAction.java @@ -31,7 +31,7 @@ import java.util.Collections; import java.util.Map; import java.util.Objects; -public class AllocateAction extends LifecycleAction implements ToXContentObject { +public class AllocateAction implements LifecycleAction, ToXContentObject { public static final String NAME = "allocate"; static final ParseField NUMBER_OF_REPLICAS_FIELD = new ParseField("number_of_replicas"); @@ -102,6 +102,11 @@ public class AllocateAction extends LifecycleAction implements ToXContentObject return require; } + @Override + public String getName() { + return NAME; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { builder.startObject(); diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/DeleteAction.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/DeleteAction.java index e2978655c08..da3564b3b89 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/DeleteAction.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/DeleteAction.java @@ -27,7 +27,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; -public class DeleteAction extends LifecycleAction implements ToXContentObject { +public class DeleteAction implements LifecycleAction, ToXContentObject { public static final String NAME = "delete"; private static final ObjectParser PARSER = new ObjectParser<>(NAME, DeleteAction::new); @@ -46,6 +46,11 @@ public class DeleteAction extends LifecycleAction implements ToXContentObject { return builder; } + @Override + public String getName() { + return NAME; + } + @Override public int hashCode() { return 1; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ForceMergeAction.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ForceMergeAction.java index a0ef59c10e9..9698868d4e2 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ForceMergeAction.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ForceMergeAction.java @@ -28,7 +28,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import java.util.Objects; -public class ForceMergeAction extends LifecycleAction implements ToXContentObject { +public class ForceMergeAction implements LifecycleAction, ToXContentObject { public static final String NAME = "forcemerge"; private static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments"); @@ -60,6 +60,11 @@ public class ForceMergeAction extends LifecycleAction implements ToXContentObjec return maxNumSegments; } + @Override + public String getName() { + return NAME; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecycleAction.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecycleAction.java index 8765edabd22..433f0356de7 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecycleAction.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecycleAction.java @@ -19,7 +19,12 @@ package org.elasticsearch.protocol.xpack.indexlifecycle; /** - * Marker interface for index lifecycle management actions + * interface for index lifecycle management actions */ -public class LifecycleAction { +public interface LifecycleAction { + + /** + * @return the name of this action + */ + String getName(); } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecyclePolicy.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecyclePolicy.java new file mode 100644 index 00000000000..ff7bcfaae12 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/LifecyclePolicy.java @@ -0,0 +1,145 @@ +/* + * 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.protocol.xpack.indexlifecycle; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Represents the lifecycle of an index from creation to deletion. A + * {@link LifecyclePolicy} is made up of a set of {@link Phase}s which it will + * move through. + */ +public class LifecyclePolicy implements ToXContentObject { + static final ParseField PHASES_FIELD = new ParseField("phases"); + + @SuppressWarnings("unchecked") + public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("lifecycle_policy", false, + (a, name) -> { + List phases = (List) a[0]; + Map phaseMap = phases.stream().collect(Collectors.toMap(Phase::getName, Function.identity())); + return new LifecyclePolicy(name, phaseMap); + }); + private static Map> ALLOWED_ACTIONS = new HashMap<>(); + + static { + PARSER.declareNamedObjects(ConstructingObjectParser.constructorArg(), (p, c, n) -> Phase.parse(p, n), v -> { + throw new IllegalArgumentException("ordered " + PHASES_FIELD.getPreferredName() + " are not supported"); + }, PHASES_FIELD); + + ALLOWED_ACTIONS.put("hot", Sets.newHashSet(RolloverAction.NAME)); + ALLOWED_ACTIONS.put("warm", Sets.newHashSet(AllocateAction.NAME, ForceMergeAction.NAME, ReadOnlyAction.NAME, ShrinkAction.NAME)); + ALLOWED_ACTIONS.put("cold", Sets.newHashSet(AllocateAction.NAME)); + ALLOWED_ACTIONS.put("delete", Sets.newHashSet(DeleteAction.NAME)); + } + + private final String name; + private final Map phases; + + /** + * @param name + * the name of this {@link LifecyclePolicy} + * @param phases + * a {@link Map} of {@link Phase}s which make up this + * {@link LifecyclePolicy}. + */ + public LifecyclePolicy(String name, Map phases) { + phases.values().forEach(phase -> { + if (ALLOWED_ACTIONS.containsKey(phase.getName()) == false) { + throw new IllegalArgumentException("Lifecycle does not support phase [" + phase.getName() + "]"); + } + phase.getActions().forEach((actionName, action) -> { + if (ALLOWED_ACTIONS.get(phase.getName()).contains(actionName) == false) { + throw new IllegalArgumentException("invalid action [" + actionName + "] " + + "defined in phase [" + phase.getName() +"]"); + } + }); + }); + this.name = name; + this.phases = phases; + } + + public static LifecyclePolicy parse(XContentParser parser, String name) { + return PARSER.apply(parser, name); + } + + /** + * @return the name of this {@link LifecyclePolicy} + */ + public String getName() { + return name; + } + + /** + * @return the {@link Phase}s for this {@link LifecyclePolicy} in the order + * in which they will be executed. + */ + public Map getPhases() { + return phases; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.startObject(PHASES_FIELD.getPreferredName()); + for (Phase phase : phases.values()) { + builder.field(phase.getName(), phase); + } + builder.endObject(); + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(name, phases); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj.getClass() != getClass()) { + return false; + } + LifecyclePolicy other = (LifecyclePolicy) obj; + return Objects.equals(name, other.name) && + Objects.equals(phases, other.phases); + } + + @Override + public String toString() { + return Strings.toString(this, true, true); + } +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/Phase.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/Phase.java new file mode 100644 index 00000000000..2cac51e7816 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/Phase.java @@ -0,0 +1,143 @@ +/* + * 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.protocol.xpack.indexlifecycle; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser.ValueType; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Represents set of {@link LifecycleAction}s which should be executed at a + * particular point in the lifecycle of an index. + */ +public class Phase implements ToXContentObject { + + static final ParseField AFTER_FIELD = new ParseField("after"); + static final ParseField ACTIONS_FIELD = new ParseField("actions"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("phase", false, + (a, name) -> new Phase(name, (TimeValue) a[0], ((List) a[1]).stream() + .collect(Collectors.toMap(LifecycleAction::getName, Function.identity())))); + static { + PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), + (p, c) -> TimeValue.parseTimeValue(p.text(), AFTER_FIELD.getPreferredName()), AFTER_FIELD, ValueType.VALUE); + PARSER.declareNamedObjects(ConstructingObjectParser.constructorArg(), + (p, c, n) -> p.namedObject(LifecycleAction.class, n, null), v -> { + throw new IllegalArgumentException("ordered " + ACTIONS_FIELD.getPreferredName() + " are not supported"); + }, ACTIONS_FIELD); + } + + public static Phase parse(XContentParser parser, String name) { + return PARSER.apply(parser, name); + } + + private final String name; + private final Map actions; + private final TimeValue after; + + /** + * @param name + * the name of this {@link Phase}. + * @param after + * the age of the index when the index should move to this + * {@link Phase}. + * @param actions + * a {@link Map} of the {@link LifecycleAction}s to run when + * during this {@link Phase}. The keys in this map are the associated + * action names. + */ + public Phase(String name, TimeValue after, Map actions) { + this.name = name; + if (after == null) { + this.after = TimeValue.ZERO; + } else { + this.after = after; + } + this.actions = actions; + } + + /** + * @return the age of the index when the index should move to this + * {@link Phase}. + */ + public TimeValue getAfter() { + return after; + } + + /** + * @return the name of this {@link Phase} + */ + public String getName() { + return name; + } + + /** + * @return a {@link Map} of the {@link LifecycleAction}s to run when during + * this {@link Phase}. + */ + public Map getActions() { + return actions; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(AFTER_FIELD.getPreferredName(), after.seconds() + "s"); // Need a better way to get a parsable format out here + builder.field(ACTIONS_FIELD.getPreferredName(), actions); + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(name, after, actions); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj.getClass() != getClass()) { + return false; + } + Phase other = (Phase) obj; + return Objects.equals(name, other.name) && + Objects.equals(after, other.after) && + Objects.equals(actions, other.actions); + } + + @Override + public String toString() { + return Strings.toString(this, true, true); + } +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ReadOnlyAction.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ReadOnlyAction.java index cb1bd1bc5d8..ac9f9f59460 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ReadOnlyAction.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ReadOnlyAction.java @@ -26,7 +26,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; -public class ReadOnlyAction extends LifecycleAction implements ToXContentObject { +public class ReadOnlyAction implements LifecycleAction, ToXContentObject { public static final String NAME = "readonly"; private static final ObjectParser PARSER = new ObjectParser<>(NAME, false, ReadOnlyAction::new); @@ -38,6 +38,11 @@ public class ReadOnlyAction extends LifecycleAction implements ToXContentObject public ReadOnlyAction() { } + @Override + public String getName() { + return NAME; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/RolloverAction.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/RolloverAction.java index 9d5c4baf2d4..092fcad45fa 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/RolloverAction.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/RolloverAction.java @@ -32,7 +32,7 @@ import java.io.IOException; import java.util.Objects; -public class RolloverAction extends LifecycleAction implements ToXContentObject { +public class RolloverAction implements LifecycleAction, ToXContentObject { public static final String NAME = "rollover"; private static final ParseField MAX_SIZE_FIELD = new ParseField("max_size"); private static final ParseField MAX_DOCS_FIELD = new ParseField("max_docs"); @@ -76,6 +76,11 @@ public class RolloverAction extends LifecycleAction implements ToXContentObject return maxDocs; } + @Override + public String getName() { + return NAME; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ShrinkAction.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ShrinkAction.java index a2f8e0e86fc..773a2e0713e 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ShrinkAction.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/indexlifecycle/ShrinkAction.java @@ -28,7 +28,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import java.util.Objects; -public class ShrinkAction extends LifecycleAction implements ToXContentObject { +public class ShrinkAction implements LifecycleAction, ToXContentObject { public static final String NAME = "shrink"; private static final ParseField NUMBER_OF_SHARDS_FIELD = new ParseField("number_of_shards"); @@ -56,6 +56,11 @@ public class ShrinkAction extends LifecycleAction implements ToXContentObject { return numberOfShards; } + @Override + public String getName() { + return NAME; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/AllocateActionTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/AllocateActionTests.java index 0edfc327106..5f1dc5b1c6a 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/AllocateActionTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/AllocateActionTests.java @@ -29,6 +29,10 @@ public class AllocateActionTests extends AbstractXContentTestCase includes; if (randomBoolean()) { diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/ForceMergeActionTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/ForceMergeActionTests.java index e1ffdf6b49d..07e94b197e2 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/ForceMergeActionTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/ForceMergeActionTests.java @@ -44,6 +44,10 @@ public class ForceMergeActionTests extends AbstractXContentTestCase { + private static final Set VALID_HOT_ACTIONS = Sets.newHashSet(RolloverAction.NAME); + private static final Set VALID_WARM_ACTIONS = Sets.newHashSet(AllocateAction.NAME, ForceMergeAction.NAME, + ReadOnlyAction.NAME, ShrinkAction.NAME); + private static final Set VALID_COLD_ACTIONS = Sets.newHashSet(AllocateAction.NAME); + private static final Set VALID_DELETE_ACTIONS = Sets.newHashSet(DeleteAction.NAME); + + private String lifecycleName; + + @Override + protected LifecyclePolicy doParseInstance(XContentParser parser) { + return LifecyclePolicy.parse(parser, lifecycleName); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + List entries = new ArrayList<>(ClusterModule.getNamedXWriteables()); + entries.addAll(Arrays.asList( + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(AllocateAction.NAME), AllocateAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(DeleteAction.NAME), DeleteAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ForceMergeAction.NAME), ForceMergeAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ReadOnlyAction.NAME), ReadOnlyAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RolloverAction.NAME), RolloverAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse) + )); + return new NamedXContentRegistry(entries); + } + + @Override + protected LifecyclePolicy createTestInstance() { + lifecycleName = randomAlphaOfLength(5); + List phaseNames = randomSubsetOf(Arrays.asList("hot", "warm", "cold", "delete")); + Map phases = new HashMap<>(phaseNames.size()); + Function> validActions = (phase) -> { + switch (phase) { + case "hot": + return VALID_HOT_ACTIONS; + case "warm": + return VALID_WARM_ACTIONS; + case "cold": + return VALID_COLD_ACTIONS; + case "delete": + return VALID_DELETE_ACTIONS; + default: + throw new IllegalArgumentException("invalid phase [" + phase + "]"); + }}; + Function randomAction = (action) -> { + switch (action) { + case AllocateAction.NAME: + return AllocateActionTests.randomInstance(); + case DeleteAction.NAME: + return new DeleteAction(); + case ForceMergeAction.NAME: + return ForceMergeActionTests.randomInstance(); + case ReadOnlyAction.NAME: + return new ReadOnlyAction(); + case RolloverAction.NAME: + return RolloverActionTests.randomInstance(); + case ShrinkAction.NAME: + return ShrinkActionTests.randomInstance(); + default: + throw new IllegalArgumentException("invalid action [" + action + "]"); + }}; + for (String phase : phaseNames) { + TimeValue after = TimeValue.parseTimeValue(randomTimeValue(0, 1000000000, "s", "m", "h", "d"), "test_after"); + Map actions = new HashMap<>(); + List actionNames = randomSubsetOf(validActions.apply(phase)); + for (String action : actionNames) { + actions.put(action, randomAction.apply(action)); + } + phases.put(phase, new Phase(phase, after, actions)); + } + return new LifecyclePolicy(lifecycleName, phases); + } + + public void testValidatePhases() { + boolean invalid = randomBoolean(); + String phaseName = randomFrom("hot", "warm", "cold", "delete"); + if (invalid) { + phaseName += randomAlphaOfLength(5); + } + Map phases = Collections.singletonMap(phaseName, + new Phase(phaseName, TimeValue.ZERO, Collections.emptyMap())); + if (invalid) { + Exception e = expectThrows(IllegalArgumentException.class, () -> new LifecyclePolicy(lifecycleName, phases)); + assertThat(e.getMessage(), equalTo("Lifecycle does not support phase [" + phaseName + "]")); + } else { + new LifecyclePolicy(lifecycleName, phases); + } + } + + public void testValidateHotPhase() { + LifecycleAction invalidAction = null; + Map actions = randomSubsetOf(VALID_HOT_ACTIONS) + .stream().map(this::getTestAction).collect(Collectors.toMap(LifecycleAction::getName, Function.identity())); + if (randomBoolean()) { + invalidAction = getTestAction(randomFrom("allocate", "forcemerge", "delete", "shrink")); + actions.put(invalidAction.getName(), invalidAction); + } + Map hotPhase = Collections.singletonMap("hot", + new Phase("hot", TimeValue.ZERO, actions)); + + if (invalidAction != null) { + Exception e = expectThrows(IllegalArgumentException.class, + () -> new LifecyclePolicy(lifecycleName, hotPhase)); + assertThat(e.getMessage(), + equalTo("invalid action [" + invalidAction.getName() + "] defined in phase [hot]")); + } else { + new LifecyclePolicy(lifecycleName, hotPhase); + } + } + + public void testValidateWarmPhase() { + LifecycleAction invalidAction = null; + Map actions = randomSubsetOf(VALID_WARM_ACTIONS) + .stream().map(this::getTestAction).collect(Collectors.toMap(LifecycleAction::getName, Function.identity())); + if (randomBoolean()) { + invalidAction = getTestAction(randomFrom("rollover", "delete")); + actions.put(invalidAction.getName(), invalidAction); + } + Map warmPhase = Collections.singletonMap("warm", + new Phase("warm", TimeValue.ZERO, actions)); + + if (invalidAction != null) { + Exception e = expectThrows(IllegalArgumentException.class, + () -> new LifecyclePolicy(lifecycleName, warmPhase)); + assertThat(e.getMessage(), + equalTo("invalid action [" + invalidAction.getName() + "] defined in phase [warm]")); + } else { + new LifecyclePolicy(lifecycleName, warmPhase); + } + } + + public void testValidateColdPhase() { + LifecycleAction invalidAction = null; + Map actions = randomSubsetOf(VALID_COLD_ACTIONS) + .stream().map(this::getTestAction).collect(Collectors.toMap(LifecycleAction::getName, Function.identity())); + if (randomBoolean()) { + invalidAction = getTestAction(randomFrom("rollover", "delete", "forcemerge", "shrink")); + actions.put(invalidAction.getName(), invalidAction); + } + Map coldPhase = Collections.singletonMap("cold", + new Phase("cold", TimeValue.ZERO, actions)); + + if (invalidAction != null) { + Exception e = expectThrows(IllegalArgumentException.class, + () -> new LifecyclePolicy(lifecycleName, coldPhase)); + assertThat(e.getMessage(), + equalTo("invalid action [" + invalidAction.getName() + "] defined in phase [cold]")); + } else { + new LifecyclePolicy(lifecycleName, coldPhase); + } + } + + public void testValidateDeletePhase() { + LifecycleAction invalidAction = null; + Map actions = VALID_DELETE_ACTIONS + .stream().map(this::getTestAction).collect(Collectors.toMap(LifecycleAction::getName, Function.identity())); + if (randomBoolean()) { + invalidAction = getTestAction(randomFrom("allocate", "rollover", "forcemerge", "shrink")); + actions.put(invalidAction.getName(), invalidAction); + } + Map deletePhase = Collections.singletonMap("delete", + new Phase("delete", TimeValue.ZERO, actions)); + + if (invalidAction != null) { + Exception e = expectThrows(IllegalArgumentException.class, + () -> new LifecyclePolicy(lifecycleName, deletePhase)); + assertThat(e.getMessage(), + equalTo("invalid action [" + invalidAction.getName() + "] defined in phase [delete]")); + } else { + new LifecyclePolicy(lifecycleName, deletePhase); + } + } + + private LifecycleAction getTestAction(String actionName) { + switch (actionName) { + case AllocateAction.NAME: + return AllocateActionTests.randomInstance(); + case DeleteAction.NAME: + return new DeleteAction(); + case ForceMergeAction.NAME: + return ForceMergeActionTests.randomInstance(); + case ReadOnlyAction.NAME: + return new ReadOnlyAction(); + case RolloverAction.NAME: + return RolloverActionTests.randomInstance(); + case ShrinkAction.NAME: + return ShrinkActionTests.randomInstance(); + default: + throw new IllegalArgumentException("unsupported phase action [" + actionName + "]"); + } + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/PhaseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/PhaseTests.java new file mode 100644 index 00000000000..d1f49406f3a --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/PhaseTests.java @@ -0,0 +1,76 @@ +/* + * 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.protocol.xpack.indexlifecycle; + +import org.elasticsearch.cluster.ClusterModule; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.junit.Before; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class PhaseTests extends AbstractXContentTestCase { + private String phaseName; + + @Before + public void setup() { + phaseName = randomAlphaOfLength(20); + } + + @Override + protected Phase createTestInstance() { + TimeValue after = null; + if (randomBoolean()) { + after = TimeValue.parseTimeValue(randomTimeValue(0, 1000000000, "s", "m", "h", "d"), "test_after"); + } + Map actions = Collections.emptyMap(); + if (randomBoolean()) { + actions = Collections.singletonMap(DeleteAction.NAME, new DeleteAction()); + } + return new Phase(phaseName, after, actions); + } + + @Override + protected Phase doParseInstance(XContentParser parser) { + return Phase.parse(parser, phaseName); + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + List entries = new ArrayList<>(ClusterModule.getNamedXWriteables()); + entries.add(new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(DeleteAction.NAME), DeleteAction::parse)); + return new NamedXContentRegistry(entries); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + public void testDefaultAfter() { + Phase phase = new Phase(randomAlphaOfLength(20), null, Collections.emptyMap()); + assertEquals(TimeValue.ZERO, phase.getAfter()); + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/RolloverActionTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/RolloverActionTests.java index 9e644e5fec8..813fbc27a9e 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/RolloverActionTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/indexlifecycle/RolloverActionTests.java @@ -38,6 +38,10 @@ public class RolloverActionTests extends AbstractXContentTestCase { @Override protected ShrinkAction createTestInstance() { + return randomInstance(); + } + + static ShrinkAction randomInstance() { return new ShrinkAction(randomIntBetween(1, 100)); }