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
168 changes: 168 additions & 0 deletions .github/workflows/registry-rest-api-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# .github/workflows/registry-rest-api-tests.yml
name: pr-rest-API-tests

on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- labeled

jobs:
registry-rest-api-tests:
timeout-minutes: 30
if:
((github.event.action == 'labeled' && (github.event.label.name == 'approved' || github.event.label.name == 'lgtm' || github.event.label.name == 'ok-to-test')) ||
(github.event.action != 'labeled' && (contains(github.event.pull_request.labels.*.name, 'ok-to-test') || contains(github.event.pull_request.labels.*.name, 'approved') || contains(github.event.pull_request.labels.*.name, 'lgtm')))) &&
github.repository == 'feast-dev/feast'
runs-on: ubuntu-latest

services:
kind:
# Specify the Kubernetes version
image: kindest/node:v1.30.6

env:
KIND_CLUSTER: "registry-rest-api-cluster"

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@v1.3.1
with:
android: true
dotnet: true
haskell: true
large-packages: false
docker-images: false
swap-storage: false
tool-cache: false

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.9

- name: Create KIND cluster
run: |
cat <<EOF | kind create cluster --name $KIND_CLUSTER --wait 10m --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraMounts:
- hostPath: /mnt/kind
containerPath: /var/lib/containerd
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
EOF

- name: Set up kubernetes context
run: |
kubectl config use-context kind-$KIND_CLUSTER
echo "kind context is switched to cluster kind-$KIND_CLUSTER"


- name: Set up Ingress controller
run: |
echo "Installing ingress-nginx for KIND..."
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.5/deploy/static/provider/kind/deploy.yaml

echo "⏳ Waiting for ingress controller to become ready..."
kubectl wait --namespace ingress-nginx \
--for=condition=Ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=180s

- name: Add ingress DNS to /etc/hosts
run: |
echo "127.0.0.1 feast.kind.test" | sudo tee -a /etc/hosts
echo "Added 'feast.kind.test' to /etc/hosts"

- name: Build and Deploy Feast Operator images
run: |
# Create namespace
kubectl create ns feast-operator-system || true

# navigate to feast operator path
cd infra/feast-operator/

# Build Feast Operator Docker image
make docker-build IMG=localhost/feast-operator:v0.0.1

# Load Operator image into KIND
kind load docker-image localhost/feast-operator:v0.0.1 --name $KIND_CLUSTER

# Build Feast dev image
make feast-ci-dev-docker-img

# Tag Feast image for KIND compatibility
docker tag feastdev/feature-server:dev localhost/feastdev/feature-server:dev

# Load Feast image into KIND
kind load docker-image localhost/feastdev/feature-server:dev --name $KIND_CLUSTER

# Install CRDs
make install

# Deploy operator to the KIND cluster
make deploy IMG=localhost/feast-operator:v0.0.1 FS_IMG=localhost/feastdev/feature-server:dev

# Wait for controller manager to be ready
kubectl wait deployment feast-operator-controller-manager -n feast-operator-system --for=condition=Available=True --timeout=180s

- name: Setup Python
uses: actions/setup-python@v5
id: setup-python
with:
python-version: 3.11
architecture: x64

- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Install dependencies
run: make install-python-dependencies-ci

- name: Setup and Run Registry Rest API tests
run: |
echo "Running Registry REST API tests..."
cd sdk/python/tests/registry_rest_api_tests/
pytest test_feast_registry.py -s

- name: Clean up docker images
if: always()
run: |
docker images --format '{{.Repository}}:{{.Tag}}' | grep 'feast' | xargs -r docker rmi -f
docker system prune -a -f

- name: Debug KIND Cluster when there is a failure
if: failure()
run: |
kubectl get pods --all-namespaces
kubectl describe nodes

- name: Clean up
if: always()
run: |
# Delete the KIND cluster after tests
kind delete cluster --name kind-$KIND_CLUSTER

130 changes: 130 additions & 0 deletions sdk/python/tests/registry_rest_api_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import os

import pytest
import requests
from kubernetes import client, config
from support import (
applyFeastProject,
create_feast_project,
create_namespace,
create_route,
delete_namespace,
deploy_and_validate_pod,
execPodCommand,
get_pod_name_by_prefix,
run_kubectl_apply_with_sed,
run_kubectl_command,
validate_feature_store_cr_status,
)


class FeastRestClient:
def __init__(self, base_url):
self.base_url = base_url.rstrip("/")
self.api_prefix = "/api/v1"

def _build_url(self, endpoint):
if not endpoint.startswith("/"):
endpoint = "/" + endpoint
return f"{self.base_url}{self.api_prefix}{endpoint}"

