Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ private static ProjectMetadata getProjectWithDataStreamWithSettings(
Template.builder().settings(componentTemplateSettings).build(),
null,
null,
null,
null,
null
);
builder.componentTemplates(Map.of("component_template_1", componentTemplate));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
setup:
- requires:
test_runner_features: capabilities
capabilities:
- method: PUT
path: /_component_template/{id}
capabilities: [ component_template_tracking_info ]
reason: "Templates have tracking info: modified_date and created_date"

---
"Test PUT setting created_date":
- do:
catch: bad_request
cluster.put_component_template:
name: test_tracking
body:
template:
settings:
number_of_shards: 1
created_date: "2025-07-04T12:50:48.415Z"
- match: { status: 400 }
- match: { error.reason: "Validation Failed: 1: Provided a template property which is managed by the system: created_date;" }

---
"Test PUT setting modified_date":
- do:
catch: bad_request
cluster.put_component_template:
name: test_tracking
body:
template:
settings:
number_of_shards: 1
modified_date: "2025-07-04T12:50:48.415Z"
- match: { status: 400 }
- match: { error.reason: "Validation Failed: 1: Provided a template property which is managed by the system: modified_date;" }

---
"Test update preserves created_date but updates modified_date":
- do:
cluster.put_component_template:
name: test_tracking
body:
template:
settings:
number_of_shards: 1
- match: { acknowledged: true }

- do:
cluster.get_component_template:
name: test_tracking
- set: { component_templates.0.component_template.created_date: first_created }
- set: { component_templates.0.component_template.modified_date: first_modified }
- match: { $first_created: "/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/" }
- match: { $first_modified: "/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/" }
- match: { $first_created: $first_modified }

- do:
cluster.put_component_template:
name: test_tracking
body:
template:
settings:
number_of_shards: 2

- do:
cluster.get_component_template:
name: test_tracking
- set: { component_templates.0.component_template.created_date: second_created }
- match: { $second_created: $first_created }

Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ static TransportVersion def(int id) {
public static final TransportVersion SHARD_WRITE_LOAD_IN_CLUSTER_INFO = def(9_126_0_00);
public static final TransportVersion ESQL_SAMPLE_OPERATOR_STATUS = def(9_127_0_00);
public static final TransportVersion ESQL_TOPN_TIMINGS = def(9_128_0_00);
public static final TransportVersion COMPONENT_TEMPLATE_TRACKING_INFO = def(9_129_0_00);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ public ActionRequestValidationException validate() {
if (componentTemplate == null) {
validationException = addValidationError("a component template is required", validationException);
}
if (componentTemplate.createdDateMillis().isPresent()) {
validationException = addValidationError(
"Provided a template property which is managed by the system: created_date",
validationException
);
}
if (componentTemplate.modifiedDateMillis().isPresent()) {
validationException = addValidationError(
"Provided a template property which is managed by the system: modified_date",
validationException
);
}
return validationException;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ public static ComponentTemplate normalizeComponentTemplate(
template,
componentTemplate.version(),
componentTemplate.metadata(),
componentTemplate.deprecated()
componentTemplate.deprecated(),
componentTemplate.createdDateMillis().orElse(null),
componentTemplate.modifiedDateMillis().orElse(null)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@
import org.elasticsearch.xcontent.XContentParser;

import java.io.IOException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* A component template is a re-usable {@link Template} as well as metadata about the template. Each
Expand All @@ -34,23 +39,43 @@
* "foo" field. These component templates make up the individual pieces composing an index template.
*/
public class ComponentTemplate implements SimpleDiffable<ComponentTemplate>, ToXContentObject {

// always output millis even if instantSource returns millis == 0
private static final DateTimeFormatter ISO8601_WITH_MILLIS_FORMATTER = new DateTimeFormatterBuilder().appendInstant(3)
.toFormatter(Locale.ROOT);

private static final ParseField TEMPLATE = new ParseField("template");
private static final ParseField VERSION = new ParseField("version");
private static final ParseField METADATA = new ParseField("_meta");
private static final ParseField DEPRECATED = new ParseField("deprecated");
private static final ParseField CREATED_DATE = new ParseField("created_date");
private static final ParseField MODIFIED_DATE = new ParseField("modified_date");

@SuppressWarnings("unchecked")
public static final ConstructingObjectParser<ComponentTemplate, Void> PARSER = new ConstructingObjectParser<>(
"component_template",
false,
a -> new ComponentTemplate((Template) a[0], (Long) a[1], (Map<String, Object>) a[2], (Boolean) a[3])
a -> {
final String createdDate = (String) a[4];
final String modifiedDate = (String) a[5];
return new ComponentTemplate(
(Template) a[0],
(Long) a[1],
(Map<String, Object>) a[2],
(Boolean) a[3],
createdDate == null ? null : Instant.parse(createdDate).toEpochMilli(),
modifiedDate == null ? null : Instant.parse(modifiedDate).toEpochMilli()
);
}
);

static {
PARSER.declareObject(ConstructingObjectParser.constructorArg(), Template.PARSER, TEMPLATE);
PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), VERSION);
PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> p.map(), METADATA);
PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), DEPRECATED);
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), CREATED_DATE);
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), MODIFIED_DATE);
}

