Skip to content
Open
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
2 changes: 1 addition & 1 deletion .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -1564,5 +1564,5 @@
}
]
},
"generated_at": "2026-07-17T12:34:13Z"
"generated_at": "2026-07-23T08:52:32Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,22 @@ type NamespaceRegistryData struct {
Namespaces map[string][]string `json:"namespaces"`
}

// isProtectedProject checks if this CR is annotated as a protected project
func (feast *FeastServices) isProtectedProject() bool {
annotations := feast.Handler.FeatureStore.GetAnnotations()
return annotations[ProtectedProjectAnnotation] == "true"
}

// deployNamespaceRegistry creates and manages the namespace registry ConfigMap
func (feast *FeastServices) deployNamespaceRegistry() error {
// Skip namespace registry for protected projects.
// Protected projects are managed externally and should not be visible to other instances.
if feast.isProtectedProject() {
logger := log.FromContext(feast.Handler.Context)
logger.V(1).Info("Skipping namespace registry for protected project", "project", feast.Handler.FeatureStore.Spec.FeastProject)
return nil
}

// Check if we can determine the target namespace before creating any resources
targetNamespace, err := feast.getNamespaceRegistryNamespace()
if err != nil {
Expand Down Expand Up @@ -230,6 +244,11 @@ func (feast *FeastServices) getNamespaceRegistryNamespace() (string, error) {

// AddToNamespaceRegistry adds a feature store instance to the namespace registry
func (feast *FeastServices) AddToNamespaceRegistry() error {
// Skip for protected projects — they should not appear in the namespace registry.
if feast.isProtectedProject() {
return nil
}

logger := log.FromContext(feast.Handler.Context)
targetNamespace, err := feast.getNamespaceRegistryNamespace()
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions infra/feast-operator/internal/controller/services/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,20 @@ func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec) error {
if feast.isUiServer() {
feast.setContainer(&podSpec.Containers, UIFeastType, fsYamlB64)
}

// When the CR is annotated as a protected project, set FEAST_PROTECTED_PROJECT=true
// so the registry server tags its own project in the shared registry.
// Other FeatureStore instances then exclude this project automatically.
if feast.isProtectedProject() {
protectedEnv := corev1.EnvVar{
Name: "FEAST_PROTECTED_PROJECT",
Value: "true",
}
for i := range podSpec.Containers {
podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, protectedEnv)
}
}

return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ const (
NamespaceRegistryDataKey = "namespaces"
DefaultKubernetesNamespace = "feast-operator-system"

// ProtectedProjectAnnotation is the annotation key on a FeatureStore CR
// that marks its project as protected. Protected projects are excluded
// from project listings and shielded from teardown by other instances.
// When this annotation is "true", the operator sets FEAST_PROTECTED_PROJECT=true
// on the server pods, which causes the server to tag the project in the
// shared registry on startup.
ProtectedProjectAnnotation = "feast.dev/protected-project"

HttpPort = 80
HttpsPort = 443
HttpScheme = "http"
Expand Down
4 changes: 4 additions & 0 deletions sdk/python/feast/api/registry/rest/rest_registry_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@ def start_server(
):
import uvicorn

from feast.registry_server import _sync_protected_project_tag

_sync_protected_project_tag(self.store)

if tls_key_path and tls_cert_path:
logger.info("Starting REST registry server in TLS(SSL) mode")
logger.info(f"REST registry server listening on https://localhost:{port}")
Expand Down
9 changes: 9 additions & 0 deletions sdk/python/feast/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@

# Default feature server registry ttl (seconds)
DEFAULT_FEATURE_SERVER_REGISTRY_TTL = 5

# Tag key set on Feast projects that are protected.
# Protected projects are excluded from project listings,
# shielded from teardown, and hidden from delete operations.
PROTECTED_PROJECT_TAG = "feast.dev/protected-project"

# Environment variable set by the operator on protected project pods.
# When "true", the Feast server tags its own project as protected.
FEAST_PROTECTED_PROJECT_ENV = "FEAST_PROTECTED_PROJECT"
42 changes: 40 additions & 2 deletions sdk/python/feast/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -1884,14 +1884,32 @@ def _emit_openlineage_apply(self, objects: List[Any]):

def teardown(self):
"""Tears down all local and cloud resources for the feature store."""
from feast.constants import PROTECTED_PROJECT_TAG

# Prevent teardown of protected projects
try:
current = self.registry.get_project(name=self.project, allow_cache=False)
if current and current.tags.get(PROTECTED_PROJECT_TAG) == "true":
raise ValueError(
f'Teardown is not allowed on protected project "{self.project}". '
"Protected projects are managed externally and cannot be torn down via Feast."
)
except ValueError:
raise
except Exception:
pass

tables: List[BaseFeatureView] = []
tables.extend(self.list_feature_views())
tables.extend(self.list_label_views())

entities = self.list_entities()

self._get_provider().teardown_infra(self.project, tables, entities) # type: ignore[arg-type]
self.registry.teardown()

for project in self.list_projects():
self.registry.delete_project(project.name)

self._teardown_openlineage()

def _teardown_openlineage(self):
Expand Down Expand Up @@ -4732,14 +4750,20 @@ def list_projects(
"""
Retrieves the list of projects from the registry.

Protected projects (feast.dev/protected-project=true) are automatically
excluded from the results.

Args:
allow_cache: Whether to allow returning projects from a cached registry.
tags: Filter by tags.

Returns:
A list of projects.
"""
return self.registry.list_projects(allow_cache=allow_cache, tags=tags)
from feast.constants import PROTECTED_PROJECT_TAG

projects = self.registry.list_projects(allow_cache=allow_cache, tags=tags)
return [p for p in projects if p.tags.get(PROTECTED_PROJECT_TAG) != "true"]

def get_project(self, name: Optional[str]) -> Project:
"""
Expand All @@ -4766,7 +4790,21 @@ def delete_project(self, name: str, commit: bool = True) -> None:

Raises:
ProjectNotFoundException: The project could not be found.
ValueError: If the project is protected.
"""
from feast.constants import PROTECTED_PROJECT_TAG

try:
project = self.registry.get_project(name=name, allow_cache=False)
if project and project.tags.get(PROTECTED_PROJECT_TAG) == "true":
raise ValueError(
f'Cannot delete protected project "{name}". '
"Protected projects are managed externally."
)
except ValueError:
raise
except Exception:
pass
return self.registry.delete_project(name, commit=commit)

def list_saved_datasets(
Expand Down
80 changes: 71 additions & 9 deletions sdk/python/feast/registry_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1487,17 +1487,26 @@ def GetProject(self, request: RegistryServer_pb2.GetProjectRequest, context):
).to_proto()

def ListProjects(self, request: RegistryServer_pb2.ListProjectsRequest, context):
paginated_projects, pagination_metadata = apply_pagination_and_sorting(
permitted_resources(
resources=cast(
list[FeastObject],
self.proxied_registry.list_projects(
allow_cache=request.allow_cache,
tags=dict(request.tags),
),
from feast.constants import PROTECTED_PROJECT_TAG

permitted_projects = permitted_resources(
resources=cast(
list[FeastObject],
self.proxied_registry.list_projects(
allow_cache=request.allow_cache,
tags=dict(request.tags),
),
actions=AuthzedAction.DESCRIBE,
),
actions=AuthzedAction.DESCRIBE,
)

# Exclude protected projects after RBAC check
visible_projects = [
p for p in permitted_projects if p.tags.get(PROTECTED_PROJECT_TAG) != "true"
]

paginated_projects, pagination_metadata = apply_pagination_and_sorting(
visible_projects,
pagination=request.pagination,
sorting=request.sorting,
)
Expand Down Expand Up @@ -1724,13 +1733,66 @@ def GetFeature(self, request: RegistryServer_pb2.GetFeatureRequest, context):
)


def _sync_protected_project_tag(store: FeatureStore):
"""Sync the protected project tag based on FEAST_PROTECTED_PROJECT env var.

When FEAST_PROTECTED_PROJECT=true, tags the project as protected in the
shared registry. When the env var is absent or false, removes the tag
if it was previously set — allowing temporary protection that can be
reversed by removing the annotation from the FeatureStore CR.
"""
import os

from feast.constants import FEAST_PROTECTED_PROJECT_ENV, PROTECTED_PROJECT_TAG

should_protect = os.environ.get(FEAST_PROTECTED_PROJECT_ENV, "").lower() == "true"

try:
existing = store.registry.get_project(name=store.project, allow_cache=False)
except Exception:
if should_protect:
from feast.project import Project

project = Project(
name=store.project,
tags={PROTECTED_PROJECT_TAG: "true"},
)
store.registry.apply_project(project, commit=True)
logger.info(
"Tagged project '%s' as protected (%s=true)",
store.project,
PROTECTED_PROJECT_TAG,
)
return

is_protected = existing.tags.get(PROTECTED_PROJECT_TAG) == "true"

if should_protect and not is_protected:
existing.tags[PROTECTED_PROJECT_TAG] = "true"
store.registry.apply_project(existing, commit=True)
logger.info(
"Tagged project '%s' as protected (%s=true)",
store.project,
PROTECTED_PROJECT_TAG,
)
elif not should_protect and is_protected:
del existing.tags[PROTECTED_PROJECT_TAG]
store.registry.apply_project(existing, commit=True)
logger.info(
"Removed protected tag from project '%s'",
store.project,
)


def start_server(
store: FeatureStore,
port: int,
wait_for_termination: bool = True,
tls_key_path: str = "",
tls_cert_path: str = "",
):
_sync_protected_project_tag(store)

auth_manager_type = str_to_auth_manager_type(store.config.auth_config.type)
init_security_manager(auth_type=auth_manager_type, fs=store)
init_auth_manager(
Expand Down
Loading