From 0503404890c36aa7830547106b82840123470f33 Mon Sep 17 00:00:00 2001 From: Terence Date: Mon, 2 Nov 2020 11:18:51 +0800 Subject: [PATCH] Update rest endpoints Signed-off-by: Terence --- .../feast/core/config/WebSecurityConfig.java | 1 + .../controller/CoreServiceRestController.java | 46 ++++++- .../core/controller/CoreServiceRestIT.java | 121 +++++++++++++----- 3 files changed, 128 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/feast/core/config/WebSecurityConfig.java b/core/src/main/java/feast/core/config/WebSecurityConfig.java index 858361a670a..0f481115a2f 100644 --- a/core/src/main/java/feast/core/config/WebSecurityConfig.java +++ b/core/src/main/java/feast/core/config/WebSecurityConfig.java @@ -51,6 +51,7 @@ protected void configure(HttpSecurity http) throws Exception { if (feastProperties.securityProperties().isDisableRestControllerAuth()) { matchersToBypass.add("/api/v1/**"); + matchersToBypass.add("/api/v2/**"); } // Bypasses security/authentication for the following paths diff --git a/core/src/main/java/feast/core/controller/CoreServiceRestController.java b/core/src/main/java/feast/core/controller/CoreServiceRestController.java index 2673ebffb88..3e3224220a9 100644 --- a/core/src/main/java/feast/core/controller/CoreServiceRestController.java +++ b/core/src/main/java/feast/core/controller/CoreServiceRestController.java @@ -27,8 +27,12 @@ import feast.proto.core.CoreServiceProto.GetFeatureStatisticsRequest; import feast.proto.core.CoreServiceProto.GetFeatureStatisticsRequest.Builder; import feast.proto.core.CoreServiceProto.GetFeatureStatisticsResponse; +import feast.proto.core.CoreServiceProto.ListEntitiesRequest; +import feast.proto.core.CoreServiceProto.ListEntitiesResponse; import feast.proto.core.CoreServiceProto.ListFeatureSetsRequest; import feast.proto.core.CoreServiceProto.ListFeatureSetsResponse; +import feast.proto.core.CoreServiceProto.ListFeatureTablesRequest; +import feast.proto.core.CoreServiceProto.ListFeatureTablesResponse; import feast.proto.core.CoreServiceProto.ListFeaturesRequest; import feast.proto.core.CoreServiceProto.ListFeaturesResponse; import feast.proto.core.CoreServiceProto.ListProjectsResponse; @@ -55,7 +59,7 @@ @RestController @CrossOrigin @Slf4j -@RequestMapping(value = "/api/v1", produces = "application/json") +@RequestMapping(value = "/api", produces = "application/json") public class CoreServiceRestController { private final FeastProperties feastProperties; @@ -80,7 +84,7 @@ public CoreServiceRestController( * * @return (200 OK) Returns {@link GetFeastCoreVersionResponse} in JSON. */ - @RequestMapping(value = "/version", method = RequestMethod.GET) + @RequestMapping(value = "/v1/version", method = RequestMethod.GET) public GetFeastCoreVersionResponse getVersion() { GetFeastCoreVersionResponse response = GetFeastCoreVersionResponse.newBuilder().setVersion(feastProperties.getVersion()).build(); @@ -99,7 +103,7 @@ public GetFeastCoreVersionResponse getVersion() { * default. Asterisk can be used as wildcard to filter * feature sets. * @return (200 OK) Return {@link ListFeatureSetsResponse} in JSON. */ - @RequestMapping(value = "/feature-sets", method = RequestMethod.GET) + @RequestMapping(value = "/v1/feature-sets", method = RequestMethod.GET) public ListFeatureSetsResponse listFeatureSets( @RequestParam(defaultValue = Project.DEFAULT_NAME) String project, @RequestParam String name) throws InvalidProtocolBufferException { @@ -120,7 +124,7 @@ public ListFeatureSetsResponse listFeatureSets( * default. * @return (200 OK) Return {@link ListFeaturesResponse} in JSON. */ - @RequestMapping(value = "/features", method = RequestMethod.GET) + @RequestMapping(value = "/v1/features", method = RequestMethod.GET) public ListFeaturesResponse listFeatures( @RequestParam String[] entities, @RequestParam(required = false) Optional project) { ListFeaturesRequest.Filter.Builder filterBuilder = @@ -152,7 +156,7 @@ public ListFeaturesResponse listFeatures( * in the feature set will be used for statistics. * @return (200 OK) Returns {@link GetFeatureStatisticsResponse} in JSON. */ - @RequestMapping(value = "/feature-statistics", method = RequestMethod.GET) + @RequestMapping(value = "/v1/feature-statistics", method = RequestMethod.GET) public GetFeatureStatisticsResponse getFeatureStatistics( @RequestParam(name = "feature_set_id") String featureSetId, @RequestParam(required = false) Optional features, @@ -186,7 +190,7 @@ public GetFeatureStatisticsResponse getFeatureStatistics( * * @return (200 OK) Returns {@link ListProjectsResponse} in JSON. */ - @RequestMapping(value = "/projects", method = RequestMethod.GET) + @RequestMapping(value = "/v1/projects", method = RequestMethod.GET) public ListProjectsResponse listProjects() { List projects = projectService.listProjects(); return ListProjectsResponse.newBuilder() @@ -194,6 +198,36 @@ public ListProjectsResponse listProjects() { .build(); } + /** + * GET /entities : Retrieve a list of Entities according to filtering parameters of Feast project + * name. If none matches, an empty JSON response is returned. + * + * @param project Request Parameter: Name of feast project to search in. + * @return (200 OK) Return {@link ListEntitiesResponse} in JSON. + */ + @RequestMapping(value = "/v2/entities", method = RequestMethod.GET) + public ListEntitiesResponse listEntities( + @RequestParam(defaultValue = Project.DEFAULT_NAME) String project) { + ListEntitiesRequest.Filter.Builder filterBuilder = + ListEntitiesRequest.Filter.newBuilder().setProject(project); + return specService.listEntities(filterBuilder.build()); + } + + /** + * GET /feature-tables : Retrieve a list of Feature Tables according to filtering parameters of + * Feast project name. If none matches, an empty JSON response is returned. + * + * @param project Request Parameter: Name of feast project to search in. + * @return (200 OK) Return {@link ListFeatureTablesResponse} in JSON. + */ + @RequestMapping(value = "/v2/feature-tables", method = RequestMethod.GET) + public ListFeatureTablesResponse listFeatureTables( + @RequestParam(defaultValue = Project.DEFAULT_NAME) String project) { + ListFeatureTablesRequest.Filter.Builder filterBuilder = + ListFeatureTablesRequest.Filter.newBuilder().setProject(project); + return specService.listFeatureTables(filterBuilder.build()); + } + private Timestamp utcTimeStringToTimestamp(String utcTimeString) { long epochSecond = LocalDate.parse(utcTimeString, DateTimeFormatter.ISO_DATE) diff --git a/core/src/test/java/feast/core/controller/CoreServiceRestIT.java b/core/src/test/java/feast/core/controller/CoreServiceRestIT.java index 7abdb5510d8..ea295918494 100644 --- a/core/src/test/java/feast/core/controller/CoreServiceRestIT.java +++ b/core/src/test/java/feast/core/controller/CoreServiceRestIT.java @@ -27,13 +27,18 @@ import feast.common.it.SimpleCoreClient; import feast.core.model.Project; import feast.proto.core.CoreServiceGrpc; +import feast.proto.core.EntityProto; import feast.proto.core.FeatureSetProto.FeatureSet; +import feast.proto.core.FeatureTableProto; +import feast.proto.types.ValueProto; import feast.proto.types.ValueProto.ValueType.Enum; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.path.json.JsonPath; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -205,19 +210,53 @@ public void listFeatures() { .body("features", aMapWithSize(2)); } + @Test + public void listEntities() { + String uri1 = + UriComponentsBuilder.fromPath("/api/v2/entities") + .queryParam("project", "default") + .buildAndExpand() + .toString(); + String responseBody = + get(uri1) + .then() + .log() + .everything() + .assertThat() + .contentType(ContentType.JSON) + .extract() + .response() + .getBody() + .asString(); + List entityList = JsonPath.from(responseBody).getList("entities"); + assertEquals(entityList.size(), 2); + } + + @Test + public void listFeatureTables() { + String uri1 = + UriComponentsBuilder.fromPath("/api/v2/feature-tables") + .queryParam("project", "default") + .buildAndExpand() + .toString(); + String responseBody = + get(uri1) + .then() + .log() + .everything() + .assertThat() + .contentType(ContentType.JSON) + .extract() + .response() + .getBody() + .asString(); + List featureTableList = JsonPath.from(responseBody).getList("tables"); + assertEquals(featureTableList.size(), 1); + } + @BeforeEach - private void createFakeFeatureSets() { - // spec: - // name: merchant_ratings - // entities: - // - name: merchant_id - // valueType: STRING - // features: - // - name: average_rating - // valueType: DOUBLE - // - name: total_ratings - // valueType: INT64 - // project: default + private void createSpecs() { + // Apply feature sets FeatureSet merchantFeatureSet = DataGenerator.createFeatureSet( DataGenerator.getDefaultSource(), @@ -227,17 +266,6 @@ private void createFakeFeatureSets() { ImmutableMap.of("average_rating", Enum.DOUBLE, "total_ratings", Enum.INT64)); apiClient.simpleApplyFeatureSet(merchantFeatureSet); - // spec: - // name: another_merchant_ratings - // entities: - // - name: merchant_id - // valueType: STRING - // features: - // - name: another_average_rating - // valueType: DOUBLE - // - name: another_total_ratings - // valueType: INT64 - // project: default FeatureSet anotherMerchantFeatureSet = DataGenerator.createFeatureSet( DataGenerator.getDefaultSource(), @@ -249,17 +277,6 @@ private void createFakeFeatureSets() { "another_total_ratings", Enum.INT64)); apiClient.simpleApplyFeatureSet(anotherMerchantFeatureSet); - // spec: - // name: yet_another_merchant_feature_set - // entities: - // - name: merchant_id - // valueType: STRING - // features: - // - name: merchant_prop1 - // valueType: BOOL - // - name: merchant_prop2 - // valueType: FLOAT - // project: merchant FeatureSet yetAnotherMerchantFeatureSet = DataGenerator.createFeatureSet( DataGenerator.getDefaultSource(), @@ -268,6 +285,42 @@ private void createFakeFeatureSets() { ImmutableMap.of("merchant_id", Enum.STRING), ImmutableMap.of("merchant_prop1", Enum.BOOL, "merchant_prop2", Enum.FLOAT)); apiClient.simpleApplyFeatureSet(yetAnotherMerchantFeatureSet); + + // Apply entities + EntityProto.EntitySpecV2 entitySpec1 = + DataGenerator.createEntitySpecV2( + "entity1", + "Entity 1 description", + ValueProto.ValueType.Enum.STRING, + avro.shaded.com.google.common.collect.ImmutableMap.of("label_key", "label_value")); + EntityProto.EntitySpecV2 entitySpec2 = + DataGenerator.createEntitySpecV2( + "entity2", + "Entity 2 description", + ValueProto.ValueType.Enum.STRING, + avro.shaded.com.google.common.collect.ImmutableMap.of("label_key2", "label_value2")); + apiClient.simpleApplyEntity("default", entitySpec1); + apiClient.simpleApplyEntity("default", entitySpec2); + + // Apply feature table + FeatureTableProto.FeatureTableSpec featureTableSpec = + DataGenerator.createFeatureTableSpec( + "featuretable1", + Arrays.asList("entity1", "entity2"), + new HashMap<>() { + { + put("feature1", ValueProto.ValueType.Enum.STRING); + put("feature2", ValueProto.ValueType.Enum.FLOAT); + } + }, + 7200, + ImmutableMap.of("feat_key2", "feat_value2")) + .toBuilder() + .setBatchSource( + DataGenerator.createFileDataSourceSpec("file:///path/to/file", "ts_col", "")) + .build(); + apiClient.applyFeatureTable("default", featureTableSpec); + RestAssured.port = port; } }