Przeglądaj źródła

Refactor data stream lifecycle to use the template paradigm (#124593)

Mary Gouseti 7 miesięcy temu
rodzic
commit
ce04da7dea
46 zmienionych plików z 1148 dodań i 756 usunięć
  1. 3 3
      modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java
  2. 14 14
      modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/CrudDataStreamLifecycleIT.java
  3. 1 1
      modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/CrudSystemDataStreamLifecycleIT.java
  4. 17 19
      modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleServiceIT.java
  5. 8 8
      modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/ExplainDataStreamLifecycleIT.java
  6. 7 7
      modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleService.java
  7. 1 1
      modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/action/TransportGetDataStreamLifecycleStatsAction.java
  8. 51 56
      modules/data-streams/src/test/java/org/elasticsearch/datastreams/MetadataIndexTemplateServiceTests.java
  9. 1 1
      modules/data-streams/src/test/java/org/elasticsearch/datastreams/action/GetDataStreamsResponseTests.java
  10. 30 36
      modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleFixtures.java
  11. 21 28
      modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleServiceTests.java
  12. 2 2
      modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/action/TransportGetDataStreamLifecycleStatsActionTests.java
  13. 6 6
      modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/downsampling/DeleteSourceAndAddDownsampleToDSTests.java
  14. 1 0
      server/src/main/java/org/elasticsearch/TransportVersions.java
  15. 2 2
      server/src/main/java/org/elasticsearch/action/admin/indices/template/post/TransportSimulateIndexTemplateAction.java
  16. 2 2
      server/src/main/java/org/elasticsearch/action/datastreams/GetDataStreamAction.java
  17. 18 14
      server/src/main/java/org/elasticsearch/action/datastreams/lifecycle/PutDataStreamLifecycleAction.java
  18. 8 8
      server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java
  19. 406 196
      server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamLifecycle.java
  20. 2 1
      server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java
  21. 19 16
      server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
  22. 1 1
      server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java
  23. 0 1
      server/src/main/java/org/elasticsearch/cluster/metadata/ResettableValue.java
  24. 16 12
      server/src/main/java/org/elasticsearch/cluster/metadata/Template.java
  25. 4 4
      server/src/test/java/org/elasticsearch/action/admin/indices/template/get/GetComponentTemplateResponseTests.java
  26. 1 1
      server/src/test/java/org/elasticsearch/action/datastreams/GetDataStreamActionTests.java
  27. 1 5
      server/src/test/java/org/elasticsearch/action/datastreams/lifecycle/ExplainIndexDataStreamLifecycleTests.java
  28. 1 1
      server/src/test/java/org/elasticsearch/action/datastreams/lifecycle/GetDataStreamLifecycleActionTests.java
  29. 4 4
      server/src/test/java/org/elasticsearch/cluster/metadata/ComponentTemplateTests.java
  30. 6 4
      server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java
  31. 192 0
      server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamLifecycleTemplateTests.java
  32. 111 124
      server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamLifecycleTests.java
  33. 11 15
      server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamLifecycleWithRetentionWarningsTests.java
  34. 42 44
      server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java
  35. 1 1
      server/src/test/java/org/elasticsearch/cluster/metadata/MetadataDataStreamsServiceTests.java
  36. 33 30
      server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java
  37. 1 1
      test/framework/src/main/java/org/elasticsearch/cluster/metadata/DataStreamTestHelper.java
  38. 1 1
      x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/core/action/DataStreamLifecycleUsageTransportActionIT.java
  39. 3 3
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/DataStreamLifecycleUsageTransportAction.java
  40. 3 3
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/datastreams/DataStreamLifecycleFeatureSetUsageTests.java
  41. 5 7
      x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DataStreamLifecycleDownsampleDisruptionIT.java
  42. 37 23
      x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DataStreamLifecycleDownsampleIT.java
  43. 4 4
      x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DataStreamLifecycleDriver.java
  44. 21 14
      x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataStreamAndIndexLifecycleMixingTests.java
  45. 21 25
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/DataStreamLifecycleDownsamplingSecurityIT.java
  46. 8 7
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/DataStreamLifecycleServiceRuntimeSecurityIT.java

+ 3 - 3
modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java

@@ -1384,7 +1384,7 @@ public class DataStreamIT extends ESIntegTestCase {
 
     public void testGetDataStream() throws Exception {
         Settings settings = Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, maximumNumberOfReplicas() + 2).build();
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder().dataRetention(randomMillisUpToYear9999()).build();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder().dataRetention(randomPositiveTimeValue()).build();
         putComposableIndexTemplate("template_for_foo", null, List.of("metrics-foo*"), settings, null, null, lifecycle, false);
         int numDocsFoo = randomIntBetween(2, 16);
         indexDocs("metrics-foo", numDocsFoo);
@@ -1400,7 +1400,7 @@ public class DataStreamIT extends ESIntegTestCase {
         assertThat(metricsFooDataStream.getDataStreamStatus(), is(ClusterHealthStatus.YELLOW));
         assertThat(metricsFooDataStream.getIndexTemplate(), is("template_for_foo"));
         assertThat(metricsFooDataStream.getIlmPolicy(), is(nullValue()));
-        assertThat(dataStream.getLifecycle(), is(lifecycle));
+        assertThat(dataStream.getLifecycle(), is(lifecycle.toDataStreamLifecycle()));
         assertThat(metricsFooDataStream.templatePreferIlmValue(), is(true));
         GetDataStreamAction.Response.IndexProperties indexProperties = metricsFooDataStream.getIndexSettingsValues()
             .get(dataStream.getWriteIndex());
@@ -2450,7 +2450,7 @@ public class DataStreamIT extends ESIntegTestCase {
         @Nullable Settings settings,
         @Nullable Map<String, Object> metadata,
         @Nullable Map<String, AliasMetadata> aliases,
-        @Nullable DataStreamLifecycle lifecycle,
+        @Nullable DataStreamLifecycle.Template lifecycle,
         boolean withFailureStore
     ) throws IOException {
         TransportPutComposableIndexTemplateAction.Request request = new TransportPutComposableIndexTemplateAction.Request(id);

+ 14 - 14
modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/CrudDataStreamLifecycleIT.java

@@ -25,7 +25,7 @@ import java.util.Collection;
 import java.util.List;
 
 import static org.elasticsearch.datastreams.lifecycle.DataStreamLifecycleFixtures.putComposableIndexTemplate;
-import static org.elasticsearch.datastreams.lifecycle.DataStreamLifecycleFixtures.randomLifecycle;
+import static org.elasticsearch.datastreams.lifecycle.DataStreamLifecycleFixtures.randomLifecycleTemplate;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
@@ -39,7 +39,7 @@ public class CrudDataStreamLifecycleIT extends ESIntegTestCase {
     }
 
     public void testGetLifecycle() throws Exception {
-        DataStreamLifecycle lifecycle = randomLifecycle();
+        DataStreamLifecycle.Template lifecycle = randomLifecycleTemplate();
         putComposableIndexTemplate("id1", null, List.of("with-lifecycle*"), null, null, lifecycle);
         putComposableIndexTemplate("id2", null, List.of("without-lifecycle*"), null, null, null);
         {
@@ -82,9 +82,9 @@ public class CrudDataStreamLifecycleIT extends ESIntegTestCase {
             ).get();
             assertThat(response.getDataStreamLifecycles().size(), equalTo(3));
             assertThat(response.getDataStreamLifecycles().get(0).dataStreamName(), equalTo("with-lifecycle-1"));
-            assertThat(response.getDataStreamLifecycles().get(0).lifecycle(), equalTo(lifecycle));
+            assertThat(response.getDataStreamLifecycles().get(0).lifecycle(), equalTo(lifecycle.toDataStreamLifecycle()));
             assertThat(response.getDataStreamLifecycles().get(1).dataStreamName(), equalTo("with-lifecycle-2"));
-            assertThat(response.getDataStreamLifecycles().get(1).lifecycle(), equalTo(lifecycle));
+            assertThat(response.getDataStreamLifecycles().get(1).lifecycle(), equalTo(lifecycle.toDataStreamLifecycle()));
             assertThat(response.getDataStreamLifecycles().get(2).dataStreamName(), equalTo("without-lifecycle"));
             assertThat(response.getDataStreamLifecycles().get(2).lifecycle(), is(nullValue()));
             assertThat(response.getRolloverConfiguration(), nullValue());
@@ -102,9 +102,9 @@ public class CrudDataStreamLifecycleIT extends ESIntegTestCase {
             ).get();
             assertThat(response.getDataStreamLifecycles().size(), equalTo(2));
             assertThat(response.getDataStreamLifecycles().get(0).dataStreamName(), equalTo("with-lifecycle-1"));
-            assertThat(response.getDataStreamLifecycles().get(0).lifecycle(), equalTo(lifecycle));
+            assertThat(response.getDataStreamLifecycles().get(0).lifecycle(), equalTo(lifecycle.toDataStreamLifecycle()));
             assertThat(response.getDataStreamLifecycles().get(1).dataStreamName(), equalTo("with-lifecycle-2"));
-            assertThat(response.getDataStreamLifecycles().get(1).lifecycle(), is(lifecycle));
+            assertThat(response.getDataStreamLifecycles().get(1).lifecycle(), is(lifecycle.toDataStreamLifecycle()));
             assertThat(response.getRolloverConfiguration(), nullValue());
         }
 
@@ -120,7 +120,7 @@ public class CrudDataStreamLifecycleIT extends ESIntegTestCase {
             ).get();
             assertThat(response.getDataStreamLifecycles().size(), equalTo(2));
             assertThat(response.getDataStreamLifecycles().get(0).dataStreamName(), equalTo("with-lifecycle-1"));
-            assertThat(response.getDataStreamLifecycles().get(0).lifecycle(), equalTo(lifecycle));
+            assertThat(response.getDataStreamLifecycles().get(0).lifecycle(), equalTo(lifecycle.toDataStreamLifecycle()));
             assertThat(response.getRolloverConfiguration(), nullValue());
         }
 
@@ -135,9 +135,9 @@ public class CrudDataStreamLifecycleIT extends ESIntegTestCase {
         ).get();
         assertThat(responseWithRollover.getDataStreamLifecycles().size(), equalTo(3));
         assertThat(responseWithRollover.getDataStreamLifecycles().get(0).dataStreamName(), equalTo("with-lifecycle-1"));
-        assertThat(responseWithRollover.getDataStreamLifecycles().get(0).lifecycle(), equalTo(lifecycle));
+        assertThat(responseWithRollover.getDataStreamLifecycles().get(0).lifecycle(), equalTo(lifecycle.toDataStreamLifecycle()));
         assertThat(responseWithRollover.getDataStreamLifecycles().get(1).dataStreamName(), equalTo("with-lifecycle-2"));
-        assertThat(responseWithRollover.getDataStreamLifecycles().get(1).lifecycle(), equalTo(lifecycle));
+        assertThat(responseWithRollover.getDataStreamLifecycles().get(1).lifecycle(), equalTo(lifecycle.toDataStreamLifecycle()));
         assertThat(responseWithRollover.getDataStreamLifecycles().get(2).dataStreamName(), equalTo("without-lifecycle"));
         assertThat(responseWithRollover.getDataStreamLifecycles().get(2).lifecycle(), is(nullValue()));
         assertThat(responseWithRollover.getRolloverConfiguration(), notNullValue());
@@ -192,8 +192,8 @@ public class CrudDataStreamLifecycleIT extends ESIntegTestCase {
             ).get();
             assertThat(response.getDataStreamLifecycles().size(), equalTo(1));
             assertThat(response.getDataStreamLifecycles().get(0).dataStreamName(), equalTo("my-data-stream"));
-            assertThat(response.getDataStreamLifecycles().get(0).lifecycle().getDataStreamRetention(), equalTo(dataRetention));
-            assertThat(response.getDataStreamLifecycles().get(0).lifecycle().isEnabled(), equalTo(true));
+            assertThat(response.getDataStreamLifecycles().get(0).lifecycle().dataRetention(), equalTo(dataRetention));
+            assertThat(response.getDataStreamLifecycles().get(0).lifecycle().enabled(), equalTo(true));
         }
 
         // Disable the lifecycle
@@ -220,13 +220,13 @@ public class CrudDataStreamLifecycleIT extends ESIntegTestCase {
             ).get();
             assertThat(response.getDataStreamLifecycles().size(), equalTo(1));
             assertThat(response.getDataStreamLifecycles().get(0).dataStreamName(), equalTo("my-data-stream"));
-            assertThat(response.getDataStreamLifecycles().get(0).lifecycle().getDataStreamRetention(), equalTo(dataRetention));
-            assertThat(response.getDataStreamLifecycles().get(0).lifecycle().isEnabled(), equalTo(false));
+            assertThat(response.getDataStreamLifecycles().get(0).lifecycle().dataRetention(), equalTo(dataRetention));
+            assertThat(response.getDataStreamLifecycles().get(0).lifecycle().enabled(), equalTo(false));
         }
     }
 
     public void testDeleteLifecycle() throws Exception {
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder().dataRetention(randomMillisUpToYear9999()).build();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder().dataRetention(randomPositiveTimeValue()).build();
         putComposableIndexTemplate("id1", null, List.of("with-lifecycle*"), null, null, lifecycle);
         putComposableIndexTemplate("id2", null, List.of("without-lifecycle*"), null, null, null);
         {

+ 1 - 1
modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/CrudSystemDataStreamLifecycleIT.java

@@ -207,7 +207,7 @@ public class CrudSystemDataStreamLifecycleIT extends ESIntegTestCase {
                                 Template.builder()
                                     .settings(Settings.EMPTY)
                                     .mappings(mappings)
-                                    .lifecycle(DataStreamLifecycle.newBuilder().dataRetention(randomMillisUpToYear9999()).build())
+                                    .lifecycle(DataStreamLifecycle.Template.builder().dataRetention(randomPositiveTimeValue()).build())
                             )
                             .dataStreamTemplate(new DataStreamTemplate())
                             .build(),

+ 17 - 19
modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleServiceIT.java

@@ -146,7 +146,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
 
     public void testRolloverLifecycle() throws Exception {
         // empty lifecycle contains the default rollover
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
 
         putComposableIndexTemplate("id1", null, List.of("metrics-foo*"), null, null, lifecycle, false);
         String dataStreamName = "metrics-foo";
@@ -178,7 +178,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
     }
 
     public void testRolloverAndRetention() throws Exception {
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder().dataRetention(0).build();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder().dataRetention(TimeValue.ZERO).build();
 
         putComposableIndexTemplate("id1", null, List.of("metrics-foo*"), null, null, lifecycle, false);
 
@@ -321,7 +321,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
          * days ago, and one with an origination date 1 day ago. After data stream lifecycle runs, we expect the one with the old
          * origination date to have been deleted, and the one with the newer origination date to remain.
          */
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueDays(7)).build();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder().dataRetention(TimeValue.timeValueDays(7)).build();
 
         putComposableIndexTemplate("id1", null, List.of("metrics-foo*"), null, null, lifecycle, false);
 
@@ -393,7 +393,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
     }
 
     public void testUpdatingLifecycleAppliesToAllBackingIndices() throws Exception {
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
 
         putComposableIndexTemplate("id1", null, List.of("metrics-foo*"), null, null, lifecycle, false);
 
@@ -437,7 +437,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
          * because all necessary merging has already happened automatically. So in order to detect whether forcemerge has been called, we
          * use a SendRequestBehavior in the MockTransportService to detect it.
          */
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
         disableDataStreamLifecycle();
         String dataStreamName = "metrics-foo";
         putComposableIndexTemplate(
@@ -539,7 +539,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
 
     public void testErrorRecordingOnRollover() throws Exception {
         // empty lifecycle contains the default rollover
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
         /*
          * We set index.auto_expand_replicas to 0-1 so that if we get a single-node cluster it is not yellow. The cluster being yellow
          * could result in data stream lifecycle's automatic forcemerge failing, which would result in an unexpected error in the error
@@ -697,7 +697,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
     public void testErrorRecordingOnRetention() throws Exception {
         // starting with a lifecycle without retention so we can rollover the data stream and manipulate the second generation index such
         // that its retention execution fails
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
 
         /*
          * We set index.auto_expand_replicas to 0-1 so that if we get a single-node cluster it is not yellow. The cluster being yellow
@@ -871,7 +871,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
     }
 
     public void testDataLifecycleServiceConfiguresTheMergePolicy() throws Exception {
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
 
         putComposableIndexTemplate(
             "id1",
@@ -972,7 +972,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
 
     public void testReenableDataStreamLifecycle() throws Exception {
         // start with a lifecycle that's not enabled
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle(null, null, false);
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder().enabled(false).build();
 
         putComposableIndexTemplate("id1", null, List.of("metrics-foo*"), null, null, lifecycle, false);
         String dataStreamName = "metrics-foo";
@@ -1031,15 +1031,13 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
 
     public void testLifecycleAppliedToFailureStore() throws Exception {
         // We configure a lifecycle with downsampling to ensure it doesn't fail
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
-            .dataRetention(20_000)
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder()
+            .dataRetention(TimeValue.timeValueSeconds(20))
             .downsampling(
-                new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueMillis(10),
-                            new DownsampleConfig(new DateHistogramInterval("10m"))
-                        )
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(10),
+                        new DownsampleConfig(new DateHistogramInterval("10m"))
                     )
                 )
             )
@@ -1205,7 +1203,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
         List<String> patterns,
         @Nullable Settings settings,
         @Nullable Map<String, Object> metadata,
-        @Nullable DataStreamLifecycle lifecycle,
+        @Nullable DataStreamLifecycle.Template lifecycle,
         boolean withFailureStore
     ) throws IOException {
         TransportPutComposableIndexTemplateAction.Request request = new TransportPutComposableIndexTemplateAction.Request(id);
@@ -1268,7 +1266,7 @@ public class DataStreamLifecycleServiceIT extends ESIntegTestCase {
                             Template.builder()
                                 .settings(Settings.EMPTY)
                                 .lifecycle(
-                                    DataStreamLifecycle.newBuilder()
+                                    DataStreamLifecycle.Template.builder()
                                         .dataRetention(TimeValue.timeValueDays(SYSTEM_DATA_STREAM_RETENTION_DAYS))
                                 )
                         )

+ 8 - 8
modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/ExplainDataStreamLifecycleIT.java

@@ -86,7 +86,7 @@ public class ExplainDataStreamLifecycleIT extends ESIntegTestCase {
 
     public void testExplainLifecycle() throws Exception {
         // empty lifecycle contains the default rollover
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
 
         putComposableIndexTemplate("id1", null, List.of("metrics-foo*"), null, null, lifecycle);
         String dataStreamName = "metrics-foo";
@@ -134,7 +134,7 @@ public class ExplainDataStreamLifecycleIT extends ESIntegTestCase {
                 assertThat(explainIndex.isManagedByLifecycle(), is(true));
                 assertThat(explainIndex.getIndexCreationDate(), notNullValue());
                 assertThat(explainIndex.getLifecycle(), notNullValue());
-                assertThat(explainIndex.getLifecycle().getDataStreamRetention(), nullValue());
+                assertThat(explainIndex.getLifecycle().dataRetention(), nullValue());
                 if (internalCluster().numDataNodes() > 1) {
                     // If the number of nodes is 1 then the cluster will be yellow so forcemerge will report an error if it has run
                     assertThat(explainIndex.getError(), nullValue());
@@ -193,7 +193,7 @@ public class ExplainDataStreamLifecycleIT extends ESIntegTestCase {
                 assertThat(explainIndex.isManagedByLifecycle(), is(true));
                 assertThat(explainIndex.getIndexCreationDate(), notNullValue());
                 assertThat(explainIndex.getLifecycle(), notNullValue());
-                assertThat(explainIndex.getLifecycle().getDataStreamRetention(), nullValue());
+                assertThat(explainIndex.getLifecycle().dataRetention(), nullValue());
 
                 if (explainIndex.getIndex().equals(DataStream.getDefaultBackingIndexName(dataStreamName, 1))) {
                     // first generation index was rolled over
@@ -263,7 +263,7 @@ public class ExplainDataStreamLifecycleIT extends ESIntegTestCase {
                 assertThat(explainIndex.getIndexCreationDate(), notNullValue());
                 assertThat(explainIndex.getLifecycle(), notNullValue());
                 assertThat(
-                    explainIndex.getLifecycle().getDataStreamRetention(),
+                    explainIndex.getLifecycle().dataRetention(),
                     equalTo(TimeValue.timeValueDays(SYSTEM_DATA_STREAM_RETENTION_DAYS))
                 );
             }
@@ -274,7 +274,7 @@ public class ExplainDataStreamLifecycleIT extends ESIntegTestCase {
 
     public void testExplainLifecycleForIndicesWithErrors() throws Exception {
         // empty lifecycle contains the default rollover
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
 
         putComposableIndexTemplate("id1", null, List.of("metrics-foo*"), null, null, lifecycle);
 
@@ -329,7 +329,7 @@ public class ExplainDataStreamLifecycleIT extends ESIntegTestCase {
                 assertThat(explainIndex.isManagedByLifecycle(), is(true));
                 assertThat(explainIndex.getIndexCreationDate(), notNullValue());
                 assertThat(explainIndex.getLifecycle(), notNullValue());
-                assertThat(explainIndex.getLifecycle().getDataStreamRetention(), nullValue());
+                assertThat(explainIndex.getLifecycle().dataRetention(), nullValue());
                 assertThat(explainIndex.getRolloverDate(), nullValue());
                 assertThat(explainIndex.getTimeSinceRollover(System::currentTimeMillis), nullValue());
                 // index has not been rolled over yet
@@ -374,7 +374,7 @@ public class ExplainDataStreamLifecycleIT extends ESIntegTestCase {
             List.of("metrics-foo*"),
             null,
             null,
-            DataStreamLifecycle.newBuilder().enabled(false).build()
+            DataStreamLifecycle.Template.builder().enabled(false).build()
         );
         CreateDataStreamAction.Request createDataStreamRequest = new CreateDataStreamAction.Request(
             TEST_REQUEST_TIMEOUT,
@@ -439,7 +439,7 @@ public class ExplainDataStreamLifecycleIT extends ESIntegTestCase {
         List<String> patterns,
         @Nullable Settings settings,
         @Nullable Map<String, Object> metadata,
-        @Nullable DataStreamLifecycle lifecycle
+        @Nullable DataStreamLifecycle.Template lifecycle
     ) throws IOException {
         TransportPutComposableIndexTemplateAction.Request request = new TransportPutComposableIndexTemplateAction.Request(id);
         request.indexTemplate(

+ 7 - 7
modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleService.java

@@ -479,7 +479,7 @@ public class DataStreamLifecycleService implements ClusterStateListener, Closeab
         for (Index index : targetIndices) {
             IndexMetadata backingIndexMeta = metadata.getProject().index(index);
             assert backingIndexMeta != null : "the data stream backing indices must exist";
-            List<DataStreamLifecycle.Downsampling.Round> downsamplingRounds = dataStream.getDownsamplingRoundsFor(
+            List<DataStreamLifecycle.DownsamplingRound> downsamplingRounds = dataStream.getDownsamplingRoundsFor(
                 index,
                 metadata.getProject()::index,
                 nowSupplier
@@ -517,18 +517,18 @@ public class DataStreamLifecycleService implements ClusterStateListener, Closeab
     private Set<Index> waitForInProgressOrTriggerDownsampling(
         DataStream dataStream,
         IndexMetadata backingIndex,
-        List<DataStreamLifecycle.Downsampling.Round> downsamplingRounds,
+        List<DataStreamLifecycle.DownsamplingRound> downsamplingRounds,
         Metadata metadata
     ) {
         assert dataStream.getIndices().contains(backingIndex.getIndex())
             : "the provided backing index must be part of data stream:" + dataStream.getName();
         assert downsamplingRounds.isEmpty() == false : "the index should be managed and have matching downsampling rounds";
         Set<Index> affectedIndices = new HashSet<>();
-        DataStreamLifecycle.Downsampling.Round lastRound = downsamplingRounds.get(downsamplingRounds.size() - 1);
+        DataStreamLifecycle.DownsamplingRound lastRound = downsamplingRounds.get(downsamplingRounds.size() - 1);
 
         Index index = backingIndex.getIndex();
         String indexName = index.getName();
-        for (DataStreamLifecycle.Downsampling.Round round : downsamplingRounds) {
+        for (DataStreamLifecycle.DownsamplingRound round : downsamplingRounds) {
             // the downsample index name for each round is deterministic
             String downsampleIndexName = DownsampleConfig.generateDownsampleIndexName(
                 DOWNSAMPLED_INDEX_PREFIX,
@@ -566,7 +566,7 @@ public class DataStreamLifecycleService implements ClusterStateListener, Closeab
     /**
      * Issues a request downsample the source index to the downsample index for the specified round.
      */
-    private void downsampleIndexOnce(DataStreamLifecycle.Downsampling.Round round, String sourceIndex, String downsampleIndexName) {
+    private void downsampleIndexOnce(DataStreamLifecycle.DownsamplingRound round, String sourceIndex, String downsampleIndexName) {
         DownsampleAction.Request request = new DownsampleAction.Request(
             TimeValue.THIRTY_SECONDS /* TODO should this be longer/configurable? */,
             sourceIndex,
@@ -599,8 +599,8 @@ public class DataStreamLifecycleService implements ClusterStateListener, Closeab
     private Set<Index> evaluateDownsampleStatus(
         DataStream dataStream,
         IndexMetadata.DownsampleTaskStatus downsampleStatus,
-        DataStreamLifecycle.Downsampling.Round currentRound,
-        DataStreamLifecycle.Downsampling.Round lastRound,
+        DataStreamLifecycle.DownsamplingRound currentRound,
+        DataStreamLifecycle.DownsamplingRound lastRound,
         Index backingIndex,
         Index downsampleIndex
     ) {

+ 1 - 1
modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/action/TransportGetDataStreamLifecycleStatsAction.java

@@ -76,7 +76,7 @@ public class TransportGetDataStreamLifecycleStatsAction extends TransportMasterN
         Set<String> indicesInErrorStore = lifecycleService.getErrorStore().getAllIndices();
         List<GetDataStreamLifecycleStatsAction.Response.DataStreamStats> dataStreamStats = new ArrayList<>();
         for (DataStream dataStream : state.metadata().getProject().dataStreams().values()) {
-            if (dataStream.getLifecycle() != null && dataStream.getLifecycle().isEnabled()) {
+            if (dataStream.getLifecycle() != null && dataStream.getLifecycle().enabled()) {
                 int total = 0;
                 int inError = 0;
                 for (Index index : dataStream.getIndices()) {

+ 51 - 56
modules/data-streams/src/test/java/org/elasticsearch/datastreams/MetadataIndexTemplateServiceTests.java

@@ -17,13 +17,13 @@ import org.elasticsearch.cluster.metadata.DataStreamLifecycle;
 import org.elasticsearch.cluster.metadata.MetadataCreateIndexService;
 import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
+import org.elasticsearch.cluster.metadata.ResettableValue;
 import org.elasticsearch.cluster.metadata.Template;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.compress.CompressedXContent;
 import org.elasticsearch.common.settings.ClusterSettings;
 import org.elasticsearch.common.settings.IndexScopedSettings;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.index.IndexSettingProviders;
@@ -139,53 +139,53 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
     public void testLifecycleComposition() {
         // No lifecycles result to null
         {
-            List<DataStreamLifecycle> lifecycles = List.of();
+            List<DataStreamLifecycle.Template> lifecycles = List.of();
             assertThat(composeDataLifecycles(lifecycles), nullValue());
         }
         // One lifecycle results to this lifecycle as the final
         {
-            DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
+            DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder()
                 .dataRetention(randomRetention())
                 .downsampling(randomDownsampling())
                 .build();
-            List<DataStreamLifecycle> lifecycles = List.of(lifecycle);
-            DataStreamLifecycle result = composeDataLifecycles(lifecycles);
+            List<DataStreamLifecycle.Template> lifecycles = List.of(lifecycle);
+            DataStreamLifecycle result = composeDataLifecycles(lifecycles).toDataStreamLifecycle();
             // Defaults to true
-            assertThat(result.isEnabled(), equalTo(true));
-            assertThat(result.getDataStreamRetention(), equalTo(lifecycle.getDataStreamRetention()));
-            assertThat(result.getDownsamplingRounds(), equalTo(lifecycle.getDownsamplingRounds()));
+            assertThat(result.enabled(), equalTo(true));
+            assertThat(result.dataRetention(), equalTo(lifecycle.dataRetention().get()));
+            assertThat(result.downsampling(), equalTo(lifecycle.downsampling().get()));
         }
         // If the last lifecycle is missing a property (apart from enabled) we keep the latest from the previous ones
         // Enabled is always true unless it's explicitly set to false
         {
-            DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
+            DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder()
                 .enabled(false)
-                .dataRetention(randomNonEmptyRetention())
-                .downsampling(randomNonEmptyDownsampling())
+                .dataRetention(randomPositiveTimeValue())
+                .downsampling(randomRounds())
                 .build();
-            List<DataStreamLifecycle> lifecycles = List.of(lifecycle, new DataStreamLifecycle());
-            DataStreamLifecycle result = composeDataLifecycles(lifecycles);
-            assertThat(result.isEnabled(), equalTo(true));
-            assertThat(result.getDataStreamRetention(), equalTo(lifecycle.getDataStreamRetention()));
-            assertThat(result.getDownsamplingRounds(), equalTo(lifecycle.getDownsamplingRounds()));
+            List<DataStreamLifecycle.Template> lifecycles = List.of(lifecycle, DataStreamLifecycle.Template.DEFAULT);
+            DataStreamLifecycle result = composeDataLifecycles(lifecycles).toDataStreamLifecycle();
+            assertThat(result.enabled(), equalTo(true));
+            assertThat(result.dataRetention(), equalTo(lifecycle.dataRetention().get()));
+            assertThat(result.downsampling(), equalTo(lifecycle.downsampling().get()));
         }
         // If both lifecycle have all properties, then the latest one overwrites all the others
         {
-            DataStreamLifecycle lifecycle1 = DataStreamLifecycle.newBuilder()
+            DataStreamLifecycle.Template lifecycle1 = DataStreamLifecycle.Template.builder()
                 .enabled(false)
-                .dataRetention(randomNonEmptyRetention())
-                .downsampling(randomNonEmptyDownsampling())
+                .dataRetention(randomPositiveTimeValue())
+                .downsampling(randomRounds())
                 .build();
-            DataStreamLifecycle lifecycle2 = DataStreamLifecycle.newBuilder()
+            DataStreamLifecycle.Template lifecycle2 = DataStreamLifecycle.Template.builder()
                 .enabled(true)
-                .dataRetention(randomNonEmptyRetention())
-                .downsampling(randomNonEmptyDownsampling())
+                .dataRetention(randomPositiveTimeValue())
+                .downsampling(randomRounds())
                 .build();
-            List<DataStreamLifecycle> lifecycles = List.of(lifecycle1, lifecycle2);
-            DataStreamLifecycle result = composeDataLifecycles(lifecycles);
-            assertThat(result.isEnabled(), equalTo(lifecycle2.isEnabled()));
-            assertThat(result.getDataStreamRetention(), equalTo(lifecycle2.getDataStreamRetention()));
-            assertThat(result.getDownsamplingRounds(), equalTo(lifecycle2.getDownsamplingRounds()));
+            List<DataStreamLifecycle.Template> lifecycles = List.of(lifecycle1, lifecycle2);
+            DataStreamLifecycle result = composeDataLifecycles(lifecycles).toDataStreamLifecycle();
+            assertThat(result.enabled(), equalTo(lifecycle2.enabled()));
+            assertThat(result.dataRetention(), equalTo(lifecycle2.dataRetention().get()));
+            assertThat(result.downsampling(), equalTo(lifecycle2.downsampling().get()));
         }
     }
 
@@ -230,49 +230,44 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         return new ShardLimitValidator(limitOnlySettings, clusterService);
     }
 
-    @Nullable
-    private static DataStreamLifecycle.Retention randomRetention() {
-        return switch (randomInt(2)) {
-            case 0 -> null;
-            case 1 -> DataStreamLifecycle.Retention.NULL;
-            default -> randomNonEmptyRetention();
-        };
-    }
-
-    private static DataStreamLifecycle.Retention randomNonEmptyRetention() {
-        return new DataStreamLifecycle.Retention(TimeValue.timeValueMillis(randomMillisUpToYear9999()));
-    }
-
-    @Nullable
-    private static DataStreamLifecycle.Downsampling randomDownsampling() {
-        return switch (randomInt(2)) {
-            case 0 -> null;
-            case 1 -> DataStreamLifecycle.Downsampling.NULL;
-            default -> randomNonEmptyDownsampling();
-        };
-    }
-
-    private static DataStreamLifecycle.Downsampling randomNonEmptyDownsampling() {
+    private static List<DataStreamLifecycle.DownsamplingRound> randomRounds() {
         var count = randomIntBetween(0, 9);
-        List<DataStreamLifecycle.Downsampling.Round> rounds = new ArrayList<>();
-        var previous = new DataStreamLifecycle.Downsampling.Round(
+        List<DataStreamLifecycle.DownsamplingRound> rounds = new ArrayList<>();
+        var previous = new DataStreamLifecycle.DownsamplingRound(
             TimeValue.timeValueDays(randomIntBetween(1, 365)),
             new DownsampleConfig(new DateHistogramInterval(randomIntBetween(1, 24) + "h"))
         );
         rounds.add(previous);
         for (int i = 0; i < count; i++) {
-            DataStreamLifecycle.Downsampling.Round round = nextRound(previous);
+            DataStreamLifecycle.DownsamplingRound round = nextRound(previous);
             rounds.add(round);
             previous = round;
         }
-        return new DataStreamLifecycle.Downsampling(rounds);
+        return rounds;
     }
 
-    private static DataStreamLifecycle.Downsampling.Round nextRound(DataStreamLifecycle.Downsampling.Round previous) {
+    private static DataStreamLifecycle.DownsamplingRound nextRound(DataStreamLifecycle.DownsamplingRound previous) {
         var after = TimeValue.timeValueDays(previous.after().days() + randomIntBetween(1, 10));
         var fixedInterval = new DownsampleConfig(
             new DateHistogramInterval((previous.config().getFixedInterval().estimateMillis() * randomIntBetween(2, 5)) + "ms")
         );
-        return new DataStreamLifecycle.Downsampling.Round(after, fixedInterval);
+        return new DataStreamLifecycle.DownsamplingRound(after, fixedInterval);
+    }
+
+    private static ResettableValue<TimeValue> randomRetention() {
+        return switch (randomIntBetween(0, 2)) {
+            case 0 -> ResettableValue.undefined();
+            case 1 -> ResettableValue.reset();
+            case 2 -> ResettableValue.create(TimeValue.timeValueDays(randomIntBetween(1, 100)));
+            default -> throw new IllegalStateException("Unknown randomisation path");
+        };
+    }
+
+    private static ResettableValue<List<DataStreamLifecycle.DownsamplingRound>> randomDownsampling() {
+        return switch (randomIntBetween(0, 1)) {
+            case 0 -> ResettableValue.reset();
+            case 1 -> ResettableValue.create(randomRounds());
+            default -> throw new IllegalStateException("Unknown randomisation path");
+        };
     }
 }

+ 1 - 1
modules/data-streams/src/test/java/org/elasticsearch/datastreams/action/GetDataStreamsResponseTests.java

@@ -158,7 +158,7 @@ public class GetDataStreamsResponseTests extends ESTestCase {
                 .setGeneration(3)
                 .setAllowCustomRouting(true)
                 .setIndexMode(IndexMode.STANDARD)
-                .setLifecycle(new DataStreamLifecycle(null, null, false))
+                .setLifecycle(new DataStreamLifecycle(false, null, null))
                 .setDataStreamOptions(DataStreamOptions.FAILURE_STORE_ENABLED)
                 .setFailureIndices(DataStream.DataStreamIndices.failureIndicesBuilder(failureStores).build())
                 .build();

+ 30 - 36
modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleFixtures.java

@@ -19,6 +19,7 @@ import org.elasticsearch.cluster.metadata.DataStreamLifecycle;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.metadata.ProjectMetadata;
+import org.elasticsearch.cluster.metadata.ResettableValue;
 import org.elasticsearch.cluster.metadata.Template;
 import org.elasticsearch.common.compress.CompressedXContent;
 import org.elasticsearch.common.settings.Settings;
@@ -26,18 +27,18 @@ import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.index.Index;
 import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
+import org.elasticsearch.test.ESTestCase;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Supplier;
 
 import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance;
 import static org.elasticsearch.test.ESIntegTestCase.client;
 import static org.elasticsearch.test.ESTestCase.frequently;
-import static org.elasticsearch.test.ESTestCase.randomInt;
 import static org.elasticsearch.test.ESTestCase.randomIntBetween;
-import static org.elasticsearch.test.ESTestCase.randomMillisUpToYear9999;
 import static org.junit.Assert.assertTrue;
 
 /**
@@ -150,7 +151,7 @@ public class DataStreamLifecycleFixtures {
         List<String> patterns,
         @Nullable Settings settings,
         @Nullable Map<String, Object> metadata,
-        @Nullable DataStreamLifecycle lifecycle
+        @Nullable DataStreamLifecycle.Template lifecycle
     ) throws IOException {
         TransportPutComposableIndexTemplateAction.Request request = new TransportPutComposableIndexTemplateAction.Request(id);
         request.indexTemplate(
@@ -169,51 +170,44 @@ public class DataStreamLifecycleFixtures {
         assertTrue(client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet().isAcknowledged());
     }
 
-    static DataStreamLifecycle randomLifecycle() {
-        return DataStreamLifecycle.newBuilder()
-            .dataRetention(randomRetention())
-            .downsampling(randomDownsampling())
+    static DataStreamLifecycle.Template randomLifecycleTemplate() {
+        return DataStreamLifecycle.Template.builder()
+            .dataRetention(randomResettable(ESTestCase::randomTimeValue))
+            .downsampling(randomResettable(DataStreamLifecycleFixtures::randomDownsamplingRounds))
             .enabled(frequently())
             .build();
     }
 
-    @Nullable
-    private static DataStreamLifecycle.Retention randomRetention() {
-        return switch (randomInt(2)) {
-            case 0 -> null;
-            case 1 -> DataStreamLifecycle.Retention.NULL;
-            default -> new DataStreamLifecycle.Retention(TimeValue.timeValueMillis(randomMillisUpToYear9999()));
+    private static <T> ResettableValue<T> randomResettable(Supplier<T> supplier) {
+        return switch (randomIntBetween(0, 2)) {
+            case 0 -> ResettableValue.undefined();
+            case 1 -> ResettableValue.reset();
+            case 2 -> ResettableValue.create(supplier.get());
+            default -> throw new IllegalStateException("Unknown randomisation path");
         };
     }
 
-    @Nullable
-    private static DataStreamLifecycle.Downsampling randomDownsampling() {
-        return switch (randomInt(2)) {
-            case 0 -> null;
-            case 1 -> DataStreamLifecycle.Downsampling.NULL;
-            default -> {
-                var count = randomIntBetween(0, 9);
-                List<DataStreamLifecycle.Downsampling.Round> rounds = new ArrayList<>();
-                var previous = new DataStreamLifecycle.Downsampling.Round(
-                    TimeValue.timeValueDays(randomIntBetween(1, 365)),
-                    new DownsampleConfig(new DateHistogramInterval(randomIntBetween(1, 24) + "h"))
-                );
-                rounds.add(previous);
-                for (int i = 0; i < count; i++) {
-                    DataStreamLifecycle.Downsampling.Round round = nextRound(previous);
-                    rounds.add(round);
-                    previous = round;
-                }
-                yield new DataStreamLifecycle.Downsampling(rounds);
-            }
-        };
+    private static List<DataStreamLifecycle.DownsamplingRound> randomDownsamplingRounds() {
+        var count = randomIntBetween(0, 9);
+        List<DataStreamLifecycle.DownsamplingRound> rounds = new ArrayList<>();
+        var previous = new DataStreamLifecycle.DownsamplingRound(
+            TimeValue.timeValueDays(randomIntBetween(1, 365)),
+            new DownsampleConfig(new DateHistogramInterval(randomIntBetween(1, 24) + "h"))
+        );
+        rounds.add(previous);
+        for (int i = 0; i < count; i++) {
+            DataStreamLifecycle.DownsamplingRound round = nextRound(previous);
+            rounds.add(round);
+            previous = round;
+        }
+        return rounds;
     }
 
-    private static DataStreamLifecycle.Downsampling.Round nextRound(DataStreamLifecycle.Downsampling.Round previous) {
+    private static DataStreamLifecycle.DownsamplingRound nextRound(DataStreamLifecycle.DownsamplingRound previous) {
         var after = TimeValue.timeValueDays(previous.after().days() + randomIntBetween(1, 10));
         var fixedInterval = new DownsampleConfig(
             new DateHistogramInterval((previous.config().getFixedInterval().estimateMillis() * randomIntBetween(2, 5)) + "ms")
         );
-        return new DataStreamLifecycle.Downsampling.Round(after, fixedInterval);
+        return new DataStreamLifecycle.DownsamplingRound(after, fixedInterval);
     }
 }

+ 21 - 28
modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleServiceTests.java

@@ -39,8 +39,7 @@ import org.elasticsearch.cluster.block.ClusterBlocks;
 import org.elasticsearch.cluster.metadata.DataStream;
 import org.elasticsearch.cluster.metadata.DataStreamGlobalRetentionSettings;
 import org.elasticsearch.cluster.metadata.DataStreamLifecycle;
-import org.elasticsearch.cluster.metadata.DataStreamLifecycle.Downsampling;
-import org.elasticsearch.cluster.metadata.DataStreamLifecycle.Downsampling.Round;
+import org.elasticsearch.cluster.metadata.DataStreamLifecycle.DownsamplingRound;
 import org.elasticsearch.cluster.metadata.DataStreamOptions;
 import org.elasticsearch.cluster.metadata.DataStreamTestHelper;
 import org.elasticsearch.cluster.metadata.IndexAbstraction;
@@ -206,7 +205,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             numBackingIndices,
             2,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(0).build(),
+            DataStreamLifecycle.builder().dataRetention(0).build(),
             now
         );
         builder.put(dataStream);
@@ -269,7 +268,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             numBackingIndices,
             numFailureIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueDays(700)).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.timeValueDays(700)).build(),
             now
         );
         builder.put(dataStream);
@@ -302,7 +301,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             dataStream.copy()
                 .setName(dataStreamName)
                 .setGeneration(dataStream.getGeneration() + 1)
-                .setLifecycle(DataStreamLifecycle.newBuilder().dataRetention(0L).build())
+                .setLifecycle(DataStreamLifecycle.builder().dataRetention(0L).build())
                 .build()
         );
         clusterState = ClusterState.builder(clusterState).metadata(builder).build();
@@ -339,7 +338,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             dataStream.copy()
                 .setName(dataStreamName)
                 .setGeneration(dataStream.getGeneration() + 1)
-                .setLifecycle(DataStreamLifecycle.newBuilder().build())
+                .setLifecycle(DataStreamLifecycle.builder().build())
                 .build()
         );
         clusterState = ClusterState.builder(clusterState).metadata(builder).build();
@@ -374,7 +373,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueMillis(0)).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.timeValueMillis(0)).build(),
             now
         );
         builder.put(dataStream);
@@ -454,7 +453,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             Settings.builder()
                 .put(IndexMetadata.LIFECYCLE_NAME, "ILM_policy")
                 .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(0).build(),
+            DataStreamLifecycle.builder().dataRetention(0).build(),
             now
         );
         builder.put(dataStream);
@@ -543,7 +542,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueDays(700)).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.timeValueDays(700)).build(),
             now
         );
         // all backing indices are in the error store
@@ -581,7 +580,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             ilmManagedDataStreamName,
             3,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueDays(700)).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.timeValueDays(700)).build(),
             now
         );
         // all backing indices are in the error store
@@ -594,7 +593,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             dataStreamWithBackingIndicesInErrorState,
             5,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueDays(700)).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.timeValueDays(700)).build(),
             now
         );
         // put all backing indices in the error store
@@ -645,7 +644,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             numBackingIndices,
             settings(IndexVersion.current()).put(MergePolicyConfig.INDEX_MERGE_POLICY_FLOOR_SEGMENT_SETTING.getKey(), ONE_HUNDRED_MB)
                 .put(MergePolicyConfig.INDEX_MERGE_POLICY_MERGE_FACTOR_SETTING.getKey(), TARGET_MERGE_FACTOR_VALUE),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         builder.put(dataStream);
@@ -771,7 +770,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             numBackingIndices,
             settings(IndexVersion.current()).put(MergePolicyConfig.INDEX_MERGE_POLICY_FLOOR_SEGMENT_SETTING.getKey(), ONE_HUNDRED_MB)
                 .put(MergePolicyConfig.INDEX_MERGE_POLICY_MERGE_FACTOR_SETTING.getKey(), TARGET_MERGE_FACTOR_VALUE),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         builder.put(dataStream);
@@ -950,7 +949,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         builder.put(dataStream);
@@ -1142,7 +1141,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         builder.put(dataStream);
@@ -1224,11 +1223,9 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
                 .put(MergePolicyConfig.INDEX_MERGE_POLICY_MERGE_FACTOR_SETTING.getKey(), TARGET_MERGE_FACTOR_VALUE)
                 .put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
                 .put("index.routing_path", "@timestamp"),
-            DataStreamLifecycle.newBuilder()
+            DataStreamLifecycle.builder()
                 .downsampling(
-                    new Downsampling(
-                        List.of(new Round(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))))
-                    )
+                    List.of(new DownsamplingRound(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))))
                 )
                 .dataRetention(TimeValue.MAX_VALUE)
                 .build(),
@@ -1362,11 +1359,9 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
                 .put(MergePolicyConfig.INDEX_MERGE_POLICY_MERGE_FACTOR_SETTING.getKey(), TARGET_MERGE_FACTOR_VALUE)
                 .put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
                 .put("index.routing_path", "@timestamp"),
-            DataStreamLifecycle.newBuilder()
+            DataStreamLifecycle.builder()
                 .downsampling(
-                    new Downsampling(
-                        List.of(new Round(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))))
-                    )
+                    List.of(new DownsamplingRound(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))))
                 )
                 .dataRetention(TimeValue.MAX_VALUE)
                 .build(),
@@ -1550,7 +1545,7 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             numBackingIndices,
             2,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(0).build(),
+            DataStreamLifecycle.builder().dataRetention(0).build(),
             now
         ).copy().setDataStreamOptions(DataStreamOptions.FAILURE_STORE_DISABLED).build(); // failure store is managed even when disabled
         builder.put(dataStream);
@@ -1613,11 +1608,9 @@ public class DataStreamLifecycleServiceTests extends ESTestCase {
             2,
             settings(IndexVersion.current()).put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
                 .put("index.routing_path", "@timestamp"),
-            DataStreamLifecycle.newBuilder()
+            DataStreamLifecycle.builder()
                 .downsampling(
-                    new Downsampling(
-                        List.of(new Round(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))))
-                    )
+                    List.of(new DownsamplingRound(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))))
                 )
                 .dataRetention(TimeValue.timeValueMillis(1))
                 .build(),

+ 2 - 2
modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/action/TransportGetDataStreamLifecycleStatsActionTests.java

@@ -94,7 +94,7 @@ public class TransportGetDataStreamLifecycleStatsActionTests extends ESTestCase
             "dsl-managed-index",
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueDays(10)).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.timeValueDays(10)).build(),
             Clock.systemUTC().millis()
         );
         indicesInError.add(dslDataStream.getIndices().get(randomInt(numBackingIndices - 1)).getName());
@@ -130,7 +130,7 @@ public class TransportGetDataStreamLifecycleStatsActionTests extends ESTestCase
             IndexMetadata indexMetadata = indexMetaBuilder.build();
             builder.put(indexMetadata, false);
             backingIndices.add(indexMetadata.getIndex());
-            builder.put(newInstance(dataStreamName, backingIndices, 3, null, false, DataStreamLifecycle.newBuilder().build()));
+            builder.put(newInstance(dataStreamName, backingIndices, 3, null, false, DataStreamLifecycle.builder().build()));
         }
         ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(builder).build();
         when(errorStore.getAllIndices()).thenReturn(indicesInError);

+ 6 - 6
modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/downsampling/DeleteSourceAndAddDownsampleToDSTests.java

@@ -52,7 +52,7 @@ public class DeleteSourceAndAddDownsampleToDSTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         builder.put(dataStream);
@@ -80,7 +80,7 @@ public class DeleteSourceAndAddDownsampleToDSTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         String firstGenIndex = DataStream.getDefaultBackingIndexName(dataStreamName, 1);
@@ -125,7 +125,7 @@ public class DeleteSourceAndAddDownsampleToDSTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         builder.put(dataStream);
@@ -159,7 +159,7 @@ public class DeleteSourceAndAddDownsampleToDSTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         String firstGenIndex = DataStream.getDefaultBackingIndexName(dataStreamName, 1);
@@ -221,7 +221,7 @@ public class DeleteSourceAndAddDownsampleToDSTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         String firstGenIndex = DataStream.getDefaultBackingIndexName(dataStreamName, 1);
@@ -272,7 +272,7 @@ public class DeleteSourceAndAddDownsampleToDSTests extends ESTestCase {
             dataStreamName,
             numBackingIndices,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(TimeValue.MAX_VALUE).build(),
+            DataStreamLifecycle.builder().dataRetention(TimeValue.MAX_VALUE).build(),
             now
         );
         String firstGenIndex = DataStream.getDefaultBackingIndexName(dataStreamName, 1);

+ 1 - 0
server/src/main/java/org/elasticsearch/TransportVersions.java

@@ -188,6 +188,7 @@ public class TransportVersions {
     public static final TransportVersion ESQL_FAILURE_FROM_REMOTE = def(9_030_00_0);
     public static final TransportVersion INDEX_RESHARDING_METADATA = def(9_031_0_00);
     public static final TransportVersion INFERENCE_MODEL_REGISTRY_METADATA = def(9_032_0_00);
+    public static final TransportVersion INTRODUCE_LIFECYCLE_TEMPLATE = def(9_033_0_00);
 
     /*
      * STOP! READ THIS FIRST! No, really,

+ 2 - 2
server/src/main/java/org/elasticsearch/action/admin/indices/template/post/TransportSimulateIndexTemplateAction.java

@@ -344,9 +344,9 @@ public class TransportSimulateIndexTemplateAction extends TransportLocalProjectM
         );
 
         Settings settings = Settings.builder().put(additionalSettings.build()).put(templateSettings).build();
-        DataStreamLifecycle lifecycle = resolveLifecycle(simulatedProject, matchingTemplate);
+        DataStreamLifecycle.Template lifecycle = resolveLifecycle(simulatedProject, matchingTemplate);
         if (template.getDataStreamTemplate() != null && lifecycle == null && isDslOnlyMode) {
-            lifecycle = DataStreamLifecycle.DEFAULT;
+            lifecycle = DataStreamLifecycle.Template.DEFAULT;
         }
         return new Template(
             settings,

+ 2 - 2
server/src/main/java/org/elasticsearch/action/datastreams/GetDataStreamAction.java

@@ -471,7 +471,7 @@ public class GetDataStreamAction extends ActionType<GetDataStreamAction.Response
              */
             public ManagedBy getNextGenerationManagedBy() {
                 // both ILM and DSL are configured so let's check the prefer_ilm setting to see which system takes precedence
-                if (ilmPolicyName != null && dataStream.getLifecycle() != null && dataStream.getLifecycle().isEnabled()) {
+                if (ilmPolicyName != null && dataStream.getLifecycle() != null && dataStream.getLifecycle().enabled()) {
                     return templatePreferIlmValue ? ManagedBy.ILM : ManagedBy.LIFECYCLE;
                 }
 
@@ -479,7 +479,7 @@ public class GetDataStreamAction extends ActionType<GetDataStreamAction.Response
                     return ManagedBy.ILM;
                 }
 
-                if (dataStream.getLifecycle() != null && dataStream.getLifecycle().isEnabled()) {
+                if (dataStream.getLifecycle() != null && dataStream.getLifecycle().enabled()) {
                     return ManagedBy.LIFECYCLE;
                 }
 

+ 18 - 14
server/src/main/java/org/elasticsearch/action/datastreams/lifecycle/PutDataStreamLifecycleAction.java

@@ -28,11 +28,11 @@ import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
 
 import static org.elasticsearch.cluster.metadata.DataStreamLifecycle.DATA_RETENTION_FIELD;
 import static org.elasticsearch.cluster.metadata.DataStreamLifecycle.DOWNSAMPLING_FIELD;
-import static org.elasticsearch.cluster.metadata.DataStreamLifecycle.Downsampling;
 import static org.elasticsearch.cluster.metadata.DataStreamLifecycle.ENABLED_FIELD;
 
 /**
@@ -47,30 +47,34 @@ public class PutDataStreamLifecycleAction {
     public static final class Request extends AcknowledgedRequest<Request> implements IndicesRequest.Replaceable, ToXContentObject {
 
         public interface Factory {
-            Request create(@Nullable TimeValue dataRetention, @Nullable Boolean enabled, @Nullable Downsampling downsampling);
+            Request create(
+                @Nullable TimeValue dataRetention,
+                @Nullable Boolean enabled,
+                @Nullable List<DataStreamLifecycle.DownsamplingRound> downsampling
+            );
         }
 
+        @SuppressWarnings("unchecked")
         public static final ConstructingObjectParser<Request, Factory> PARSER = new ConstructingObjectParser<>(
             "put_data_stream_lifecycle_request",
             false,
-            (args, factory) -> factory.create((TimeValue) args[0], (Boolean) args[1], (Downsampling) args[2])
+            (args, factory) -> factory.create((TimeValue) args[0], (Boolean) args[1], (List<DataStreamLifecycle.DownsamplingRound>) args[2])
         );
 
         static {
             PARSER.declareField(
                 ConstructingObjectParser.optionalConstructorArg(),
-                (p, c) -> TimeValue.parseTimeValue(p.textOrNull(), DATA_RETENTION_FIELD.getPreferredName()),
+                (p, c) -> TimeValue.parseTimeValue(p.text(), DATA_RETENTION_FIELD.getPreferredName()),
                 DATA_RETENTION_FIELD,
-                ObjectParser.ValueType.STRING_OR_NULL
+                ObjectParser.ValueType.STRING
             );
             PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), ENABLED_FIELD);
-            PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
-                if (p.currentToken() == XContentParser.Token.VALUE_NULL) {
-                    return Downsampling.NULL;
-                } else {
-                    return new Downsampling(AbstractObjectParser.parseArray(p, null, Downsampling.Round::fromXContent));
-                }
-            }, DOWNSAMPLING_FIELD, ObjectParser.ValueType.OBJECT_ARRAY_OR_NULL);
+            PARSER.declareField(
+                ConstructingObjectParser.optionalConstructorArg(),
+                (p, c) -> AbstractObjectParser.parseArray(p, null, DataStreamLifecycle.DownsamplingRound::fromXContent),
+                DOWNSAMPLING_FIELD,
+                ObjectParser.ValueType.OBJECT_ARRAY
+            );
         }
 
         public static Request parseRequest(XContentParser parser, Factory factory) {
@@ -141,11 +145,11 @@ public class PutDataStreamLifecycleAction {
             String[] names,
             @Nullable TimeValue dataRetention,
             @Nullable Boolean enabled,
-            @Nullable Downsampling downsampling
+            @Nullable List<DataStreamLifecycle.DownsamplingRound> downsampling
         ) {
             super(masterNodeTimeout, ackTimeout);
             this.names = names;
-            this.lifecycle = DataStreamLifecycle.newBuilder()
+            this.lifecycle = DataStreamLifecycle.builder()
                 .dataRetention(dataRetention)
                 .enabled(enabled == null || enabled)
                 .downsampling(downsampling)

+ 8 - 8
server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java

@@ -23,7 +23,7 @@ import org.elasticsearch.action.admin.indices.rollover.RolloverInfo;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.cluster.Diff;
 import org.elasticsearch.cluster.SimpleDiffable;
-import org.elasticsearch.cluster.metadata.DataStreamLifecycle.Downsampling.Round;
+import org.elasticsearch.cluster.metadata.DataStreamLifecycle.DownsamplingRound;
 import org.elasticsearch.common.ParsingException;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesReference;
@@ -946,7 +946,7 @@ public final class DataStream implements SimpleDiffable<DataStream>, ToXContentO
         DataStreamGlobalRetention globalRetention
     ) {
         if (lifecycle == null
-            || lifecycle.isEnabled() == false
+            || lifecycle.enabled() == false
             || lifecycle.getEffectiveDataRetention(globalRetention, isInternal()) == null) {
             return List.of();
         }
@@ -967,13 +967,13 @@ public final class DataStream implements SimpleDiffable<DataStream>, ToXContentO
      *
      * An empty list is returned for indices that are not time series.
      */
-    public List<Round> getDownsamplingRoundsFor(
+    public List<DownsamplingRound> getDownsamplingRoundsFor(
         Index index,
         Function<String, IndexMetadata> indexMetadataSupplier,
         LongSupplier nowSupplier
     ) {
         assert backingIndices.indices.contains(index) : "the provided index must be a backing index for this datastream";
-        if (lifecycle == null || lifecycle.getDownsamplingRounds() == null) {
+        if (lifecycle == null || lifecycle.downsampling() == null) {
             return List.of();
         }
 
@@ -986,8 +986,8 @@ public final class DataStream implements SimpleDiffable<DataStream>, ToXContentO
         if (indexGenerationTime != null) {
             long nowMillis = nowSupplier.getAsLong();
             long indexGenerationTimeMillis = indexGenerationTime.millis();
-            List<Round> orderedRoundsForIndex = new ArrayList<>(lifecycle.getDownsamplingRounds().size());
-            for (Round round : lifecycle.getDownsamplingRounds()) {
+            List<DownsamplingRound> orderedRoundsForIndex = new ArrayList<>(lifecycle.downsampling().size());
+            for (DownsamplingRound round : lifecycle.downsampling()) {
                 if (nowMillis >= indexGenerationTimeMillis + round.after().getMillis()) {
                     orderedRoundsForIndex.add(round);
                 }
@@ -1076,11 +1076,11 @@ public final class DataStream implements SimpleDiffable<DataStream>, ToXContentO
      * access method.
      */
     private boolean isIndexManagedByDataStreamLifecycle(IndexMetadata indexMetadata) {
-        if (indexMetadata.getLifecyclePolicyName() != null && lifecycle != null && lifecycle.isEnabled()) {
+        if (indexMetadata.getLifecyclePolicyName() != null && lifecycle != null && lifecycle.enabled()) {
             // when both ILM and data stream lifecycle are configured, choose depending on the configured preference for this backing index
             return PREFER_ILM_SETTING.get(indexMetadata.getSettings()) == false;
         }
-        return lifecycle != null && lifecycle.isEnabled();
+        return lifecycle != null && lifecycle.enabled();
     }
 
     /**

+ 406 - 196
server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamLifecycle.java

@@ -31,7 +31,6 @@ import org.elasticsearch.xcontent.ConstructingObjectParser;
 import org.elasticsearch.xcontent.ObjectParser;
 import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ToXContent;
-import org.elasticsearch.xcontent.ToXContentFragment;
 import org.elasticsearch.xcontent.ToXContentObject;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
@@ -98,51 +97,60 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
     public static final ParseField DOWNSAMPLING_FIELD = new ParseField("downsampling");
     private static final ParseField ROLLOVER_FIELD = new ParseField("rollover");
 
+    @SuppressWarnings("unchecked")
     public static final ConstructingObjectParser<DataStreamLifecycle, Void> PARSER = new ConstructingObjectParser<>(
         "lifecycle",
         false,
-        (args, unused) -> new DataStreamLifecycle((Retention) args[0], (Downsampling) args[1], (Boolean) args[2])
+        (args, unused) -> new DataStreamLifecycle((Boolean) args[0], (TimeValue) args[1], (List<DownsamplingRound>) args[2])
     );
 
     static {
+        PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), ENABLED_FIELD);
+        // For the retention and the downsampling, there was a bug that would allow an explicit null value to be
+        // stored in the data stream when the lifecycle was not composed with another one. We need to be able to read
+        // from a previous cluster state so we allow here explicit null values also to be parsed.
         PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
             String value = p.textOrNull();
             if (value == null) {
-                return Retention.NULL;
+                return null;
             } else {
-                return new Retention(TimeValue.parseTimeValue(value, DATA_RETENTION_FIELD.getPreferredName()));
+                return TimeValue.parseTimeValue(value, DATA_RETENTION_FIELD.getPreferredName());
             }
         }, DATA_RETENTION_FIELD, ObjectParser.ValueType.STRING_OR_NULL);
         PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
             if (p.currentToken() == XContentParser.Token.VALUE_NULL) {
-                return Downsampling.NULL;
+                return null;
             } else {
-                return new Downsampling(AbstractObjectParser.parseArray(p, c, Downsampling.Round::fromXContent));
+                return AbstractObjectParser.parseArray(p, c, DownsamplingRound::fromXContent);
             }
         }, DOWNSAMPLING_FIELD, ObjectParser.ValueType.OBJECT_ARRAY_OR_NULL);
-        PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), ENABLED_FIELD);
     }
 
+    private final boolean enabled;
     @Nullable
-    private final Retention dataRetention;
+    private final TimeValue dataRetention;
     @Nullable
-    private final Downsampling downsampling;
-    private final boolean enabled;
+    private final List<DownsamplingRound> downsampling;
 
     public DataStreamLifecycle() {
         this(null, null, null);
     }
 
-    public DataStreamLifecycle(@Nullable Retention dataRetention, @Nullable Downsampling downsampling, @Nullable Boolean enabled) {
+    public DataStreamLifecycle(
+        @Nullable Boolean enabled,
+        @Nullable TimeValue dataRetention,
+        @Nullable List<DownsamplingRound> downsampling
+    ) {
         this.enabled = enabled == null || enabled;
         this.dataRetention = dataRetention;
+        DownsamplingRound.validateRounds(downsampling);
         this.downsampling = downsampling;
     }
 
     /**
-     * Returns true, if this data stream lifecycle configuration is enabled and false otherwise
+     * Returns true, if this data stream lifecycle configuration is enabled, false otherwise
      */
-    public boolean isEnabled() {
+    public boolean enabled() {
         return enabled;
     }
 
@@ -173,22 +181,21 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
         boolean isInternalDataStream
     ) {
         // If lifecycle is disabled there is no effective retention
-        if (enabled == false) {
+        if (enabled() == false) {
             return INFINITE_RETENTION;
         }
-        var dataStreamRetention = getDataStreamRetention();
         if (globalRetention == null || isInternalDataStream) {
-            return Tuple.tuple(dataStreamRetention, RetentionSource.DATA_STREAM_CONFIGURATION);
+            return Tuple.tuple(dataRetention(), RetentionSource.DATA_STREAM_CONFIGURATION);
         }
-        if (dataStreamRetention == null) {
+        if (dataRetention() == null) {
             return globalRetention.defaultRetention() != null
                 ? Tuple.tuple(globalRetention.defaultRetention(), RetentionSource.DEFAULT_GLOBAL_RETENTION)
                 : Tuple.tuple(globalRetention.maxRetention(), RetentionSource.MAX_GLOBAL_RETENTION);
         }
-        if (globalRetention.maxRetention() != null && globalRetention.maxRetention().getMillis() < dataStreamRetention.getMillis()) {
+        if (globalRetention.maxRetention() != null && globalRetention.maxRetention().getMillis() < dataRetention().getMillis()) {
             return Tuple.tuple(globalRetention.maxRetention(), RetentionSource.MAX_GLOBAL_RETENTION);
         } else {
-            return Tuple.tuple(dataStreamRetention, RetentionSource.DATA_STREAM_CONFIGURATION);
+            return Tuple.tuple(dataRetention(), RetentionSource.DATA_STREAM_CONFIGURATION);
         }
     }
 
@@ -198,8 +205,8 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
      * @return the time period or null, null represents that data should never be deleted.
      */
     @Nullable
-    public TimeValue getDataStreamRetention() {
-        return dataRetention == null ? null : dataRetention.value;
+    public TimeValue dataRetention() {
+        return dataRetention;
     }
 
     /**
@@ -228,10 +235,10 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
                     + "] will be applied."
             );
             case MAX_GLOBAL_RETENTION -> {
-                String retentionProvidedPart = getDataStreamRetention() == null
+                String retentionProvidedPart = dataRetention() == null
                     ? "Not providing a retention is not allowed for this project."
                     : "The retention provided ["
-                        + (getDataStreamRetention() == null ? "infinite" : getDataStreamRetention().getStringRep())
+                        + (dataRetention() == null ? "infinite" : dataRetention().getStringRep())
                         + "] is exceeding the max allowed data retention of this project ["
                         + effectiveRetentionStringRep
                         + "].";
@@ -244,35 +251,12 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
         }
     }
 
-    /**
-     * The configuration as provided by the user about the least amount of time data should be kept by elasticsearch.
-     * This method differentiates between a missing retention and a nullified retention and this is useful for template
-     * composition.
-     * @return one of the following:
-     * - `null`, represents that the user did not provide data retention, this represents the user has no opinion about retention
-     * - `Retention{value = null}`, represents that the user explicitly wants to have infinite retention
-     * - `Retention{value = "10d"}`, represents that the user has requested the data to be kept at least 10d.
-     */
-    @Nullable
-    Retention getDataRetention() {
-        return dataRetention;
-    }
-
     /**
      * The configured downsampling rounds with the `after` and the `fixed_interval` per round. If downsampling is
      * not configured then it returns null.
      */
     @Nullable
-    public List<Downsampling.Round> getDownsamplingRounds() {
-        return downsampling == null ? null : downsampling.rounds();
-    }
-
-    /**
-     * Returns the configured wrapper object as it was defined in the template. This should be used only during
-     * template composition.
-     */
-    @Nullable
-    Downsampling getDownsampling() {
+    public List<DownsamplingRound> downsampling() {
         return downsampling;
     }
 
@@ -289,28 +273,45 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
 
     @Override
     public int hashCode() {
-        return Objects.hash(dataRetention, downsampling, enabled);
+        return Objects.hash(enabled, dataRetention, downsampling);
     }
 
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_9_X)) {
-            out.writeOptionalWriteable(dataRetention);
+            if (out.getTransportVersion().onOrAfter(TransportVersions.INTRODUCE_LIFECYCLE_TEMPLATE)) {
+                out.writeOptionalTimeValue(dataRetention);
+            } else {
+                writeLegacyOptionalValue(dataRetention, out, StreamOutput::writeTimeValue);
+            }
+
         }
         if (out.getTransportVersion().onOrAfter(ADDED_ENABLED_FLAG_VERSION)) {
-            out.writeOptionalWriteable(downsampling);
-            out.writeBoolean(enabled);
+            if (out.getTransportVersion().onOrAfter(TransportVersions.INTRODUCE_LIFECYCLE_TEMPLATE)) {
+                out.writeOptionalCollection(downsampling);
+            } else {
+                writeLegacyOptionalValue(downsampling, out, StreamOutput::writeCollection);
+            }
+            out.writeBoolean(enabled());
         }
     }
 
     public DataStreamLifecycle(StreamInput in) throws IOException {
         if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_9_X)) {
-            dataRetention = in.readOptionalWriteable(Retention::read);
+            if (in.getTransportVersion().onOrAfter(TransportVersions.INTRODUCE_LIFECYCLE_TEMPLATE)) {
+                dataRetention = in.readOptionalTimeValue();
+            } else {
+                dataRetention = readLegacyOptionalValue(in, StreamInput::readTimeValue);
+            }
         } else {
             dataRetention = null;
         }
         if (in.getTransportVersion().onOrAfter(ADDED_ENABLED_FLAG_VERSION)) {
-            downsampling = in.readOptionalWriteable(Downsampling::read);
+            if (in.getTransportVersion().onOrAfter(TransportVersions.INTRODUCE_LIFECYCLE_TEMPLATE)) {
+                downsampling = in.readOptionalCollectionAsList(DownsamplingRound::read);
+            } else {
+                downsampling = readLegacyOptionalValue(in, is -> is.readCollectionAsList(DownsamplingRound::read));
+            }
             enabled = in.readBoolean();
         } else {
             downsampling = null;
@@ -318,6 +319,43 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
         }
     }
 
+    /**
+     * Previous versions were serialising <code>value</code> in way that also captures an explicit null. Meaning, first they serialise
+     * a boolean flag signaling if this value is defined, and then another boolean flag if the value is non-null. We do not need explicit
+     * null values anymore, so we treat them the same as non defined, but for bwc reasons we still need to write all the flags.
+     * @param value value to be serialised that used to be explicitly nullable.
+     * @param writer the writer of the value, it should NOT be an optional writer
+     * @throws IOException
+     */
+    private static <T> void writeLegacyOptionalValue(T value, StreamOutput out, Writer<T> writer) throws IOException {
+        boolean isDefined = value != null;
+        out.writeBoolean(isDefined);
+        if (isDefined) {
+            // There are no explicit null values anymore, so it's always true
+            out.writeBoolean(true);
+            writer.write(out, value);
+        }
+    }
+
+    /**
+     * Previous versions were de-serialising <code>value</code> in way that also captures an explicit null. Meaning, first they de-serialise
+     * a boolean flag signaling if this value is defined, and then another boolean flag if the value is non-null. We do not need explicit
+     * null values anymore, so we treat them the same as non defined, but for bwc reasons we still need to read all the flags.
+     * @param reader the reader of the value, it should NOT be an optional reader
+     * @throws IOException
+     */
+    private static <T> T readLegacyOptionalValue(StreamInput in, Reader<T> reader) throws IOException {
+        T value = null;
+        boolean isDefined = in.readBoolean();
+        if (isDefined) {
+            boolean isNotNull = in.readBoolean();
+            if (isNotNull) {
+                value = reader.read(in);
+            }
+        }
+        return value;
+    }
+
     public static Diff<DataStreamLifecycle> readDiffFrom(StreamInput in) throws IOException {
         return SimpleDiffable.readDiffFrom(DataStreamLifecycle::new, in);
     }
@@ -349,11 +387,7 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
         builder.startObject();
         builder.field(ENABLED_FIELD.getPreferredName(), enabled);
         if (dataRetention != null) {
-            if (dataRetention.value() == null) {
-                builder.nullField(DATA_RETENTION_FIELD.getPreferredName());
-            } else {
-                builder.field(DATA_RETENTION_FIELD.getPreferredName(), dataRetention.value().getStringRep());
-            }
+            builder.field(DATA_RETENTION_FIELD.getPreferredName(), dataRetention.getStringRep());
         }
         Tuple<TimeValue, RetentionSource> effectiveDataRetentionWithSource = getEffectiveDataRetentionWithSource(
             globalRetention,
@@ -367,8 +401,7 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
         }
 
         if (downsampling != null) {
-            builder.field(DOWNSAMPLING_FIELD.getPreferredName());
-            downsampling.toXContent(builder, params);
+            builder.field(DOWNSAMPLING_FIELD.getPreferredName(), downsampling);
         }
         if (rolloverConfiguration != null) {
             builder.field(ROLLOVER_FIELD.getPreferredName());
@@ -397,218 +430,395 @@ public class DataStreamLifecycle implements SimpleDiffable<DataStreamLifecycle>,
         return new DelegatingMapParams(INCLUDE_EFFECTIVE_RETENTION_PARAMS, params);
     }
 
-    public static Builder newBuilder(DataStreamLifecycle lifecycle) {
-        return new Builder().dataRetention(lifecycle.getDataRetention())
-            .downsampling(lifecycle.getDownsampling())
-            .enabled(lifecycle.isEnabled());
+    public static Builder builder(DataStreamLifecycle lifecycle) {
+        return new Builder(lifecycle);
     }
 
-    public static Builder newBuilder() {
-        return new Builder();
+    public static Builder builder() {
+        return new Builder(null);
     }
 
     /**
      * This builder helps during the composition of the data stream lifecycle templates.
      */
     public static class Builder {
+        private boolean enabled = true;
         @Nullable
-        private Retention dataRetention = null;
+        private TimeValue dataRetention = null;
         @Nullable
-        private Downsampling downsampling = null;
-        private boolean enabled = true;
+        private List<DownsamplingRound> downsampling = null;
 
-        public Builder enabled(boolean value) {
-            enabled = value;
-            return this;
+        private Builder(@Nullable DataStreamLifecycle lifecycle) {
+            if (lifecycle != null) {
+                enabled = lifecycle.enabled;
+                dataRetention = lifecycle.dataRetention;
+                downsampling = lifecycle.downsampling;
+            }
         }
 
-        public Builder dataRetention(@Nullable Retention value) {
-            dataRetention = value;
+        public Builder enabled(boolean value) {
+            enabled = value;
             return this;
         }
 
         public Builder dataRetention(@Nullable TimeValue value) {
-            dataRetention = value == null ? null : new Retention(value);
+            dataRetention = value;
             return this;
         }
 
         public Builder dataRetention(long value) {
-            dataRetention = new Retention(TimeValue.timeValueMillis(value));
+            dataRetention = TimeValue.timeValueMillis(value);
             return this;
         }
 
-        public Builder downsampling(@Nullable Downsampling value) {
-            downsampling = value;
+        public Builder downsampling(@Nullable List<DownsamplingRound> rounds) {
+            downsampling = rounds;
             return this;
         }
 
         public DataStreamLifecycle build() {
-            return new DataStreamLifecycle(dataRetention, downsampling, enabled);
+            return new DataStreamLifecycle(enabled, dataRetention, downsampling);
         }
     }
 
     /**
-     * Retention is the least amount of time that the data will be kept by elasticsearch. Public for testing.
-     * @param value is a time period or null. Null represents an explicitly set infinite retention period
+     * This enum represents all configuration sources that can influence the retention of a data stream.
      */
-    public record Retention(@Nullable TimeValue value) implements Writeable {
-
-        // For testing
-        public static final Retention NULL = new Retention(null);
-
-        public static Retention read(StreamInput in) throws IOException {
-            return new Retention(in.readOptionalTimeValue());
-        }
+    public enum RetentionSource {
+        DATA_STREAM_CONFIGURATION,
+        DEFAULT_GLOBAL_RETENTION,
+        MAX_GLOBAL_RETENTION;
 
-        @Override
-        public void writeTo(StreamOutput out) throws IOException {
-            out.writeOptionalTimeValue(value);
+        public String displayName() {
+            return this.toString().toLowerCase(Locale.ROOT);
         }
     }
 
     /**
-     * Downsampling holds the configuration about when should elasticsearch downsample a backing index.
-     * @param rounds is a list of downsampling configuration which instructs when a backing index should be downsampled (`after`) and at
-     *               which interval (`fixed_interval`). Null represents an explicit no downsampling during template composition.
+     * A round represents the configuration for when and how elasticsearch will downsample a backing index.
+     * @param after is a TimeValue configuring how old (based on generation age) should a backing index be before downsampling
+     * @param config contains the interval that the backing index is going to be downsampled.
      */
-    public record Downsampling(@Nullable List<Round> rounds) implements Writeable, ToXContentFragment {
+    public record DownsamplingRound(TimeValue after, DownsampleConfig config) implements Writeable, ToXContentObject {
 
+        public static final ParseField AFTER_FIELD = new ParseField("after");
+        public static final ParseField FIXED_INTERVAL_FIELD = new ParseField("fixed_interval");
         public static final long FIVE_MINUTES_MILLIS = TimeValue.timeValueMinutes(5).getMillis();
 
-        /**
-         * A round represents the configuration for when and how elasticsearch will downsample a backing index.
-         * @param after is a TimeValue configuring how old (based on generation age) should a backing index be before downsampling
-         * @param config contains the interval that the backing index is going to be downsampled.
-         */
-        public record Round(TimeValue after, DownsampleConfig config) implements Writeable, ToXContentObject {
-
-            public static final ParseField AFTER_FIELD = new ParseField("after");
-            public static final ParseField FIXED_INTERVAL_FIELD = new ParseField("fixed_interval");
+        private static final ConstructingObjectParser<DownsamplingRound, Void> PARSER = new ConstructingObjectParser<>(
+            "downsampling_round",
+            false,
+            (args, unused) -> new DownsamplingRound((TimeValue) args[0], new DownsampleConfig((DateHistogramInterval) args[1]))
+        );
 
-            private static final ConstructingObjectParser<Round, Void> PARSER = new ConstructingObjectParser<>(
-                "downsampling_round",
-                false,
-                (args, unused) -> new Round((TimeValue) args[0], new DownsampleConfig((DateHistogramInterval) args[1]))
+        static {
+            PARSER.declareString(
+                ConstructingObjectParser.optionalConstructorArg(),
+                value -> TimeValue.parseTimeValue(value, AFTER_FIELD.getPreferredName()),
+                AFTER_FIELD
+            );
+            PARSER.declareField(
+                constructorArg(),
+                p -> new DateHistogramInterval(p.text()),
+                new ParseField(FIXED_INTERVAL_FIELD.getPreferredName()),
+                ObjectParser.ValueType.STRING
             );
+        }
 
-            static {
-                PARSER.declareString(
-                    ConstructingObjectParser.optionalConstructorArg(),
-                    value -> TimeValue.parseTimeValue(value, AFTER_FIELD.getPreferredName()),
-                    AFTER_FIELD
+        static void validateRounds(List<DownsamplingRound> rounds) {
+            if (rounds == null) {
+                return;
+            }
+            if (rounds.isEmpty()) {
+                throw new IllegalArgumentException("Downsampling configuration should have at least one round configured.");
+            }
+            if (rounds.size() > 10) {
+                throw new IllegalArgumentException(
+                    "Downsampling configuration supports maximum 10 configured rounds. Found: " + rounds.size()
                 );
-                PARSER.declareField(
-                    constructorArg(),
-                    p -> new DateHistogramInterval(p.text()),
-                    new ParseField(FIXED_INTERVAL_FIELD.getPreferredName()),
-                    ObjectParser.ValueType.STRING
+            }
+            DownsamplingRound previous = null;
+            for (DownsamplingRound round : rounds) {
+                if (previous == null) {
+                    previous = round;
+                } else {
+                    if (round.after.compareTo(previous.after) < 0) {
+                        throw new IllegalArgumentException(
+                            "A downsampling round must have a later 'after' value than the proceeding, "
+                                + round.after.getStringRep()
+                                + " is not after "
+                                + previous.after.getStringRep()
+                                + "."
+                        );
+                    }
+                    DownsampleConfig.validateSourceAndTargetIntervals(previous.config(), round.config());
+                }
+            }
+        }
+
+        public static DownsamplingRound read(StreamInput in) throws IOException {
+            return new DownsamplingRound(in.readTimeValue(), new DownsampleConfig(in));
+        }
+
+        public DownsamplingRound {
+            if (config.getFixedInterval().estimateMillis() < FIVE_MINUTES_MILLIS) {
+                throw new IllegalArgumentException(
+                    "A downsampling round must have a fixed interval of at least five minutes but found: " + config.getFixedInterval()
                 );
             }
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeTimeValue(after);
+            out.writeWriteable(config);
+        }
 
-            public static Round read(StreamInput in) throws IOException {
-                return new Round(in.readTimeValue(), new DownsampleConfig(in));
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field(AFTER_FIELD.getPreferredName(), after.getStringRep());
+            config.toXContentFragment(builder);
+            builder.endObject();
+            return builder;
+        }
+
+        public static DownsamplingRound fromXContent(XContentParser parser, Void context) throws IOException {
+            return PARSER.parse(parser, context);
+        }
+
+        @Override
+        public String toString() {
+            return Strings.toString(this, true, true);
+        }
+    }
+
+    /**
+     * Represents the template configuration of a lifecycle. It supports explicitly resettable values
+     * to allow value reset during template composition.
+     */
+    public record Template(
+        boolean enabled,
+        ResettableValue<TimeValue> dataRetention,
+        ResettableValue<List<DataStreamLifecycle.DownsamplingRound>> downsampling
+    ) implements ToXContentObject, Writeable {
+
+        public Template {
+            if (downsampling.isDefined() && downsampling.get() != null) {
+                DownsamplingRound.validateRounds(downsampling.get());
             }
+        }
+
+        public static final DataStreamLifecycle.Template DEFAULT = new DataStreamLifecycle.Template(
+            true,
+            ResettableValue.undefined(),
+            ResettableValue.undefined()
+        );
 
-            public Round {
-                if (config.getFixedInterval().estimateMillis() < FIVE_MINUTES_MILLIS) {
-                    throw new IllegalArgumentException(
-                        "A downsampling round must have a fixed interval of at least five minutes but found: " + config.getFixedInterval()
-                    );
+        @SuppressWarnings("unchecked")
+        public static final ConstructingObjectParser<DataStreamLifecycle.Template, Void> PARSER = new ConstructingObjectParser<>(
+            "lifecycle_template",
+            false,
+            (args, unused) -> new DataStreamLifecycle.Template(
+                args[0] == null || (boolean) args[0],
+                args[1] == null ? ResettableValue.undefined() : (ResettableValue<TimeValue>) args[1],
+                args[2] == null ? ResettableValue.undefined() : (ResettableValue<List<DataStreamLifecycle.DownsamplingRound>>) args[2]
+            )
+        );
+
+        static {
+            PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), ENABLED_FIELD);
+            PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
+                String value = p.textOrNull();
+                return value == null
+                    ? ResettableValue.reset()
+                    : ResettableValue.create(TimeValue.parseTimeValue(value, DATA_RETENTION_FIELD.getPreferredName()));
+            }, DATA_RETENTION_FIELD, ObjectParser.ValueType.STRING_OR_NULL);
+            PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> {
+                if (p.currentToken() == XContentParser.Token.VALUE_NULL) {
+                    return ResettableValue.reset();
+                } else {
+                    return ResettableValue.create(AbstractObjectParser.parseArray(p, c, DownsamplingRound::fromXContent));
                 }
-            }
+            }, DOWNSAMPLING_FIELD, ObjectParser.ValueType.OBJECT_ARRAY_OR_NULL);
+        }
 
-            @Override
-            public void writeTo(StreamOutput out) throws IOException {
-                out.writeTimeValue(after);
-                out.writeWriteable(config);
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            // The order of the fields is like this for bwc reasons
+            if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_9_X)) {
+                if (out.getTransportVersion().onOrAfter(TransportVersions.INTRODUCE_LIFECYCLE_TEMPLATE)) {
+                    ResettableValue.write(out, dataRetention, StreamOutput::writeTimeValue);
+                } else {
+                    writeLegacyValue(out, dataRetention, StreamOutput::writeTimeValue);
+                }
             }
-
-            @Override
-            public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-                builder.startObject();
-                builder.field(AFTER_FIELD.getPreferredName(), after.getStringRep());
-                config.toXContentFragment(builder);
-                builder.endObject();
-                return builder;
+            if (out.getTransportVersion().onOrAfter(ADDED_ENABLED_FLAG_VERSION)) {
+                if (out.getTransportVersion().onOrAfter(TransportVersions.INTRODUCE_LIFECYCLE_TEMPLATE)) {
+                    ResettableValue.write(out, downsampling, StreamOutput::writeCollection);
+                } else {
+                    writeLegacyValue(out, downsampling, StreamOutput::writeCollection);
+                }
+                out.writeBoolean(enabled);
             }
+        }
 
-            public static Round fromXContent(XContentParser parser, Void context) throws IOException {
-                return PARSER.parse(parser, context);
+        /**
+         * Before the introduction of the ResettableValues we used to serialise the explicit nulls differently. Legacy codes defines the
+         * two boolean flags as "isDefined" and "hasValue" while the ResettableValue, "isDefined" and "shouldReset". This inverts the
+         * semantics of the second flag and that's why we use this method.
+         */
+        private static <T> void writeLegacyValue(StreamOutput out, ResettableValue<T> value, Writeable.Writer<T> writer)
+            throws IOException {
+            out.writeBoolean(value.isDefined());
+            if (value.isDefined()) {
+                out.writeBoolean(value.shouldReset() == false);
+                if (value.shouldReset() == false) {
+                    writer.write(out, value.get());
+                }
             }
+        }
 
-            @Override
-            public String toString() {
-                return Strings.toString(this, true, true);
+        /**
+         * Before the introduction of the ResettableValues we used to serialise the explicit nulls differently. Legacy codes defines the
+         * two boolean flags as "isDefined" and "hasValue" while the ResettableValue, "isDefined" and "shouldReset". This inverts the
+         * semantics of the second flag and that's why we use this method.
+         */
+        static <T> ResettableValue<T> readLegacyValues(StreamInput in, Writeable.Reader<T> reader) throws IOException {
+            boolean isDefined = in.readBoolean();
+            if (isDefined == false) {
+                return ResettableValue.undefined();
+            }
+            boolean hasNonNullValue = in.readBoolean();
+            if (hasNonNullValue == false) {
+                return ResettableValue.reset();
             }
+            T value = reader.read(in);
+            return ResettableValue.create(value);
         }
 
-        // For testing
-        public static final Downsampling NULL = new Downsampling(null);
-
-        public Downsampling {
-            if (rounds != null) {
-                if (rounds.isEmpty()) {
-                    throw new IllegalArgumentException("Downsampling configuration should have at least one round configured.");
+        public static Template read(StreamInput in) throws IOException {
+            boolean enabled = true;
+            ResettableValue<TimeValue> dataRetention = ResettableValue.undefined();
+            ResettableValue<List<DownsamplingRound>> downsampling = ResettableValue.undefined();
+
+            // The order of the fields is like this for bwc reasons
+            if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_9_X)) {
+                if (in.getTransportVersion().onOrAfter(TransportVersions.INTRODUCE_LIFECYCLE_TEMPLATE)) {
+                    dataRetention = ResettableValue.read(in, StreamInput::readTimeValue);
+                } else {
+                    dataRetention = readLegacyValues(in, StreamInput::readTimeValue);
                 }
-                if (rounds.size() > 10) {
-                    throw new IllegalArgumentException(
-                        "Downsampling configuration supports maximum 10 configured rounds. Found: " + rounds.size()
-                    );
-                }
-                Round previous = null;
-                for (Round round : rounds) {
-                    if (previous == null) {
-                        previous = round;
-                    } else {
-                        if (round.after.compareTo(previous.after) < 0) {
-                            throw new IllegalArgumentException(
-                                "A downsampling round must have a later 'after' value than the proceeding, "
-                                    + round.after.getStringRep()
-                                    + " is not after "
-                                    + previous.after.getStringRep()
-                                    + "."
-                            );
-                        }
-                        DownsampleConfig.validateSourceAndTargetIntervals(previous.config(), round.config());
-                    }
+            }
+            if (in.getTransportVersion().onOrAfter(ADDED_ENABLED_FLAG_VERSION)) {
+                if (in.getTransportVersion().onOrAfter(TransportVersions.INTRODUCE_LIFECYCLE_TEMPLATE)) {
+                    downsampling = ResettableValue.read(in, i -> i.readCollectionAsList(DownsamplingRound::read));
+                } else {
+                    downsampling = readLegacyValues(in, i -> i.readCollectionAsList(DownsamplingRound::read));
                 }
+                enabled = in.readBoolean();
             }
+            return new Template(enabled, dataRetention, downsampling);
         }
 
-        public static Downsampling read(StreamInput in) throws IOException {
-            return new Downsampling(in.readOptionalCollectionAsList(Round::read));
+        public static Template fromXContent(XContentParser parser) throws IOException {
+            return PARSER.parse(parser, null);
         }
 
+        /**
+         * Converts the template to XContent, depending on the {@param params} set by {@link ResettableValue#hideResetValues(Params)}
+         * it may or may not display any explicit nulls when the value is to be reset.
+         */
         @Override
-        public void writeTo(StreamOutput out) throws IOException {
-            out.writeOptionalCollection(rounds, StreamOutput::writeWriteable);
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            return toXContent(builder, params, null, null, false);
         }
 
-        @Override
-        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-            if (rounds == null) {
-                builder.nullValue();
-            } else {
-                builder.startArray();
-                for (Round round : rounds) {
-                    round.toXContent(builder, params);
-                }
-                builder.endArray();
+        /**
+         * Converts the template to XContent, depending on the {@param params} set by {@link ResettableValue#hideResetValues(Params)}
+         * it may or may not display any explicit nulls when the value is to be reset.
+         */
+        public XContentBuilder toXContent(
+            XContentBuilder builder,
+            Params params,
+            @Nullable RolloverConfiguration rolloverConfiguration,
+            @Nullable DataStreamGlobalRetention globalRetention,
+            boolean isInternalDataStream
+        ) throws IOException {
+            builder.startObject();
+            builder.field(ENABLED_FIELD.getPreferredName(), enabled);
+            dataRetention.toXContent(builder, params, DATA_RETENTION_FIELD.getPreferredName(), TimeValue::getStringRep);
+            downsampling.toXContent(builder, params, DOWNSAMPLING_FIELD.getPreferredName());
+            if (rolloverConfiguration != null) {
+                builder.field(ROLLOVER_FIELD.getPreferredName());
+                rolloverConfiguration.evaluateAndConvertToXContent(
+                    builder,
+                    params,
+                    toDataStreamLifecycle().getEffectiveDataRetention(globalRetention, isInternalDataStream)
+                );
             }
+            builder.endObject();
             return builder;
         }
-    }
 
-    /**
-     * This enum represents all configuration sources that can influence the retention of a data stream.
-     */
-    public enum RetentionSource {
-        DATA_STREAM_CONFIGURATION,
-        DEFAULT_GLOBAL_RETENTION,
-        MAX_GLOBAL_RETENTION;
+        public static Builder builder(DataStreamLifecycle.Template template) {
+            return new Builder(template);
+        }
 
-        public String displayName() {
-            return this.toString().toLowerCase(Locale.ROOT);
+        public static Builder builder() {
+            return new Builder(null);
+        }
+
+        public static class Builder {
+            private boolean enabled = true;
+            private ResettableValue<TimeValue> dataRetention = ResettableValue.undefined();
+            private ResettableValue<List<DownsamplingRound>> downsampling = ResettableValue.undefined();
+
+            private Builder(Template template) {
+                if (template != null) {
+                    enabled = template.enabled();
+                    dataRetention = template.dataRetention();
+                    downsampling = template.downsampling();
+                }
+            }
+
+            public Builder enabled(boolean enabled) {
+                this.enabled = enabled;
+                return this;
+            }
+
+            public Builder dataRetention(ResettableValue<TimeValue> dataRetention) {
+                this.dataRetention = dataRetention;
+                return this;
+            }
+
+            public Builder dataRetention(@Nullable TimeValue dataRetention) {
+                this.dataRetention = ResettableValue.create(dataRetention);
+                return this;
+            }
+
+            public Builder downsampling(ResettableValue<List<DownsamplingRound>> downsampling) {
+                this.downsampling = downsampling;
+                return this;
+            }
+
+            public Builder downsampling(@Nullable List<DownsamplingRound> downsampling) {
+                this.downsampling = ResettableValue.create(downsampling);
+                return this;
+            }
+
+            public Template build() {
+                return new Template(enabled, dataRetention, downsampling);
+            }
+        }
+
+        public DataStreamLifecycle toDataStreamLifecycle() {
+            return new DataStreamLifecycle(enabled, dataRetention.get(), downsampling.get());
+        }
+
+        @Override
+        public String toString() {
+            return Strings.toString(this, true, true);
         }
     }
 }

+ 2 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java

@@ -325,9 +325,10 @@ public class MetadataCreateDataStreamService {
         dsBackingIndices.add(writeIndex.getIndex());
         boolean hidden = isSystem || template.getDataStreamTemplate().isHidden();
         final IndexMode indexMode = newProject.retrieveIndexModeFromTemplate(template);
-        final DataStreamLifecycle lifecycle = isSystem
+        final DataStreamLifecycle.Template lifecycleTemplate = isSystem
             ? MetadataIndexTemplateService.resolveLifecycle(template, systemDataStreamDescriptor.getComponentTemplates())
             : MetadataIndexTemplateService.resolveLifecycle(template, newProject.componentTemplates());
+        final DataStreamLifecycle lifecycle = lifecycleTemplate == null ? null : lifecycleTemplate.toDataStreamLifecycle();
         List<Index> failureIndices = failureStoreIndex == null ? List.of() : List.of(failureStoreIndex.getIndex());
         DataStream newDataStream = new DataStream(
             dataStreamName,

+ 19 - 16
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java

@@ -379,7 +379,10 @@ public class MetadataIndexTemplateService {
 
         if (finalComponentTemplate.template().lifecycle() != null) {
             // We do not know if this lifecycle will belong to an internal data stream, so we fall back to a non internal.
-            finalComponentTemplate.template().lifecycle().addWarningHeaderIfDataRetentionNotEffective(globalRetentionSettings.get(), false);
+            finalComponentTemplate.template()
+                .lifecycle()
+                .toDataStreamLifecycle()
+                .addWarningHeaderIfDataRetentionNotEffective(globalRetentionSettings.get(), false);
         }
 
         logger.info("{} component template [{}]", existing == null ? "adding" : "updating", name);
@@ -816,7 +819,7 @@ public class MetadataIndexTemplateService {
         ComposableIndexTemplate template,
         @Nullable DataStreamGlobalRetention globalRetention
     ) {
-        DataStreamLifecycle lifecycle = resolveLifecycle(template, project.componentTemplates());
+        DataStreamLifecycle.Template lifecycle = resolveLifecycle(template, project.componentTemplates());
         if (lifecycle != null) {
             if (template.getDataStreamTemplate() == null) {
                 throw new IllegalArgumentException(
@@ -829,7 +832,7 @@ public class MetadataIndexTemplateService {
                 // We cannot know for sure if the template will apply to internal data streams, so we use a simpler heuristic:
                 // If all the index patterns start with a dot, we consider that all the connected data streams are internal.
                 boolean isInternalDataStream = template.indexPatterns().stream().allMatch(indexPattern -> indexPattern.charAt(0) == '.');
-                lifecycle.addWarningHeaderIfDataRetentionNotEffective(globalRetention, isInternalDataStream);
+                lifecycle.toDataStreamLifecycle().addWarningHeaderIfDataRetentionNotEffective(globalRetention, isInternalDataStream);
             }
         }
     }
@@ -1620,7 +1623,7 @@ public class MetadataIndexTemplateService {
      * Resolve the given v2 template into a {@link DataStreamLifecycle} object
      */
     @Nullable
-    public static DataStreamLifecycle resolveLifecycle(ProjectMetadata metadata, final String templateName) {
+    public static DataStreamLifecycle.Template resolveLifecycle(ProjectMetadata metadata, final String templateName) {
         final ComposableIndexTemplate template = metadata.templatesV2().get(templateName);
         assert template != null
             : "attempted to resolve lifecycle for a template [" + templateName + "] that did not exist in the cluster state";
@@ -1634,19 +1637,19 @@ public class MetadataIndexTemplateService {
      * Resolve the provided v2 template and component templates into a {@link DataStreamLifecycle} object
      */
     @Nullable
-    public static DataStreamLifecycle resolveLifecycle(
+    public static DataStreamLifecycle.Template resolveLifecycle(
         ComposableIndexTemplate template,
         Map<String, ComponentTemplate> componentTemplates
     ) {
         Objects.requireNonNull(template, "attempted to resolve lifecycle for a null template");
         Objects.requireNonNull(componentTemplates, "attempted to resolve lifecycle with null component templates");
 
-        List<DataStreamLifecycle> lifecycles = new ArrayList<>();
+        List<DataStreamLifecycle.Template> lifecycles = new ArrayList<>();
         for (String componentTemplateName : template.composedOf()) {
             if (componentTemplates.containsKey(componentTemplateName) == false) {
                 continue;
             }
-            DataStreamLifecycle lifecycle = componentTemplates.get(componentTemplateName).template().lifecycle();
+            DataStreamLifecycle.Template lifecycle = componentTemplates.get(componentTemplateName).template().lifecycle();
             if (lifecycle != null) {
                 lifecycles.add(lifecycle);
             }
@@ -1697,18 +1700,18 @@ public class MetadataIndexTemplateService {
      * @return the final lifecycle
      */
     @Nullable
-    public static DataStreamLifecycle composeDataLifecycles(List<DataStreamLifecycle> lifecycles) {
-        DataStreamLifecycle.Builder builder = null;
-        for (DataStreamLifecycle current : lifecycles) {
+    public static DataStreamLifecycle.Template composeDataLifecycles(List<DataStreamLifecycle.Template> lifecycles) {
+        DataStreamLifecycle.Template.Builder builder = null;
+        for (DataStreamLifecycle.Template current : lifecycles) {
             if (builder == null) {
-                builder = DataStreamLifecycle.newBuilder(current);
+                builder = DataStreamLifecycle.Template.builder(current);
             } else {
-                builder.enabled(current.isEnabled());
-                if (current.getDataRetention() != null) {
-                    builder.dataRetention(current.getDataRetention());
+                builder.enabled(current.enabled());
+                if (current.dataRetention().isDefined()) {
+                    builder.dataRetention(current.dataRetention());
                 }
-                if (current.getDownsampling() != null) {
-                    builder.downsampling(current.getDownsampling());
+                if (current.downsampling().isDefined()) {
+                    builder.downsampling(current.downsampling());
                 }
             }
         }

+ 1 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java

@@ -1079,7 +1079,7 @@ public class ProjectMetadata implements Iterable<IndexMetadata>, Diffable<Projec
         }
 
         DataStream parentDataStream = indexAbstraction.getParentDataStream();
-        if (parentDataStream != null && parentDataStream.getLifecycle() != null && parentDataStream.getLifecycle().isEnabled()) {
+        if (parentDataStream != null && parentDataStream.getLifecycle() != null && parentDataStream.getLifecycle().enabled()) {
             // index has both ILM and data stream lifecycle configured so let's check which is preferred
             return PREFER_ILM_SETTING.get(indexMetadata.getSettings());
         }

+ 0 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/ResettableValue.java

@@ -118,7 +118,6 @@ public class ResettableValue<T> {
      *
      * @throws IOException
      */
-    @Nullable
     static <T> ResettableValue<T> read(StreamInput in, Writeable.Reader<T> reader) throws IOException {
         boolean isDefined = in.readBoolean();
         if (isDefined == false) {

+ 16 - 12
server/src/main/java/org/elasticsearch/cluster/metadata/Template.java

@@ -57,7 +57,7 @@ public class Template implements SimpleDiffable<Template>, ToXContentObject {
             (Settings) a[0],
             (CompressedXContent) a[1],
             (Map<String, AliasMetadata>) a[2],
-            (DataStreamLifecycle) a[3],
+            (DataStreamLifecycle.Template) a[3],
             a[4] == null ? ResettableValue.undefined() : (ResettableValue<DataStreamOptions.Template>) a[4]
         )
     );
@@ -88,7 +88,11 @@ public class Template implements SimpleDiffable<Template>, ToXContentObject {
             }
             return aliasMap;
         }, ALIASES);
-        PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> DataStreamLifecycle.fromXContent(p), LIFECYCLE);
+        PARSER.declareObject(
+            ConstructingObjectParser.optionalConstructorArg(),
+            (p, c) -> DataStreamLifecycle.Template.fromXContent(p),
+            LIFECYCLE
+        );
         PARSER.declareObjectOrNull(
             ConstructingObjectParser.optionalConstructorArg(),
             (p, c) -> ResettableValue.create(DataStreamOptions.Template.fromXContent(p)),
@@ -105,14 +109,14 @@ public class Template implements SimpleDiffable<Template>, ToXContentObject {
     private final Map<String, AliasMetadata> aliases;
 
     @Nullable
-    private final DataStreamLifecycle lifecycle;
+    private final DataStreamLifecycle.Template lifecycle;
     private final ResettableValue<DataStreamOptions.Template> dataStreamOptions;
 
     public Template(
         @Nullable Settings settings,
         @Nullable CompressedXContent mappings,
         @Nullable Map<String, AliasMetadata> aliases,
-        @Nullable DataStreamLifecycle lifecycle,
+        @Nullable DataStreamLifecycle.Template lifecycle,
         ResettableValue<DataStreamOptions.Template> dataStreamOptions
     ) {
         this.settings = settings;
@@ -144,13 +148,13 @@ public class Template implements SimpleDiffable<Template>, ToXContentObject {
             this.aliases = null;
         }
         if (in.getTransportVersion().onOrAfter(DataStreamLifecycle.ADDED_ENABLED_FLAG_VERSION)) {
-            this.lifecycle = in.readOptionalWriteable(DataStreamLifecycle::new);
+            this.lifecycle = in.readOptionalWriteable(DataStreamLifecycle.Template::read);
         } else if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_9_X)) {
             boolean isExplicitNull = in.readBoolean();
             if (isExplicitNull) {
-                this.lifecycle = DataStreamLifecycle.newBuilder().enabled(false).build();
+                this.lifecycle = DataStreamLifecycle.Template.builder().enabled(false).build();
             } else {
-                this.lifecycle = in.readOptionalWriteable(DataStreamLifecycle::new);
+                this.lifecycle = in.readOptionalWriteable(DataStreamLifecycle.Template::read);
             }
         } else {
             this.lifecycle = null;
@@ -179,7 +183,7 @@ public class Template implements SimpleDiffable<Template>, ToXContentObject {
     }
 
     @Nullable
-    public DataStreamLifecycle lifecycle() {
+    public DataStreamLifecycle.Template lifecycle() {
         return lifecycle;
     }
 
@@ -215,7 +219,7 @@ public class Template implements SimpleDiffable<Template>, ToXContentObject {
         if (out.getTransportVersion().onOrAfter(DataStreamLifecycle.ADDED_ENABLED_FLAG_VERSION)) {
             out.writeOptionalWriteable(lifecycle);
         } else if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_9_X)) {
-            boolean isExplicitNull = lifecycle != null && lifecycle.isEnabled() == false;
+            boolean isExplicitNull = lifecycle != null && lifecycle.enabled() == false;
             out.writeBoolean(isExplicitNull);
             if (isExplicitNull == false) {
                 out.writeOptionalWriteable(lifecycle);
@@ -345,7 +349,7 @@ public class Template implements SimpleDiffable<Template>, ToXContentObject {
         private Settings settings = null;
         private CompressedXContent mappings = null;
         private Map<String, AliasMetadata> aliases = null;
-        private DataStreamLifecycle lifecycle = null;
+        private DataStreamLifecycle.Template lifecycle = null;
         private ResettableValue<DataStreamOptions.Template> dataStreamOptions = ResettableValue.undefined();
 
         private Builder() {}
@@ -378,12 +382,12 @@ public class Template implements SimpleDiffable<Template>, ToXContentObject {
             return this;
         }
 
-        public Builder lifecycle(DataStreamLifecycle lifecycle) {
+        public Builder lifecycle(DataStreamLifecycle.Template lifecycle) {
             this.lifecycle = lifecycle;
             return this;
         }
 
-        public Builder lifecycle(DataStreamLifecycle.Builder lifecycle) {
+        public Builder lifecycle(DataStreamLifecycle.Template.Builder lifecycle) {
             this.lifecycle = lifecycle.build();
             return this;
         }

+ 4 - 4
server/src/test/java/org/elasticsearch/action/admin/indices/template/get/GetComponentTemplateResponseTests.java

@@ -39,7 +39,7 @@ public class GetComponentTemplateResponseTests extends ESTestCase {
         Settings settings = null;
         CompressedXContent mappings = null;
         Map<String, AliasMetadata> aliases = null;
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
         ResettableValue<DataStreamOptions.Template> dataStreamOptions = ResettableValue.undefined();
         if (randomBoolean()) {
             settings = randomSettings();
@@ -68,9 +68,9 @@ public class GetComponentTemplateResponseTests extends ESTestCase {
             response.toXContent(builder, EMPTY_PARAMS);
             String serialized = Strings.toString(builder);
             assertThat(serialized, containsString("rollover"));
-            for (String label : rolloverConfiguration.resolveRolloverConditions(lifecycle.getEffectiveDataRetention(null, randomBoolean()))
-                .getConditions()
-                .keySet()) {
+            for (String label : rolloverConfiguration.resolveRolloverConditions(
+                lifecycle.toDataStreamLifecycle().getEffectiveDataRetention(null, randomBoolean())
+            ).getConditions().keySet()) {
                 assertThat(serialized, containsString(label));
             }
         }

+ 1 - 1
server/src/test/java/org/elasticsearch/action/datastreams/GetDataStreamActionTests.java

@@ -99,7 +99,7 @@ public class GetDataStreamActionTests extends ESTestCase {
 
     private static DataStream newDataStreamInstance(boolean isSystem, TimeValue retention) {
         List<Index> indices = List.of(new Index(randomAlphaOfLength(10), randomAlphaOfLength(10)));
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle(new DataStreamLifecycle.Retention(retention), null, null);
+        DataStreamLifecycle lifecycle = new DataStreamLifecycle(true, retention, null);
         return DataStream.builder(randomAlphaOfLength(50), indices)
             .setGeneration(randomLongBetween(1, 1000))
             .setMetadata(Map.of())

+ 1 - 5
server/src/test/java/org/elasticsearch/action/datastreams/lifecycle/ExplainIndexDataStreamLifecycleTests.java

@@ -200,11 +200,7 @@ public class ExplainIndexDataStreamLifecycleTests extends AbstractWireSerializin
         TimeValue configuredRetention = TimeValue.timeValueDays(100);
         TimeValue globalDefaultRetention = TimeValue.timeValueDays(10);
         TimeValue globalMaxRetention = TimeValue.timeValueDays(50);
-        DataStreamLifecycle dataStreamLifecycle = new DataStreamLifecycle(
-            new DataStreamLifecycle.Retention(configuredRetention),
-            null,
-            null
-        );
+        DataStreamLifecycle dataStreamLifecycle = new DataStreamLifecycle(true, configuredRetention, null);
         {
             boolean isSystemDataStream = true;
             ExplainIndexDataStreamLifecycle explainIndexDataStreamLifecycle = createManagedIndexDataStreamLifecycleExplanation(

+ 1 - 1
server/src/test/java/org/elasticsearch/action/datastreams/lifecycle/GetDataStreamLifecycleActionTests.java

@@ -96,7 +96,7 @@ public class GetDataStreamLifecycleActionTests extends ESTestCase {
         TimeValue globalDefaultRetention = TimeValue.timeValueDays(10);
         TimeValue globalMaxRetention = TimeValue.timeValueDays(50);
         DataStreamGlobalRetention globalRetention = new DataStreamGlobalRetention(globalDefaultRetention, globalMaxRetention);
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle(new DataStreamLifecycle.Retention(configuredRetention), null, null);
+        DataStreamLifecycle lifecycle = new DataStreamLifecycle(true, configuredRetention, null);
         {
             boolean isInternalDataStream = true;
             GetDataStreamLifecycleAction.Response.DataStreamLifecycle explainIndexDataStreamLifecycle = createDataStreamLifecycle(

+ 4 - 4
server/src/test/java/org/elasticsearch/cluster/metadata/ComponentTemplateTests.java

@@ -90,7 +90,7 @@ public class ComponentTemplateTests extends SimpleDiffableSerializationTestCase<
             templateBuilder.aliases(randomAliases());
         }
         if (randomBoolean() && supportsDataStreams) {
-            templateBuilder.lifecycle(DataStreamLifecycleTests.randomLifecycle());
+            templateBuilder.lifecycle(DataStreamLifecycleTemplateTests.randomLifecycleTemplate());
         }
         if (randomBoolean() && supportsDataStreams) {
             templateBuilder.dataStreamOptions(randomDataStreamOptionsTemplate());
@@ -182,7 +182,7 @@ public class ComponentTemplateTests extends SimpleDiffableSerializationTestCase<
                     );
                     case 3 -> new ComponentTemplate(
                         Template.builder(ot)
-                            .lifecycle(randomValueOtherThan(ot.lifecycle(), DataStreamLifecycleTests::randomLifecycle))
+                            .lifecycle(randomValueOtherThan(ot.lifecycle(), DataStreamLifecycleTemplateTests::randomLifecycleTemplate))
                             .build(),
                         orig.version(),
                         orig.metadata(),
@@ -286,7 +286,7 @@ public class ComponentTemplateTests extends SimpleDiffableSerializationTestCase<
         if (randomBoolean()) {
             dataStreamOptions = randomDataStreamOptionsTemplate();
         }
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
         ComponentTemplate template = new ComponentTemplate(
             new Template(settings, mappings, aliases, lifecycle, dataStreamOptions),
             randomNonNegativeLong(),
@@ -302,7 +302,7 @@ public class ComponentTemplateTests extends SimpleDiffableSerializationTestCase<
             String serialized = Strings.toString(builder);
             assertThat(serialized, containsString("rollover"));
             for (String label : rolloverConfiguration.resolveRolloverConditions(
-                lifecycle.getEffectiveDataRetention(globalRetention, randomBoolean())
+                lifecycle.toDataStreamLifecycle().getEffectiveDataRetention(globalRetention, randomBoolean())
             ).getConditions().keySet()) {
                 assertThat(serialized, containsString(label));
             }

+ 6 - 4
server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java

@@ -74,7 +74,7 @@ public class ComposableIndexTemplateTests extends SimpleDiffableSerializationTes
                 builder.aliases(randomAliases());
             }
             if (dataStreamTemplate != null && randomBoolean()) {
-                builder.lifecycle(DataStreamLifecycleTests.randomLifecycle());
+                builder.lifecycle(DataStreamLifecycleTemplateTests.randomLifecycleTemplate());
             }
             template = builder.build();
         }
@@ -173,7 +173,9 @@ public class ComposableIndexTemplateTests extends SimpleDiffableSerializationTes
                                 .settings(randomSettings())
                                 .mappings(randomMappings(orig.getDataStreamTemplate()))
                                 .aliases(randomAliases())
-                                .lifecycle(orig.getDataStreamTemplate() == null ? null : DataStreamLifecycleTests.randomLifecycle())
+                                .lifecycle(
+                                    orig.getDataStreamTemplate() == null ? null : DataStreamLifecycleTemplateTests.randomLifecycleTemplate()
+                                )
                                 .build()
                         )
                     )
@@ -234,7 +236,7 @@ public class ComposableIndexTemplateTests extends SimpleDiffableSerializationTes
             dataStreamOptions = ComponentTemplateTests.randomDataStreamOptionsTemplate();
         }
         // We use the empty lifecycle so the global retention can be in effect
-        DataStreamLifecycle lifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.DEFAULT;
         Template template = new Template(settings, mappings, aliases, lifecycle, dataStreamOptions);
         ComposableIndexTemplate.builder()
             .indexPatterns(List.of(randomAlphaOfLength(4)))
@@ -254,7 +256,7 @@ public class ComposableIndexTemplateTests extends SimpleDiffableSerializationTes
             String serialized = Strings.toString(builder);
             assertThat(serialized, containsString("rollover"));
             for (String label : rolloverConfiguration.resolveRolloverConditions(
-                lifecycle.getEffectiveDataRetention(globalRetention, randomBoolean())
+                lifecycle.toDataStreamLifecycle().getEffectiveDataRetention(globalRetention, randomBoolean())
             ).getConditions().keySet()) {
                 assertThat(serialized, containsString(label));
             }

+ 192 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamLifecycleTemplateTests.java

@@ -0,0 +1,192 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.cluster.metadata;
+
+import org.elasticsearch.action.downsample.DownsampleConfig;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
+import org.elasticsearch.test.AbstractXContentSerializingTestCase;
+import org.elasticsearch.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class DataStreamLifecycleTemplateTests extends AbstractXContentSerializingTestCase<DataStreamLifecycle.Template> {
+
+    @Override
+    protected Writeable.Reader<DataStreamLifecycle.Template> instanceReader() {
+        return DataStreamLifecycle.Template::read;
+    }
+
+    @Override
+    protected DataStreamLifecycle.Template createTestInstance() {
+        return randomLifecycleTemplate();
+    }
+
+    @Override
+    protected DataStreamLifecycle.Template mutateInstance(DataStreamLifecycle.Template instance) throws IOException {
+        var enabled = instance.enabled();
+        var retention = instance.dataRetention();
+        var downsampling = instance.downsampling();
+        switch (randomInt(2)) {
+            case 0 -> enabled = enabled == false;
+            case 1 -> retention = randomValueOtherThan(retention, DataStreamLifecycleTemplateTests::randomRetention);
+            case 2 -> downsampling = randomValueOtherThan(downsampling, DataStreamLifecycleTemplateTests::randomDownsampling);
+            default -> throw new AssertionError("Illegal randomisation branch");
+        }
+        return DataStreamLifecycle.Template.builder().dataRetention(retention).downsampling(downsampling).enabled(enabled).build();
+    }
+
+    @Override
+    protected DataStreamLifecycle.Template doParseInstance(XContentParser parser) throws IOException {
+        return DataStreamLifecycle.Template.fromXContent(parser);
+    }
+
+    public void testInvalidDownsamplingConfiguration() {
+        {
+            IllegalArgumentException exception = expectThrows(
+                IllegalArgumentException.class,
+                () -> DataStreamLifecycle.Template.builder()
+                    .downsampling(
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(10),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(3),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            )
+                        )
+                    )
+                    .build()
+            );
+            assertThat(
+                exception.getMessage(),
+                equalTo("A downsampling round must have a later 'after' value than the proceeding, 3d is not after 10d.")
+            );
+        }
+        {
+            IllegalArgumentException exception = expectThrows(
+                IllegalArgumentException.class,
+                () -> DataStreamLifecycle.Template.builder()
+                    .downsampling(
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(10),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(30),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            )
+                        )
+                    )
+                    .build()
+            );
+            assertThat(exception.getMessage(), equalTo("Downsampling interval [2h] must be greater than the source index interval [2h]."));
+        }
+        {
+            IllegalArgumentException exception = expectThrows(
+                IllegalArgumentException.class,
+                () -> DataStreamLifecycle.Template.builder()
+                    .downsampling(
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(10),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(30),
+                                new DownsampleConfig(new DateHistogramInterval("3h"))
+                            )
+                        )
+                    )
+                    .build()
+            );
+            assertThat(exception.getMessage(), equalTo("Downsampling interval [3h] must be a multiple of the source index interval [2h]."));
+        }
+        {
+            IllegalArgumentException exception = expectThrows(
+                IllegalArgumentException.class,
+                () -> DataStreamLifecycle.Template.builder().downsampling((List.of())).build()
+            );
+            assertThat(exception.getMessage(), equalTo("Downsampling configuration should have at least one round configured."));
+        }
+        {
+            IllegalArgumentException exception = expectThrows(
+                IllegalArgumentException.class,
+                () -> DataStreamLifecycle.Template.builder()
+                    .downsampling(
+                        Stream.iterate(1, i -> i * 2)
+                            .limit(12)
+                            .map(
+                                i -> new DataStreamLifecycle.DownsamplingRound(
+                                    TimeValue.timeValueDays(i),
+                                    new DownsampleConfig(new DateHistogramInterval(i + "h"))
+                                )
+                            )
+                            .toList()
+                    )
+                    .build()
+            );
+            assertThat(exception.getMessage(), equalTo("Downsampling configuration supports maximum 10 configured rounds. Found: 12"));
+        }
+
+        {
+            IllegalArgumentException exception = expectThrows(
+                IllegalArgumentException.class,
+                () -> DataStreamLifecycle.Template.builder()
+                    .downsampling(
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(10),
+                                new DownsampleConfig(new DateHistogramInterval("2m"))
+                            )
+                        )
+                    )
+                    .build()
+            );
+            assertThat(
+                exception.getMessage(),
+                equalTo("A downsampling round must have a fixed interval of at least five minutes but found: 2m")
+            );
+        }
+    }
+
+    public static DataStreamLifecycle.Template randomLifecycleTemplate() {
+        return DataStreamLifecycle.Template.builder()
+            .enabled(randomBoolean())
+            .dataRetention(randomRetention())
+            .downsampling(randomDownsampling())
+            .build();
+    }
+
+    private static ResettableValue<TimeValue> randomRetention() {
+        return switch (randomIntBetween(0, 2)) {
+            case 0 -> ResettableValue.undefined();
+            case 1 -> ResettableValue.reset();
+            case 2 -> ResettableValue.create(TimeValue.timeValueDays(randomIntBetween(1, 100)));
+            default -> throw new IllegalStateException("Unknown randomisation path");
+        };
+    }
+
+    private static ResettableValue<List<DataStreamLifecycle.DownsamplingRound>> randomDownsampling() {
+        return switch (randomIntBetween(0, 1)) {
+            case 0 -> ResettableValue.reset();
+            case 1 -> ResettableValue.create(DataStreamLifecycleTests.randomDownsampling());
+            default -> throw new IllegalStateException("Unknown randomisation path");
+        };
+    }
+}

+ 111 - 124
server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamLifecycleTests.java

@@ -18,7 +18,6 @@ import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.settings.ClusterSettings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.ByteSizeValue;
-import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
@@ -59,50 +58,29 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
 
     @Override
     protected DataStreamLifecycle mutateInstance(DataStreamLifecycle instance) throws IOException {
-        var enabled = instance.isEnabled();
-        var retention = instance.getDataRetention();
-        var downsampling = instance.getDownsampling();
+        var enabled = instance.enabled();
+        var retention = instance.dataRetention();
+        var downsampling = instance.downsampling();
         switch (randomInt(2)) {
             case 0 -> {
-                if (retention == null || retention == DataStreamLifecycle.Retention.NULL) {
-                    retention = randomValueOtherThan(retention, DataStreamLifecycleTests::randomRetention);
+                if (retention == null) {
+                    retention = randomPositiveTimeValue();
                 } else {
-                    retention = switch (randomInt(2)) {
-                        case 0 -> null;
-                        case 1 -> DataStreamLifecycle.Retention.NULL;
-                        default -> new DataStreamLifecycle.Retention(
-                            TimeValue.timeValueMillis(
-                                randomValueOtherThan(retention.value().millis(), ESTestCase::randomMillisUpToYear9999)
-                            )
-                        );
-                    };
+                    retention = randomBoolean() ? null : randomValueOtherThan(retention, ESTestCase::randomPositiveTimeValue);
                 }
             }
             case 1 -> {
-                if (downsampling == null || downsampling == DataStreamLifecycle.Downsampling.NULL) {
-                    downsampling = randomValueOtherThan(downsampling, DataStreamLifecycleTests::randomDownsampling);
+                if (downsampling == null) {
+                    downsampling = randomDownsampling();
                 } else {
-                    downsampling = switch (randomInt(2)) {
-                        case 0 -> null;
-                        case 1 -> DataStreamLifecycle.Downsampling.NULL;
-                        default -> {
-                            if (downsampling.rounds().size() == 1) {
-                                yield new DataStreamLifecycle.Downsampling(
-                                    List.of(downsampling.rounds().get(0), nextRound(downsampling.rounds().get(0)))
-                                );
-
-                            } else {
-                                var updatedRounds = new ArrayList<>(downsampling.rounds());
-                                updatedRounds.remove(randomInt(downsampling.rounds().size() - 1));
-                                yield new DataStreamLifecycle.Downsampling(updatedRounds);
-                            }
-                        }
-                    };
+                    downsampling = randomBoolean()
+                        ? null
+                        : randomValueOtherThan(downsampling, DataStreamLifecycleTests::randomDownsampling);
                 }
             }
             default -> enabled = enabled == false;
         }
-        return DataStreamLifecycle.newBuilder().dataRetention(retention).downsampling(downsampling).enabled(enabled).build();
+        return new DataStreamLifecycle(enabled, retention, downsampling);
     }
 
     @Override
@@ -130,14 +108,14 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
                 assertThat(serialized, containsString("[automatic]"));
             }
             // We check that even if there was no retention provided by the user, the global retention applies
-            if (lifecycle.getDataRetention() == null) {
+            if (lifecycle.dataRetention() == null) {
                 assertThat(serialized, not(containsString("data_retention")));
             } else {
                 assertThat(serialized, containsString("data_retention"));
             }
             boolean globalRetentionIsNotNull = globalRetention.defaultRetention() != null || globalRetention.maxRetention() != null;
-            boolean configuredLifeCycleIsNotNull = lifecycle.getDataRetention() != null && lifecycle.getDataRetention().value() != null;
-            if (lifecycle.isEnabled() && (globalRetentionIsNotNull || configuredLifeCycleIsNotNull)) {
+            boolean configuredLifeCycleIsNotNull = lifecycle.dataRetention() != null;
+            if (lifecycle.enabled() && (globalRetentionIsNotNull || configuredLifeCycleIsNotNull)) {
                 assertThat(serialized, containsString("effective_retention"));
             } else {
                 assertThat(serialized, not(containsString("effective_retention")));
@@ -145,6 +123,22 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
         }
     }
 
+    public void testXContentDeSerializationWithNullValues() throws IOException {
+        String lifecycleJson = """
+            {
+              "data_retention": null,
+              "downsampling": null
+            }
+            """;
+        try (XContentParser parser = createParser(XContentType.JSON.xContent(), lifecycleJson)) {
+            assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken());
+            var parsed = DataStreamLifecycle.fromXContent(parser);
+            assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken());
+            assertNull(parser.nextToken());
+            assertThat(parsed, equalTo(DataStreamLifecycle.DEFAULT));
+        }
+    }
+
     public void testDefaultClusterSetting() {
         ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
         RolloverConfiguration rolloverConfiguration = clusterSettings.get(DataStreamLifecycle.CLUSTER_LIFECYCLE_DEFAULT_ROLLOVER_SETTING);
@@ -178,18 +172,20 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
         {
             IllegalArgumentException exception = expectThrows(
                 IllegalArgumentException.class,
-                () -> new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueDays(10),
-                            new DownsampleConfig(new DateHistogramInterval("2h"))
-                        ),
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueDays(3),
-                            new DownsampleConfig(new DateHistogramInterval("2h"))
+                () -> DataStreamLifecycle.builder()
+                    .downsampling(
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(10),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(3),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            )
                         )
                     )
-                )
+                    .build()
             );
             assertThat(
                 exception.getMessage(),
@@ -199,60 +195,66 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
         {
             IllegalArgumentException exception = expectThrows(
                 IllegalArgumentException.class,
-                () -> new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueDays(10),
-                            new DownsampleConfig(new DateHistogramInterval("2h"))
-                        ),
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueDays(30),
-                            new DownsampleConfig(new DateHistogramInterval("2h"))
+                () -> DataStreamLifecycle.builder()
+                    .downsampling(
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(10),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(30),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            )
                         )
                     )
-                )
+                    .build()
             );
             assertThat(exception.getMessage(), equalTo("Downsampling interval [2h] must be greater than the source index interval [2h]."));
         }
         {
             IllegalArgumentException exception = expectThrows(
                 IllegalArgumentException.class,
-                () -> new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueDays(10),
-                            new DownsampleConfig(new DateHistogramInterval("2h"))
-                        ),
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueDays(30),
-                            new DownsampleConfig(new DateHistogramInterval("3h"))
+                () -> DataStreamLifecycle.builder()
+                    .downsampling(
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(10),
+                                new DownsampleConfig(new DateHistogramInterval("2h"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(30),
+                                new DownsampleConfig(new DateHistogramInterval("3h"))
+                            )
                         )
                     )
-                )
+                    .build()
             );
             assertThat(exception.getMessage(), equalTo("Downsampling interval [3h] must be a multiple of the source index interval [2h]."));
         }
         {
             IllegalArgumentException exception = expectThrows(
                 IllegalArgumentException.class,
-                () -> new DataStreamLifecycle.Downsampling(List.of())
+                () -> DataStreamLifecycle.builder().downsampling((List.of())).build()
             );
             assertThat(exception.getMessage(), equalTo("Downsampling configuration should have at least one round configured."));
         }
         {
             IllegalArgumentException exception = expectThrows(
                 IllegalArgumentException.class,
-                () -> new DataStreamLifecycle.Downsampling(
-                    Stream.iterate(1, i -> i * 2)
-                        .limit(12)
-                        .map(
-                            i -> new DataStreamLifecycle.Downsampling.Round(
-                                TimeValue.timeValueDays(i),
-                                new DownsampleConfig(new DateHistogramInterval(i + "h"))
+                () -> DataStreamLifecycle.builder()
+                    .downsampling(
+                        Stream.iterate(1, i -> i * 2)
+                            .limit(12)
+                            .map(
+                                i -> new DataStreamLifecycle.DownsamplingRound(
+                                    TimeValue.timeValueDays(i),
+                                    new DownsampleConfig(new DateHistogramInterval(i + "h"))
+                                )
                             )
-                        )
-                        .toList()
-                )
+                            .toList()
+                    )
+                    .build()
             );
             assertThat(exception.getMessage(), equalTo("Downsampling configuration supports maximum 10 configured rounds. Found: 12"));
         }
@@ -260,14 +262,16 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
         {
             IllegalArgumentException exception = expectThrows(
                 IllegalArgumentException.class,
-                () -> new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueDays(10),
-                            new DownsampleConfig(new DateHistogramInterval("2m"))
+                () -> DataStreamLifecycle.builder()
+                    .downsampling(
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueDays(10),
+                                new DownsampleConfig(new DateHistogramInterval("2m"))
+                            )
                         )
                     )
-                )
+                    .build()
             );
             assertThat(
                 exception.getMessage(),
@@ -279,7 +283,7 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
     public void testEffectiveRetention() {
         // No retention in the data stream lifecycle
         {
-            DataStreamLifecycle noRetentionLifecycle = DataStreamLifecycle.newBuilder().downsampling(randomDownsampling()).build();
+            DataStreamLifecycle noRetentionLifecycle = DataStreamLifecycle.builder().downsampling(randomDownsampling()).build();
             TimeValue maxRetention = TimeValue.timeValueDays(randomIntBetween(50, 100));
             TimeValue defaultRetention = TimeValue.timeValueDays(randomIntBetween(1, 50));
             Tuple<TimeValue, DataStreamLifecycle.RetentionSource> effectiveDataRetentionWithSource = noRetentionLifecycle
@@ -312,7 +316,7 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
         // With retention in the data stream lifecycle
         {
             TimeValue dataStreamRetention = TimeValue.timeValueDays(randomIntBetween(5, 100));
-            DataStreamLifecycle lifecycleRetention = DataStreamLifecycle.newBuilder()
+            DataStreamLifecycle lifecycleRetention = DataStreamLifecycle.builder()
                 .dataRetention(dataStreamRetention)
                 .downsampling(randomDownsampling())
                 .build();
@@ -361,7 +365,7 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
                 TimeValue.timeValueDays(7),
                 TimeValue.timeValueDays(90)
             );
-            DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder().dataRetention(dataStreamRetention).build();
+            DataStreamLifecycle lifecycle = DataStreamLifecycle.builder().dataRetention(dataStreamRetention).build();
 
             // Verify that global retention should have kicked in
             var effectiveDataRetentionWithSource = lifecycle.getEffectiveDataRetentionWithSource(globalRetention, false);
@@ -394,52 +398,35 @@ public class DataStreamLifecycleTests extends AbstractXContentSerializingTestCas
         }
     }
 
-    @Nullable
     public static DataStreamLifecycle randomLifecycle() {
-        return DataStreamLifecycle.newBuilder()
-            .dataRetention(randomRetention())
-            .downsampling(randomDownsampling())
-            .enabled(frequently())
+        return DataStreamLifecycle.builder()
+            .dataRetention(randomBoolean() ? null : randomTimeValue(1, 365, TimeUnit.DAYS))
+            .downsampling(randomBoolean() ? null : randomDownsampling())
+            .enabled(randomBoolean())
             .build();
     }
 
-    @Nullable
-    private static DataStreamLifecycle.Retention randomRetention() {
-        return switch (randomInt(2)) {
-            case 0 -> null;
-            case 1 -> DataStreamLifecycle.Retention.NULL;
-            default -> new DataStreamLifecycle.Retention(randomTimeValue(1, 365, TimeUnit.DAYS));
-        };
-    }
-
-    @Nullable
-    static DataStreamLifecycle.Downsampling randomDownsampling() {
-        return switch (randomInt(2)) {
-            case 0 -> null;
-            case 1 -> DataStreamLifecycle.Downsampling.NULL;
-            default -> {
-                var count = randomIntBetween(0, 9);
-                List<DataStreamLifecycle.Downsampling.Round> rounds = new ArrayList<>();
-                var previous = new DataStreamLifecycle.Downsampling.Round(
-                    randomTimeValue(1, 365, TimeUnit.DAYS),
-                    new DownsampleConfig(new DateHistogramInterval(randomIntBetween(1, 24) + "h"))
-                );
-                rounds.add(previous);
-                for (int i = 0; i < count; i++) {
-                    DataStreamLifecycle.Downsampling.Round round = nextRound(previous);
-                    rounds.add(round);
-                    previous = round;
-                }
-                yield new DataStreamLifecycle.Downsampling(rounds);
-            }
-        };
+    static List<DataStreamLifecycle.DownsamplingRound> randomDownsampling() {
+        var count = randomIntBetween(0, 9);
+        List<DataStreamLifecycle.DownsamplingRound> rounds = new ArrayList<>();
+        var previous = new DataStreamLifecycle.DownsamplingRound(
+            randomTimeValue(1, 365, TimeUnit.DAYS),
+            new DownsampleConfig(new DateHistogramInterval(randomIntBetween(1, 24) + "h"))
+        );
+        rounds.add(previous);
+        for (int i = 0; i < count; i++) {
+            DataStreamLifecycle.DownsamplingRound round = nextRound(previous);
+            rounds.add(round);
+            previous = round;
+        }
+        return rounds;
     }
 
-    private static DataStreamLifecycle.Downsampling.Round nextRound(DataStreamLifecycle.Downsampling.Round previous) {
+    private static DataStreamLifecycle.DownsamplingRound nextRound(DataStreamLifecycle.DownsamplingRound previous) {
         var after = TimeValue.timeValueDays(previous.after().days() + randomIntBetween(1, 10));
         var fixedInterval = new DownsampleConfig(
             new DateHistogramInterval((previous.config().getFixedInterval().estimateMillis() * randomIntBetween(2, 5)) + "ms")
         );
-        return new DataStreamLifecycle.Downsampling.Round(after, fixedInterval);
+        return new DataStreamLifecycle.DownsamplingRound(after, fixedInterval);
     }
 }

+ 11 - 15
server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamLifecycleWithRetentionWarningsTests.java

@@ -58,13 +58,13 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
         ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
         HeaderWarning.setThreadContext(threadContext);
 
-        DataStreamLifecycle noRetentionLifecycle = DataStreamLifecycle.newBuilder().downsampling(randomDownsampling()).build();
+        DataStreamLifecycle noRetentionLifecycle = DataStreamLifecycle.builder().downsampling(randomDownsampling()).build();
         noRetentionLifecycle.addWarningHeaderIfDataRetentionNotEffective(null, randomBoolean());
         Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
         assertThat(responseHeaders.isEmpty(), is(true));
 
         TimeValue dataStreamRetention = TimeValue.timeValueDays(randomIntBetween(5, 100));
-        DataStreamLifecycle lifecycleWithRetention = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle lifecycleWithRetention = DataStreamLifecycle.builder()
             .dataRetention(dataStreamRetention)
             .downsampling(randomDownsampling())
             .build();
@@ -81,7 +81,7 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
         ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
         HeaderWarning.setThreadContext(threadContext);
 
-        DataStreamLifecycle noRetentionLifecycle = DataStreamLifecycle.newBuilder().downsampling(randomDownsampling()).build();
+        DataStreamLifecycle noRetentionLifecycle = DataStreamLifecycle.builder().downsampling(randomDownsampling()).build();
         DataStreamGlobalRetention globalRetention = new DataStreamGlobalRetention(
             randomTimeValue(2, 10, TimeUnit.DAYS),
             randomBoolean() ? null : TimeValue.timeValueDays(20)
@@ -103,7 +103,7 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
         ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
         HeaderWarning.setThreadContext(threadContext);
         TimeValue maxRetention = randomTimeValue(2, 100, TimeUnit.DAYS);
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle lifecycle = DataStreamLifecycle.builder()
             .dataRetention(randomBoolean() ? null : TimeValue.timeValueDays(maxRetention.days() + 1))
             .downsampling(randomDownsampling())
             .build();
@@ -111,10 +111,10 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
         lifecycle.addWarningHeaderIfDataRetentionNotEffective(globalRetention, false);
         Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
         assertThat(responseHeaders.size(), is(1));
-        String userRetentionPart = lifecycle.getDataStreamRetention() == null
+        String userRetentionPart = lifecycle.dataRetention() == null
             ? "Not providing a retention is not allowed for this project."
             : "The retention provided ["
-                + lifecycle.getDataStreamRetention().getStringRep()
+                + lifecycle.dataRetention().getStringRep()
                 + "] is exceeding the max allowed data retention of this project ["
                 + maxRetention.getStringRep()
                 + "].";
@@ -169,7 +169,7 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
             ProjectMetadata.builder(randomProjectIdOrDefault()).build(),
             randomAlphaOfLength(10),
             ComposableIndexTemplate.builder()
-                .template(Template.builder().lifecycle(DataStreamLifecycle.DEFAULT))
+                .template(Template.builder().lifecycle(DataStreamLifecycle.Template.DEFAULT))
                 .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate())
                 .indexPatterns(List.of(randomAlphaOfLength(10)))
                 .build(),
@@ -195,7 +195,7 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
             ProjectMetadata.builder(randomProjectIdOrDefault()).build(),
             randomAlphaOfLength(10),
             ComposableIndexTemplate.builder()
-                .template(Template.builder().lifecycle(DataStreamLifecycle.DEFAULT))
+                .template(Template.builder().lifecycle(DataStreamLifecycle.Template.DEFAULT))
                 .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate())
                 .indexPatterns(List.of("." + randomAlphaOfLength(10)))
                 .build(),
@@ -220,11 +220,7 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
                         new ComponentTemplate(
                             Template.builder()
                                 .lifecycle(
-                                    new DataStreamLifecycle(
-                                        new DataStreamLifecycle.Retention(randomTimeValue(2, 100, TimeUnit.DAYS)),
-                                        null,
-                                        null
-                                    )
+                                    DataStreamLifecycle.Template.builder().dataRetention(randomTimeValue(2, 100, TimeUnit.DAYS)).build()
                                 )
                                 .build(),
                             null,
@@ -235,7 +231,7 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
                 .build(),
             randomAlphaOfLength(10),
             ComposableIndexTemplate.builder()
-                .template(Template.builder().lifecycle(DataStreamLifecycle.DEFAULT))
+                .template(Template.builder().lifecycle(DataStreamLifecycle.Template.DEFAULT))
                 .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate())
                 .indexPatterns(List.of(randomAlphaOfLength(10)))
                 .componentTemplates(List.of("component-template"))
@@ -288,7 +284,7 @@ public class DataStreamLifecycleWithRetentionWarningsTests extends ESTestCase {
         Template template = Template.builder()
             .settings(ComponentTemplateTests.randomSettings())
             .aliases(ComponentTemplateTests.randomAliases())
-            .lifecycle(DataStreamLifecycle.DEFAULT)
+            .lifecycle(DataStreamLifecycle.Template.DEFAULT)
             .build();
         ComponentTemplate componentTemplate = new ComponentTemplate(template, 1L, new HashMap<>());
         project = metadataIndexTemplateService.addComponentTemplate(project, false, "foo", componentTemplate);

+ 42 - 44
server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java

@@ -139,7 +139,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
                 : randomValueOtherThan(indexMode, () -> randomFrom(IndexMode.values()));
             case 9 -> lifecycle = randomBoolean() && lifecycle != null
                 ? null
-                : DataStreamLifecycle.newBuilder().dataRetention(randomMillisUpToYear9999()).build();
+                : DataStreamLifecycle.builder().dataRetention(randomMillisUpToYear9999()).build();
             case 10 -> failureIndices = randomValueOtherThan(failureIndices, DataStreamTestHelper::randomIndexInstances);
             case 11 -> dataStreamOptions = dataStreamOptions.isEmpty() ? new DataStreamOptions(new DataStreamFailureStore(randomBoolean()))
                 : randomBoolean() ? DataStreamOptions.EMPTY
@@ -1474,7 +1474,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
                 dataStreamName,
                 creationAndRolloverTimes,
                 settings(IndexVersion.current()),
-                DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueSeconds(2500)).build()
+                DataStreamLifecycle.builder().dataRetention(TimeValue.timeValueSeconds(2500)).build()
             );
             Metadata metadata = builder.build();
 
@@ -1496,7 +1496,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
                 dataStreamName,
                 creationAndRolloverTimes,
                 settings(IndexVersion.current()),
-                DataStreamLifecycle.newBuilder().dataRetention(0).build()
+                DataStreamLifecycle.builder().dataRetention(0).build()
             );
             Metadata metadata = builder.build();
 
@@ -1521,7 +1521,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
                 dataStreamName,
                 creationAndRolloverTimes,
                 settings(IndexVersion.current()),
-                DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueSeconds(6000)).build()
+                DataStreamLifecycle.builder().dataRetention(TimeValue.timeValueSeconds(6000)).build()
             );
             Metadata metadata = builder.build();
 
@@ -1543,7 +1543,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
                 Settings.builder()
                     .put(IndexMetadata.LIFECYCLE_NAME, "ILM_policy")
                     .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()),
-                DataStreamLifecycle.newBuilder().dataRetention(0).build()
+                DataStreamLifecycle.builder().dataRetention(0).build()
             );
             Metadata metadata = builder.build();
 
@@ -1577,7 +1577,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
             creationAndRolloverTimes,
             settings(IndexVersion.current()),
             new DataStreamLifecycle() {
-                public TimeValue getDataStreamRetention() {
+                public TimeValue dataRetention() {
                     return testRetentionReference.get();
                 }
             }
@@ -1641,22 +1641,21 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
                 creationAndRolloverTimes,
                 settings(IndexVersion.current()).put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
                     .put("index.routing_path", "@timestamp"),
-                DataStreamLifecycle.newBuilder()
+                DataStreamLifecycle.builder()
                     .downsampling(
-                        new DataStreamLifecycle.Downsampling(
-                            List.of(
-                                new DataStreamLifecycle.Downsampling.Round(
-                                    TimeValue.timeValueMillis(2000),
-                                    new DownsampleConfig(new DateHistogramInterval("10m"))
-                                ),
-                                new DataStreamLifecycle.Downsampling.Round(
-                                    TimeValue.timeValueMillis(3200),
-                                    new DownsampleConfig(new DateHistogramInterval("100m"))
-                                ),
-                                new DataStreamLifecycle.Downsampling.Round(
-                                    TimeValue.timeValueMillis(3500),
-                                    new DownsampleConfig(new DateHistogramInterval("1000m"))
-                                )
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueMillis(2000),
+                                new DownsampleConfig(new DateHistogramInterval("10m"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueMillis(3200),
+                                new DownsampleConfig(new DateHistogramInterval("100m"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueMillis(3500),
+                                new DownsampleConfig(new DateHistogramInterval("1000m"))
+
                             )
                         )
                     )
@@ -1667,7 +1666,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
             // generation time is now - 2000
             String thirdGeneration = DataStream.getDefaultBackingIndexName(dataStreamName, 3);
             Index thirdIndex = metadata.getProject().index(thirdGeneration).getIndex();
-            List<DataStreamLifecycle.Downsampling.Round> roundsForThirdIndex = dataStream.getDownsamplingRoundsFor(
+            List<DataStreamLifecycle.DownsamplingRound> roundsForThirdIndex = dataStream.getDownsamplingRoundsFor(
                 thirdIndex,
                 metadata.getProject()::index,
                 () -> now
@@ -1679,7 +1678,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
             // generation time is now - 40000
             String firstGeneration = DataStream.getDefaultBackingIndexName(dataStreamName, 1);
             Index firstIndex = metadata.getProject().index(firstGeneration).getIndex();
-            List<DataStreamLifecycle.Downsampling.Round> roundsForFirstIndex = dataStream.getDownsamplingRoundsFor(
+            List<DataStreamLifecycle.DownsamplingRound> roundsForFirstIndex = dataStream.getDownsamplingRoundsFor(
                 firstIndex,
                 metadata.getProject()::index,
                 () -> now
@@ -1688,7 +1687,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
             assertThat(roundsForFirstIndex.size(), is(3));
             // assert the order is maintained
             assertThat(
-                roundsForFirstIndex.stream().map(DataStreamLifecycle.Downsampling.Round::after).toList(),
+                roundsForFirstIndex.stream().map(DataStreamLifecycle.DownsamplingRound::after).toList(),
                 is(List.of(TimeValue.timeValueMillis(2000), TimeValue.timeValueMillis(3200), TimeValue.timeValueMillis(3500)))
             );
         }
@@ -1702,24 +1701,23 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
                 creationAndRolloverTimes,
                 // no TSDB settings
                 settings(IndexVersion.current()),
-                DataStreamLifecycle.newBuilder()
+                DataStreamLifecycle.builder()
                     .downsampling(
-                        new DataStreamLifecycle.Downsampling(
-                            List.of(
-                                new DataStreamLifecycle.Downsampling.Round(
-                                    TimeValue.timeValueMillis(2000),
-                                    new DownsampleConfig(new DateHistogramInterval("10m"))
-                                ),
-                                new DataStreamLifecycle.Downsampling.Round(
-                                    TimeValue.timeValueMillis(3200),
-                                    new DownsampleConfig(new DateHistogramInterval("100m"))
-                                ),
-                                new DataStreamLifecycle.Downsampling.Round(
-                                    TimeValue.timeValueMillis(3500),
-                                    new DownsampleConfig(new DateHistogramInterval("1000m"))
-                                )
+                        List.of(
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueMillis(2000),
+                                new DownsampleConfig(new DateHistogramInterval("10m"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueMillis(3200),
+                                new DownsampleConfig(new DateHistogramInterval("100m"))
+                            ),
+                            new DataStreamLifecycle.DownsamplingRound(
+                                TimeValue.timeValueMillis(3500),
+                                new DownsampleConfig(new DateHistogramInterval("1000m"))
                             )
                         )
+
                     )
                     .build()
             );
@@ -1728,7 +1726,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
             // generation time is now - 40000
             String firstGeneration = DataStream.getDefaultBackingIndexName(dataStreamName, 1);
             Index firstIndex = metadata.getProject().index(firstGeneration).getIndex();
-            List<DataStreamLifecycle.Downsampling.Round> roundsForFirstIndex = dataStream.getDownsamplingRoundsFor(
+            List<DataStreamLifecycle.DownsamplingRound> roundsForFirstIndex = dataStream.getDownsamplingRoundsFor(
                 firstIndex,
                 metadata.getProject()::index,
                 () -> now
@@ -1752,7 +1750,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
             // generation time is now - 40000
             String firstGeneration = DataStream.getDefaultBackingIndexName(dataStreamName, 1);
             Index firstIndex = metadata.getProject().index(firstGeneration).getIndex();
-            List<DataStreamLifecycle.Downsampling.Round> roundsForFirstIndex = dataStream.getDownsamplingRoundsFor(
+            List<DataStreamLifecycle.DownsamplingRound> roundsForFirstIndex = dataStream.getDownsamplingRoundsFor(
                 firstIndex,
                 metadata.getProject()::index,
                 () -> now
@@ -1769,13 +1767,13 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
                 creationAndRolloverTimes,
                 settings(IndexVersion.current()).put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
                     .put("index.routing_path", "@timestamp"),
-                DataStreamLifecycle.newBuilder().build()
+                DataStreamLifecycle.builder().build()
             );
             Metadata metadata = builder.build();
 
             String firstGeneration = DataStream.getDefaultBackingIndexName(dataStreamName, 1);
             Index firstIndex = metadata.getProject().index(firstGeneration).getIndex();
-            List<DataStreamLifecycle.Downsampling.Round> roundsForFirstIndex = dataStream.getDownsamplingRoundsFor(
+            List<DataStreamLifecycle.DownsamplingRound> roundsForFirstIndex = dataStream.getDownsamplingRoundsFor(
                 firstIndex,
                 metadata.getProject()::index,
                 () -> now
@@ -1801,7 +1799,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase<DataStr
             dataStreamName,
             creationAndRolloverTimes,
             settings(IndexVersion.current()),
-            DataStreamLifecycle.newBuilder().dataRetention(0).build()
+            DataStreamLifecycle.builder().dataRetention(0).build()
         );
         Metadata metadata = builder.build();
 

+ 1 - 1
server/src/test/java/org/elasticsearch/cluster/metadata/MetadataDataStreamsServiceTests.java

@@ -415,7 +415,7 @@ public class MetadataDataStreamsServiceTests extends MapperServiceTestCase {
 
     public void testUpdateLifecycle() {
         String dataStream = randomAlphaOfLength(5);
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder().dataRetention(randomMillisUpToYear9999()).build();
+        DataStreamLifecycle lifecycle = DataStreamLifecycle.builder().dataRetention(randomMillisUpToYear9999()).build();
         final var projectId = randomProjectIdOrDefault();
         ProjectMetadata before = DataStreamTestHelper.getClusterStateWithDataStreams(
             projectId,

+ 33 - 30
server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java

@@ -1488,21 +1488,21 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         MetadataIndexTemplateService service = getMetadataIndexTemplateService();
         ProjectMetadata project = ProjectMetadata.builder(randomProjectIdOrDefault()).build();
 
-        DataStreamLifecycle emptyLifecycle = new DataStreamLifecycle();
+        DataStreamLifecycle.Template emptyLifecycle = DataStreamLifecycle.Template.DEFAULT;
 
-        DataStreamLifecycle lifecycle30d = DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueDays(30)).build();
+        DataStreamLifecycle.Template lifecycle30d = DataStreamLifecycle.Template.builder()
+            .dataRetention(TimeValue.timeValueDays(30))
+            .build();
         String ct30d = "ct_30d";
         project = addComponentTemplate(service, project, ct30d, lifecycle30d);
 
-        DataStreamLifecycle lifecycle45d = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle.Template lifecycle45d = DataStreamLifecycle.Template.builder()
             .dataRetention(TimeValue.timeValueDays(45))
             .downsampling(
-                new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueDays(30),
-                            new DownsampleConfig(new DateHistogramInterval("3h"))
-                        )
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueDays(30),
+                        new DownsampleConfig(new DateHistogramInterval("3h"))
                     )
                 )
             )
@@ -1510,7 +1510,8 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         String ct45d = "ct_45d";
         project = addComponentTemplate(service, project, ct45d, lifecycle45d);
 
-        DataStreamLifecycle lifecycleNullRetention = new DataStreamLifecycle.Builder().dataRetention(DataStreamLifecycle.Retention.NULL)
+        DataStreamLifecycle.Template lifecycleNullRetention = DataStreamLifecycle.Template.builder()
+            .dataRetention(ResettableValue.reset())
             .build();
         String ctNullRetention = "ct_null_retention";
         project = addComponentTemplate(service, project, ctNullRetention, lifecycleNullRetention);
@@ -1519,10 +1520,15 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         project = addComponentTemplate(service, project, ctEmptyLifecycle, emptyLifecycle);
 
         String ctDisabledLifecycle = "ct_disabled_lifecycle";
-        project = addComponentTemplate(service, project, ctDisabledLifecycle, DataStreamLifecycle.newBuilder().enabled(false).build());
+        project = addComponentTemplate(
+            service,
+            project,
+            ctDisabledLifecycle,
+            DataStreamLifecycle.Template.builder().enabled(false).build()
+        );
 
         String ctNoLifecycle = "ct_no_lifecycle";
-        project = addComponentTemplate(service, project, ctNoLifecycle, (DataStreamLifecycle) null);
+        project = addComponentTemplate(service, project, ctNoLifecycle, (DataStreamLifecycle.Template) null);
 
         // Component A: -
         // Component B: "lifecycle": {"enabled": true}
@@ -1551,9 +1557,9 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
             project,
             List.of(ctEmptyLifecycle, ct45d),
             lifecycle30d,
-            DataStreamLifecycle.newBuilder()
-                .dataRetention(lifecycle30d.getDataRetention())
-                .downsampling(lifecycle45d.getDownsampling())
+            DataStreamLifecycle.Template.builder()
+                .dataRetention(lifecycle30d.dataRetention())
+                .downsampling(lifecycle45d.downsampling())
                 .build()
         );
 
@@ -1575,10 +1581,7 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
             project,
             List.of(ctEmptyLifecycle, ct45d),
             lifecycleNullRetention,
-            DataStreamLifecycle.newBuilder()
-                .dataRetention(DataStreamLifecycle.Retention.NULL)
-                .downsampling(lifecycle45d.getDownsampling())
-                .build()
+            DataStreamLifecycle.Template.builder().dataRetention(ResettableValue.reset()).downsampling(lifecycle45d.downsampling()).build()
         );
 
         // Component A: "lifecycle": {"retention": "30d"}
@@ -1589,10 +1592,10 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
             service,
             project,
             List.of(ct30d, ct45d),
-            DataStreamLifecycle.newBuilder().enabled(false).build(),
-            DataStreamLifecycle.newBuilder()
-                .dataRetention(lifecycle45d.getDataRetention())
-                .downsampling(lifecycle45d.getDownsampling())
+            DataStreamLifecycle.Template.builder().enabled(false).build(),
+            DataStreamLifecycle.Template.builder()
+                .dataRetention(lifecycle45d.dataRetention())
+                .downsampling(lifecycle45d.downsampling())
                 .enabled(false)
                 .build()
         );
@@ -1606,7 +1609,7 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
             project,
             List.of(ct30d, ctDisabledLifecycle),
             null,
-            DataStreamLifecycle.newBuilder().dataRetention(lifecycle30d.getDataRetention()).enabled(false).build()
+            DataStreamLifecycle.Template.builder().dataRetention(lifecycle30d.dataRetention()).enabled(false).build()
         );
 
         // Component A: "lifecycle": {"retention": "30d"}
@@ -1717,7 +1720,7 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         MetadataIndexTemplateService service,
         ProjectMetadata project,
         String name,
-        DataStreamLifecycle lifecycle
+        DataStreamLifecycle.Template lifecycle
     ) throws Exception {
         return addComponentTemplate(service, project, name, null, lifecycle);
     }
@@ -1736,7 +1739,7 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         ProjectMetadata project,
         String name,
         DataStreamOptions.Template dataStreamOptions,
-        DataStreamLifecycle lifecycle
+        DataStreamLifecycle.Template lifecycle
     ) throws Exception {
         ComponentTemplate ct = new ComponentTemplate(
             Template.builder().dataStreamOptions(dataStreamOptions).lifecycle(lifecycle).build(),
@@ -1750,8 +1753,8 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         MetadataIndexTemplateService service,
         ProjectMetadata project,
         List<String> composeOf,
-        DataStreamLifecycle lifecycleZ,
-        DataStreamLifecycle expected
+        DataStreamLifecycle.Template lifecycleZ,
+        DataStreamLifecycle.Template expected
     ) throws Exception {
         ComposableIndexTemplate it = ComposableIndexTemplate.builder()
             .indexPatterns(List.of(randomAlphaOfLength(10) + "*"))
@@ -1763,7 +1766,7 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
             .build();
         project = service.addIndexTemplateV2(project, true, "my-template", it);
 
-        DataStreamLifecycle resolvedLifecycle = MetadataIndexTemplateService.resolveLifecycle(project, "my-template");
+        DataStreamLifecycle.Template resolvedLifecycle = MetadataIndexTemplateService.resolveLifecycle(project, "my-template");
         assertThat(resolvedLifecycle, equalTo(expected));
     }
 
@@ -2006,7 +2009,7 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
         ProjectMetadata project = ProjectMetadata.builder(randomProjectIdOrDefault()).build();
 
         ComponentTemplate ct = new ComponentTemplate(
-            Template.builder().lifecycle(DataStreamLifecycle.newBuilder().dataRetention(randomMillisUpToYear9999())).build(),
+            Template.builder().lifecycle(DataStreamLifecycle.Template.builder().dataRetention(randomPositiveTimeValue())).build(),
             null,
             null
         );

+ 1 - 1
test/framework/src/main/java/org/elasticsearch/cluster/metadata/DataStreamTestHelper.java

@@ -361,7 +361,7 @@ public final class DataStreamTestHelper {
             timeProvider,
             randomBoolean(),
             randomBoolean() ? IndexMode.STANDARD : null, // IndexMode.TIME_SERIES triggers validation that many unit tests doesn't pass
-            randomBoolean() ? DataStreamLifecycle.newBuilder().dataRetention(randomMillisUpToYear9999()).build() : null,
+            randomBoolean() ? DataStreamLifecycle.builder().dataRetention(randomMillisUpToYear9999()).build() : null,
             failureStore ? DataStreamOptions.FAILURE_STORE_ENABLED : DataStreamOptions.EMPTY,
             DataStream.DataStreamIndices.backingIndicesBuilder(indices)
                 .setRolloverOnWrite(replicated == false && randomBoolean())

+ 1 - 1
x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/core/action/DataStreamLifecycleUsageTransportActionIT.java

@@ -157,7 +157,7 @@ public class DataStreamLifecycleUsageTransportActionIT extends ESIntegTestCase {
                             }
                             atLeastOne = true;
                         }
-                        lifecycle = DataStreamLifecycle.newBuilder().dataRetention(retentionMillis).enabled(isEnabled).build();
+                        lifecycle = DataStreamLifecycle.builder().dataRetention(retentionMillis).enabled(isEnabled).build();
                     }
                 } else {
                     lifecycle = null;

+ 3 - 3
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/DataStreamLifecycleUsageTransportAction.java

@@ -80,11 +80,11 @@ public class DataStreamLifecycleUsageTransportAction extends XPackUsageFeatureTr
         LongSummaryStatistics effectiveRetentionStats = new LongSummaryStatistics();
 
         for (DataStream dataStream : dataStreams) {
-            if (dataStream.getLifecycle() != null && dataStream.getLifecycle().isEnabled()) {
+            if (dataStream.getLifecycle() != null && dataStream.getLifecycle().enabled()) {
                 dataStreamsWithLifecycles++;
                 // Track data retention
-                if (dataStream.getLifecycle().getDataStreamRetention() != null) {
-                    dataRetentionStats.accept(dataStream.getLifecycle().getDataStreamRetention().getMillis());
+                if (dataStream.getLifecycle().dataRetention() != null) {
+                    dataRetentionStats.accept(dataStream.getLifecycle().dataRetention().getMillis());
                 }
                 // Track effective retention
                 Tuple<TimeValue, DataStreamLifecycle.RetentionSource> effectiveDataRetentionWithSource = dataStream.getLifecycle()

+ 3 - 3
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/datastreams/DataStreamLifecycleFeatureSetUsageTests.java

@@ -117,7 +117,7 @@ public class DataStreamLifecycleFeatureSetUsageTests extends AbstractWireSeriali
                 1L,
                 null,
                 false,
-                new DataStreamLifecycle(new DataStreamLifecycle.Retention(TimeValue.timeValueSeconds(50)), null, true)
+                new DataStreamLifecycle(true, TimeValue.timeValueSeconds(50), null)
             ),
             DataStreamTestHelper.newInstance(
                 randomAlphaOfLength(10),
@@ -125,7 +125,7 @@ public class DataStreamLifecycleFeatureSetUsageTests extends AbstractWireSeriali
                 1L,
                 null,
                 false,
-                new DataStreamLifecycle(new DataStreamLifecycle.Retention(TimeValue.timeValueMillis(150)), null, true)
+                new DataStreamLifecycle(true, TimeValue.timeValueMillis(150), null)
             ),
             DataStreamTestHelper.newInstance(
                 randomAlphaOfLength(10),
@@ -133,7 +133,7 @@ public class DataStreamLifecycleFeatureSetUsageTests extends AbstractWireSeriali
                 1L,
                 null,
                 false,
-                new DataStreamLifecycle(new DataStreamLifecycle.Retention(TimeValue.timeValueSeconds(5)), null, false)
+                new DataStreamLifecycle(false, TimeValue.timeValueSeconds(5), null)
             ),
             DataStreamTestHelper.newInstance(
                 randomAlphaOfLength(10),

+ 5 - 7
x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DataStreamLifecycleDownsampleDisruptionIT.java

@@ -64,14 +64,12 @@ public class DataStreamLifecycleDownsampleDisruptionIT extends ESIntegTestCase {
         ensureGreen();
 
         final String dataStreamName = "metrics-foo";
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder()
             .downsampling(
-                new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueMillis(0),
-                            new DownsampleConfig(new DateHistogramInterval("5m"))
-                        )
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(0),
+                        new DownsampleConfig(new DateHistogramInterval("5m"))
                     )
                 )
             )

+ 37 - 23
x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DataStreamLifecycleDownsampleIT.java

@@ -12,7 +12,6 @@ import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
 import org.elasticsearch.action.datastreams.lifecycle.PutDataStreamLifecycleAction;
 import org.elasticsearch.action.downsample.DownsampleConfig;
 import org.elasticsearch.cluster.metadata.DataStreamLifecycle;
-import org.elasticsearch.cluster.metadata.DataStreamLifecycle.Downsampling;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.datastreams.DataStreamsPlugin;
@@ -55,12 +54,16 @@ public class DataStreamLifecycleDownsampleIT extends ESIntegTestCase {
     public void testDownsampling() throws Exception {
         String dataStreamName = "metrics-foo";
 
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder()
             .downsampling(
-                new Downsampling(
-                    List.of(
-                        new Downsampling.Round(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))),
-                        new Downsampling.Round(TimeValue.timeValueSeconds(10), new DownsampleConfig(new DateHistogramInterval("10m")))
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(0),
+                        new DownsampleConfig(new DateHistogramInterval("5m"))
+                    ),
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueSeconds(10),
+                        new DownsampleConfig(new DateHistogramInterval("10m"))
                     )
                 )
             )
@@ -124,14 +127,18 @@ public class DataStreamLifecycleDownsampleIT extends ESIntegTestCase {
     public void testDownsamplingOnlyExecutesTheLastMatchingRound() throws Exception {
         String dataStreamName = "metrics-bar";
 
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder()
             .downsampling(
-                new Downsampling(
-                    List.of(
-                        new Downsampling.Round(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))),
-                        // data stream lifecycle runs every 1 second, so by the time we forcemerge the backing index it would've been at
-                        // least 2 seconds since rollover. only the 10 seconds round should be executed.
-                        new Downsampling.Round(TimeValue.timeValueMillis(10), new DownsampleConfig(new DateHistogramInterval("10m")))
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(0),
+                        new DownsampleConfig(new DateHistogramInterval("5m"))
+                    ),
+                    // data stream lifecycle runs every 1 second, so by the time we forcemerge the backing index it would've been at
+                    // least 2 seconds since rollover. only the 10 seconds round should be executed.
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(10),
+                        new DownsampleConfig(new DateHistogramInterval("10m"))
                     )
                 )
             )
@@ -188,14 +195,18 @@ public class DataStreamLifecycleDownsampleIT extends ESIntegTestCase {
         // we expect the earlier round to be ignored
         String dataStreamName = "metrics-baz";
 
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder()
             .downsampling(
-                new Downsampling(
-                    List.of(
-                        new Downsampling.Round(TimeValue.timeValueMillis(0), new DownsampleConfig(new DateHistogramInterval("5m"))),
-                        // data stream lifecycle runs every 1 second, so by the time we forcemerge the backing index it would've been at
-                        // least 2 seconds since rollover. only the 10 seconds round should be executed.
-                        new Downsampling.Round(TimeValue.timeValueMillis(10), new DownsampleConfig(new DateHistogramInterval("10m")))
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(0),
+                        new DownsampleConfig(new DateHistogramInterval("5m"))
+                    ),
+                    // data stream lifecycle runs every 1 second, so by the time we forcemerge the backing index it would've been at
+                    // least 2 seconds since rollover. only the 10 seconds round should be executed.
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(10),
+                        new DownsampleConfig(new DateHistogramInterval("10m"))
                     )
                 )
             )
@@ -248,10 +259,13 @@ public class DataStreamLifecycleDownsampleIT extends ESIntegTestCase {
         // update the lifecycle so that it only has one round, for the same `after` parameter as before, but a different interval
         // the different interval should yield a different downsample index name so we expect the data stream lifecycle to get the previous
         // `10s` interval downsample index, downsample it to `30s` and replace it in the data stream instead of the `10s` one.
-        DataStreamLifecycle updatedLifecycle = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle updatedLifecycle = DataStreamLifecycle.builder()
             .downsampling(
-                new Downsampling(
-                    List.of(new Downsampling.Round(TimeValue.timeValueMillis(10), new DownsampleConfig(new DateHistogramInterval("20m"))))
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(10),
+                        new DownsampleConfig(new DateHistogramInterval("20m"))
+                    )
                 )
             )
             .build();

+ 4 - 4
x-pack/plugin/downsample/src/internalClusterTest/java/org/elasticsearch/xpack/downsample/DataStreamLifecycleDriver.java

@@ -70,7 +70,7 @@ public class DataStreamLifecycleDriver {
         String dataStreamName,
         @Nullable String startTime,
         @Nullable String endTime,
-        DataStreamLifecycle lifecycle,
+        DataStreamLifecycle.Template lifecycle,
         int docCount,
         String firstDocTimestamp
     ) throws IOException {
@@ -94,7 +94,7 @@ public class DataStreamLifecycleDriver {
         String pattern,
         @Nullable String startTime,
         @Nullable String endTime,
-        DataStreamLifecycle lifecycle
+        DataStreamLifecycle.Template lifecycle
     ) throws IOException {
         Settings.Builder settings = indexSettings(1, 0).put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
             .putList(IndexMetadata.INDEX_ROUTING_PATH.getKey(), List.of(FIELD_DIMENSION_1));
@@ -138,8 +138,8 @@ public class DataStreamLifecycleDriver {
         List<String> patterns,
         @Nullable Settings settings,
         @Nullable Map<String, Object> metadata,
-        @Nullable DataStreamLifecycle lifecycle
-    ) throws IOException {
+        @Nullable DataStreamLifecycle.Template lifecycle
+    ) {
         TransportPutComposableIndexTemplateAction.Request request = new TransportPutComposableIndexTemplateAction.Request(id);
         request.indexTemplate(
             ComposableIndexTemplate.builder()

+ 21 - 14
x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataStreamAndIndexLifecycleMixingTests.java

@@ -173,7 +173,9 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
 
         // we'll rollover the data stream by indexing 2 documents (like ILM expects) and assert that the rollover happens once so the
         // data stream has 3 backing indices, two managed by ILM and one will be managed by the data stream lifecycle
-        DataStreamLifecycle customLifecycle = customEnabledLifecycle();
+        DataStreamLifecycle.Template customLifecycle = DataStreamLifecycle.Template.builder()
+            .dataRetention(randomPositiveTimeValue())
+            .build();
         putComposableIndexTemplate(indexTemplateName, null, List.of(dataStreamName + "*"), Settings.EMPTY, null, customLifecycle);
 
         indexDocs(dataStreamName, 2);
@@ -263,7 +265,7 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
                 TEST_REQUEST_TIMEOUT,
                 TEST_REQUEST_TIMEOUT,
                 new String[] { dataStreamName },
-                customLifecycle.getDataStreamRetention()
+                customLifecycle.dataRetention().get()
             )
         ).actionGet();
 
@@ -305,7 +307,7 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
             assertThat(dataStreamLifecycleExplainResponse.getIndices().size(), is(2));
             for (ExplainIndexDataStreamLifecycle index : dataStreamLifecycleExplainResponse.getIndices()) {
                 assertThat(index.isManagedByLifecycle(), is(true));
-                assertThat(index.getLifecycle(), equalTo(customLifecycle));
+                assertThat(index.getLifecycle(), equalTo(customLifecycle.toDataStreamLifecycle()));
             }
         });
     }
@@ -385,7 +387,7 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
             List.of(dataStreamName + "*"),
             Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, policy).build(),
             null,
-            new DataStreamLifecycle()
+            DataStreamLifecycle.Template.DEFAULT
         );
 
         indexDocs(dataStreamName, 2);
@@ -445,7 +447,7 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
             List.of(dataStreamName + "*"),
             Settings.builder().put(IndexSettings.PREFER_ILM, false).put(LifecycleSettings.LIFECYCLE_NAME, policy).build(),
             null,
-            new DataStreamLifecycle()
+            DataStreamLifecycle.Template.DEFAULT
         );
 
         // note that all indices now are still managed by ILM, so we index 2 documents. the new write index will be managed by the data
@@ -571,7 +573,9 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
 
         // we'll rollover the data stream by indexing 2 documents (like ILM expects) and assert that the rollover happens once so the
         // data stream has 3 backing indices, 2 managed by ILM and 1 by the default data stream lifecycle
-        DataStreamLifecycle customLifecycle = customEnabledLifecycle();
+        DataStreamLifecycle.Template customLifecycle = DataStreamLifecycle.Template.builder()
+            .dataRetention(randomPositiveTimeValue())
+            .build();
         putComposableIndexTemplate(
             indexTemplateName,
             null,
@@ -649,7 +653,7 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
                 TEST_REQUEST_TIMEOUT,
                 TEST_REQUEST_TIMEOUT,
                 new String[] { dataStreamName },
-                customLifecycle.getDataStreamRetention()
+                customLifecycle.dataRetention().get()
             )
         ).actionGet();
 
@@ -705,7 +709,7 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
             assertThat(dataStreamLifecycleExplainResponse.getIndices().size(), is(2));
             for (ExplainIndexDataStreamLifecycle index : dataStreamLifecycleExplainResponse.getIndices()) {
                 assertThat(index.isManagedByLifecycle(), is(true));
-                assertThat(index.getLifecycle(), equalTo(customLifecycle));
+                assertThat(index.getLifecycle(), equalTo(customLifecycle.toDataStreamLifecycle()));
             }
         });
 
@@ -763,7 +767,14 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
 
     public void testUpdateIndexTemplateToMigrateFromDataStreamLifecycleToIlm() throws Exception {
         // starting with a data stream managed by the data stream lifecycle (rolling over every 1 doc)
-        putComposableIndexTemplate(indexTemplateName, null, List.of(dataStreamName + "*"), null, null, new DataStreamLifecycle());
+        putComposableIndexTemplate(
+            indexTemplateName,
+            null,
+            List.of(dataStreamName + "*"),
+            null,
+            null,
+            DataStreamLifecycle.Template.DEFAULT
+        );
 
         // this will create the data stream and trigger a rollover so we will end up with a data stream with 2 backing indices
         indexDocs(dataStreamName, 1);
@@ -1075,7 +1086,7 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
         List<String> patterns,
         @Nullable Settings settings,
         @Nullable Map<String, Object> metadata,
-        @Nullable DataStreamLifecycle lifecycle
+        @Nullable DataStreamLifecycle.Template lifecycle
     ) throws IOException {
         TransportPutComposableIndexTemplateAction.Request request = new TransportPutComposableIndexTemplateAction.Request(name);
         request.indexTemplate(
@@ -1094,10 +1105,6 @@ public class DataStreamAndIndexLifecycleMixingTests extends ESIntegTestCase {
         client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet();
     }
 
-    private static DataStreamLifecycle customEnabledLifecycle() {
-        return DataStreamLifecycle.newBuilder().dataRetention(TimeValue.timeValueMillis(randomMillisUpToYear9999())).build();
-    }
-
     private List<String> getBackingIndices(String dataStreamName) {
         GetDataStreamAction.Request getDataStreamRequest = new GetDataStreamAction.Request(
             TEST_REQUEST_TIMEOUT,

+ 21 - 25
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/DataStreamLifecycleDownsamplingSecurityIT.java

@@ -119,18 +119,16 @@ public class DataStreamLifecycleDownsamplingSecurityIT extends SecurityIntegTest
     public void testDownsamplingAuthorized() throws Exception {
         String dataStreamName = "metrics-foo";
 
-        DataStreamLifecycle lifecycle = DataStreamLifecycle.newBuilder()
+        DataStreamLifecycle.Template lifecycle = DataStreamLifecycle.Template.builder()
             .downsampling(
-                new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueMillis(0),
-                            new DownsampleConfig(new DateHistogramInterval("5m"))
-                        ),
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueSeconds(10),
-                            new DownsampleConfig(new DateHistogramInterval("10m"))
-                        )
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(0),
+                        new DownsampleConfig(new DateHistogramInterval("5m"))
+                    ),
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueSeconds(10),
+                        new DownsampleConfig(new DateHistogramInterval("10m"))
                     )
                 )
             )
@@ -284,7 +282,7 @@ public class DataStreamLifecycleDownsamplingSecurityIT extends SecurityIntegTest
         String dataStreamName,
         @Nullable String startTime,
         @Nullable String endTime,
-        DataStreamLifecycle lifecycle,
+        DataStreamLifecycle.Template lifecycle,
         int docCount,
         String firstDocTimestamp
     ) throws IOException {
@@ -298,7 +296,7 @@ public class DataStreamLifecycleDownsamplingSecurityIT extends SecurityIntegTest
         String pattern,
         @Nullable String startTime,
         @Nullable String endTime,
-        DataStreamLifecycle lifecycle
+        DataStreamLifecycle.Template lifecycle
     ) throws IOException {
         Settings.Builder settings = indexSettings(1, 0).put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
             .putList(IndexMetadata.INDEX_ROUTING_PATH.getKey(), List.of(FIELD_DIMENSION_1));
@@ -336,7 +334,7 @@ public class DataStreamLifecycleDownsamplingSecurityIT extends SecurityIntegTest
         List<String> patterns,
         @Nullable Settings settings,
         @Nullable Map<String, Object> metadata,
-        @Nullable DataStreamLifecycle lifecycle
+        @Nullable DataStreamLifecycle.Template lifecycle
     ) {
         TransportPutComposableIndexTemplateAction.Request request = new TransportPutComposableIndexTemplateAction.Request(id);
         request.indexTemplate(
@@ -411,18 +409,16 @@ public class DataStreamLifecycleDownsamplingSecurityIT extends SecurityIntegTest
 
     public static class SystemDataStreamWithDownsamplingConfigurationPlugin extends Plugin implements SystemIndexPlugin {
 
-        public static final DataStreamLifecycle LIFECYCLE = DataStreamLifecycle.newBuilder()
+        public static final DataStreamLifecycle.Template LIFECYCLE = DataStreamLifecycle.Template.builder()
             .downsampling(
-                new DataStreamLifecycle.Downsampling(
-                    List.of(
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueMillis(0),
-                            new DownsampleConfig(new DateHistogramInterval("5m"))
-                        ),
-                        new DataStreamLifecycle.Downsampling.Round(
-                            TimeValue.timeValueSeconds(10),
-                            new DownsampleConfig(new DateHistogramInterval("10m"))
-                        )
+                List.of(
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueMillis(0),
+                        new DownsampleConfig(new DateHistogramInterval("5m"))
+                    ),
+                    new DataStreamLifecycle.DownsamplingRound(
+                        TimeValue.timeValueSeconds(10),
+                        new DownsampleConfig(new DateHistogramInterval("10m"))
                     )
                 )
             )

+ 8 - 7
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/DataStreamLifecycleServiceRuntimeSecurityIT.java

@@ -24,6 +24,7 @@ import org.elasticsearch.cluster.metadata.Template;
 import org.elasticsearch.common.compress.CompressedXContent;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Nullable;
+import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.datastreams.DataStreamsPlugin;
 import org.elasticsearch.datastreams.lifecycle.DataStreamLifecycleErrorStore;
 import org.elasticsearch.datastreams.lifecycle.DataStreamLifecycleService;
@@ -93,7 +94,7 @@ public class DataStreamLifecycleServiceRuntimeSecurityIT extends SecurityIntegTe
     public void testRolloverLifecycleAndForceMergeAuthorized() throws Exception {
         String dataStreamName = randomDataStreamName();
         // empty lifecycle contains the default rollover
-        prepareDataStreamAndIndex(dataStreamName, new DataStreamLifecycle());
+        prepareDataStreamAndIndex(dataStreamName, DataStreamLifecycle.Template.DEFAULT);
 
         assertBusy(() -> {
             assertNoAuthzErrors();
@@ -116,7 +117,7 @@ public class DataStreamLifecycleServiceRuntimeSecurityIT extends SecurityIntegTe
 
     public void testRolloverAndRetentionAuthorized() throws Exception {
         String dataStreamName = randomDataStreamName();
-        prepareDataStreamAndIndex(dataStreamName, DataStreamLifecycle.newBuilder().dataRetention(0).build());
+        prepareDataStreamAndIndex(dataStreamName, DataStreamLifecycle.Template.builder().dataRetention(TimeValue.ZERO).build());
 
         assertBusy(() -> {
             assertNoAuthzErrors();
@@ -132,7 +133,7 @@ public class DataStreamLifecycleServiceRuntimeSecurityIT extends SecurityIntegTe
     public void testUnauthorized() throws Exception {
         // this is an example index pattern for a system index that the data stream lifecycle does not have access for. Data stream
         // lifecycle will therefore fail at runtime with an authz exception
-        prepareDataStreamAndIndex(SECURITY_MAIN_ALIAS, new DataStreamLifecycle());
+        prepareDataStreamAndIndex(SECURITY_MAIN_ALIAS, DataStreamLifecycle.Template.DEFAULT);
 
         assertBusy(() -> {
             Map<String, String> indicesAndErrors = collectErrorsFromStoreAsMap();
@@ -180,8 +181,8 @@ public class DataStreamLifecycleServiceRuntimeSecurityIT extends SecurityIntegTe
         return indicesAndErrors;
     }
 
-    private void prepareDataStreamAndIndex(String dataStreamName, DataStreamLifecycle lifecycle) throws IOException, InterruptedException,
-        ExecutionException {
+    private void prepareDataStreamAndIndex(String dataStreamName, DataStreamLifecycle.Template lifecycle) throws IOException,
+        InterruptedException, ExecutionException {
         putComposableIndexTemplate("id1", null, List.of(dataStreamName + "*"), null, null, lifecycle);
         CreateDataStreamAction.Request createDataStreamRequest = new CreateDataStreamAction.Request(
             TEST_REQUEST_TIMEOUT,
@@ -221,7 +222,7 @@ public class DataStreamLifecycleServiceRuntimeSecurityIT extends SecurityIntegTe
         List<String> patterns,
         @Nullable Settings settings,
         @Nullable Map<String, Object> metadata,
-        @Nullable DataStreamLifecycle lifecycle
+        @Nullable DataStreamLifecycle.Template lifecycle
     ) throws IOException {
         TransportPutComposableIndexTemplateAction.Request request = new TransportPutComposableIndexTemplateAction.Request(id);
         request.indexTemplate(
@@ -271,7 +272,7 @@ public class DataStreamLifecycleServiceRuntimeSecurityIT extends SecurityIntegTe
                     SystemDataStreamDescriptor.Type.EXTERNAL,
                     ComposableIndexTemplate.builder()
                         .indexPatterns(List.of(SYSTEM_DATA_STREAM_NAME))
-                        .template(Template.builder().lifecycle(DataStreamLifecycle.newBuilder().dataRetention(0)))
+                        .template(Template.builder().lifecycle(DataStreamLifecycle.Template.builder().dataRetention(TimeValue.ZERO)))
                         .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate())
                         .build(),
                     Map.of(),