Browse Source

[Transform] Implement the ability to preview the existing transform (#76697)

Przemysław Witek 4 năm trước cách đây
mục cha
commit
676d4de3de

+ 9 - 4
client/rest-high-level/src/main/java/org/elasticsearch/client/TransformRequestConverters.java

@@ -137,11 +137,16 @@ final class TransformRequestConverters {
     }
 
     static Request previewTransform(PreviewTransformRequest previewRequest) throws IOException {
-        String endpoint = new RequestConverters.EndpointBuilder()
-                .addPathPartAsIs("_transform", "_preview")
-                .build();
+        RequestConverters.EndpointBuilder endpointBuilder = new RequestConverters.EndpointBuilder().addPathPartAsIs("_transform");
+        if (previewRequest.getTransformId() != null) {
+            endpointBuilder.addPathPart(previewRequest.getTransformId());
+        }
+        endpointBuilder.addPathPartAsIs("_preview");
+        String endpoint = endpointBuilder.build();
         Request request = new Request(HttpPost.METHOD_NAME, endpoint);
-        request.setEntity(createEntity(previewRequest, REQUEST_BODY_CONTENT_TYPE));
+        if (previewRequest.getTransformId() == null) {
+            request.setEntity(createEntity(previewRequest, REQUEST_BODY_CONTENT_TYPE));
+        }
         return request;
     }
 

+ 24 - 8
client/rest-high-level/src/main/java/org/elasticsearch/client/transform/PreviewTransformRequest.java

@@ -21,10 +21,21 @@ import java.util.Optional;
 
 public class PreviewTransformRequest implements ToXContentObject, Validatable {
 
+    private final String transformId;
     private final TransformConfig config;
 
+    public PreviewTransformRequest(String transformId) {
+        this.transformId = Objects.requireNonNull(transformId);
+        this.config = null;
+    }
+
     public PreviewTransformRequest(TransformConfig config) {
-        this.config = config;
+        this.transformId = null;
+        this.config = Objects.requireNonNull(config);
+    }
+
+    public String getTransformId() {
+        return transformId;
     }
 
     public TransformConfig getConfig() {
@@ -33,16 +44,20 @@ public class PreviewTransformRequest implements ToXContentObject, Validatable {
 
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
-        return config.toXContent(builder, params);
+        if (this.config != null) {
+            return this.config.toXContent(builder, params);
+        } else {
+            return builder
+                .startObject()
+                .field(TransformConfig.ID.getPreferredName(), this.transformId)
+                .endObject();
+        }
     }
 
     @Override
     public Optional<ValidationException> validate() {
         ValidationException validationException = new ValidationException();
-        if (config == null) {
-            validationException.addValidationError("preview requires a non-null transform config");
-            return Optional.of(validationException);
-        } else {
+        if (config != null) {
             if (config.getSource() == null) {
                 validationException.addValidationError("transform source cannot be null");
             }
@@ -57,7 +72,7 @@ public class PreviewTransformRequest implements ToXContentObject, Validatable {
 
     @Override
     public int hashCode() {
-        return Objects.hash(config);
+        return Objects.hash(transformId, config);
     }
 
     @Override
@@ -69,6 +84,7 @@ public class PreviewTransformRequest implements ToXContentObject, Validatable {
             return false;
         }
         PreviewTransformRequest other = (PreviewTransformRequest) obj;
-        return Objects.equals(config, other.config);
+        return Objects.equals(transformId, other.transformId)
+            && Objects.equals(config, other.config);
     }
 }

+ 21 - 6
client/rest-high-level/src/test/java/org/elasticsearch/client/TransformIT.java

@@ -336,7 +336,6 @@ public class TransformIT extends ESRestHighLevelClientTestCase {
         assertThat(taskState, is(TransformStats.State.STOPPED));
     }
 
-    @SuppressWarnings("unchecked")
     public void testPreview() throws IOException {
         String sourceIndex = "transform-source";
         createIndex(sourceIndex);
@@ -345,12 +344,28 @@ public class TransformIT extends ESRestHighLevelClientTestCase {
         TransformConfig transform = validDataFrameTransformConfig("test-preview", sourceIndex, null);
 
         TransformClient client = highLevelClient().transform();
-        PreviewTransformResponse preview = execute(
-            new PreviewTransformRequest(transform),
-            client::previewTransform,
-            client::previewTransformAsync
-        );
+        PreviewTransformResponse preview =
+            execute(new PreviewTransformRequest(transform), client::previewTransform, client::previewTransformAsync);
+        assertExpectedPreview(preview);
+    }
+
+    public void testPreviewById() throws IOException {
+        String sourceIndex = "transform-source";
+        createIndex(sourceIndex);
+        indexData(sourceIndex);
 
+        String transformId = "test-preview-by-id";
+        TransformConfig transform = validDataFrameTransformConfig(transformId, sourceIndex, "pivot-dest");
+        putTransform(transform);
+
+        TransformClient client = highLevelClient().transform();
+        PreviewTransformResponse preview =
+            execute(new PreviewTransformRequest(transformId), client::previewTransform, client::previewTransformAsync);
+        assertExpectedPreview(preview);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static void assertExpectedPreview(PreviewTransformResponse preview) {
         List<Map<String, Object>> docs = preview.getDocs();
         assertThat(docs, hasSize(2));
         Optional<Map<String, Object>> theresa = docs.stream().filter(doc -> "theresa".equals(doc.get("reviewer"))).findFirst();

+ 15 - 4
client/rest-high-level/src/test/java/org/elasticsearch/client/TransformRequestConvertersTests.java

@@ -17,6 +17,7 @@ import org.elasticsearch.client.transform.DeleteTransformRequest;
 import org.elasticsearch.client.transform.GetTransformRequest;
 import org.elasticsearch.client.transform.GetTransformStatsRequest;
 import org.elasticsearch.client.transform.PreviewTransformRequest;
+import org.elasticsearch.client.transform.PreviewTransformRequestTests;
 import org.elasticsearch.client.transform.PutTransformRequest;
 import org.elasticsearch.client.transform.StartTransformRequest;
 import org.elasticsearch.client.transform.StopTransformRequest;
@@ -44,6 +45,7 @@ import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasEntry;
 import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
 
 public class TransformRequestConvertersTests extends ESTestCase {
 
@@ -176,19 +178,28 @@ public class TransformRequestConvertersTests extends ESTestCase {
     }
 
     public void testPreviewDataFrameTransform() throws IOException {
-        PreviewTransformRequest previewRequest = new PreviewTransformRequest(
-                TransformConfigTests.randomTransformConfig());
+        PreviewTransformRequest previewRequest = new PreviewTransformRequest(TransformConfigTests.randomTransformConfig());
         Request request = TransformRequestConverters.previewTransform(previewRequest);
 
         assertEquals(HttpPost.METHOD_NAME, request.getMethod());
         assertThat(request.getEndpoint(), equalTo("/_transform/_preview"));
 
         try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) {
-            TransformConfig parsedConfig = TransformConfig.PARSER.apply(parser, null);
-            assertThat(parsedConfig, equalTo(previewRequest.getConfig()));
+            PreviewTransformRequest parsedRequest = PreviewTransformRequestTests.fromXContent(parser);
+            assertThat(parsedRequest, equalTo(previewRequest));
         }
     }
 
+    public void testPreviewDataFrameTransformById() throws IOException {
+        String transformId = randomAlphaOfLengthBetween(1, 10);
+        PreviewTransformRequest previewRequest = new PreviewTransformRequest(transformId);
+        Request request = TransformRequestConverters.previewTransform(previewRequest);
+
+        assertEquals(HttpPost.METHOD_NAME, request.getMethod());
+        assertThat(request.getEndpoint(), equalTo("/_transform/" + transformId + "/_preview"));
+        assertThat(request.getEntity(), nullValue());
+    }
+
     public void testGetDataFrameTransformStats() {
         GetTransformStatsRequest getStatsRequest = new GetTransformStatsRequest("foo");
         Request request = TransformRequestConverters.getTransformStats(getStatsRequest);

+ 36 - 4
client/rest-high-level/src/test/java/org/elasticsearch/client/transform/PreviewTransformRequestTests.java

@@ -13,15 +13,21 @@ import org.elasticsearch.client.transform.transforms.TransformConfig;
 import org.elasticsearch.client.transform.transforms.TransformConfigTests;
 import org.elasticsearch.client.transform.transforms.latest.LatestConfigTests;
 import org.elasticsearch.client.transform.transforms.pivot.PivotConfigTests;
+import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
 import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.search.SearchModule;
 import org.elasticsearch.test.AbstractXContentTestCase;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
 import static org.elasticsearch.client.transform.transforms.SourceConfigTests.randomSourceConfig;
@@ -32,12 +38,35 @@ import static org.hamcrest.Matchers.containsString;
 public class PreviewTransformRequestTests extends AbstractXContentTestCase<PreviewTransformRequest> {
     @Override
     protected PreviewTransformRequest createTestInstance() {
-        return new PreviewTransformRequest(TransformConfigTests.randomTransformConfig());
+        return randomBoolean()
+            ? new PreviewTransformRequest(randomAlphaOfLengthBetween(1, 10))
+            : new PreviewTransformRequest(TransformConfigTests.randomTransformConfig());
     }
 
     @Override
     protected PreviewTransformRequest doParseInstance(XContentParser parser) throws IOException {
-        return new PreviewTransformRequest(TransformConfig.fromXContent(parser));
+        return fromXContent(parser);
+    }
+
+    public static PreviewTransformRequest fromXContent(XContentParser parser) throws IOException {
+        Map<String, Object> content = parser.map();
+        if (content.size() == 1 && content.containsKey(TransformConfig.ID.getPreferredName())) {
+            // The request only contains transform id so instead of parsing TransformConfig (which is pointless in this case),
+            // let's just fetch the transform config by id later on.
+            String transformId = (String) content.get(TransformConfig.ID.getPreferredName());
+            return new PreviewTransformRequest(transformId);
+        }
+        try (
+            XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().map(content);
+            XContentParser newParser = XContentType.JSON.xContent()
+                .createParser(
+                    parser.getXContentRegistry(),
+                    LoggingDeprecationHandler.INSTANCE,
+                    BytesReference.bytes(xContentBuilder).streamInput()
+                )
+        ) {
+            return new PreviewTransformRequest(TransformConfig.fromXContent(newParser));
+        }
     }
 
     @Override
@@ -54,10 +83,13 @@ public class PreviewTransformRequestTests extends AbstractXContentTestCase<Previ
         return new NamedXContentRegistry(namedXContents);
     }
 
+    public void testConstructorThrowsNPE() {
+        expectThrows(NullPointerException.class, () -> new PreviewTransformRequest((String) null));
+        expectThrows(NullPointerException.class, () -> new PreviewTransformRequest((TransformConfig) null));
+    }
+
     public void testValidate() {
         assertThat(new PreviewTransformRequest(TransformConfigTests.randomTransformConfig()).validate(), isEmpty());
-        assertThat(new PreviewTransformRequest(null).validate().get().getMessage(),
-                containsString("preview requires a non-null transform config"));
 
         // null id and destination is valid
         TransformConfig config = TransformConfig.forPreview(randomSourceConfig(), PivotConfigTests.randomPivotConfig());

+ 19 - 0
docs/reference/transform/apis/preview-transform.asciidoc

@@ -13,8 +13,16 @@ Previews a {transform}.
 [[preview-transform-request]]
 == {api-request-title}
 
+
+`GET _transform/<transform_id>/_preview` +
+
+`POST _transform/<transform_id>/_preview` +
+
+`GET _transform/_preview` +
+
 `POST _transform/_preview`
 
+
 [[preview-transform-prereq]]
 == {api-prereq-title}
 
@@ -46,6 +54,17 @@ You must choose either the `latest` or `pivot` method for your {transform}; you
 cannot use both in a single {transform}.
 
 [role="child_attributes"]
+
+[[preview-transform-path-params]]
+== {api-path-parms-title}
+
+`<transform_id>`::
+(Optional, string)
+Id of the {transform} to preview.
++
+NOTE: If you provide the `<transform_id>` as a path parameter, you cannot
+provide {transform} configuration details in the request body.
+
 [[preview-transform-request-body]]
 == {api-request-body-title}
 

+ 15 - 1
rest-api-spec/src/main/resources/rest-api-spec/api/transform.preview_transform.json

@@ -12,9 +12,23 @@
     },
     "url":{
       "paths":[
+        {
+          "path":"/_transform/{transform_id}/_preview",
+          "methods":[
+            "GET",
+            "POST"
+          ],
+          "parts":{
+            "transform_id":{
+              "type":"string",
+              "description":"The id of the transform to preview."
+            }
+          }
+        },
         {
           "path":"/_transform/_preview",
           "methods":[
+            "GET",
             "POST"
           ]
         }
@@ -22,7 +36,7 @@
     },
     "body":{
       "description":"The definition for the transform to preview",
-      "required":true
+      "required":false
     }
   }
 }

+ 75 - 0
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/preview_transforms.yml

@@ -150,6 +150,52 @@ setup:
   - match: { generated_dest_index.mappings.properties.by-hour.type: "date" }
   - match: { generated_dest_index.mappings.properties.avg_response.type: "double" }
 
+---
+"Test preview transform by id":
+  - do:
+      transform.put_transform:
+        transform_id: "airline-transform"
+        body: >
+          {
+            "source": { "index": "airline-data" },
+            "dest": { "index": "dest-airline-data-by-airline-and-hour" },
+            "pivot": {
+              "group_by": {
+                "airline": {"terms": {"field": "airline"}},
+                "by-hour": {"date_histogram": {"fixed_interval": "1h", "field": "time"}}},
+              "aggs": {
+                "avg_response": {"avg": {"field": "responsetime"}},
+                "time.max": {"max": {"field": "time"}},
+                "time.min": {"min": {"field": "time"}}
+              }
+            }
+          }
+  - match: { acknowledged: true }
+
+  - do:
+      transform.preview_transform:
+        transform_id: "airline-transform"
+  - match: { preview.0.airline: foo }
+  - match: { preview.0.by-hour: "2017-02-18T00:00:00.000Z" }
+  - match: { preview.0.avg_response: 1.0 }
+  - match: { preview.0.time.max: "2017-02-18T00:30:00.000Z" }
+  - match: { preview.0.time.min: "2017-02-18T00:00:00.000Z" }
+  - match: { preview.1.airline: bar }
+  - match: { preview.1.by-hour: "2017-02-18T01:00:00.000Z" }
+  - match: { preview.1.avg_response: 42.0 }
+  - match: { preview.1.time.max: "2017-02-18T01:00:00.000Z" }
+  - match: { preview.1.time.min: "2017-02-18T01:00:00.000Z" }
+  - match: { preview.2.airline: foo }
+  - match: { preview.2.by-hour: "2017-02-18T01:00:00.000Z" }
+  - match: { preview.2.avg_response: 42.0 }
+  - match: { preview.2.time.max: "2017-02-18T01:01:00.000Z" }
+  - match: { preview.2.time.min: "2017-02-18T01:01:00.000Z" }
+  - match: { generated_dest_index.mappings.properties.airline.type: "keyword" }
+  - match: { generated_dest_index.mappings.properties.by-hour.type: "date" }
+  - match: { generated_dest_index.mappings.properties.avg_response.type: "double" }
+  - match: { generated_dest_index.mappings.properties.time\.max.type: "date" }
+  - match: { generated_dest_index.mappings.properties.time\.min.type: "date" }
+
 ---
 "Test preview transform latest":
   - do:
@@ -204,6 +250,35 @@ setup:
   - match: { preview.1.my_field: 42 }
   - match: { generated_dest_index.mappings.properties: {} }
 
+---
+"Test preview non-existent transform":
+  - do:
+      catch: /Transform with id \[non-existent-transform\] could not be found/
+      transform.preview_transform:
+        transform_id: "non-existent-transform"
+
+---
+"Test preview without id and body":
+  - do:
+      catch: /Please provide a transform \[id\] or the config object/
+      transform.preview_transform: {}
+
+---
+"Test preview with both id and body":
+  - do:
+      catch: /Please provide either a transform \[id\] or the config object but not both/
+      transform.preview_transform:
+        transform_id: "non-existent-transform"
+        body: >
+          {
+            "source": { "index": "airline-data" },
+            "dest": { "pipeline": "transform_latest_simple_pipeline" },
+            "latest": {
+              "unique_key": ["airline"],
+              "sort": "time"
+            }
+          }
+
 ---
 "Test preview transform with invalid config":
   - do:

+ 11 - 0
x-pack/plugin/transform/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/80_transform.yml

@@ -149,6 +149,17 @@ teardown:
   # we added test_index_2, which has 2 more docs:
   - match: { transforms.0.checkpointing.operations_behind: 2 }
 
+  - do:
+      headers: { Authorization: "Basic am9lOnRyYW5zZm9ybS1wYXNzd29yZA==" }  # This is joe
+      transform.preview_transform:
+        transform_id: "simple-remote-transform"
+
+  - do:
+      catch: /Cannot preview transform \[simple-remote-transform\] because user bob lacks all the required permissions for indices. \[my_remote_cluster:remote_test_index, my_remote_cluster:remote_test_index_2, simple-remote-transform\]/
+      headers: { Authorization: "Basic Ym9iOnRyYW5zZm9ybS1wYXNzd29yZA==" }  # This is bob
+      transform.preview_transform:
+        transform_id: "simple-remote-transform"
+
 ---
 "Batch transform preview from remote cluster":
   - do:

+ 58 - 5
x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestPreviewTransformAction.java

@@ -7,24 +7,36 @@
 
 package org.elasticsearch.xpack.transform.rest.action;
 
+import org.apache.lucene.util.SetOnce;
+import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.client.node.NodeClient;
-import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.action.RestToXContentListener;
 import org.elasticsearch.xpack.core.transform.TransformField;
+import org.elasticsearch.xpack.core.transform.action.GetTransformAction;
 import org.elasticsearch.xpack.core.transform.action.PreviewTransformAction;
+import org.elasticsearch.xpack.core.transform.transforms.TransformConfig;
+import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.stream.Collectors;
 
+import static org.elasticsearch.rest.RestRequest.Method.GET;
 import static org.elasticsearch.rest.RestRequest.Method.POST;
 
 public class RestPreviewTransformAction extends BaseRestHandler {
 
     @Override
     public List<Route> routes() {
-        return List.of(new Route(POST, TransformField.REST_BASE_PATH_TRANSFORMS + "_preview"));
+        return List.of(
+            new Route(GET, TransformField.REST_BASE_PATH_TRANSFORMS + "_preview"),
+            new Route(GET, TransformField.REST_BASE_PATH_TRANSFORMS_BY_ID + "_preview"),
+            new Route(POST, TransformField.REST_BASE_PATH_TRANSFORMS + "_preview"),
+            new Route(POST, TransformField.REST_BASE_PATH_TRANSFORMS_BY_ID + "_preview")
+        );
     }
 
     @Override
@@ -34,9 +46,50 @@ public class RestPreviewTransformAction extends BaseRestHandler {
 
     @Override
     protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
-        XContentParser parser = restRequest.contentParser();
+        String transformId = restRequest.param(TransformField.ID.getPreferredName());
 
-        PreviewTransformAction.Request request = PreviewTransformAction.Request.fromXContent(parser);
-        return channel -> client.execute(PreviewTransformAction.INSTANCE, request, new RestToXContentListener<>(channel));
+        if (Strings.isNullOrEmpty(transformId) && restRequest.hasContentOrSourceParam() == false) {
+            throw ExceptionsHelper.badRequestException(
+                "Please provide a transform [{}] or the config object",
+                TransformField.ID.getPreferredName());
+        }
+
+        if (Strings.isNullOrEmpty(transformId) == false && restRequest.hasContentOrSourceParam()) {
+            throw ExceptionsHelper.badRequestException(
+                "Please provide either a transform [{}] or the config object but not both",
+                TransformField.ID.getPreferredName()
+            );
+        }
+
+        SetOnce<PreviewTransformAction.Request> previewRequestHolder = new SetOnce<>();
+        if (Strings.isNullOrEmpty(transformId)) {
+            previewRequestHolder.set(PreviewTransformAction.Request.fromXContent(restRequest.contentOrSourceParamParser()));
+        }
+
+        return channel -> {
+            RestToXContentListener<PreviewTransformAction.Response> listener = new RestToXContentListener<>(channel);
+
+            if (Strings.isNullOrEmpty(transformId)) {
+                PreviewTransformAction.Request previewRequest = previewRequestHolder.get();
+                client.execute(PreviewTransformAction.INSTANCE, previewRequest, listener);
+            } else {
+                GetTransformAction.Request getRequest = new GetTransformAction.Request(transformId);
+                getRequest.setAllowNoResources(false);
+                client.execute(GetTransformAction.INSTANCE, getRequest, ActionListener.wrap(getResponse -> {
+                    List<TransformConfig> transforms = getResponse.getResources().results();
+                    if (transforms.size() > 1) {
+                        listener.onFailure(
+                            ExceptionsHelper.badRequestException(
+                                "expected only one config but matched {}",
+                                transforms.stream().map(TransformConfig::getId).collect(Collectors.toList())
+                            )
+                        );
+                    } else {
+                        PreviewTransformAction.Request previewRequest = new PreviewTransformAction.Request(transforms.get(0));
+                        client.execute(PreviewTransformAction.INSTANCE, previewRequest, listener);
+                    }
+                }, listener::onFailure));
+            }
+        };
     }
 }