private final Template template;
Expand All @@ -60,6 +85,10 @@ public class ComponentTemplate implements SimpleDiffable<ComponentTemplate>, ToX
private final Map<String, Object> metadata;
@Nullable
private final Boolean deprecated;
@Nullable
private final Long createdDateMillis;
@Nullable
private final Long modifiedDateMillis;

static Diff<ComponentTemplate> readComponentTemplateDiffFrom(StreamInput in) throws IOException {
return SimpleDiffable.readDiffFrom(ComponentTemplate::new, in);
Expand All @@ -70,19 +99,23 @@ public static ComponentTemplate parse(XContentParser parser) {
}

public ComponentTemplate(Template template, @Nullable Long version, @Nullable Map<String, Object> metadata) {
this(template, version, metadata, null);
this(template, version, metadata, null, null, null);
}

public ComponentTemplate(
Template template,
@Nullable Long version,
@Nullable Map<String, Object> metadata,
@Nullable Boolean deprecated
@Nullable Boolean deprecated,
@Nullable Long createdDateMillis,
@Nullable Long modifiedDateMillis
) {
this.template = template;
this.version = version;
this.metadata = metadata;
this.deprecated = deprecated;
this.createdDateMillis = createdDateMillis;
this.modifiedDateMillis = modifiedDateMillis;
}

public ComponentTemplate(StreamInput in) throws IOException {
Expand All @@ -98,6 +131,13 @@ public ComponentTemplate(StreamInput in) throws IOException {
} else {
deprecated = null;
}
if (in.getTransportVersion().onOrAfter(TransportVersions.COMPONENT_TEMPLATE_TRACKING_INFO)) {
this.createdDateMillis = in.readOptionalLong();
this.modifiedDateMillis = in.readOptionalLong();
} else {
this.createdDateMillis = null;
this.modifiedDateMillis = null;
}
}

public Template template() {
Expand All @@ -122,6 +162,14 @@ public boolean isDeprecated() {
return Boolean.TRUE.equals(deprecated);
}

public Optional<Long> createdDateMillis() {
return Optional.ofNullable(createdDateMillis);
}

public Optional<Long> modifiedDateMillis() {
return Optional.ofNullable(modifiedDateMillis);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
this.template.writeTo(out);
Expand All @@ -135,11 +183,15 @@ public void writeTo(StreamOutput out) throws IOException {
if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) {
out.writeOptionalBoolean(this.deprecated);
}
if (out.getTransportVersion().onOrAfter(TransportVersions.COMPONENT_TEMPLATE_TRACKING_INFO)) {
out.writeOptionalLong(this.createdDateMillis);
out.writeOptionalLong(this.modifiedDateMillis);
}
}

@Override
public int hashCode() {
return Objects.hash(template, version, metadata, deprecated);
return Objects.hash(template, version, metadata, deprecated, createdDateMillis, modifiedDateMillis);
}

@Override
Expand All @@ -154,7 +206,9 @@ public boolean equals(Object obj) {
return Objects.equals(template, other.template)
&& Objects.equals(version, other.version)
&& Objects.equals(metadata, other.metadata)
&& Objects.equals(deprecated, other.deprecated);
&& Objects.equals(deprecated, other.deprecated)
&& Objects.equals(createdDateMillis, other.createdDateMillis)
&& Objects.equals(modifiedDateMillis, other.modifiedDateMillis);
}

@Override
Expand Down Expand Up @@ -184,7 +238,17 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params, @Nulla
if (this.deprecated != null) {
builder.field(DEPRECATED.getPreferredName(), this.deprecated);
}
if (this.createdDateMillis != null) {
builder.field(CREATED_DATE.getPreferredName(), formatDate(this.createdDateMillis));
}
if (this.modifiedDateMillis != null) {
builder.field(MODIFIED_DATE.getPreferredName(), formatDate(this.modifiedDateMillis));
}
builder.endObject();
return builder;
}

private static String formatDate(long epochMillis) {
return ISO8601_WITH_MILLIS_FORMATTER.format(Instant.ofEpochMilli(epochMillis));
}
}
Loading