Skip to content

Commit

Permalink
Add REST endpoints for Feast UI (#878)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
SwampertX and terryyylim authored Aug 17, 2020
1 parent b02191f commit 7d8126b
Show file tree
Hide file tree
Showing 8 changed files with 635 additions and 3 deletions.
3 changes: 3 additions & 0 deletions auth/src/main/java/feast/auth/config/SecurityProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class SecurityProperties {
private AuthenticationProperties authentication;
private AuthorizationProperties authorization;

// Bypass Authentication and Authorization at all HTTP endpoints at /api/v1
private boolean disableRestControllerAuth;

@Getter
@Setter
public static class AuthenticationProperties {
Expand Down
21 changes: 19 additions & 2 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>dev.feast</groupId>
<artifactId>feast-parent</artifactId>
Expand Down Expand Up @@ -394,5 +393,23 @@
<version>1.6.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-path</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>xml-path</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
17 changes: 16 additions & 1 deletion core/src/main/java/feast/core/config/WebSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
*/
package feast.core.config;

import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
Expand All @@ -29,6 +32,13 @@
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

private final FeastProperties feastProperties;

@Autowired
public WebSecurityConfig(FeastProperties feastProperties) {
this.feastProperties = feastProperties;
}

/**
* Allows for custom web security rules to be applied.
*
Expand All @@ -37,10 +47,15 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
List<String> matchersToBypass = new ArrayList<>(List.of("/actuator/**", "/metrics/**"));

if (feastProperties.securityProperties().isDisableRestControllerAuth()) {
matchersToBypass.add("/api/v1/**");
}

// Bypasses security/authentication for the following paths
http.authorizeRequests()
.antMatchers("/actuator/**", "/metrics/**")
.antMatchers(matchersToBypass.toArray(new String[0]))
.permitAll()
.anyRequest()
.authenticated()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2018-2019 The Feast Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feast.core.controller;

import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Timestamp;
import feast.core.config.FeastProperties;
import feast.core.model.Project;
import feast.core.service.ProjectService;
import feast.core.service.SpecService;
import feast.core.service.StatsService;
import feast.proto.core.CoreServiceProto.GetFeastCoreVersionResponse;
import feast.proto.core.CoreServiceProto.GetFeatureStatisticsRequest;
import feast.proto.core.CoreServiceProto.GetFeatureStatisticsRequest.Builder;
import feast.proto.core.CoreServiceProto.GetFeatureStatisticsResponse;
import feast.proto.core.CoreServiceProto.ListFeatureSetsRequest;
import feast.proto.core.CoreServiceProto.ListFeatureSetsResponse;
import feast.proto.core.CoreServiceProto.ListFeaturesRequest;
import feast.proto.core.CoreServiceProto.ListFeaturesResponse;
import feast.proto.core.CoreServiceProto.ListProjectsResponse;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* EXPERIMENTAL: Controller for HTTP endpoints to Feast Core. These endpoints are subject to change.
*/
@RestController
@CrossOrigin
@Slf4j
@RequestMapping(value = "/api/v1", produces = "application/json")
public class CoreServiceRestController {

private final FeastProperties feastProperties;
private SpecService specService;
private StatsService statsService;
private ProjectService projectService;

@Autowired
public CoreServiceRestController(
FeastProperties feastProperties,
SpecService specService,
StatsService statsService,
ProjectService projectService) {
this.feastProperties = feastProperties;
this.specService = specService;
this.statsService = statsService;
this.projectService = projectService;
}

/**
* GET /version : Fetches the version of Feast Core.
*
* @return (200 OK) Returns {@link GetFeastCoreVersionResponse} in JSON.
*/
@RequestMapping(value = "/version", method = RequestMethod.GET)
public GetFeastCoreVersionResponse getVersion() {
GetFeastCoreVersionResponse response =
GetFeastCoreVersionResponse.newBuilder().setVersion(feastProperties.getVersion()).build();
return response;
}

/**
* GET /feature-sets : Retrieve a list of Feature Sets according to filtering parameters of Feast
* project name and feature set name. If none matches, an empty JSON response is returned.
*
* @param project Request Parameter: Name of feast project to search in. If set to <code>"*"
* </code>, all existing projects will be filtered. However, asterisk can NOT be
* combined with other strings (for example <code>"merchant_*"</code>) to use as wildcard to
* filter feature sets.
* @param name Request Parameter: Feature set name. If set to "*", filter * all feature sets by
* 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)
public ListFeatureSetsResponse listFeatureSets(
@RequestParam(defaultValue = Project.DEFAULT_NAME) String project, @RequestParam String name)
throws InvalidProtocolBufferException {
ListFeatureSetsRequest.Filter.Builder filterBuilder =
ListFeatureSetsRequest.Filter.newBuilder().setProject(project).setFeatureSetName(name);
return specService.listFeatureSets(filterBuilder.build());
}

/**
* GET /features : List Features based on project and entities.
*
* @param entities Request Parameter: List of all entities every returned feature should belong
* to. At least one entity is required. For example, if <code>entity1</code> and <code>entity2
* </code> are given, then all features returned (if any) will belong to BOTH
* entities.
* @param project (Optional) Request Parameter: A single project where the feature set of all
* features returned is under. If not provided, the default project will be used, usually
* <code>default</code>.
* @return (200 OK) Return {@link ListFeaturesResponse} in JSON.
*/
@RequestMapping(value = "/features", method = RequestMethod.GET)
public ListFeaturesResponse listFeatures(
@RequestParam String[] entities, @RequestParam(required = false) Optional<String> project) {
ListFeaturesRequest.Filter.Builder filterBuilder =
ListFeaturesRequest.Filter.newBuilder().addAllEntities(Arrays.asList(entities));
project.ifPresent(filterBuilder::setProject);
return specService.listFeatures(filterBuilder.build());
}

/**
* GET /feature-statistics : Fetches statistics for a dataset speficied by the parameters. Either
* both (start_date, end_date) need to be given or ingestion_ids are required. If both are given,
* (start_date, end_date) will be ignored.
*
* @param ingestionIds Request Parameter: List of ingestion IDs. If missing, both startDate and
* endDate should be provided.
* @param startDate Request Parameter: UTC+0 starting date (inclusive) in the ISO format, from
* <code>0001-01-01</code> to <code>9999-12-31</code>. Time given will be ignored. This
* parameter will be ignored if any ingestionIds is provided.
* @param endDate Request Parameter: UTC+0 ending date (exclusive) in the ISO format, from <code>
* 0001-01-01</code> to <code>9999-12-31</code>. Time given will be ignored. This parameter
* will be ignored if any ingestionIds is provided.
* @param store Request Parameter: The name of the historical store used in Feast Serving. Online
* store is not allowed.
* @param featureSetId Request Parameter: Feature set ID, which has the form of <code>
* project/feature_set_name</code>.
* @param forceRefresh Request Parameter: whether to override the values in the cache. Accepts
* <code>true</code>, <code>false</code>.
* @param features (Optional) Request Parameter: List of features. If none provided, all features
* in the feature set will be used for statistics.
* @return (200 OK) Returns {@link GetFeatureStatisticsResponse} in JSON.
*/
@RequestMapping(value = "/feature-statistics", method = RequestMethod.GET)
public GetFeatureStatisticsResponse getFeatureStatistics(
@RequestParam(name = "feature_set_id") String featureSetId,
@RequestParam(required = false) Optional<String[]> features,
@RequestParam String store,
@RequestParam(name = "start_date", required = false) Optional<String> startDate,
@RequestParam(name = "end_date", required = false) Optional<String> endDate,
@RequestParam(name = "ingestion_ids", required = false) Optional<String[]> ingestionIds,
@RequestParam(name = "force_refresh") boolean forceRefresh)
throws IOException {

Builder requestBuilder =
GetFeatureStatisticsRequest.newBuilder()
.setForceRefresh(forceRefresh)
.setFeatureSetId(featureSetId)
.setStore(store);

// set optional request parameters if they are provided
features.ifPresent(theFeatures -> requestBuilder.addAllFeatures(Arrays.asList(theFeatures)));
startDate.ifPresent(
startDateStr -> requestBuilder.setStartDate(utcTimeStringToTimestamp(startDateStr)));
endDate.ifPresent(
endDateStr -> requestBuilder.setEndDate(utcTimeStringToTimestamp(endDateStr)));
ingestionIds.ifPresent(
theIngestionIds -> requestBuilder.addAllIngestionIds(Arrays.asList(theIngestionIds)));

return statsService.getFeatureStatistics(requestBuilder.build());
}

/**
* GET /projects : Get the list of existing feast projects.
*
* @return (200 OK) Returns {@link ListProjectsResponse} in JSON.
*/
@RequestMapping(value = "/projects", method = RequestMethod.GET)
public ListProjectsResponse listProjects() {
List<Project> projects = projectService.listProjects();
return ListProjectsResponse.newBuilder()
.addAllProjects(projects.stream().map(Project::getName).collect(Collectors.toList()))
.build();
}

private Timestamp utcTimeStringToTimestamp(String utcTimeString) {
long epochSecond =
LocalDate.parse(utcTimeString, DateTimeFormatter.ISO_DATE)
.toEpochSecond(LocalTime.MIN, ZoneOffset.UTC);
return Timestamp.newBuilder().setSeconds(epochSecond).setNanos(0).build();
}
}
Loading

0 comments on commit 7d8126b

Please sign in to comment.