Skip to content

Commit f9923bc

Browse files
committed
feat: Allow users to have protected project on shared registry
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent b221036 commit f9923bc

8 files changed

Lines changed: 166 additions & 12 deletions

File tree

.secrets.baseline

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infra/feast-operator/internal/controller/services/namespace_registry.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,22 @@ type NamespaceRegistryData struct {
3636
Namespaces map[string][]string `json:"namespaces"`
3737
}
3838

39+
// isProtectedProject checks if this CR is annotated as a protected project
40+
func (feast *FeastServices) isProtectedProject() bool {
41+
annotations := feast.Handler.FeatureStore.GetAnnotations()
42+
return annotations[ProtectedProjectAnnotation] == "true"
43+
}
44+
3945
// deployNamespaceRegistry creates and manages the namespace registry ConfigMap
4046
func (feast *FeastServices) deployNamespaceRegistry() error {
47+
// Skip namespace registry for protected projects.
48+
// Protected projects are managed externally and should not be visible to other instances.
49+
if feast.isProtectedProject() {
50+
logger := log.FromContext(feast.Handler.Context)
51+
logger.V(1).Info("Skipping namespace registry for protected project", "project", feast.Handler.FeatureStore.Spec.FeastProject)
52+
return nil
53+
}
54+
4155
// Check if we can determine the target namespace before creating any resources
4256
targetNamespace, err := feast.getNamespaceRegistryNamespace()
4357
if err != nil {
@@ -230,6 +244,11 @@ func (feast *FeastServices) getNamespaceRegistryNamespace() (string, error) {
230244

231245
// AddToNamespaceRegistry adds a feature store instance to the namespace registry
232246
func (feast *FeastServices) AddToNamespaceRegistry() error {
247+
// Skip for protected projects — they should not appear in the namespace registry.
248+
if feast.isProtectedProject() {
249+
return nil
250+
}
251+
233252
logger := log.FromContext(feast.Handler.Context)
234253
targetNamespace, err := feast.getNamespaceRegistryNamespace()
235254
if err != nil {

infra/feast-operator/internal/controller/services/services.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,20 @@ func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec) error {
472472
if feast.isUiServer() {
473473
feast.setContainer(&podSpec.Containers, UIFeastType, fsYamlB64)
474474
}
475+
476+
// When the CR is annotated as a protected project, set FEAST_PROTECTED_PROJECT=true
477+
// so the registry server tags its own project in the shared registry.
478+
// Other FeatureStore instances then exclude this project automatically.
479+
if feast.isProtectedProject() {
480+
protectedEnv := corev1.EnvVar{
481+
Name: "FEAST_PROTECTED_PROJECT",
482+
Value: "true",
483+
}
484+
for i := range podSpec.Containers {
485+
podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, protectedEnv)
486+
}
487+
}
488+
475489
return nil
476490
}
477491

infra/feast-operator/internal/controller/services/services_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ const (
4040
NamespaceRegistryDataKey = "namespaces"
4141
DefaultKubernetesNamespace = "feast-operator-system"
4242

43+
// ProtectedProjectAnnotation is the annotation key on a FeatureStore CR
44+
// that marks its project as protected. Protected projects are excluded
45+
// from project listings and shielded from teardown by other instances.
46+
// When this annotation is "true", the operator sets FEAST_PROTECTED_PROJECT=true
47+
// on the server pods, which causes the server to tag the project in the
48+
// shared registry on startup.
49+
ProtectedProjectAnnotation = "feast.dev/protected-project"
50+
4351
HttpPort = 80
4452
HttpsPort = 443
4553
HttpScheme = "http"

sdk/python/feast/api/registry/rest/rest_registry_server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,10 @@ def start_server(
327327
):
328328
import uvicorn
329329

330+
from feast.registry_server import _sync_protected_project_tag
331+
332+
_sync_protected_project_tag(self.store)
333+
330334
if tls_key_path and tls_cert_path:
331335
logger.info("Starting REST registry server in TLS(SSL) mode")
332336
logger.info(f"REST registry server listening on https://localhost:{port}")

sdk/python/feast/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,12 @@
4646

4747
# Default feature server registry ttl (seconds)
4848
DEFAULT_FEATURE_SERVER_REGISTRY_TTL = 5
49+
50+
# Tag key set on Feast projects that are protected.
51+
# Protected projects are excluded from project listings,
52+
# shielded from teardown, and hidden from delete operations.
53+
PROTECTED_PROJECT_TAG = "feast.dev/protected-project"
54+
55+
# Environment variable set by the operator on protected project pods.
56+
# When "true", the Feast server tags its own project as protected.
57+
FEAST_PROTECTED_PROJECT_ENV = "FEAST_PROTECTED_PROJECT"

sdk/python/feast/feature_store.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1884,14 +1884,32 @@ def _emit_openlineage_apply(self, objects: List[Any]):
18841884

18851885
def teardown(self):
18861886
"""Tears down all local and cloud resources for the feature store."""
1887+
from feast.constants import PROTECTED_PROJECT_TAG
1888+
1889+
# Prevent teardown of protected projects
1890+
try:
1891+
current = self.registry.get_project(name=self.project, allow_cache=False)
1892+
if current and current.tags.get(PROTECTED_PROJECT_TAG) == "true":
1893+
raise ValueError(
1894+
f'Teardown is not allowed on protected project "{self.project}". '
1895+
"Protected projects are managed externally and cannot be torn down via Feast."
1896+
)
1897+
except ValueError:
1898+
raise
1899+
except Exception:
1900+
pass
1901+
18871902
tables: List[BaseFeatureView] = []
18881903
tables.extend(self.list_feature_views())
18891904
tables.extend(self.list_label_views())
18901905

18911906
entities = self.list_entities()
18921907

18931908
self._get_provider().teardown_infra(self.project, tables, entities) # type: ignore[arg-type]
1894-
self.registry.teardown()
1909+
1910+
for project in self.list_projects():
1911+
self.registry.delete_project(project.name)
1912+
18951913
self._teardown_openlineage()
18961914

18971915
def _teardown_openlineage(self):
@@ -4732,14 +4750,20 @@ def list_projects(
47324750
"""
47334751
Retrieves the list of projects from the registry.
47344752
4753+
Protected projects (feast.dev/protected-project=true) are automatically
4754+
excluded from the results.
4755+
47354756
Args:
47364757
allow_cache: Whether to allow returning projects from a cached registry.
47374758
tags: Filter by tags.
47384759
47394760
Returns:
47404761
A list of projects.
47414762
"""
4742-
return self.registry.list_projects(allow_cache=allow_cache, tags=tags)
4763+
from feast.constants import PROTECTED_PROJECT_TAG
4764+
4765+
projects = self.registry.list_projects(allow_cache=allow_cache, tags=tags)
4766+
return [p for p in projects if p.tags.get(PROTECTED_PROJECT_TAG) != "true"]
47434767

47444768
def get_project(self, name: Optional[str]) -> Project:
47454769
"""
@@ -4766,7 +4790,21 @@ def delete_project(self, name: str, commit: bool = True) -> None:
47664790
47674791
Raises:
47684792
ProjectNotFoundException: The project could not be found.
4793+
ValueError: If the project is protected.
47694794
"""
4795+
from feast.constants import PROTECTED_PROJECT_TAG
4796+
4797+
try:
4798+
project = self.registry.get_project(name=name, allow_cache=False)
4799+
if project and project.tags.get(PROTECTED_PROJECT_TAG) == "true":
4800+
raise ValueError(
4801+
f'Cannot delete protected project "{name}". '
4802+
"Protected projects are managed externally."
4803+
)
4804+
except ValueError:
4805+
raise
4806+
except Exception:
4807+
pass
47704808
return self.registry.delete_project(name, commit=commit)
47714809

47724810
def list_saved_datasets(

sdk/python/feast/registry_server.py

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,17 +1487,26 @@ def GetProject(self, request: RegistryServer_pb2.GetProjectRequest, context):
14871487
).to_proto()
14881488

14891489
def ListProjects(self, request: RegistryServer_pb2.ListProjectsRequest, context):
1490-
paginated_projects, pagination_metadata = apply_pagination_and_sorting(
1491-
permitted_resources(
1492-
resources=cast(
1493-
list[FeastObject],
1494-
self.proxied_registry.list_projects(
1495-
allow_cache=request.allow_cache,
1496-
tags=dict(request.tags),
1497-
),
1490+
from feast.constants import PROTECTED_PROJECT_TAG
1491+
1492+
permitted_projects = permitted_resources(
1493+
resources=cast(
1494+
list[FeastObject],
1495+
self.proxied_registry.list_projects(
1496+
allow_cache=request.allow_cache,
1497+
tags=dict(request.tags),
14981498
),
1499-
actions=AuthzedAction.DESCRIBE,
15001499
),
1500+
actions=AuthzedAction.DESCRIBE,
1501+
)
1502+
1503+
# Exclude protected projects after RBAC check
1504+
visible_projects = [
1505+
p for p in permitted_projects if p.tags.get(PROTECTED_PROJECT_TAG) != "true"
1506+
]
1507+
1508+
paginated_projects, pagination_metadata = apply_pagination_and_sorting(
1509+
visible_projects,
15011510
pagination=request.pagination,
15021511
sorting=request.sorting,
15031512
)
@@ -1724,13 +1733,66 @@ def GetFeature(self, request: RegistryServer_pb2.GetFeatureRequest, context):
17241733
)
17251734

17261735

1736+
def _sync_protected_project_tag(store: FeatureStore):
1737+
"""Sync the protected project tag based on FEAST_PROTECTED_PROJECT env var.
1738+
1739+
When FEAST_PROTECTED_PROJECT=true, tags the project as protected in the
1740+
shared registry. When the env var is absent or false, removes the tag
1741+
if it was previously set — allowing temporary protection that can be
1742+
reversed by removing the annotation from the FeatureStore CR.
1743+
"""
1744+
import os
1745+
1746+
from feast.constants import FEAST_PROTECTED_PROJECT_ENV, PROTECTED_PROJECT_TAG
1747+
1748+
should_protect = os.environ.get(FEAST_PROTECTED_PROJECT_ENV, "").lower() == "true"
1749+
1750+
try:
1751+
existing = store.registry.get_project(name=store.project, allow_cache=False)
1752+
except Exception:
1753+
if should_protect:
1754+
from feast.project import Project
1755+
1756+
project = Project(
1757+
name=store.project,
1758+
tags={PROTECTED_PROJECT_TAG: "true"},
1759+
)
1760+
store.registry.apply_project(project, commit=True)
1761+
logger.info(
1762+
"Tagged project '%s' as protected (%s=true)",
1763+
store.project,
1764+
PROTECTED_PROJECT_TAG,
1765+
)
1766+
return
1767+
1768+
is_protected = existing.tags.get(PROTECTED_PROJECT_TAG) == "true"
1769+
1770+
if should_protect and not is_protected:
1771+
existing.tags[PROTECTED_PROJECT_TAG] = "true"
1772+
store.registry.apply_project(existing, commit=True)
1773+
logger.info(
1774+
"Tagged project '%s' as protected (%s=true)",
1775+
store.project,
1776+
PROTECTED_PROJECT_TAG,
1777+
)
1778+
elif not should_protect and is_protected:
1779+
del existing.tags[PROTECTED_PROJECT_TAG]
1780+
store.registry.apply_project(existing, commit=True)
1781+
logger.info(
1782+
"Removed protected tag from project '%s'",
1783+
store.project,
1784+
)
1785+
1786+
17271787
def start_server(
17281788
store: FeatureStore,
17291789
port: int,
17301790
wait_for_termination: bool = True,
17311791
tls_key_path: str = "",
17321792
tls_cert_path: str = "",
17331793
):
1794+
_sync_protected_project_tag(store)
1795+
17341796
auth_manager_type = str_to_auth_manager_type(store.config.auth_config.type)
17351797
init_security_manager(auth_type=auth_manager_type, fs=store)
17361798
init_auth_manager(

0 commit comments

Comments
 (0)