Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ The list below contains the functionality that contributors are planning to deve
* **Feature Serving**
* [x] Python Client
* [x] [Python feature server](https://docs.feast.dev/reference/feature-servers/python-feature-server)
* [x] [Feast Operator (alpha)](https://github.com/feast-dev/feast/blob/master/infra/feast-operator/README.md)
* [x] [Java feature server (alpha)](https://github.com/feast-dev/feast/blob/master/infra/charts/feast/README.md)
* [x] [Go feature server (alpha)](https://docs.feast.dev/reference/feature-servers/go-feature-server)
* [x] [Offline Feature Server (alpha)](https://docs.feast.dev/reference/feature-servers/offline-feature-server)
Expand Down
8 changes: 6 additions & 2 deletions docs/reference/feature-servers/registry-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

## Description

The Registry server uses the gRPC communication protocol to exchange data.
This enables users to communicate with the server using any programming language that can make gRPC requests.
The Registry server supports both gRPC and REST interfaces for interacting with feature metadata. While gRPC remains the default protocol—enabling clients in any language with gRPC support—the REST API allows users to interact with the registry over standard HTTP using any REST-capable tool or language.

## How to configure the server

Expand All @@ -13,6 +12,11 @@ There is a CLI command that starts the Registry server: `feast serve_registry`.
To start the Registry Server in TLS mode, you need to provide the private and public keys using the `--key` and `--cert` arguments.
More info about TLS mode can be found in [feast-client-connecting-to-remote-registry-sever-started-in-tls-mode](../../how-to-guides/starting-feast-servers-tls-mode.md#starting-feast-registry-server-in-tls-mode)

To enable REST API support, start the registry server with REST mode enabled :

`feast serve_registry --rest-api`


## How to configure the client

Please see the detail how to configure Remote Registry client [remote.md](../registries/remote.md)
Expand Down
Empty file.
Empty file.
19 changes: 19 additions & 0 deletions sdk/python/feast/api/registry/rest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi import FastAPI

from feast.api.registry.rest.data_sources import get_data_source_router
from feast.api.registry.rest.entities import get_entity_router
from feast.api.registry.rest.feature_services import get_feature_service_router
from feast.api.registry.rest.feature_views import get_feature_view_router
from feast.api.registry.rest.permissions import get_permission_router
from feast.api.registry.rest.projects import get_project_router
from feast.api.registry.rest.saved_datasets import get_saved_dataset_router


def register_all_routes(app: FastAPI, grpc_handler):
app.include_router(get_entity_router(grpc_handler))
app.include_router(get_data_source_router(grpc_handler))
app.include_router(get_feature_service_router(grpc_handler))
app.include_router(get_feature_view_router(grpc_handler))
app.include_router(get_permission_router(grpc_handler))
app.include_router(get_project_router(grpc_handler))
app.include_router(get_saved_dataset_router(grpc_handler))
42 changes: 42 additions & 0 deletions sdk/python/feast/api/registry/rest/data_sources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import logging
from typing import Dict

from fastapi import APIRouter, Depends, Query

from feast.api.registry.rest.rest_utils import grpc_call, parse_tags
from feast.protos.feast.registry import RegistryServer_pb2

logger = logging.getLogger(__name__)


def get_data_source_router(grpc_handler) -> APIRouter:
router = APIRouter()

@router.get("/data_sources")
def list_data_sources(
project: str = Query(...),
allow_cache: bool = Query(default=True),
tags: Dict[str, str] = Depends(parse_tags),
):
req = RegistryServer_pb2.ListDataSourcesRequest(
project=project,
allow_cache=allow_cache,
tags=tags,
)
response = grpc_call(grpc_handler.ListDataSources, req)
return {"data_sources": response.get("dataSources", [])}

@router.get("/data_sources/{name}")
def get_data_source(
name: str,
project: str = Query(...),
allow_cache: bool = Query(default=True),
):
req = RegistryServer_pb2.GetDataSourceRequest(
name=name,
project=project,
allow_cache=allow_cache,
)
return grpc_call(grpc_handler.GetDataSource, req)

return router
35 changes: 35 additions & 0 deletions sdk/python/feast/api/registry/rest/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging

from fastapi import APIRouter, Query

from feast.api.registry.rest.rest_utils import grpc_call
from feast.protos.feast.registry import RegistryServer_pb2

logger = logging.getLogger(__name__)


def get_entity_router(grpc_handler) -> APIRouter:
router = APIRouter()

@router.get("/entities")
def list_entities(
project: str = Query(...),
):
req = RegistryServer_pb2.ListEntitiesRequest(project=project)
response = grpc_call(grpc_handler.ListEntities, req)
return {"entities": response.get("entities", [])}

@router.get("/entities/{name}")
def get_entity(
name: str,
project: str = Query(...),
allow_cache: bool = Query(default=True),
):
req = RegistryServer_pb2.GetEntityRequest(
name=name,
project=project,
allow_cache=allow_cache,
)
return grpc_call(grpc_handler.GetEntity, req)

return router
38 changes: 38 additions & 0 deletions sdk/python/feast/api/registry/rest/feature_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Dict

from fastapi import APIRouter, Depends, Query

from feast.api.registry.rest.rest_utils import grpc_call, parse_tags
from feast.protos.feast.registry import RegistryServer_pb2


def get_feature_service_router(grpc_handler) -> APIRouter:
router = APIRouter()

@router.get("/feature_services")
def list_feature_services(
project: str = Query(...),
allow_cache: bool = Query(default=True),
tags: Dict[str, str] = Depends(parse_tags),
):
req = RegistryServer_pb2.ListFeatureServicesRequest(
project=project,
allow_cache=allow_cache,
tags=tags,
)
return grpc_call(grpc_handler.ListFeatureServices, req)

@router.get("/feature_services/{name}")
def get_feature_service(
name: str,
project: str = Query(...),
allow_cache: bool = Query(default=True),
):
req = RegistryServer_pb2.GetFeatureServiceRequest(
name=name,
project=project,
allow_cache=allow_cache,
)
return grpc_call(grpc_handler.GetFeatureService, req)

return router
39 changes: 39 additions & 0 deletions sdk/python/feast/api/registry/rest/feature_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import Dict

from fastapi import APIRouter, Depends, Query

from feast.api.registry.rest.rest_utils import grpc_call, parse_tags
from feast.registry_server import RegistryServer_pb2


def get_feature_view_router(grpc_handler) -> APIRouter:
router = APIRouter()

@router.get("/feature_views/{name}")
def get_any_feature_view(
name: str,
project: str = Query(...),
allow_cache: bool = Query(True),
):
req = RegistryServer_pb2.GetAnyFeatureViewRequest(
name=name,
project=project,
allow_cache=allow_cache,
)
response = grpc_call(grpc_handler.GetAnyFeatureView, req)
return response.get("anyFeatureView", {})

@router.get("/feature_views")
def list_all_feature_views(
project: str = Query(...),
allow_cache: bool = Query(True),
tags: Dict[str, str] = Depends(parse_tags),
):
req = RegistryServer_pb2.ListAllFeatureViewsRequest(
project=project,
allow_cache=allow_cache,
tags=tags,
)
return grpc_call(grpc_handler.ListAllFeatureViews, req)

return router
34 changes: 34 additions & 0 deletions sdk/python/feast/api/registry/rest/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from fastapi import APIRouter, Query

from feast.api.registry.rest.rest_utils import grpc_call
from feast.registry_server import RegistryServer_pb2


def get_permission_router(grpc_handler) -> APIRouter:
router = APIRouter()

@router.get("/permissions/{name}")
def get_permission(
name: str,
project: str = Query(...),
allow_cache: bool = Query(True),
):
req = RegistryServer_pb2.GetPermissionRequest(
name=name,
project=project,
allow_cache=allow_cache,
)
return {"permission": grpc_call(grpc_handler.GetPermission, req)}

@router.get("/permissions")
def list_permissions(
project: str = Query(...),
allow_cache: bool = Query(True),
):
req = RegistryServer_pb2.ListPermissionsRequest(
project=project,
allow_cache=allow_cache,
)
return grpc_call(grpc_handler.ListPermissions, req)

return router
31 changes: 31 additions & 0 deletions sdk/python/feast/api/registry/rest/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from fastapi import APIRouter, Query

from feast.api.registry.rest.rest_utils import grpc_call
from feast.protos.feast.registry import RegistryServer_pb2


def get_project_router(grpc_handler) -> APIRouter:
router = APIRouter()

@router.get("/projects/{name}")
def get_project(
name: str,
allow_cache: bool = Query(True),
):
req = RegistryServer_pb2.GetProjectRequest(
name=name,
allow_cache=allow_cache,
)
return grpc_call(grpc_handler.GetProject, req)

@router.get("/projects")
def list_projects(
allow_cache: bool = Query(True),
):
req = RegistryServer_pb2.ListProjectsRequest(
allow_cache=allow_cache,
)
response = grpc_call(grpc_handler.ListProjects, req)
return {"projects": response.get("projects", [])}

return router
104 changes: 104 additions & 0 deletions sdk/python/feast/api/registry/rest/rest_registry_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import logging

from fastapi import Depends, FastAPI, status

from feast import FeatureStore
from feast.api.registry.rest import register_all_routes
from feast.permissions.auth.auth_manager import get_auth_manager
from feast.permissions.server.rest import inject_user_details
from feast.permissions.server.utils import (
ServerType,
init_auth_manager,
init_security_manager,
str_to_auth_manager_type,
)
from feast.registry_server import RegistryServer

logger = logging.getLogger(__name__)


class RestRegistryServer:
def __init__(self, store: FeatureStore):
self.store = store
self.registry = store.registry
self.grpc_handler = RegistryServer(self.registry)
self.app = FastAPI(
title="Feast REST Registry Server",
description="Feast REST Registry Server",
dependencies=[Depends(inject_user_details)],
version="1.0.0",
openapi_url="/openapi.json",
docs_url="/",
redoc_url="/docs",
default_status_code=status.HTTP_200_OK,
default_headers={
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"X-Frame-Options": "DENY",
},
)
self._add_openapi_security()
self._init_auth()
self._register_routes()

def _add_openapi_security(self):
if self.app.openapi_schema:
return
original_openapi = self.app.openapi

def custom_openapi():
if self.app.openapi_schema:
return self.app.openapi_schema
schema = original_openapi()
schema.setdefault("components", {}).setdefault("securitySchemes", {})[
"BearerAuth"
] = {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
}
schema.setdefault("security", []).append({"BearerAuth": []})
self.app.openapi_schema = schema
return self.app.openapi_schema

self.app.openapi = custom_openapi

def _init_auth(self):
auth_type = str_to_auth_manager_type(self.store.config.auth_config.type)
init_security_manager(auth_type=auth_type, fs=self.store)
init_auth_manager(
auth_type=auth_type,
server_type=ServerType.REST,
auth_config=self.store.config.auth_config,
)
self.auth_manager = get_auth_manager()

def _register_routes(self):
register_all_routes(self.app, self.grpc_handler)

def start_server(
self,
port: int,
tls_key_path: str = "",
tls_cert_path: str = "",
):
import uvicorn

if tls_key_path and tls_cert_path:
logger.info("Starting REST registry server in TLS(SSL) mode")
print(f"REST registry server listening on https://localhost:{port}")
uvicorn.run(
self.app,
host="0.0.0.0",
port=port,
ssl_keyfile=tls_key_path,
ssl_certfile=tls_cert_path,
)
else:
print("Starting REST registry server in non-TLS(SSL) mode")
print(f"REST registry server listening on http://localhost:{port}")
uvicorn.run(
self.app,
host="0.0.0.0",
port=port,
)
Loading
Loading