Skip to content

Commit 7d8126b

Browse files
Add REST endpoints for Feast UI (#878)
* Add RestController in Core with version method * Add exception handlers for missing URL parameters * Mark variables as optional * Make format-java * Make feature_set_id non-required per gRPC * Add Unit Tests for Core REST Controller * Add javadoc, remove json printer * Fix Javadocs Errors and typo * Modify Unit tests to not rely on Mockmvc * Add Integration Tests for CoreServiceRestController * Remove labels as it is not yet fully tested * Remove getFeatureSet REST endpoint Since it is a subset of the listFeatureSet endpoint. * Add integration tests for REST controller * Remove default query params for grpc consistency * Remove Unit Tests * Keep get feature-statistics consistent with grpc * Add comments on exception handlers for core http * Use WebTestClient for core HTTP ITs * Use reactor-test and remove autowired params * Use RestAssured for REST IT * Replace ProjectService w/ AccessManagementService * Handle retrieval errors caused by getFeatureStats * Add disableRestControllerAuth flag * Add documentation comment on application.yml * Fix integration test Co-authored-by: Terence <terencelimxp@gmail.com>
1 parent b02191f commit 7d8126b

8 files changed

Lines changed: 635 additions & 3 deletions

File tree

auth/src/main/java/feast/auth/config/SecurityProperties.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public class SecurityProperties {
2727
private AuthenticationProperties authentication;
2828
private AuthorizationProperties authorization;
2929

30+
// Bypass Authentication and Authorization at all HTTP endpoints at /api/v1
31+
private boolean disableRestControllerAuth;
32+
3033
@Getter
3134
@Setter
3235
public static class AuthenticationProperties {

core/pom.xml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2020
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
2121
<modelVersion>4.0.0</modelVersion>
22-
2322
<parent>
2423
<groupId>dev.feast</groupId>
2524
<artifactId>feast-parent</artifactId>
@@ -394,5 +393,23 @@
394393
<version>1.6.6</version>
395394
<scope>provided</scope>
396395
</dependency>
397-
</dependencies>
396+
<dependency>
397+
<groupId>io.rest-assured</groupId>
398+
<artifactId>rest-assured</artifactId>
399+
<version>4.2.0</version>
400+
<scope>test</scope>
401+
</dependency>
402+
<dependency>
403+
<groupId>io.rest-assured</groupId>
404+
<artifactId>json-path</artifactId>
405+
<version>4.2.0</version>
406+
<scope>test</scope>
407+
</dependency>
408+
<dependency>
409+
<groupId>io.rest-assured</groupId>
410+
<artifactId>xml-path</artifactId>
411+
<version>4.2.0</version>
412+
<scope>test</scope>
413+
</dependency>
414+
</dependencies>
398415
</project>

core/src/main/java/feast/core/config/WebSecurityConfig.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717
package feast.core.config;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import org.springframework.beans.factory.annotation.Autowired;
1922
import org.springframework.context.annotation.Configuration;
2023
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2124
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -29,6 +32,13 @@
2932
@EnableWebSecurity
3033
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
3134

35+
private final FeastProperties feastProperties;
36+
37+
@Autowired
38+
public WebSecurityConfig(FeastProperties feastProperties) {
39+
this.feastProperties = feastProperties;
40+
}
41+
3242
/**
3343
* Allows for custom web security rules to be applied.
3444
*
@@ -37,10 +47,15 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
3747
*/
3848
@Override
3949
protected void configure(HttpSecurity http) throws Exception {
50+
List<String> matchersToBypass = new ArrayList<>(List.of("/actuator/**", "/metrics/**"));
51+
52+
if (feastProperties.securityProperties().isDisableRestControllerAuth()) {
53+
matchersToBypass.add("/api/v1/**");
54+
}
4055

4156
// Bypasses security/authentication for the following paths
4257
http.authorizeRequests()
43-
.antMatchers("/actuator/**", "/metrics/**")
58+
.antMatchers(matchersToBypass.toArray(new String[0]))
4459
.permitAll()
4560
.anyRequest()
4661
.authenticated()
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright 2018-2019 The Feast Authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package feast.core.controller;
18+
19+
import com.google.protobuf.InvalidProtocolBufferException;
20+
import com.google.protobuf.Timestamp;
21+
import feast.core.config.FeastProperties;
22+
import feast.core.model.Project;
23+
import feast.core.service.ProjectService;
24+
import feast.core.service.SpecService;
25+
import feast.core.service.StatsService;
26+
import feast.proto.core.CoreServiceProto.GetFeastCoreVersionResponse;
27+
import feast.proto.core.CoreServiceProto.GetFeatureStatisticsRequest;
28+
import feast.proto.core.CoreServiceProto.GetFeatureStatisticsRequest.Builder;
29+
import feast.proto.core.CoreServiceProto.GetFeatureStatisticsResponse;
30+
import feast.proto.core.CoreServiceProto.ListFeatureSetsRequest;
31+
import feast.proto.core.CoreServiceProto.ListFeatureSetsResponse;
32+
import feast.proto.core.CoreServiceProto.ListFeaturesRequest;
33+
import feast.proto.core.CoreServiceProto.ListFeaturesResponse;
34+
import feast.proto.core.CoreServiceProto.ListProjectsResponse;
35+
import java.io.IOException;
36+
import java.time.LocalDate;
37+
import java.time.LocalTime;
38+
import java.time.ZoneOffset;
39+
import java.time.format.DateTimeFormatter;
40+
import java.util.Arrays;
41+
import java.util.List;
42+
import java.util.Optional;
43+
import java.util.stream.Collectors;
44+
import lombok.extern.slf4j.Slf4j;
45+
import org.springframework.beans.factory.annotation.Autowired;
46+
import org.springframework.web.bind.annotation.CrossOrigin;
47+
import org.springframework.web.bind.annotation.RequestMapping;
48+
import org.springframework.web.bind.annotation.RequestMethod;
49+
import org.springframework.web.bind.annotation.RequestParam;
50+
import org.springframework.web.bind.annotation.RestController;
51+
52+
/**
53+
* EXPERIMENTAL: Controller for HTTP endpoints to Feast Core. These endpoints are subject to change.
54+
*/
55+
@RestController
56+
@CrossOrigin
57+
@Slf4j
58+
@RequestMapping(value = "/api/v1", produces = "application/json")
59+
public class CoreServiceRestController {
60+
61+
private final FeastProperties feastProperties;
62+
private SpecService specService;
63+
private StatsService statsService;
64+
private ProjectService projectService;
65+
66+
@Autowired
67+
public CoreServiceRestController(
68+
FeastProperties feastProperties,
69+
SpecService specService,
70+
StatsService statsService,
71+
ProjectService projectService) {
72+
this.feastProperties = feastProperties;
73+
this.specService = specService;
74+
this.statsService = statsService;
75+
this.projectService = projectService;
76+
}
77+
78+
/**
79+
* GET /version : Fetches the version of Feast Core.
80+
*
81+
* @return (200 OK) Returns {@link GetFeastCoreVersionResponse} in JSON.
82+
*/
83+
@RequestMapping(value = "/version", method = RequestMethod.GET)
84+
public GetFeastCoreVersionResponse getVersion() {
85+
GetFeastCoreVersionResponse response =
86+
GetFeastCoreVersionResponse.newBuilder().setVersion(feastProperties.getVersion()).build();
87+
return response;
88+
}
89+
90+
/**
91+
* GET /feature-sets : Retrieve a list of Feature Sets according to filtering parameters of Feast
92+
* project name and feature set name. If none matches, an empty JSON response is returned.
93+
*
94+
* @param project Request Parameter: Name of feast project to search in. If set to <code>"*"
95+
* </code>, all existing projects will be filtered. However, asterisk can NOT be
96+
* combined with other strings (for example <code>"merchant_*"</code>) to use as wildcard to
97+
* filter feature sets.
98+
* @param name Request Parameter: Feature set name. If set to "*", filter * all feature sets by
99+
* default. Asterisk can be used as wildcard to filter * feature sets.
100+
* @return (200 OK) Return {@link ListFeatureSetsResponse} in JSON.
101+
*/
102+
@RequestMapping(value = "/feature-sets", method = RequestMethod.GET)
103+
public ListFeatureSetsResponse listFeatureSets(
104+
@RequestParam(defaultValue = Project.DEFAULT_NAME) String project, @RequestParam String name)
105+
throws InvalidProtocolBufferException {
106+
ListFeatureSetsRequest.Filter.Builder filterBuilder =
107+
ListFeatureSetsRequest.Filter.newBuilder().setProject(project).setFeatureSetName(name);
108+
return specService.listFeatureSets(filterBuilder.build());
109+
}
110+
111+
/**
112+
* GET /features : List Features based on project and entities.
113+
*
114+
* @param entities Request Parameter: List of all entities every returned feature should belong
115+
* to. At least one entity is required. For example, if <code>entity1</code> and <code>entity2
116+
* </code> are given, then all features returned (if any) will belong to BOTH
117+
* entities.
118+
* @param project (Optional) Request Parameter: A single project where the feature set of all
119+
* features returned is under. If not provided, the default project will be used, usually
120+
* <code>default</code>.
121+
* @return (200 OK) Return {@link ListFeaturesResponse} in JSON.
122+
*/
123+
@RequestMapping(value = "/features", method = RequestMethod.GET)
124+
public ListFeaturesResponse listFeatures(
125+
@RequestParam String[] entities, @RequestParam(required = false) Optional<String> project) {
126+
ListFeaturesRequest.Filter.Builder filterBuilder =
127+
ListFeaturesRequest.Filter.newBuilder().addAllEntities(Arrays.asList(entities));
128+
project.ifPresent(filterBuilder::setProject);
129+
return specService.listFeatures(filterBuilder.build());
130+
}
131+
132+
/**
133+
* GET /feature-statistics : Fetches statistics for a dataset speficied by the parameters. Either
134+
* both (start_date, end_date) need to be given or ingestion_ids are required. If both are given,
135+
* (start_date, end_date) will be ignored.
136+
*
137+
* @param ingestionIds Request Parameter: List of ingestion IDs. If missing, both startDate and
138+
* endDate should be provided.
139+
* @param startDate Request Parameter: UTC+0 starting date (inclusive) in the ISO format, from
140+
* <code>0001-01-01</code> to <code>9999-12-31</code>. Time given will be ignored. This
141+
* parameter will be ignored if any ingestionIds is provided.
142+
* @param endDate Request Parameter: UTC+0 ending date (exclusive) in the ISO format, from <code>
143+
* 0001-01-01</code> to <code>9999-12-31</code>. Time given will be ignored. This parameter
144+
* will be ignored if any ingestionIds is provided.
145+
* @param store Request Parameter: The name of the historical store used in Feast Serving. Online
146+
* store is not allowed.
147+
* @param featureSetId Request Parameter: Feature set ID, which has the form of <code>
148+
* project/feature_set_name</code>.
149+
* @param forceRefresh Request Parameter: whether to override the values in the cache. Accepts
150+
* <code>true</code>, <code>false</code>.
151+
* @param features (Optional) Request Parameter: List of features. If none provided, all features
152+
* in the feature set will be used for statistics.
153+
* @return (200 OK) Returns {@link GetFeatureStatisticsResponse} in JSON.
154+
*/
155+
@RequestMapping(value = "/feature-statistics", method = RequestMethod.GET)
156+
public GetFeatureStatisticsResponse getFeatureStatistics(
157+
@RequestParam(name = "feature_set_id") String featureSetId,
158+
@RequestParam(required = false) Optional<String[]> features,
159+
@RequestParam String store,
160+
@RequestParam(name = "start_date", required = false) Optional<String> startDate,
161+
@RequestParam(name = "end_date", required = false) Optional<String> endDate,
162+
@RequestParam(name = "ingestion_ids", required = false) Optional<String[]> ingestionIds,
163+
@RequestParam(name = "force_refresh") boolean forceRefresh)
164+
throws IOException {
165+
166+
Builder requestBuilder =
167+
GetFeatureStatisticsRequest.newBuilder()
168+
.setForceRefresh(forceRefresh)
169+
.setFeatureSetId(featureSetId)
170+
.setStore(store);
171+
172+
// set optional request parameters if they are provided
173+
features.ifPresent(theFeatures -> requestBuilder.addAllFeatures(Arrays.asList(theFeatures)));
174+
startDate.ifPresent(
175+
startDateStr -> requestBuilder.setStartDate(utcTimeStringToTimestamp(startDateStr)));
176+
endDate.ifPresent(
177+
endDateStr -> requestBuilder.setEndDate(utcTimeStringToTimestamp(endDateStr)));
178+
ingestionIds.ifPresent(
179+
theIngestionIds -> requestBuilder.addAllIngestionIds(Arrays.asList(theIngestionIds)));
180+
181+
return statsService.getFeatureStatistics(requestBuilder.build());
182+
}
183+
184+
/**
185+
* GET /projects : Get the list of existing feast projects.
186+
*
187+
* @return (200 OK) Returns {@link ListProjectsResponse} in JSON.
188+
*/
189+
@RequestMapping(value = "/projects", method = RequestMethod.GET)
190+
public ListProjectsResponse listProjects() {
191+
List<Project> projects = projectService.listProjects();
192+
return ListProjectsResponse.newBuilder()
193+
.addAllProjects(projects.stream().map(Project::getName).collect(Collectors.toList()))
194+
.build();
195+
}
196+
197+
private Timestamp utcTimeStringToTimestamp(String utcTimeString) {
198+
long epochSecond =
199+
LocalDate.parse(utcTimeString, DateTimeFormatter.ISO_DATE)
200+
.toEpochSecond(LocalTime.MIN, ZoneOffset.UTC);
201+
return Timestamp.newBuilder().setSeconds(epochSecond).setNanos(0).build();
202+
}
203+
}

0 commit comments

Comments
 (0)