def get(self, endpoint, params=None):
params = params or {}
params.setdefault("allow_cache", "false")
url = self._build_url(endpoint)
return requests.get(url, params=params, verify=False)


@pytest.fixture(scope="session")
def feast_rest_client():
# Load kubeconfig and initialize Kubernetes client
config.load_kube_config()
api_instance = client.CoreV1Api()

# Constants and environment values
namespace = "test-ns-feast-rest"
credit_scoring = "credit-scoring"
driver_ranking = "driver-ranking"
service_name = "feast-test-s3-registry-rest"
run_on_openshift = os.getenv("RUN_ON_OPENSHIFT_CI", "false").lower() == "true"

# Create test namespace
create_namespace(api_instance, namespace)

try:
if not run_on_openshift:
# Deploy dependencies
deploy_and_validate_pod(namespace, "resource/redis.yaml", "app=redis")
deploy_and_validate_pod(namespace, "resource/postgres.yaml", "app=postgres")

# Create and validate FeatureStore CRs
create_feast_project(
"resource/feast_config_credit_scoring.yaml", namespace, credit_scoring
)
validate_feature_store_cr_status(namespace, credit_scoring)

create_feast_project(
"resource/feast_config_driver_ranking.yaml", namespace, driver_ranking
)
validate_feature_store_cr_status(namespace, driver_ranking)

# Deploy ingress and get route URL
run_kubectl_command(
["apply", "-f", "resource/feast-registry-nginx.yaml", "-n", namespace]
)
ingress_host = run_kubectl_command(
[
"get",
"ingress",
"feast-registry-ingress",
"-n",
namespace,
"-o",
"jsonpath={.spec.rules[0].host}",
]
)
route_url = f"http://{ingress_host}"

# Apply feast projects

applyFeastProject(namespace, credit_scoring)

applyFeastProject(namespace, driver_ranking)

# Create Saved Datasets and Permissions
pod_name = get_pod_name_by_prefix(namespace, credit_scoring)

# Apply datasets
execPodCommand(
namespace, pod_name, ["python", "create_ui_visible_datasets.py"]
)

# Apply permissions
execPodCommand(namespace, pod_name, ["python", "permissions_apply.py"])

else:
# OpenShift cluster setup using S3-based registry
aws_access_key = os.getenv("AWS_ACCESS_KEY")
aws_secret_key = os.getenv("AWS_SECRET_KEY")
aws_bucket = os.getenv("AWS_BUCKET_NAME")
registry_path = os.getenv("AWS_REGISTRY_FILE_PATH")

run_kubectl_apply_with_sed(
aws_access_key,
aws_secret_key,
aws_bucket,
registry_path,
"resource/feast_config_rhoai.yaml",
namespace,
)
validate_feature_store_cr_status(namespace, "test-s3")
route_url = create_route(namespace, credit_scoring, service_name)
if not route_url:
raise RuntimeError("Route URL could not be fetched.")

print(f"\n Connected to Feast REST at: {route_url}")
yield FeastRestClient(route_url)

finally:
print(f"\n Deleting namespace: {namespace}")
delete_namespace(api_instance, namespace)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: feast-registry-ingress
namespace: test-ns-feast-rest
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
ingressClassName: nginx
rules:
- host: feast.kind.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: feast-credit-scoring-registry-rest
port:
number: 80
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
apiVersion: v1
kind: Secret
metadata:
name: feast-data-stores
namespace: test-ns-feast-rest
stringData:
redis: |
connection_string: redis.test-ns-feast-rest.svc.cluster.local:6379
sql: |
path: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres.test-ns-feast-rest.svc.cluster.local:5432/${POSTGRES_DB}
cache_ttl_seconds: 60
sqlalchemy_config_kwargs:
echo: false
pool_pre_ping: true
---
apiVersion: feast.dev/v1alpha1
kind: FeatureStore
metadata:
name: credit-scoring
namespace: test-ns-feast-rest
spec:
feastProject: credit_scoring_local
feastProjectDir:
git:
url: https://github.com/feast-dev/feast-credit-score-local-tutorial.git
ref: 5dbd21c
services:
offlineStore:
persistence:
file:
type: duckdb
onlineStore:
persistence:
store:
type: redis
secretRef:
name: feast-data-stores
server:
envFrom:
- secretRef:
name: postgres-secret
env:
- name: MPLCONFIGDIR
value: /tmp
resources:
requests:
cpu: 150m
memory: 128Mi
registry:
local:
persistence:
store:
type: sql
secretRef:
name: feast-data-stores
server:
envFrom:
- secretRef:
name: postgres-secret
grpc: true
restAPI: true
Loading
Loading