diff --git a/.claude/rules/feast-components.md b/.claude/rules/feast-components.md new file mode 100644 index 00000000000..02b1c6f4dd4 --- /dev/null +++ b/.claude/rules/feast-components.md @@ -0,0 +1,45 @@ +--- +paths: + - sdk/python/feast/infra/online_stores/** + - sdk/python/feast/infra/offline_stores/** + - sdk/python/feast/infra/registry/** + - sdk/python/tests/unit/infra/** + - go/** + - infra/feast-operator/** +--- + +Read `skills/feast-architecture/SKILL.md` for the relevant component section: + +| Working in… | Read section | +|---|---| +| `online_stores/` | Online Store — config pattern, entity key serialization, async support, registration | +| `offline_stores/` | Offline Store — interface, PIT join, pull_latest, adding a new backend | +| `infra/registry/` | Registry — proto vs SQL backend, caching, adding new object types | +| `go/` | Go Feature Server — entry point, serving path, online store interface, build commands | +| `infra/feast-operator/` | Feast Operator — CRD spec, reconcile loop, RBAC markers, dev workflow | + +For testing patterns and debugging, also read `skills/feast-testing/SKILL.md`. + +## When making any component change + +- **Unit tests**: add or update tests in `sdk/python/tests/unit/infra//` +- **Integration tests**: run `make test-python-integration-local`; add a universal test case in `sdk/python/tests/integration/` if the change affects retrieval or materialization behavior +- **SQL registry binary columns**: in `infra/registry/sql.py`, a new column that stores a serialized proto or blob metadata must use `ProtoBytes`, not `LargeBinary` directly — `LargeBinary` maps to MySQL `BLOB` (64 KB cap) and silently truncates large protos +- **Protos**: if you add a field to a proto message, recompile with `make protos` and update serialization helpers in `proto_registry_utils.py` +- **Both SDKs**: if the change affects online serving, check whether the Go server (`go/`) also needs updating +- **Skills/Rules**: if the change introduces new patterns, interfaces, or conventions that agents should follow, update the relevant section in `skills/feast-architecture/SKILL.md` (and `skills/feast-testing/SKILL.md` if testing patterns changed) + +## Documentation — where to add/update + +| Change type | Doc location | Also update | +|---|---|---| +| New **online store** | `docs/reference/online-stores/.md` (copy an existing one as template) | `docs/reference/online-stores/README.md`, `docs/SUMMARY.md` (under "Online stores") | +| New **offline store** | `docs/reference/offline-stores/.md` | `docs/reference/offline-stores/README.md`, `docs/reference/offline-stores/overview.md`, `docs/SUMMARY.md` | +| New **registry backend** | `docs/reference/registries/.md` | `docs/SUMMARY.md` | +| Config option change | `docs/reference/feature-store-yaml.md` | — | +| New CLI flag or command | `docs/reference/feast-cli-commands.md` | — | +| How-to / integration guide | `docs/how-to-guides/customizing-feast/` or `docs/how-to-guides/` | `docs/SUMMARY.md` | +| Architecture / concept | `docs/getting-started/architecture/` or `docs/getting-started/components/` | `docs/SUMMARY.md` | +| Blog post | `/infra/website/docs/blog/` (NOT `docs/blog/`) | — | + +All `docs/` pages are rendered by GitBook via `docs/SUMMARY.md`. Any new page must be added to `SUMMARY.md` or it won't appear in the site navigation. diff --git a/.claude/rules/feast-skills-maintenance.md b/.claude/rules/feast-skills-maintenance.md new file mode 100644 index 00000000000..8be40b3563b --- /dev/null +++ b/.claude/rules/feast-skills-maintenance.md @@ -0,0 +1,28 @@ +--- +paths: + - skills/** + - AGENTS.md + - .cursor/rules/** + - .claude/rules/** +--- + +## When editing skills or rules + +Skills and rules are only useful if they accurately reflect the real codebase. Before finalising any skill/rule edit: + +**Verify against source code:** +- Command examples (lint, test, type-check) — confirm they still match `Makefile` targets and `pyproject.toml` +- File paths and class names — confirm they exist in the repo +- Interface signatures (e.g. `OnlineStore`, `OfflineStore`, `BaseRegistry`) — confirm against the actual base class files +- Config field names — confirm against `RepoConfig` and `FeastConfigBaseModel` subclasses in `sdk/python/feast/repo_config.py` + +**Keep scope consistent:** +- `AGENTS.md` — entry point only; commands, skills table, code style. Max ~120 lines. +- `skills/feast-architecture/SKILL.md` — how each component works internally; data flows; adding new backends +- `skills/feast-testing/SKILL.md` — how to run, write, and debug tests +- `skills/feast-dev/SKILL.md` — contributor workflow; setup; Docker; docs locations; PR process +- `skills/feast-user-guide/SKILL.md` — how to use Feast as an end user; feature definitions; retrieval; RAG +- `.cursor/rules/feast-components.mdc` / `.claude/rules/feast-components.md` — component checklist (tests, docs, skills); keep in sync with each other + +**Keep the two rule files in sync:** +`.cursor/rules/feast-components.mdc` and `.claude/rules/feast-components.md` contain the same content with only different frontmatter (`globs:` vs `paths:`). Any content change must be applied to both. diff --git a/.claude/skills/feast-architecture/SKILL.md b/.claude/skills/feast-architecture/SKILL.md new file mode 120000 index 00000000000..2075b8d0450 --- /dev/null +++ b/.claude/skills/feast-architecture/SKILL.md @@ -0,0 +1 @@ +../../../skills/feast-architecture/SKILL.md \ No newline at end of file diff --git a/.claude/skills/feast-dev/SKILL.md b/.claude/skills/feast-dev/SKILL.md new file mode 120000 index 00000000000..ba44334bd0b --- /dev/null +++ b/.claude/skills/feast-dev/SKILL.md @@ -0,0 +1 @@ +../../../skills/feast-dev/SKILL.md \ No newline at end of file diff --git a/.claude/skills/feast-testing/SKILL.md b/.claude/skills/feast-testing/SKILL.md new file mode 120000 index 00000000000..9af0ba0de43 --- /dev/null +++ b/.claude/skills/feast-testing/SKILL.md @@ -0,0 +1 @@ +../../../skills/feast-testing/SKILL.md \ No newline at end of file diff --git a/.claude/skills/feast-user-guide/SKILL.md b/.claude/skills/feast-user-guide/SKILL.md new file mode 120000 index 00000000000..9fd50297875 --- /dev/null +++ b/.claude/skills/feast-user-guide/SKILL.md @@ -0,0 +1 @@ +../../../skills/feast-user-guide/SKILL.md \ No newline at end of file diff --git a/.codecov.yaml b/.codecov.yaml new file mode 100644 index 00000000000..2fa599642ec --- /dev/null +++ b/.codecov.yaml @@ -0,0 +1,48 @@ +codecov: + require_ci_to_pass: true + +coverage: + precision: 2 + round: down + range: "50...70" + + status: + project: + default: + informational: true + target: auto + threshold: 1% + patch: + default: + informational: true + target: 70% + +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: false + require_base: false + require_head: true + show_carryforward_flags: true + +flags: + python-unit: + paths: + - sdk/python/feast/ + carryforward: true + go-feature-server: + paths: + - go/ + carryforward: true + +ignore: + - "sdk/python/tests/**" + - "**/*_pb2.py" + - "**/*_pb2_grpc.py" + - "sdk/python/feast/protos/**" + - "sdk/python/feast/embedded_go/**" + - "protos/**" + - "docs/**" + - "ui/**" + - "java/**" + - "infra/feast-operator/test/**" diff --git a/.cursor/rules/feast-components.mdc b/.cursor/rules/feast-components.mdc new file mode 100644 index 00000000000..f015619020d --- /dev/null +++ b/.cursor/rules/feast-components.mdc @@ -0,0 +1,41 @@ +--- +description: Component-level guidance for Feast subsystems +globs: sdk/python/feast/infra/online_stores/**,sdk/python/feast/infra/offline_stores/**,sdk/python/feast/infra/registry/**,sdk/python/tests/unit/infra/**,go/**,infra/feast-operator/** +alwaysApply: false +--- + +Read `skills/feast-architecture/SKILL.md` for the relevant component section: + +| Working in… | Read section | +|---|---| +| `online_stores/` | Online Store — config pattern, entity key serialization, async support, registration | +| `offline_stores/` | Offline Store — interface, PIT join, pull_latest, adding a new backend | +| `infra/registry/` | Registry — proto vs SQL backend, caching, adding new object types | +| `go/` | Go Feature Server — entry point, serving path, online store interface, build commands | +| `infra/feast-operator/` | Feast Operator — CRD spec, reconcile loop, RBAC markers, dev workflow | + +For testing patterns and debugging, also read `skills/feast-testing/SKILL.md`. + +## When making any component change + +- **Unit tests**: add or update tests in `sdk/python/tests/unit/infra//` +- **Integration tests**: run `make test-python-integration-local`; add a universal test case in `sdk/python/tests/integration/` if the change affects retrieval or materialization behavior +- **SQL registry binary columns**: in `infra/registry/sql.py`, a new column that stores a serialized proto or blob metadata must use `ProtoBytes`, not `LargeBinary` directly — `LargeBinary` maps to MySQL `BLOB` (64 KB cap) and silently truncates large protos +- **Protos**: if you add a field to a proto message, recompile with `make protos` and update serialization helpers in `proto_registry_utils.py` +- **Both SDKs**: if the change affects online serving, check whether the Go server (`go/`) also needs updating +- **Skills/Rules**: if the change introduces new patterns, interfaces, or conventions that agents should follow, update the relevant section in `skills/feast-architecture/SKILL.md` (and `skills/feast-testing/SKILL.md` if testing patterns changed) + +## Documentation — where to add/update + +| Change type | Doc location | Also update | +|---|---|---| +| New **online store** | `docs/reference/online-stores/.md` (copy an existing one as template) | `docs/reference/online-stores/README.md`, `docs/SUMMARY.md` (under "Online stores") | +| New **offline store** | `docs/reference/offline-stores/.md` | `docs/reference/offline-stores/README.md`, `docs/reference/offline-stores/overview.md`, `docs/SUMMARY.md` | +| New **registry backend** | `docs/reference/registries/.md` | `docs/SUMMARY.md` | +| Config option change | `docs/reference/feature-store-yaml.md` | — | +| New CLI flag or command | `docs/reference/feast-cli-commands.md` | — | +| How-to / integration guide | `docs/how-to-guides/customizing-feast/` or `docs/how-to-guides/` | `docs/SUMMARY.md` | +| Architecture / concept | `docs/getting-started/architecture/` or `docs/getting-started/components/` | `docs/SUMMARY.md` | +| Blog post | `/infra/website/docs/blog/` (NOT `docs/blog/`) | — | + +All `docs/` pages are rendered by GitBook via `docs/SUMMARY.md`. Any new page must be added to `SUMMARY.md` or it won't appear in the site navigation. diff --git a/.cursor/rules/feast-skills-maintenance.mdc b/.cursor/rules/feast-skills-maintenance.mdc new file mode 100644 index 00000000000..588cce66b6d --- /dev/null +++ b/.cursor/rules/feast-skills-maintenance.mdc @@ -0,0 +1,26 @@ +--- +description: Guidance for keeping skills and rules accurate and up to date +globs: skills/**,AGENTS.md,.cursor/rules/**,.claude/rules/** +alwaysApply: false +--- + +## When editing skills or rules + +Skills and rules are only useful if they accurately reflect the real codebase. Before finalising any skill/rule edit: + +**Verify against source code:** +- Command examples (lint, test, type-check) — confirm they still match `Makefile` targets and `pyproject.toml` +- File paths and class names — confirm they exist in the repo +- Interface signatures (e.g. `OnlineStore`, `OfflineStore`, `BaseRegistry`) — confirm against the actual base class files +- Config field names — confirm against `RepoConfig` and `FeastConfigBaseModel` subclasses in `sdk/python/feast/repo_config.py` + +**Keep scope consistent:** +- `AGENTS.md` — entry point only; commands, skills table, code style. Max ~120 lines. +- `skills/feast-architecture/SKILL.md` — how each component works internally; data flows; adding new backends +- `skills/feast-testing/SKILL.md` — how to run, write, and debug tests +- `skills/feast-dev/SKILL.md` — contributor workflow; setup; Docker; docs locations; PR process +- `skills/feast-user-guide/SKILL.md` — how to use Feast as an end user; feature definitions; retrieval; RAG +- `.cursor/rules/feast-components.mdc` / `.claude/rules/feast-components.md` — component checklist (tests, docs, skills); keep in sync with each other + +**Keep the two rule files in sync:** +`.cursor/rules/feast-components.mdc` and `.claude/rules/feast-components.md` contain the same content with only different frontmatter (`globs:` vs `paths:`). Any content change must be applied to both. diff --git a/.cursor/rules/feast-ui.mdc b/.cursor/rules/feast-ui.mdc new file mode 100644 index 00000000000..9072cbe335f --- /dev/null +++ b/.cursor/rules/feast-ui.mdc @@ -0,0 +1,19 @@ +--- +description: Formatting and lint rules for the Feast UI (React/TypeScript) +globs: ui/src/** +alwaysApply: false +--- + +## After editing any file under `ui/src/` + +1. **Run Prettier** before considering the task complete: + ```bash + cd ui && yarn prettier --write + ``` +2. **Verify** formatting passes: + ```bash + cd ui && yarn format:check + ``` + CI runs `yarn format:check` and will reject PRs with style violations. + +3. Prettier config lives in `ui/package.json` (no separate `.prettierrc`). Do not override it. diff --git a/.gitbook.yaml b/.gitbook.yaml index bbdd0c57e3b..8441cf23dd7 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -6,3 +6,6 @@ structure: redirects: reference/telemetry: ./reference/usage.md quickstart: ./getting-started/quickstart.md + reference/feature-store-yaml: ./reference/feature-repository/feature-store-yaml.md + reference/feast-ignore: ./reference/feature-repository/feast-ignore.md + reference/feature-repository: ./reference/feature-repository/README.md diff --git a/.github/actions/get-semantic-release-version/action.yml b/.github/actions/get-semantic-release-version/action.yml index 89f6a8f81c1..a53bc337b44 100644 --- a/.github/actions/get-semantic-release-version/action.yml +++ b/.github/actions/get-semantic-release-version/action.yml @@ -2,7 +2,7 @@ name: Get semantic release version description: "" inputs: custom_version: # Optional input for a custom version - description: "Custom version to publish (e.g., v1.2.3) -- only edit if you know what you are doing" + description: "Custom version to publish (e.g., v1.2.3 or v1.2.3.dev4) -- only edit if you know what you are doing" required: false token: description: "Personal Access Token" @@ -10,10 +10,10 @@ inputs: default: "" outputs: release_version: - description: "The release version to use (e.g., v1.2.3)" + description: "The release version to use (e.g., v1.2.3 or v1.2.3.dev4)" value: ${{ steps.get_release_version.outputs.release_version }} version_without_prefix: - description: "The release version to use without 'v' (e.g., 1.2.3)" + description: "The release version to use without 'v' (e.g., 1.2.3 or 1.2.3.dev4)" value: ${{ steps.get_release_version_without_prefix.outputs.version_without_prefix }} highest_semver_tag: description: "The highest semantic version tag without the 'v' prefix (e.g., 1.2.3)" @@ -32,10 +32,10 @@ runs: GIT_COMMITTER_EMAIL: feast-ci-bot@willem.co run: | if [[ -n "${{ inputs.custom_version }}" ]]; then - VERSION_REGEX="^v[0-9]+\.[0-9]+\.[0-9]+$" + VERSION_REGEX="^v[0-9]+\.[0-9]+\.[0-9]+(\.dev[0-9]+)?$" echo "Using custom version: ${{ inputs.custom_version }}" if [[ ! "${{ inputs.custom_version }}" =~ $VERSION_REGEX ]]; then - echo "Error: custom_version must match semantic versioning (e.g., v1.2.3)." + echo "Error: custom_version must match semantic versioning (e.g., v1.2.3 or v1.2.3.dev4)." exit 1 fi echo "::set-output name=release_version::${{ inputs.custom_version }}" @@ -84,4 +84,4 @@ runs: run: | echo $RELEASE_VERSION echo $VERSION_WITHOUT_PREFIX - echo $HIGHEST_SEMVER_TAG \ No newline at end of file + echo $HIGHEST_SEMVER_TAG diff --git a/.github/fork_workflows/fork_pr_integration_tests_aws.yml b/.github/fork_workflows/fork_pr_integration_tests_aws.yml index d8a547f5359..6722e225b91 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_aws.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_aws.yml @@ -40,11 +40,11 @@ jobs: architecture: x64 - name: Setup Go id: setup-go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.18.0 - name: Set up AWS SDK - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/fork_workflows/fork_pr_integration_tests_gcp.yml b/.github/fork_workflows/fork_pr_integration_tests_gcp.yml index 563111727b9..c6810e1dac2 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_gcp.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_gcp.yml @@ -40,15 +40,15 @@ jobs: architecture: x64 - name: Setup Go id: setup-go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.18.0 - name: Authenticate to Google Cloud - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' with: credentials_json: '${{ secrets.GCP_SA_KEY }}' - name: Set up gcloud SDK - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 with: project_id: ${{ secrets.GCP_PROJECT_ID }} - name: Use gcloud CLI diff --git a/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml b/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml index 1983189b4ee..0db580ce7db 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml @@ -40,7 +40,7 @@ jobs: architecture: x64 - name: Setup Go id: setup-go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.18.0 - name: Install the latest version of uv @@ -72,7 +72,7 @@ jobs: SNOWFLAKE_CI_WAREHOUSE: ${{ secrets.SNOWFLAKE_CI_WAREHOUSE }} # Run only Snowflake BigQuery and File tests without dynamo and redshift tests. run: | - pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "Snowflake and not dynamo and not Redshift and not Bigquery and not gcp and not minio_registry" + pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "(Snowflake or snowflake_registry) and not dynamo and not Redshift and not Bigquery and not gcp and not minio_registry" pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "File and not dynamo and not Redshift and not Bigquery and not gcp and not minio_registry" - name: Minimize uv cache run: uv cache prune --ci diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 40986a87db9..48bb592ed84 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,7 +11,7 @@ # What this PR does / why we need it: # Which issue(s) this PR fixes: @@ -20,6 +20,16 @@ Outline what you're doing Usage: `Fixes #`, or `Fixes (paste link of issue)`. --> +# Checks +- [ ] I've made sure the tests are passing. +- [ ] My commits are signed off (`git commit -s`) +- [ ] My PR title follows [conventional commits](https://www.conventionalcommits.org/) format + +## Testing Strategy +- [ ] Unit tests +- [ ] Integration tests +- [ ] Manual tests +- [ ] Testing is not required for this change # Misc |manages| Registry + Operator -->|manages| OnlineServer + OnlineServer -->|reads/writes| Redis + OnlineServer -->|reads metadata| Registry + MaterializationJob -->|reads source data| OfflineStore + MaterializationJob -->|writes features| Redis + NotebookPod -->|online features REST| OnlineServer + NotebookPod -->|metadata gRPC| Registry + + Client(["Client / ML Service"]) -->|REST port 6566| OnlineServer + + style Kubernetes fill:#f0f4ff,stroke:#3366cc,color:#000 + style BackingStores fill:#fafafa,stroke:#999,color:#000 + style Operator fill:#e8f5e9,stroke:#388e3c,color:#000 + style Registry fill:#fff3e0,stroke:#f57c00,color:#000 + style OnlineServer fill:#e3f2fd,stroke:#1976d2,color:#000 + style Redis fill:#fce4ec,stroke:#c62828,color:#000 + style OfflineStore fill:#f3e5f5,stroke:#7b1fa2,color:#000 + style MaterializationJob fill:#fff8e1,stroke:#f9a825,color:#000 + style NotebookPod fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +### Components + +| Component | Configuration | Notes | +|---|---|---| +| **Feast Operator** | Default install | Manages all Feast CRDs | +| **Registry** | REST, 1 replica | Single point of metadata | +| **Online Feature Server** | 1 replica, no autoscaling | Serves online features | +| **Online Store** | Redis standalone (example) | SQLite is simplest for development; Redis for production. See [supported online stores](../reference/online-stores/README.md) for all options | +| **Offline Store** | File-based or MinIO | DuckDB or file-based for development; MinIO/S3 for production. See [supported offline stores](../reference/offline-stores/README.md) for all options | +| **Compute Engine** | In-process (default) | Suitable for small datasets and development; use Spark, Ray, or Snowflake Engine for larger workloads | + +### Sample FeatureStore CR + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: minimal-production +spec: + feastProject: my_project + services: + onlineStore: + persistence: + store: + type: redis + secretRef: + name: feast-online-store + server: + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi + offlineStore: + persistence: + file: + type: duckdb # Use type: file for generic file-based; swap for S3/MinIO in production + pvc: + create: + storageClassName: standard + resources: + requests: + storage: 10Gi + mountPath: /data/offline + registry: + local: + server: + restAPI: true + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi +``` + +### Limitations + +{% hint style="warning" %} +* **No high availability** — a single replica failure causes downtime +* **No automatic failover** — manual intervention required on failure +* **Manual scaling** — no HPA configured +* **Limited security** — no TLS, no ingress, no RBAC by default +{% endhint %} + +--- + +## 2. Standard Production (Recommended) + +### When to use + +* Most production ML workloads +* Teams with moderate traffic that need reliability +* Environments that require TLS, RBAC, and automated scaling + +### Architecture + +```mermaid +graph TD + Client(["Client / ML Service"]) -->|HTTPS| Ingress + + subgraph Kubernetes["Kubernetes"] + Ingress["Ingress Controller
(TLS termination)"] + Operator["Feast Operator"] + MaterializationJob["Materialization Job
(CronJob / batchEngine)"] + NotebookPod(["Notebook / Training Pod
(Feast SDK — remote online, offline, registry)"]) + + subgraph FeastDeployment["Feast Deployment (HPA autoscaled — all containers scale together)"] + Registry["Registry Server"] + OnlineServer["Online Feature Server"] + OfflineServer["Offline Feature Server"] + end + + Ingress -->|port 6566| OnlineServer + Operator -->|manages| Registry + Operator -->|manages| OnlineServer + Operator -->|manages| OfflineServer + OnlineServer -->|reads metadata| Registry + OfflineServer -->|reads metadata| Registry + MaterializationJob -->|reads metadata| Registry + NotebookPod -->|online features REST| OnlineServer + NotebookPod -->|historical features Arrow Flight| OfflineServer + NotebookPod -->|metadata gRPC| Registry + end + + subgraph BackingStores["Backing Stores (inside or outside Kubernetes)"] + RedisCluster["Online Store
(e.g. Redis Cluster, DynamoDB, etc.)"] + OfflineStore["Offline Store
(e.g. PostgreSQL, Redshift, BigQuery, etc.)"] + end + + OnlineServer -->|reads/writes| RedisCluster + MaterializationJob -->|writes features| RedisCluster + OfflineServer -->|historical features| OfflineStore + MaterializationJob -->|reads source data| OfflineStore + + style Kubernetes fill:#f0f4ff,stroke:#3366cc,color:#000 + style FeastDeployment fill:#e8f5e9,stroke:#388e3c,color:#000 + style BackingStores fill:#fafafa,stroke:#999,color:#000 + style Ingress fill:#fff9c4,stroke:#f9a825,color:#000 + style Operator fill:#e8f5e9,stroke:#388e3c,color:#000 + style Registry fill:#fff3e0,stroke:#f57c00,color:#000 + style OnlineServer fill:#e3f2fd,stroke:#1976d2,color:#000 + style OfflineServer fill:#ede7f6,stroke:#512da8,color:#000 + style RedisCluster fill:#fce4ec,stroke:#c62828,color:#000 + style OfflineStore fill:#f3e5f5,stroke:#7b1fa2,color:#000 + style MaterializationJob fill:#fff8e1,stroke:#f9a825,color:#000 + style NotebookPod fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +### Components + +**Core** + +| Component | Configuration | Notes | +|---|---|---| +| **Feast Operator** | Default install | Manages all Feast CRDs | +| **Registry** | SQL-backed (PostgreSQL) | Database-backed for consistency and concurrent access | +| **Online Feature Server** | HPA (min 2 replicas, max based on peak load) | Separate container — serves online features from the online store | +| **Offline Feature Server** | Scales with the same Deployment | Separate container — serves historical features and materialization source reads from the offline store | + +**Storage** + +| Component | Configuration | Notes | +|---|---|---| +| **Online Store** | Redis Cluster (example) | Multi-node for availability and low latency; other production stores are also supported — see [supported online stores](../reference/online-stores/README.md) | +| **Offline Store** | PostgreSQL (example) | Platform-agnostic DB-backed store; use Redshift/Athena for AWS, BigQuery for GCP, Spark for S3/MinIO pipelines — see [supported offline stores](../reference/offline-stores/README.md) for all options | +| **Compute Engine** | Spark, Ray (KubeRay), or Snowflake Engine | Distributed compute for materialization and historical retrieval at scale | + +**Networking & Security** + +| Component | Configuration | Notes | +|---|---|---| +| **Ingress** | TLS-terminated | Secure external access | +| **RBAC** | Kubernetes RBAC | Namespace-scoped permissions | +| **Secrets** | Kubernetes Secrets + `${ENV_VAR}` substitution | Store credentials via `secretRef` / `envFrom` in the FeatureStore CR; inject into `feature_store.yaml` with [environment variable syntax](./running-feast-in-production.md#5-using-environment-variables-in-your-yaml-configuration) | + +### Sample FeatureStore CR + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: standard-production +spec: + feastProject: my_project + authz: + kubernetes: + roles: + - feast-admin-role + - feast-user-role + batchEngine: + configMapRef: + name: feast-batch-engine + services: + scaling: + autoscaling: + minReplicas: 2 + maxReplicas: 10 # Set based on your peak load + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + podDisruptionBudgets: + maxUnavailable: 1 + onlineStore: + persistence: + store: + type: redis + secretRef: + name: feast-online-store + server: + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + offlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-offline-store + server: + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-registry-store + server: + restAPI: true + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: feast-batch-engine +data: + config: | + type: ray + address: auto # KubeRay cluster address; replace with explicit URL if not using auto-discovery +``` + +{% hint style="success" %} +**Key features:** + +* **High availability** — multi-replica deployment with auto-injected pod anti-affinity and topology spread constraints +* **Scalable serving** — HPA adjusts the shared deployment replicas (all services scale together) based on demand +* **Secure external access** — TLS-terminated ingress with RBAC +* **Persistent storage** — Online Store (Redis Cluster shown as example; see [supported online stores](../reference/online-stores/README.md) for all options) + Offline Store (PostgreSQL shown as example; see [supported offline stores](../reference/offline-stores/README.md) for all options) for durability + +See [Horizontal Scaling with the Feast Operator](./scaling-feast.md#horizontal-scaling-with-the-feast-operator) for full scaling configuration details. +{% endhint %} + +--- + +## 3. Enterprise Production + +### When to use + +* Large organizations with multiple ML teams +* Multi-tenant environments requiring strict isolation +* High-scale deployments with governance, compliance, and SLA requirements + +### Architecture — Isolated Registries (per namespace) + +Each team gets its own registry and online feature server in a dedicated namespace. This provides the strongest isolation but has notable trade-offs: feature discovery is siloed per team (no cross-project visibility), and each registry requires its own [Feast UI](../reference/alpha-web-ui.md) deployment — you cannot view multiple projects in a single UI instance. + +```mermaid +graph TD + Client(["Clients / ML Services"]) -->|HTTPS| Gateway + + subgraph Kubernetes["Kubernetes"] + Gateway["API Gateway / Ingress
(TLS + rate limiting)"] + Operator["Feast Operator
(cluster-scoped)"] + + subgraph NamespaceA["Namespace A — Team A"] + subgraph DeployA["Feast Deployment (HPA autoscaled)"] + RegistryA["Registry Server"] + OnlineServerA["Online Feature Server"] + OfflineServerA["Offline Feature Server"] + end + NotebookPodA(["Notebook — Team A
(Feast SDK)"]) + MaterializationJobA["Materialization Job"] + end + + subgraph NamespaceB["Namespace B — Team B"] + subgraph DeployB["Feast Deployment (HPA autoscaled)"] + RegistryB["Registry Server"] + OnlineServerB["Online Feature Server"] + OfflineServerB["Offline Feature Server"] + end + NotebookPodB(["Notebook — Team B
(Feast SDK)"]) + MaterializationJobB["Materialization Job"] + end + + Gateway -->|port 6566| OnlineServerA + Gateway -->|port 6566| OnlineServerB + Operator -->|manages| RegistryA + Operator -->|manages| OnlineServerA + Operator -->|manages| OfflineServerA + Operator -->|manages| RegistryB + Operator -->|manages| OnlineServerB + Operator -->|manages| OfflineServerB + OnlineServerA -->|metadata| RegistryA + OnlineServerB -->|metadata| RegistryB + OfflineServerA -->|metadata| RegistryA + OfflineServerB -->|metadata| RegistryB + NotebookPodA -.->|online REST| OnlineServerA + NotebookPodA -.->|Arrow Flight| OfflineServerA + NotebookPodA -.->|metadata| RegistryA + NotebookPodB -.->|online REST| OnlineServerB + NotebookPodB -.->|Arrow Flight| OfflineServerB + NotebookPodB -.->|metadata| RegistryB + MaterializationJobA -->|metadata| RegistryA + MaterializationJobB -->|metadata| RegistryB + end + + subgraph BackingStores["Backing Stores (inside or outside Kubernetes)"] + RedisA["Online Store — Team A
(e.g. Redis, DynamoDB, etc.)"] + RedisB["Online Store — Team B
(e.g. Redis, DynamoDB, etc.)"] + OfflineStore["Offline Store — shared instance
(e.g. BigQuery, Redshift, etc.)
isolated via per-team datasets / schemas"] + end + + subgraph Observability["Observability Stack"] + OTel["OpenTelemetry
(traces + metrics)"] + Prometheus["Prometheus"] + Grafana["Grafana"] + Jaeger["Jaeger"] + end + + OnlineServerA -->|reads/writes| RedisA + OnlineServerB -->|reads/writes| RedisB + OfflineServerA -->|Team A dataset| OfflineStore + OfflineServerB -->|Team B dataset| OfflineStore + MaterializationJobA -->|Team A data| OfflineStore + MaterializationJobA -->|writes| RedisA + MaterializationJobB -->|Team B data| OfflineStore + MaterializationJobB -->|writes| RedisB + + style Kubernetes fill:#f0f4ff,stroke:#3366cc,color:#000 + style NamespaceA fill:#e8f5e9,stroke:#388e3c,color:#000 + style NamespaceB fill:#e3f2fd,stroke:#1976d2,color:#000 + style DeployA fill:#c8e6c9,stroke:#2e7d32,color:#000 + style DeployB fill:#bbdefb,stroke:#1565c0,color:#000 + style BackingStores fill:#fafafa,stroke:#999,color:#000 + style Observability fill:#fff3e0,stroke:#f57c00,color:#000 + style Gateway fill:#fff9c4,stroke:#f9a825,color:#000 + style Operator fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +### Architecture — Shared Registry (cross-namespace) + +Alternatively, a single centralized registry server can serve multiple tenant namespaces. Tenant online feature servers connect to the shared registry via the [Remote Registry](../reference/registries/remote.md) gRPC client. This reduces operational overhead, enables cross-team feature discovery, and allows a single [Feast UI](../reference/alpha-web-ui.md) deployment to browse all projects — while Feast [permissions](#feast-permissions-and-rbac) enforce tenant isolation at the data level. + +```mermaid +graph TD + Client(["Clients / ML Services"]) -->|HTTPS| Gateway + + subgraph Kubernetes["Kubernetes"] + Gateway["API Gateway / Ingress
(TLS + rate limiting)"] + + subgraph SharedInfra["Shared Infrastructure Namespace"] + Operator["Feast Operator
(cluster-scoped)"] + subgraph SharedDeploy["Feast Deployment (3 replicas)"] + SharedRegistry["Registry Server
(gRPC + REST)"] + end + SharedDB[("SQL Database
(PostgreSQL)")] + SharedRegistry -->|persists| SharedDB + end + + subgraph NamespaceA["Namespace A — Team A"] + subgraph DeployA2["Feast Deployment (HPA autoscaled)"] + OnlineServerA["Online Feature Server"] + OfflineServerA["Offline Feature Server"] + end + NotebookPodA(["Notebook — Team A
(Feast SDK)"]) + MaterializationJobA["Materialization Job"] + ConfigA["registry_type: remote
path: shared-registry:6570"] + end + + subgraph NamespaceB["Namespace B — Team B"] + subgraph DeployB2["Feast Deployment (HPA autoscaled)"] + OnlineServerB["Online Feature Server"] + OfflineServerB["Offline Feature Server"] + end + NotebookPodB(["Notebook — Team B
(Feast SDK)"]) + MaterializationJobB["Materialization Job"] + ConfigB["registry_type: remote
path: shared-registry:6570"] + end + + Gateway -->|port 6566| OnlineServerA + Gateway -->|port 6566| OnlineServerB + OnlineServerA -->|gRPC port 6570| SharedRegistry + OnlineServerB -->|gRPC port 6570| SharedRegistry + OfflineServerA -->|gRPC port 6570| SharedRegistry + OfflineServerB -->|gRPC port 6570| SharedRegistry + Operator -->|manages| SharedRegistry + Operator -->|manages| OnlineServerA + Operator -->|manages| OfflineServerA + Operator -->|manages| OnlineServerB + Operator -->|manages| OfflineServerB + NotebookPodA -.->|online REST| OnlineServerA + NotebookPodA -.->|Arrow Flight| OfflineServerA + NotebookPodA -.->|metadata| SharedRegistry + NotebookPodB -.->|online REST| OnlineServerB + NotebookPodB -.->|Arrow Flight| OfflineServerB + NotebookPodB -.->|metadata| SharedRegistry + MaterializationJobA -->|gRPC port 6570| SharedRegistry + MaterializationJobB -->|gRPC port 6570| SharedRegistry + end + + subgraph BackingStores["Backing Stores (inside or outside Kubernetes)"] + RedisA["Online Store — Team A
(e.g. Redis, DynamoDB, etc.)"] + RedisB["Online Store — Team B
(e.g. Redis, DynamoDB, etc.)"] + OfflineStore["Offline Store — shared instance
(e.g. BigQuery, Redshift, etc.)
isolated via per-team datasets / schemas"] + end + + OnlineServerA -->|reads/writes| RedisA + OnlineServerB -->|reads/writes| RedisB + OfflineServerA -->|Team A dataset| OfflineStore + OfflineServerB -->|Team B dataset| OfflineStore + MaterializationJobA -->|Team A data| OfflineStore + MaterializationJobA -->|writes| RedisA + MaterializationJobB -->|Team B data| OfflineStore + MaterializationJobB -->|writes| RedisB + + style Kubernetes fill:#f0f4ff,stroke:#3366cc,color:#000 + style SharedInfra fill:#fff3e0,stroke:#f57c00,color:#000 + style SharedDeploy fill:#ffe0b2,stroke:#e65100,color:#000 + style NamespaceA fill:#e8f5e9,stroke:#388e3c,color:#000 + style NamespaceB fill:#e3f2fd,stroke:#1976d2,color:#000 + style DeployA2 fill:#c8e6c9,stroke:#2e7d32,color:#000 + style DeployB2 fill:#bbdefb,stroke:#1565c0,color:#000 + style BackingStores fill:#fafafa,stroke:#999,color:#000 + style Gateway fill:#fff9c4,stroke:#f9a825,color:#000 + style Operator fill:#e8f5e9,stroke:#388e3c,color:#000 + style OfflineStore fill:#f3e5f5,stroke:#7b1fa2,color:#000 +``` + +**Shared registry client configuration** — each tenant's `feature_store.yaml` points to the centralized registry: + +```yaml +registry: + registry_type: remote + path: shared-registry.feast-system.svc.cluster.local:6570 +``` + +{% hint style="info" %} +**Shared vs isolated registries:** + +| | Shared Registry | Isolated Registries | +|---|---|---| +| **Feature discovery** | Cross-team — all projects visible | Siloed — each team sees only its own | +| **Feast UI** | Single deployment serves all projects | Separate UI deployment per registry | +| **Isolation** | Logical (Feast permissions + tags) | Physical (separate metadata stores) | +| **Operational cost** | Lower — one registry to manage | Higher — N registries to maintain | +| **Best for** | Feature reuse, shared ML platform | Regulatory/compliance separation | + +Use a shared registry when teams need to discover and reuse features across projects, and rely on Feast permissions for access control. Use isolated registries when regulatory or compliance requirements demand physical separation of metadata. +{% endhint %} + +### Components + +**Multi-tenancy** + +| Aspect | Configuration | Notes | +|---|---|---| +| **Isolation model** | Namespace-per-team | Physical isolation via Kubernetes namespaces | +| **Registry strategy** | Shared (remote) or isolated (per-namespace) | See architecture variants above | +| **Network boundaries** | NetworkPolicy enforced | Cross-namespace traffic denied by default (allow-listed for shared registry) | + +**Storage** + +| Component | Configuration | Notes | +|---|---|---| +| **Online Store** | Managed Redis / DynamoDB / Elasticsearch | Cloud-managed, per-tenant instances; see [supported online stores](../reference/online-stores/README.md) for all options | +| **Offline Store** | External data warehouse (Snowflake, BigQuery) | Shared or per-tenant access controls; see [supported offline stores](../reference/offline-stores/README.md) for all options | + +**Scaling** + +| Component | Configuration | Notes | +|---|---|---| +| **FeatureStore Deployment** | HPA + Cluster Autoscaler | All services (Online Feature Server, Registry, Offline Feature Server) scale together per tenant; set `maxReplicas` based on your peak load. Independent scaling across tenants. | +| **Cluster** | Multi-zone node pools | Zone-aware scheduling with auto-injected topology spread constraints | + +**Security** + +| Component | Configuration | Notes | +|---|---|---| +| **Authentication** | OIDC via Keycloak | Centralized identity provider | +| **Authorization** | Feast permissions + Kubernetes RBAC | See [Permissions and RBAC](#feast-permissions-and-rbac) below | +| **Network** | NetworkPolicies per namespace | Microsegmentation | +| **Secrets** | Kubernetes Secrets (`secretRef` / `envFrom`) | Credentials injected via FeatureStore CR; use Kubernetes-native tooling (e.g. External Secrets Operator) to sync from external vaults if needed | + +**Observability** + +| Component | Purpose | Notes | +|---|---|---| +| **[OpenTelemetry](../getting-started/components/open-telemetry.md)** | Traces + metrics export | Built-in Feast integration; emits spans for feature retrieval, materialization, and registry operations | +| **Prometheus** | Metrics collection | Collects OpenTelemetry metrics from Online Feature Server + Online Store | +| **Grafana** | Dashboards + traces | Per-tenant and aggregate views; can display OpenTelemetry traces via Tempo or Jaeger data source | +| **Jaeger** | Distributed tracing | Visualize OpenTelemetry traces for request latency analysis and debugging | + +**Reliability & Disaster Recovery** + +| Aspect | Configuration | Notes | +|---|---|---| +| **PodDisruptionBudgets** | Configured per deployment | Protects against voluntary disruptions | +| **Multi-zone** | Topology spread constraints | Auto-injected by operator when scaling; survives single zone failures | +| **Backup / Restore** | See recovery priority below | Strategy depends on component criticality | + +**Recovery priority guidance** + +Not all Feast components carry the same recovery urgency. The table below ranks components by restoration priority and provides guidance for **RPO** (Recovery Point Objective — maximum acceptable data loss) and **RTO** (Recovery Time Objective — maximum acceptable downtime). Specific targets depend on your backing store SLAs and organizational requirements. + +| Priority | Component | RPO guidance | RTO guidance | Rationale | +|---|---|---|---|---| +| 1 — Critical | **Registry DB** (PostgreSQL / MySQL) | Minutes (continuous replication or frequent backups) | Minutes (failover to standby) | Contains all feature definitions and metadata; without it, no service can resolve features | +| 2 — High | **Online Store** (Redis / DynamoDB) | Reconstructible via materialization | Minutes to hours (depends on data volume) | Can be fully rebuilt by re-running materialization from the offline store; no unique data to lose | +| 3 — Medium | **Offline Store** (Redshift / BigQuery) | Per data warehouse SLA | Per data warehouse SLA | Source of truth for historical data; typically managed by the cloud provider with built-in replication | +| 4 — Low | **Feast Operator + CRDs** | N/A (declarative, stored in Git) | Minutes (re-apply manifests) | Stateless; redeployable from version-controlled manifests | + +{% hint style="info" %} +**Key insight:** The online store is *reconstructible* — it can always be rebuilt from the offline store by re-running materialization. This means its RPO is effectively zero (no unique data to lose), but RTO depends on how long full materialization takes for your dataset volume. For large datasets, consider maintaining Redis persistence (RDB snapshots or AOF) to reduce recovery time. +{% endhint %} + +**Backup recommendations by topology** + +| Topology | Registry | Online Store | Offline Store | +|---|---|---|---| +| **Minimal** | Manual file backups; accept downtime on failure | Not backed up (re-materialize) | N/A (file-based) | +| **Standard** | Automated PostgreSQL backups (daily + WAL archiving) | Redis RDB snapshots or AOF persistence | Per cloud provider SLA | +| **Enterprise** | Managed DB replication (multi-AZ); cross-region replicas for DR | Managed Redis with automatic failover (ElastiCache Multi-AZ, Memorystore HA) | Managed warehouse replication (Redshift cross-region, BigQuery cross-region) | + +### Sample FeatureStore CR (per tenant) + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: team-a-production + namespace: team-a +spec: + feastProject: team_a + authz: + oidc: + secretRef: + name: feast-oidc-secret # Secret keys: client_id, client_secret, auth_discovery_url + batchEngine: + configMapRef: + name: feast-batch-engine + services: + scaling: + autoscaling: + minReplicas: 3 + maxReplicas: 20 # Set based on your peak load + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 65 + podDisruptionBudgets: + minAvailable: 2 + onlineStore: + persistence: + store: + type: redis + secretRef: + name: feast-online-store + server: + resources: + requests: + cpu: "2" + memory: 2Gi + limits: + cpu: "4" + memory: 4Gi + offlineStore: + persistence: + store: + type: bigquery # Use snowflake.offline, redshift, etc. as alternatives — see supported offline stores + secretRef: + name: feast-offline-store + server: + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-registry-store + server: + restAPI: true + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: feast-batch-engine + namespace: team-a +data: + config: | + type: spark + spark_master: k8s://https://kubernetes.default.svc:443 + spark_app_name: feast-materialization +``` + +--- + +## Feast Permissions and RBAC + +Feast provides a built-in permissions framework that secures resources at the application level, independently of Kubernetes RBAC. Permissions are defined as Python objects in your feature repository and registered via `feast apply`. + +For full details, see the [Permission concept](../getting-started/concepts/permission.md) and [RBAC architecture](../getting-started/architecture/rbac.md) docs. + +### How it works + +```mermaid +graph LR + subgraph Client["Client Request"] + Token["Auth Token
(OIDC / K8s SA)"] + end + + subgraph Server["Feast Server"] + Extractor["Token Extractor"] + Parser["Token Parser"] + Enforcer["Policy Enforcer"] + end + + subgraph Registry["Registry"] + Permissions["Permission Objects"] + end + + Token --> Extractor + Extractor --> Parser + Parser -->|user roles/groups/ns| Enforcer + Enforcer -->|match resource + action| Permissions + Enforcer -->|allow / deny| Response(["Response"]) + + style Client fill:#e3f2fd,stroke:#1976d2,color:#000 + style Server fill:#fff3e0,stroke:#f57c00,color:#000 + style Registry fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +Permission enforcement happens on the server side (Online Feature Server, Offline Feature Server, Registry Server). There is no enforcement when using the Feast SDK with a local provider. + +### Actions + +Feast defines eight granular actions: + +| Action | Description | +|---|---| +| `CREATE` | Create a new Feast object | +| `DESCRIBE` | Read object metadata/state | +| `UPDATE` | Modify an existing object | +| `DELETE` | Remove an object | +| `READ_ONLINE` | Read from the online store | +| `READ_OFFLINE` | Read from the offline store | +| `WRITE_ONLINE` | Write to the online store | +| `WRITE_OFFLINE` | Write to the offline store | + +Convenience aliases are provided: + +| Alias | Includes | +|---|---| +| `ALL_ACTIONS` | All eight actions | +| `READ` | `READ_ONLINE` + `READ_OFFLINE` | +| `WRITE` | `WRITE_ONLINE` + `WRITE_OFFLINE` | +| `CRUD` | `CREATE` + `DESCRIBE` + `UPDATE` + `DELETE` | + +### Protected resource types + +Permissions can be applied to any of these Feast object types: + +`Project`, `Entity`, `FeatureView`, `OnDemandFeatureView`, `BatchFeatureView`, `StreamFeatureView`, `FeatureService`, `DataSource`, `ValidationReference`, `SavedDataset`, `Permission` + +The constant `ALL_RESOURCE_TYPES` includes all of the above. `ALL_FEATURE_VIEW_TYPES` includes all feature view subtypes. + +### Policy types + +| Policy | Match criteria | Use case | +|---|---|---| +| `RoleBasedPolicy(roles=[...])` | User must have at least one of the listed roles | Kubernetes RBAC roles, OIDC roles | +| `GroupBasedPolicy(groups=[...])` | User must belong to at least one of the listed groups | LDAP/OIDC group membership | +| `NamespaceBasedPolicy(namespaces=[...])` | User's service account must be in one of the listed namespaces | Kubernetes namespace-level isolation | +| `CombinedGroupNamespacePolicy(groups=[...], namespaces=[...])` | User must match at least one group **or** one namespace | Flexible cross-cutting policies | +| `AllowAll` | Always grants access | Development / unsecured resources | + +### Example: Role-based permissions + +This is the most common pattern — separate admin and read-only roles: + +```python +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.permissions.action import READ, ALL_ACTIONS, AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy + +admin_perm = Permission( + name="feast_admin_permission", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=["feast-admin-role"]), + actions=ALL_ACTIONS, +) + +user_perm = Permission( + name="feast_user_permission", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=["feast-user-role"]), + actions=[AuthzedAction.DESCRIBE] + READ, +) +``` + +### Example: Namespace-based isolation for multi-tenant deployments + +Use `NamespaceBasedPolicy` to restrict access based on the Kubernetes namespace of the calling service account — ideal for the shared-registry enterprise topology. + +Each team gets two permissions: full access to its own resources (matched by `team` tag), and read-only access to resources any team has explicitly published as shared (matched by `visibility: shared` tag). The two `required_tags` target **different** resources — a feature view tagged `team: team-b, visibility: shared` matches only the second permission for Team A, enabling cross-team discovery without granting write access: + +```python +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.permissions.action import ALL_ACTIONS, READ, AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import NamespaceBasedPolicy + +# Team A: full access to its own resources +team_a_own = Permission( + name="team_a_full_access", + types=ALL_RESOURCE_TYPES, + required_tags={"team": "team-a"}, # matches only Team A's resources + policy=NamespaceBasedPolicy(namespaces=["team-a"]), + actions=ALL_ACTIONS, +) + +# Team A: read-only access to shared resources published by ANY team +# e.g. a Team B feature view tagged {team: team-b, visibility: shared} +# satisfies required_tags here but NOT team_a_own above +team_a_read_shared = Permission( + name="team_a_read_shared", + types=ALL_RESOURCE_TYPES, + required_tags={"visibility": "shared"}, # matches shared resources from any team + policy=NamespaceBasedPolicy(namespaces=["team-a"]), + actions=[AuthzedAction.DESCRIBE] + READ, +) + +# Team B: mirror of the above — full access to its own, read-only to shared +team_b_own = Permission( + name="team_b_full_access", + types=ALL_RESOURCE_TYPES, + required_tags={"team": "team-b"}, + policy=NamespaceBasedPolicy(namespaces=["team-b"]), + actions=ALL_ACTIONS, +) + +team_b_read_shared = Permission( + name="team_b_read_shared", + types=ALL_RESOURCE_TYPES, + required_tags={"visibility": "shared"}, + policy=NamespaceBasedPolicy(namespaces=["team-b"]), + actions=[AuthzedAction.DESCRIBE] + READ, +) +``` + +### Example: Combined group + namespace policy + +For organizations that use both OIDC groups and Kubernetes namespaces for identity — ideal when platform engineers lack a dedicated namespace but need cross-team visibility, or when OIDC group membership and namespace ownership should independently grant access: + +```python +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.permissions.action import ALL_ACTIONS, READ, AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import CombinedGroupNamespacePolicy + +# Platform engineers (OIDC group) OR any team namespace can read shared features. +# This covers platform engineers who have no dedicated K8s namespace of their own +# but need cross-team feature discovery. +platform_read_shared = Permission( + name="platform_read_shared", + types=ALL_RESOURCE_TYPES, + required_tags={"visibility": "shared"}, + policy=CombinedGroupNamespacePolicy( + groups=["ml-platform"], # OIDC group for platform/infra engineers + namespaces=["team-a", "team-b"], # team namespaces from enterprise topology + ), + actions=[AuthzedAction.DESCRIBE] + READ, +) + +# ML engineers (OIDC) OR team namespace owners have full write access. +# Either identity alone is sufficient — useful during namespace migration or +# when the same person holds both the OIDC role and the team namespace. +ml_engineer_write = Permission( + name="ml_engineer_full_access", + types=ALL_RESOURCE_TYPES, + policy=CombinedGroupNamespacePolicy( + groups=["ml-engineers"], + namespaces=["team-a", "team-b"], + ), + actions=ALL_ACTIONS, +) +``` + +### Example: Fine-grained resource filtering + +Permissions support `name_patterns` (regex) and `required_tags` for targeting specific resources: + +```python +from feast.feature_view import FeatureView +from feast.data_source import DataSource +from feast.permissions.action import AuthzedAction, READ +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy + +sensitive_fv_perm = Permission( + name="sensitive_feature_reader", + types=[FeatureView], + name_patterns=[".*sensitive.*", ".*pii.*"], + policy=RoleBasedPolicy(roles=["trusted-reader"]), + actions=[AuthzedAction.READ_OFFLINE], +) + +high_risk_ds_writer = Permission( + name="high_risk_ds_writer", + types=[DataSource], + required_tags={"risk_level": "high"}, + policy=RoleBasedPolicy(roles=["admin", "data_team"]), + actions=[AuthzedAction.WRITE_ONLINE, AuthzedAction.WRITE_OFFLINE], +) +``` + +### Authorization configuration + +Enable auth enforcement in `feature_store.yaml`: + +```yaml +auth: + type: kubernetes # or: oidc +``` + +For OIDC: + +```yaml +auth: + type: oidc + client_id: feast-client + auth_server_url: https://keycloak.example.com/realms/feast + auth_discovery_url: https://keycloak.example.com/realms/feast/.well-known/openid-configuration +``` + +{% hint style="warning" %} +**Permission granting order:** Feast uses an *affirmative* decision strategy — if **any** matching permission grants access, the request is allowed. Access is denied only when **all** matching permissions deny the user. If no permission matches a resource + action combination, access is **denied**. Resources that do not match any configured permission are unsecured. Always define explicit coverage for all critical resources. +{% endhint %} + +### Recommended RBAC by topology + +| Topology | Auth type | Policy type | Guidance | +|---|---|---|---| +| **Minimal** | `no_auth` or `kubernetes` | `RoleBasedPolicy` | Basic admin/reader roles | +| **Standard** | `kubernetes` | `RoleBasedPolicy` | K8s service account roles | +| **Enterprise (isolated)** | `oidc` or `kubernetes` | `RoleBasedPolicy` + `GroupBasedPolicy` | Per-team OIDC groups | +| **Enterprise (shared registry)** | `kubernetes` | `NamespaceBasedPolicy` or `CombinedGroupNamespacePolicy` | Namespace isolation with tag-based resource scoping | + +--- + +## Infrastructure-Specific Recommendations + +Choosing the right online store, offline store, and registry backend depends on your cloud environment and existing infrastructure. The table below maps common deployment environments to recommended Feast components. + +### Recommendation matrix + +```mermaid +graph TD + subgraph AWS["AWS / EKS / ROSA"] + A_Online["Online: ElastiCache Redis
or DynamoDB"] + A_Offline["Offline: Redshift
or Snowflake or Athena"] + A_Registry["Registry: RDS PostgreSQL (SQL)
or S3"] + A_Compute["Compute: Snowflake Engine
or Spark on EMR
or Ray (KubeRay)"] + end + + subgraph GCP["GCP / GKE"] + G_Online["Online: Memorystore Redis
or Bigtable or Datastore"] + G_Offline["Offline: BigQuery
or Snowflake"] + G_Registry["Registry: Cloud SQL PostgreSQL
or GCS"] + G_Compute["Compute: Snowflake Engine
or Spark on Dataproc
or Ray (KubeRay)"] + end + + subgraph OnPrem["On-Premise / OpenShift"] + O_Online["Online: Redis
or PostgreSQL"] + O_Offline["Offline: Spark + MinIO
or PostgreSQL or Trino or Oracle"] + O_Registry["Registry: PostgreSQL (SQL)"] + O_Compute["Compute: Spark
or Ray (KubeRay)"] + end + + style AWS fill:#fff3e0,stroke:#f57c00,color:#000 + style GCP fill:#e3f2fd,stroke:#1976d2,color:#000 + style OnPrem fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +### AWS / EKS / ROSA + +| Component | Recommended | Alternative | Notes | +|---|---|---|---| +| **Online Store** | **Redis** (ElastiCache) | DynamoDB | Redis offers TTL at retrieval, concurrent writes, Java/Go SDK support. DynamoDB is fully managed with zero ops. | +| **Offline Store** | **Redshift** | Snowflake, Athena (contrib), Spark | Redshift is the core AWS offline store. Use Snowflake if it's already your warehouse. Athena for S3-native query patterns. | +| **Registry** | **SQL** (RDS PostgreSQL) | S3 | SQL registry required for concurrent materialization writers. S3 registry is simpler but limited to single-writer. | +| **Compute Engine** | **Snowflake Engine** | Spark on EMR, [Ray (KubeRay)](../reference/compute-engine/ray.md) | Snowflake engine when your offline/online stores are Snowflake. Spark for S3-based pipelines. Ray with KubeRay for Kubernetes-native distributed processing. | + +{% hint style="info" %} +**ROSA (Red Hat OpenShift on AWS):** Same store recommendations as EKS. Use OpenShift Routes instead of Ingress for TLS termination. Leverage OpenShift's built-in OAuth for `auth.type: kubernetes` integration. +{% endhint %} + +### GCP / GKE + +| Component | Recommended | Alternative | Notes | +|---|---|---|---| +| **Online Store** | **Redis** (Memorystore) | Bigtable, Datastore | Redis for latency-sensitive workloads. Bigtable for very large-scale feature storage. Datastore is GCP-native and zero-ops. | +| **Offline Store** | **BigQuery** | Snowflake, Spark (Dataproc) | BigQuery is the core GCP offline store with full feature support. | +| **Registry** | **SQL** (Cloud SQL PostgreSQL) | GCS | SQL for multi-writer. GCS for simple single-writer setups. | +| **Compute Engine** | **Snowflake Engine** | Spark on Dataproc, [Ray (KubeRay)](../reference/compute-engine/ray.md) | Use Snowflake engine if your offline store is Snowflake. Spark for BigQuery + GCS pipelines. Ray with KubeRay for Kubernetes-native distributed processing. | + +### On-Premise / OpenShift / Self-Managed Kubernetes + +| Component | Recommended | Alternative | Notes | +|---|---|---|---| +| **Online Store** | **Redis** (self-managed or operator) | PostgreSQL (contrib) | Redis for best performance. PostgreSQL if you want to minimize infrastructure components. | +| **Offline Store** | **Spark** + MinIO (contrib) | PostgreSQL (contrib), Trino (contrib), Oracle (contrib), DuckDB | Spark for scale. PostgreSQL for simpler setups. Oracle for enterprise customers with existing Oracle infrastructure. DuckDB for development only. | +| **Registry** | **SQL** (PostgreSQL) | — | Always use SQL registry in production on-prem. File-based registries do not support concurrent writers. | +| **Compute Engine** | **Spark** | [Ray (KubeRay)](../reference/compute-engine/ray.md) | Run Spark on Kubernetes or standalone. Ray with KubeRay for Kubernetes-native distributed DAG execution. | + +{% hint style="warning" %} +**Multi-replica constraint:** When scaling any Feast service to multiple replicas (via the Feast Operator), you **must** use database-backed persistence for all enabled services. File-based stores (SQLite, DuckDB, `registry.db`) are incompatible with multi-replica deployments. See [Scaling Feast](./scaling-feast.md#horizontal-scaling-with-the-feast-operator) for details. +{% endhint %} + +--- + +## Air-Gapped / Disconnected Environment Deployments + +Production environments in regulated industries (finance, government, defense) often have no outbound internet access from the Kubernetes cluster. The Feast Operator supports air-gapped deployments through custom container images, init container controls, and standard Kubernetes image-pull mechanisms. + +### Default init container behavior + +When `feastProjectDir` is set on the FeatureStore CR, the operator creates up to two init containers unless `services.disableInitContainers` is `true`: + +1. **`feast-init`** — bootstraps the feature repository by running `git clone`, `feast init`, or copying a repository from `feastProjectDir.packaged.featureRepoPath`. It then writes the operator-generated `feature_store.yaml` into the initialized repository. +2. **`feast-apply`** — runs `feast apply` to register feature definitions in the registry. Controlled by `runFeastApplyOnInit` (defaults to `true`). Skipped when `disableInitContainers` is `true`. + +In air-gapped environments, use `feastProjectDir.packaged` to identify a feature repository baked into an image. The operator supports two lifecycle modes: + +* Keep init containers enabled to refresh shared storage from the image, generate configuration from the FeatureStore CR, and optionally run `feast apply`. +* Set `services.disableInitContainers: true` to run directly from the baked path and treat its `feature_store.yaml` as authoritative. + +### Air-gapped deployment workflow + +```mermaid +graph TD + subgraph BuildEnv["Build Environment (internet access)"] + Code["Feature repo source code"] + Base["Base Feast image
(feastdev/feature-server)"] + Custom["Custom image with
bundled feature repo"] + Code --> Custom + Base --> Custom + end + + subgraph InternalRegistry["Internal Container Registry"] + Mirror["registry.internal.example.com
/feast/feature-server:release"] + end + + subgraph AirGappedCluster["Air-Gapped Kubernetes Cluster"] + SA["ServiceAccount
(imagePullSecrets)"] + CR["FeatureStore CR
feastProjectDir.packaged
disableInitContainers: true"] + Deploy["Feast Deployment
(no init containers)"] + SA --> Deploy + CR --> Deploy + end + + Custom -->|push| Mirror + Mirror -->|pull| Deploy +``` + +**Steps:** + +1. **Build a custom container image** that bundles the feature repository and all Python dependencies into the Feast base image. +2. **Push** the image to your internal container registry. +3. **Configure `feastProjectDir.packaged`** with the image and the canonical absolute path to the bundled repository. Do not use `.`, `..`, repeated separators, or a trailing separator, and keep the path outside operator-mounted locations such as `/feast-data` so it cannot overlap the staged repository. +4. **Choose the lifecycle:** leave init containers enabled for operator-managed configuration and `feast apply`, or set `services.disableInitContainers: true` to use the baked repository and configuration directly. +5. **Set `imagePullPolicy: IfNotPresent`** (or `Never` if images are pre-loaded on nodes). +6. **Configure `imagePullSecrets`** on the namespace's ServiceAccount — the FeatureStore CRD does not expose an `imagePullSecrets` field, so use the standard Kubernetes approach of attaching secrets to the ServiceAccount that the pods run under. + +### Sample FeatureStore CR (air-gapped) + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: airgap-production +spec: + feastProject: my_project + feastProjectDir: + packaged: + image: registry.internal.example.com/feast/feature-server:release + featureRepoPath: /opt/feast/feature_repo + services: + disableInitContainers: true + onlineStore: + persistence: + store: + type: redis + secretRef: + name: feast-online-store + server: + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-registry-store + server: + imagePullPolicy: IfNotPresent +``` + +The packaged image is the default for every Feast service and for the `feast-init` and +`feast-apply` init containers. A per-service `image` still takes precedence for that +service, and `services.initImage` takes precedence for both init containers. Remove +`disableInitContainers: true` to use operator-managed staging and startup apply instead. + +{% hint style="info" %} +**Pre-populating the registry:** With init containers disabled, `feast apply` does not run on pod startup. You can populate the registry by: + +1. **Running `feast apply` from your CI/CD pipeline** that has network access to the registry DB. +2. **Using the FeatureStore CR's built-in CronJob** (`spec.cronJob`) — the operator creates a Kubernetes CronJob that runs `feast apply` and `feast materialize-incremental` on a schedule. The CronJob runs inside the cluster (no external access needed) and can use a custom image just like the main deployment. This is the recommended approach for air-gapped environments. +3. **Running `feast apply` manually** from the build environment before deploying the CR. +{% endhint %} + +### Air-gapped deployment checklist + +{% hint style="warning" %} +**Pre-stage the following artifacts before deploying Feast in an air-gapped environment:** + +* **Container images** — Feast feature server image (with bundled feature repo) pushed to internal registry +* **CRD manifests** — Feast Operator CRDs and operator deployment manifests available locally +* **Store credentials** — Kubernetes Secrets for online store, offline store, and registry DB connections created in the target namespace +* **Python packages** (if using custom on-demand transforms) — bundled into the custom image or available from an internal PyPI mirror +* **ServiceAccount configuration** — `imagePullSecrets` attached to the ServiceAccount used by the Feast deployment +{% endhint %} + +--- + +## Hybrid Store Configuration + +The hybrid store feature allows a single Feast deployment to route feature operations to multiple backends based on tags or data sources. This is useful when different feature views have different latency, cost, or compliance requirements. + +### Hybrid online store + +The `HybridOnlineStore` routes online operations to different backends based on a configurable tag on the `FeatureView`. + +```mermaid +graph LR + FS["Online Feature Server"] --> Router["HybridOnlineStore
(routes by tag)"] + Router -->|"tag: dynamodb"| DDB["DynamoDB"] + Router -->|"tag: redis"| RD["Redis"] + + style Router fill:#fff3e0,stroke:#f57c00,color:#000 + style DDB fill:#e3f2fd,stroke:#1976d2,color:#000 + style RD fill:#fce4ec,stroke:#c62828,color:#000 +``` + +**`feature_store.yaml` configuration:** + +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: hybrid + routing_tag: team + online_stores: + - type: dynamodb + conf: + region: us-east-1 + - type: redis + conf: + connection_string: "redis-cluster:6379" + redis_type: redis_cluster +``` + +**Feature view with routing tag:** + +```python +from feast import FeatureView + +user_features = FeatureView( + name="user_features", + entities=[user_entity], + source=user_source, + tags={"team": "dynamodb"}, # Routes to DynamoDB backend +) + +transaction_features = FeatureView( + name="transaction_features", + entities=[txn_entity], + source=txn_source, + tags={"team": "redis"}, # Routes to Redis backend +) +``` + +The tag value must match the online store `type` name (e.g. `dynamodb`, `redis`, `bigtable`). + +### Hybrid offline store + +The `HybridOfflineStore` routes offline operations to different backends based on the `batch_source` type of each `FeatureView`. + +```mermaid +graph LR + Client["Materialization /
Training Job"] --> Router["HybridOfflineStore
(routes by source type)"] + Router -->|"SparkSource"| Spark["Spark"] + Router -->|"RedshiftSource"| RS["Redshift"] + + style Router fill:#fff3e0,stroke:#f57c00,color:#000 + style Spark fill:#e8f5e9,stroke:#388e3c,color:#000 + style RS fill:#e3f2fd,stroke:#1976d2,color:#000 +``` + +**`feature_store.yaml` configuration:** + +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +offline_store: + type: hybrid_offline_store.HybridOfflineStore + offline_stores: + - type: spark + conf: + spark_master: local[*] + spark_app_name: feast_spark_app + - type: redshift + conf: + cluster_id: my-redshift-cluster + region: us-east-1 + database: feast_db + user: feast_user + s3_staging_location: s3://my-bucket/feast-staging + iam_role: arn:aws:iam::123456789012:role/FeastRedshiftRole +``` + +**Feature views with different sources:** + +```python +from feast import FeatureView, Entity, ValueType +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import SparkSource +from feast.infra.offline_stores.redshift_source import RedshiftSource + +user_features = FeatureView( + name="user_features", + entities=[user_entity], + source=SparkSource(path="s3://bucket/user_features"), # Routes to Spark +) + +activity_features = FeatureView( + name="user_activity", + entities=[user_entity], + source=RedshiftSource( # Routes to Redshift + table="user_activity", + event_timestamp_column="event_ts", + ), +) +``` + +{% hint style="warning" %} +**Hybrid offline store constraint:** `get_historical_features` requires all requested feature views to share the same `batch_source` type within a single call. You cannot join features across different offline engines in one retrieval request. +{% endhint %} + +--- + +## Performance Considerations + +For detailed server-level tuning (worker counts, timeouts, keep-alive, etc.), see the [Online Server Performance Tuning](./online-server-performance-tuning.md) guide. + +### Online feature server sizing + +| Traffic tier | Replicas | CPU (per pod) | Memory (per pod) | Notes | +|---|---|---|---|---| +| Low (<100 RPS) | 1–2 | 500m–1 | 512Mi–1Gi | Minimal production | +| Medium (100–1000 RPS) | 2–5 (HPA) | 1–2 | 1–2Gi | Standard production | +| High (>1000 RPS) | 5–20 (HPA) | 2–4 | 2–4Gi | Enterprise, per-tenant | + +### Online store latency guidelines + +| Store | p50 latency | p99 latency | Best for | +|---|---|---|---| +| **Redis** (single) | <1ms | <5ms | Lowest latency, small-medium datasets | +| **Redis Cluster** | <2ms | <10ms | High availability + low latency | +| **DynamoDB** | <5ms | <20ms | Serverless, variable traffic | +| **PostgreSQL** | <5ms | <30ms | On-prem, simplicity | +| **Remote (HTTP)** | <10ms | <50ms | Client-server separation | + +### Connection pooling for remote online store + +When using the [Remote Online Store](../reference/online-stores/remote.md) (client-server architecture), connection pooling significantly reduces latency by reusing TCP/TLS connections: + +```yaml +online_store: + type: remote + path: http://feast-feature-server:80 + connection_pool_size: 50 # Max connections in pool (default: 50) + connection_idle_timeout: 300 # Idle timeout in seconds (default: 300) + connection_retries: 3 # Retry count with exponential backoff +``` + +**Tuning by workload:** + +| Workload | `connection_pool_size` | `connection_idle_timeout` | `connection_retries` | +|---|---|---|---| +| High-throughput inference | 100 | 600 | 5 | +| Long-running batch service | 50 | 0 (never close) | 3 | +| Resource-constrained edge | 10 | 60 | 2 | + +### Registry performance + +* **SQL registry** (PostgreSQL, MySQL) is required for concurrent materialization jobs writing to the registry simultaneously. +* **File-based registries** (S3, GCS, local) serialize the entire registry on each write — suitable only for single-writer scenarios. +* For read-heavy workloads, scale the Registry Server to multiple replicas (all connecting to the same database). + +### Registry cache tuning at scale + +Each Feast server pod maintains its own in-memory copy of the registry metadata. With multiple Gunicorn workers per pod, the total number of independent registry copies is **replicas x workers**. For example, 5 replicas with 4 workers each means 20 copies of the registry in memory, each refreshing independently. + +With the default `cache_mode: sync`, the refresh is **synchronous** — when the TTL expires, the next request blocks until the full registry is re-downloaded. At scale, this causes periodic latency spikes across multiple pods simultaneously. + +**Recommendation:** Use `cache_mode: thread` with a higher TTL in production to avoid refresh storms: + +```yaml +# In the Operator secret for SQL/DB-backed registries: +registry: + registry_type: sql + path: postgresql://:@:5432/feast + cache_mode: thread + cache_ttl_seconds: 300 +``` + +For the server-side refresh interval, set `registryTTLSeconds` on the CR: + +```yaml +spec: + services: + onlineStore: + server: + workerConfigs: + registryTTLSeconds: 300 +``` + +| Scenario | `cache_mode` | `cache_ttl_seconds` | `registryTTLSeconds` | +|---|---|---|---| +| Development / iteration | `sync` (default) | 5–10 | 5 | +| Production (low-latency) | `thread` | 300 | 300 | +| Production (frequent schema changes) | `thread` | 60 | 60 | + +{% hint style="info" %} +`registryTTLSeconds` on the CR controls the **server-side** refresh interval. `cache_ttl_seconds` in the registry secret controls the **SDK client** refresh. In Operator deployments, the CR field is what matters for serving performance. For a deep dive into sync vs thread mode trade-offs, memory impact, and freshness considerations, see the [Registry Cache Tuning](./online-server-performance-tuning.md#registry-cache-tuning) section in the performance tuning guide. +{% endhint %} + +### Materialization performance + +| Data volume | Recommended engine | Notes | +|---|---|---| +| <1M rows | In-process (default) | Simple, no external dependencies | +| 1M–100M rows | Snowflake Engine, Spark, or Ray | Distributed processing | +| >100M rows | Spark on Kubernetes / EMR / Dataproc, or Ray via KubeRay | Full cluster-scale materialization with distributed DAG execution | + +For detailed engine configuration, see [Scaling Materialization](./scaling-feast.md#scaling-materialization). + +### Redis sizing guidelines + +| Metric | Guideline | +|---|---| +| **Memory** | ~100 bytes per feature value (varies by data type). For 1M entities x 50 features = ~5GB. | +| **Connections** | Each online feature server replica opens a connection pool. Plan for `replicas x pool_size`. | +| **TTL** | Set `key_ttl_seconds` in `feature_store.yaml` to auto-expire stale data and bound memory usage. | +| **Cluster mode** | Use Redis Cluster for >25GB datasets or >10K connections. | + +--- + +## Design Principles + +Understanding the following principles helps you choose and customize the right topology. + +### Control plane vs data plane + +```mermaid +graph LR + subgraph ControlPlane["Control Plane"] + Operator["Feast Operator"] + Registry["Registry Server"] + end + + subgraph DataPlane["Data Plane"] + OnlineServer["Online Feature Server"] + OfflineServer["Offline Feature Server"] + end + + subgraph BackingStores["Backing Stores (external)"] + OnlineStore["Online Store
(e.g. Redis, DynamoDB, PostgreSQL, etc.)"] + OfflineStore["Offline Store
(e.g. Redshift, BigQuery, Spark, etc.)"] + RegistryDB["Registry DB
(PostgreSQL / MySQL)"] + end + + ControlPlane -->|configures| DataPlane + DataPlane -->|reports status| ControlPlane + OnlineServer --> OnlineStore + OfflineServer --> OfflineStore + Registry --> RegistryDB + + style ControlPlane fill:#e8f5e9,stroke:#388e3c,color:#000 + style DataPlane fill:#e3f2fd,stroke:#1976d2,color:#000 + style BackingStores fill:#fce4ec,stroke:#c62828,color:#000 +``` + +* **Control plane** (Operator + Registry Server) manages feature definitions, metadata, and lifecycle. It changes infrequently and should be highly available. +* **Data plane** (Online Feature Server + Offline Feature Server) handles the actual feature reads/writes at request time. It must scale with traffic. +* **Backing stores** (databases, object storage) hold the actual data. These are stateful and managed independently. + +### Stateless vs stateful components + +The Feast Operator deploys all Feast services (Online Feature Server, Offline Feature Server, Registry Server) in a **single shared Deployment**. When scaling (`spec.replicas > 1` or HPA autoscaling), all services scale together. + +{% hint style="warning" %} +**Scaling requires DB-backed persistence for all enabled services.** The operator enforces this via CRD validation: + +* **Online Store** — must use DB persistence (e.g. `type: redis`, `type: dynamodb`, `type: postgres`) +* **Offline Store** — if enabled, must use DB persistence (e.g. `type: redshift`, `type: bigquery`, `type: spark`, `type: postgres`) +* **Registry** — must use SQL persistence (`type: sql`), a remote registry, or S3/GCS file-backed registry + +File-based stores (SQLite, DuckDB, `registry.db`) are **rejected** when `replicas > 1` or autoscaling is configured. +{% endhint %} + +| Component | Type | Scaling | DB-backed requirement | +|---|---|---|---| +| Online Feature Server | **Stateless** (server) | Scales with the shared Deployment (HPA or `spec.replicas`) | Online store must use DB persistence (e.g. Redis, DynamoDB, PostgreSQL) | +| Offline Feature Server | **Stateless** (server) | Scales with the shared Deployment (HPA or `spec.replicas`) | Offline store must use DB persistence (e.g. Redshift, BigQuery, Spark, PostgreSQL) | +| Registry Server | **Stateless** (server) | Scales with the shared Deployment (HPA or `spec.replicas`) | Registry must use SQL, remote, or S3/GCS persistence | +| Online Store (Redis, DynamoDB, etc.) | **Stateful** (backing store) | Scale via managed service or clustering | Managed independently of Feast services | +| Offline Store (Redshift, BigQuery, etc.) | **Stateful** (backing store) | Scale via cloud-managed infrastructure | Managed independently of Feast services | +| Registry DB (PostgreSQL, MySQL) | **Stateful** (backing store) | Scale via managed database service | Managed independently of Feast services | + +### Scalability guidelines + +```mermaid +graph TD + Read["Read traffic increase"] -->|scale| FS["Online Feature Server replicas (HPA)"] + Write["Write / materialization load"] -->|scale| Engine["Compute Engine
(Spark / Ray / Snowflake)"] + Storage["Data volume growth"] -->|scale| Store["Online / Offline Store capacity"] + + FS -.- Independent["Scale independently"] + Engine -.- Independent + Store -.- Independent + + style Read fill:#e3f2fd,stroke:#1976d2,color:#000 + style Write fill:#fff3e0,stroke:#f57c00,color:#000 + style Storage fill:#f3e5f5,stroke:#7b1fa2,color:#000 + style FS fill:#e3f2fd,stroke:#1976d2,color:#000 + style Engine fill:#fff3e0,stroke:#f57c00,color:#000 + style Store fill:#f3e5f5,stroke:#7b1fa2,color:#000 +``` + +* **Read scaling** — increase Online Feature Server replicas; they are stateless and scale linearly. +* **Write scaling** — use a distributed compute engine ([Spark](../reference/compute-engine/spark.md), [Ray/KubeRay](../reference/compute-engine/ray.md), or [Snowflake](../reference/compute-engine/snowflake.md)) for materialization. +* **Storage scaling** — scale online and offline stores independently based on data volume and query patterns. + +For detailed scaling configuration, see [Scaling Feast](./scaling-feast.md). + +--- + +## Topology Comparison + +| Capability | Minimal | Standard | Enterprise | +|---|:---:|:---:|:---:| +| **High availability** | No | Yes | Yes | +| **Autoscaling** | No | HPA | HPA + Cluster Autoscaler | +| **TLS / Ingress** | No | Yes | Yes + API Gateway | +| **RBAC** | No | Kubernetes RBAC | OIDC + fine-grained RBAC | +| **Multi-tenancy** | No | No | Namespace-per-team | +| **Shared registry** | N/A | N/A | Optional (remote registry) | +| **Hybrid stores** | No | Optional | Recommended for mixed backends | +| **Observability** | Logs only | Basic metrics | OpenTelemetry + Prometheus + Grafana + Jaeger | +| **Disaster recovery** | No | Partial | Full backup/restore | +| **Network policies** | No | Optional | Enforced | +| **Recommended team size** | 1–3 | 3–15 | 15+ | + +--- + +## Next Steps + +* [Feast on Kubernetes](./feast-on-kubernetes.md) — install the Feast Operator and deploy your first FeatureStore CR +* [Scaling Feast](./scaling-feast.md) — detailed HPA, registry scaling, and materialization engine configuration +* [Online Server Performance Tuning](./online-server-performance-tuning.md) — worker counts, timeouts, keep-alive, and server-level tuning +* [Starting Feast Servers in TLS Mode](./starting-feast-servers-tls-mode.md) — enable TLS for secure communication +* [Running Feast in Production](./running-feast-in-production.md) — CI/CD, materialization scheduling, and model serving patterns +* [Multi-Team Feature Store Setup](./federated-feature-store.md) — federated feature store for multi-team environments +* [Permission Concepts](../getting-started/concepts/permission.md) — full permission model reference +* [RBAC Architecture](../getting-started/architecture/rbac.md) — authorization architecture details +* [OpenTelemetry Integration](../getting-started/components/open-telemetry.md) — traces and metrics for Feast servers +* [Hybrid Online Store](../reference/online-stores/hybrid.md) — hybrid online store configuration reference +* [Hybrid Offline Store](../reference/offline-stores/hybrid.md) — hybrid offline store configuration reference diff --git a/docs/how-to-guides/running-feast-in-production.md b/docs/how-to-guides/running-feast-in-production.md index be6fd2afeb4..d26e0234b2e 100644 --- a/docs/how-to-guides/running-feast-in-production.md +++ b/docs/how-to-guides/running-feast-in-production.md @@ -18,6 +18,10 @@ For example, you might not have a stream source and, thus, no need to write feat Additionally, please check the how-to guide for some specific recommendations on [how to scale Feast](./scaling-feast.md). {% endhint %} +{% hint style="info" %} +**Looking for production deployment patterns?** See the [Feast Production Deployment Topologies](./production-deployment-topologies.md) guide for three Kubernetes-ready topologies (Minimal, Standard, Enterprise), sample FeatureStore CRs, RBAC policies, infrastructure recommendations, and scaling best practices. +{% endhint %} + In this guide we will show you how to: 1. Deploy your feature store and keep your infrastructure in sync with your feature repository @@ -71,6 +75,15 @@ Feast keeps the history of materialization in its registry so that the choice co However, the amount of work can quickly outgrow the resources of a single machine. That happens because the materialization job needs to repackage all rows before writing them to an online store. That leads to high utilization of CPU and memory. In this case, you might want to use a job orchestrator to run multiple jobs in parallel using several workers. Kubernetes Jobs or Airflow are good choices for more comprehensive job orchestration. +For large datasets, you can also reduce peak memory on the materialization worker by setting `online_write_batch_size` in `feature_store.yaml`. This breaks the proto conversion and write into chunks instead of loading the entire dataset into memory at once: + +```yaml +materialization: + online_write_batch_size: 10000 # rows per write batch; reduces peak memory proportionally +``` + +See the [Materialization write performance](./online-server-performance-tuning.md#materialization-write-performance) guide for sizing recommendations and the full option reference in [feature_store.yaml](../reference/feature-repository/feature-store-yaml.md#online_write_batch_size). + If you are using Airflow as a scheduler, Feast can be invoked through a [PythonOperator](https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/python.html) after the [Python SDK](https://pypi.org/project/feast/) has been installed into a virtual environment and your feature repo has been synced: ```python diff --git a/docs/how-to-guides/scaling-feast.md b/docs/how-to-guides/scaling-feast.md index 23c13bfe5ad..5982f917674 100644 --- a/docs/how-to-guides/scaling-feast.md +++ b/docs/how-to-guides/scaling-feast.md @@ -86,6 +86,8 @@ spec: target: type: Utilization averageUtilization: 70 + podDisruptionBudgets: + maxUnavailable: 1 onlineStore: persistence: store: @@ -107,7 +109,7 @@ spec: ``` {% hint style="info" %} -When autoscaling is configured, the operator automatically sets the deployment strategy to `RollingUpdate` (instead of the default `Recreate`) to ensure zero-downtime scaling. You can override this by explicitly setting `deploymentStrategy` in the CR. +When autoscaling is configured, the operator automatically sets the deployment strategy to `RollingUpdate` (instead of the default `Recreate`) to ensure zero-downtime scaling, and auto-injects soft pod anti-affinity and zone topology spread constraints. You can override any of these by explicitly setting `deploymentStrategy`, `affinity`, or `topologySpreadConstraints` in the CR. {% endhint %} #### Validation Rules @@ -117,6 +119,72 @@ The operator enforces the following rules: - Scaling with `replicas > 1` or any `autoscaling` config is **rejected** if any enabled service uses file-based persistence. - S3 (`s3://`) and GCS (`gs://`) backed registry file persistence is allowed with scaling, since these object stores support concurrent readers. +#### High Availability + +When scaling is enabled (`replicas > 1` or `autoscaling`), the operator provides HA features to improve resilience: + +**Pod Anti-Affinity** — The operator automatically injects a soft (`preferredDuringSchedulingIgnoredDuringExecution`) pod anti-affinity rule that prefers spreading pods across different nodes. This prevents multiple replicas from being co-located on the same node, improving resilience to node failures. You can override this by providing your own `affinity` configuration: + +```yaml +spec: + replicas: 3 + services: + # Override with custom affinity (e.g. strict anti-affinity) + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + feast.dev/name: my-feast + # ... +``` + +**Topology Spread Constraints** — The operator automatically injects a soft zone-spread constraint (`whenUnsatisfiable: ScheduleAnyway`) that distributes pods across availability zones. This is a best-effort spread — if zones are unavailable, pods will still be scheduled. You can override this with explicit constraints or disable it with an empty array: + +```yaml +spec: + replicas: 3 + services: + # Override with custom topology spread (e.g. strict zone spreading) + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + feast.dev/name: my-feast + # ... +``` + +To disable the auto-injected topology spread: + +```yaml +spec: + replicas: 3 + services: + topologySpreadConstraints: [] + # ... +``` + +**PodDisruptionBudget** — You can configure a PDB to limit voluntary disruptions (e.g. during node drains or cluster upgrades). The PDB is only created when scaling is enabled. Exactly one of `minAvailable` or `maxUnavailable` must be set: + +```yaml +spec: + replicas: 3 + services: + podDisruptionBudgets: + maxUnavailable: 1 # at most 1 pod unavailable during disruptions + # -- OR -- + # podDisruptionBudgets: + # minAvailable: "50%" # at least 50% of pods must remain available + # ... +``` + +{% hint style="info" %} +The PDB is not auto-injected — you must explicitly configure it. This is intentional because a misconfigured PDB (e.g. `minAvailable` equal to the replica count) can block node drains and cluster upgrades. +{% endhint %} + #### Using KEDA (Kubernetes Event-Driven Autoscaling) [KEDA](https://keda.sh) is also supported as an external autoscaler. KEDA should target the FeatureStore's scale sub-resource directly (since it implements the Kubernetes scale API). This is the recommended approach because the operator manages the Deployment's replica count from `spec.replicas` — targeting the Deployment directly would conflict with the operator's reconciliation. diff --git a/docs/how-to-guides/starting-feast-servers-tls-mode.md b/docs/how-to-guides/starting-feast-servers-tls-mode.md index ffc7e5d9e90..c3696a35532 100644 --- a/docs/how-to-guides/starting-feast-servers-tls-mode.md +++ b/docs/how-to-guides/starting-feast-servers-tls-mode.md @@ -128,6 +128,52 @@ auth: `cert` is an optional configuration to the public certificate path when the registry server starts in TLS(SSL) mode. Typically, this file ends with `*.crt`, `*.cer`, or `*.pem`. +### Feast client connecting to remote registry server with mTLS + +If the Registry Server requires mutual TLS (mTLS), the client must present a certificate and private key in addition to trusting the server's CA certificate. Add `client_cert` and `client_key` to the registry configuration: + +```yaml +project: feast-project +registry: + registry_type: remote + path: feature-registry.example.com:443 + cert: /path/to/ca.crt + client_cert: /path/to/tls.crt + client_key: /path/to/tls.key +provider: local +online_store: + path: http://localhost:6566 + type: remote +entity_key_serialization_version: 3 +auth: + type: no_auth +``` + +* `cert` — CA certificate used to verify the server (or use the `SSL_CERT_FILE` / `REQUESTS_CA_BUNDLE` environment variable). +* `client_cert` — Client certificate presented to the server. Must be paired with `client_key`. +* `client_key` — Private key for the client certificate. + +#### Connecting through a tunnel or proxy + +When connecting through a tunnel (e.g. `gcloud compute start-iap-tunnel`) the client connects to `localhost`, but the server certificate is issued for the real service hostname. Set the `authority` field so that gRPC's TLS hostname verification passes: + +```shell +# In one terminal — start the tunnel: +gcloud compute start-iap-tunnel feature-registry.example.com 443 --local-host-port=localhost:8443 +``` + +```yaml +registry: + registry_type: remote + path: localhost:8443 + cert: /path/to/ca.crt + client_cert: /path/to/tls.crt + client_key: /path/to/tls.key + authority: feature-registry.example.com +``` + +Without `authority`, the gRPC client would check the server certificate against `localhost`, which would fail because the certificate's Subject Alternative Name (SAN) is `feature-registry.example.com`. + ## Starting feast offline server in TLS mode To start the offline server in TLS mode, you need to provide the private and public keys using the `--key` and `--cert` arguments with the `feast serve_offline` command. diff --git a/docs/project/contributing.md b/docs/project/contributing.md index cded378951d..25ccd80703f 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -5,6 +5,8 @@ After familiarizing yourself with the documentation, the simplest way to get sta 1. Setup your developer environment by following [development guide](development-guide.md). 2. Either create a [GitHub issue](https://github.com/feast-dev/feast/issues) or make a draft PR (following [development guide](development-guide.md)) to get the ball rolling! +> **Reporting a security vulnerability?** Do not open an issue or PR. Report it privately through [GitHub's advisory form](https://github.com/feast-dev/feast/security/advisories/new); see the [security policy](https://github.com/feast-dev/feast/blob/master/SECURITY.md). + ## Decision making process *See [governance](../../community/governance.md) for more details here* @@ -22,9 +24,22 @@ PRs that are submitted by the general public need to be identified as `ok-to-tes See also [Making a pull request](development-guide.md#making-a-pull-request) for other guidelines on making pull requests in Feast. +## RFCs and Architecture Decision Records + +For substantial changes (new features, architecture changes, removing features), we use an RFC process. See the [governance document](../../community/governance.md#rfcs-process) for details. + +Once an RFC is finalized and approved, it should be recorded as an Architecture Decision Record (ADR) in the [`docs/adr/`](../adr/README.md) directory. This ensures that architectural decisions are version-controlled alongside the codebase and easily accessible to all contributors. + +To add a finalized RFC as an ADR: + +1. Copy the [ADR template](../adr/ADR-TEMPLATE.md) to a new file with the next sequential number. +2. Summarize the RFC's context, decision, and consequences. +3. Submit a pull request with the new ADR. + ## Resources - [Community](../community.md) for other ways to get involved with the community - [Development guide](development-guide.md) for tips on how to contribute - [Feast GitHub issues](https://github.com/feast-dev/feast/issues) to see what others are working on -- [Feast RFCs](https://drive.google.com/drive/u/0/folders/1msUsgmDbVBaysmhBlg9lklYLLTMk4bC3) for a folder of previously written RFCs \ No newline at end of file +- [Feast RFCs](https://drive.google.com/drive/u/0/folders/1msUsgmDbVBaysmhBlg9lklYLLTMk4bC3) for a folder of previously written RFCs +- [Architecture Decision Records](../adr/README.md) for documented architectural decisions \ No newline at end of file diff --git a/docs/project/development-guide.md b/docs/project/development-guide.md index 4f915f64e5c..ee5cc8cfcce 100644 --- a/docs/project/development-guide.md +++ b/docs/project/development-guide.md @@ -217,7 +217,7 @@ make test-python-integration-local To test across clouds, on top of setting up Redis, you also need GCP / AWS / Snowflake setup. > Note: you can manually control what tests are run today by inspecting -> [RepoConfiguration](https://github.com/feast-dev/feast/blob/master/sdk/python/tests/integration/feature_repos/repo_configuration.py) +> [RepoConfiguration](https://github.com/feast-dev/feast/blob/master/sdk/python/tests/universal/feature_repos/repo_configuration.py) > and commenting out tests that are added to `DEFAULT_FULL_REPO_CONFIGS` **GCP** diff --git a/docs/reference/alpha-feature-view-versioning.md b/docs/reference/alpha-feature-view-versioning.md new file mode 100644 index 00000000000..5cdf2845ebc --- /dev/null +++ b/docs/reference/alpha-feature-view-versioning.md @@ -0,0 +1,229 @@ +# \[Alpha\] Feature View Versioning + +{% hint style="warning" %} +**Warning**: This is an _experimental_ feature. It is stable but there are still rough edges. Contributions are welcome! +{% endhint %} + +## Overview + +Feature view versioning automatically tracks schema and UDF changes to feature views. Every time `feast apply` detects a change, a versioned snapshot is saved to the registry. This enables: + +- **Audit trail** — see what a feature view looked like at any point in time +- **Safe rollback** — pin serving to a prior version with `version="v0"` in your definition +- **Multi-version serving** — serve both old and new schemas simultaneously using `@v` syntax +- **Staged publishing** — use `feast apply --no-promote` to publish a new version without making it the default + +## How It Works + +Version tracking is fully automatic. You don't need to set any version parameter — just use `feast apply` as usual: + +1. **First apply** — Your feature view definition is saved as **v0**. +2. **Change something and re-apply** — Feast detects the change, saves the old definition as a snapshot, and saves the new one as **v1**. The version number auto-increments on each real change. +3. **Re-apply without changes** — Nothing happens. Feast compares the new definition against the active one and skips creating a version if they're identical (idempotent). +4. **Another change** — Creates **v2**, and so on. + +``` +feast apply # First apply → v0 +# ... edit schema ... +feast apply # Detects change → v1 +feast apply # No change detected → still v1 (no new version) +# ... edit source ... +feast apply # Detects change → v2 +``` + +**Key details:** + +* **Automatic snapshots**: Versions are created only when Feast detects an actual change to the feature view definition (schema or UDF). Metadata-only changes (description, tags, TTL) update in place without creating a new version. +* **Separate history storage**: Version history is stored separately from the active feature view definition, keeping the main registry lightweight. +* **Backward compatible**: The `version` parameter is fully optional. Omitting it (or setting `version="latest"`) preserves existing behavior — you get automatic versioning with zero changes to your code. + +## Configuration + +{% hint style="info" %} +Version history tracking is **always active** — no configuration needed. Every `feast apply` that changes a feature view automatically records a version snapshot. + +To enable **versioned online reads** (e.g., `fv@v2:feature`), add `enable_online_feature_view_versioning: true` to your registry config in `feature_store.yaml`: + +```yaml +registry: + path: data/registry.db + enable_online_feature_view_versioning: true +``` + +When this flag is off, version-qualified refs (e.g., `fv@v2:feature`) in online reads will raise errors, but version history, version listing, version pinning, and version lookups all work normally. +{% endhint %} + +## Pinning to a Specific Version + +You can pin a feature view to a specific historical version by setting the `version` parameter. When pinned, `feast apply` replaces the active feature view with the snapshot from that version. This is useful for reverting to a known-good definition. + +```python +from feast import FeatureView + +# Default behavior: always use the latest version (auto-increments on changes) +driver_stats = FeatureView( + name="driver_stats", + entities=[driver], + schema=[...], + source=my_source, +) + +# Pin to a specific version (reverts the active definition to v2's snapshot) +driver_stats = FeatureView( + name="driver_stats", + entities=[driver], + schema=[...], + source=my_source, + version="v2", # also accepts "version2" +) +``` + +When pinning, the feature view definition (schema, source, transformations, etc.) must match the currently active definition. If you've also modified the definition alongside the pin, `feast apply` will raise a `FeatureViewPinConflict` error. To apply changes, use `version="latest"`. To revert, only change the `version` parameter. + +The snapshot's content replaces the active feature view. Version history is not modified by a pin; the existing v0, v1, v2, etc. snapshots remain intact. + +After reverting with a pin, you can go back to normal auto-incrementing behavior by removing the `version` parameter (or setting it to `"latest"`) and running `feast apply` again. If the restored definition differs from the pinned snapshot, a new version will be created. + +### Version string formats + +| Format | Meaning | +|--------|---------| +| `"latest"` (or omitted) | Always use the latest version (auto-increments on changes) | +| `"v0"`, `"v1"`, `"v2"`, ... | Pin to a specific version number | +| `"version0"`, `"version1"`, ... | Equivalent long form (case-insensitive) | + +## Staged Publishing (`--no-promote`) + +By default, `feast apply` atomically saves a version snapshot **and** promotes it to the active definition. For breaking schema changes, you may want to stage the new version without disrupting unversioned consumers. + +The `--no-promote` flag saves the version snapshot without updating the active feature view definition. The new version is accessible only via explicit `@v` reads and `--version` materialization. + +**CLI usage:** + +```bash +feast apply --no-promote +``` + +**Python SDK equivalent:** + +```python +store.apply([entity, feature_view], no_promote=True) +``` + +### Phased rollout workflow + +1. **Stage the new version:** + ```bash + feast apply --no-promote + ``` + This publishes v2 without promoting it. All unversioned consumers continue using v1. + +2. **Populate the v2 online table:** + ```bash + feast materialize --views driver_stats --version v2 ... + ``` + +3. **Migrate consumers one at a time:** + - Consumer A switches to `driver_stats@v2:trips_today` + - Consumer B switches to `driver_stats@v2:avg_rating` + +4. **Promote v2 as the default:** + ```bash + feast apply + ``` + Or pin to v2: set `version="v2"` in the definition and run `feast apply`. + +## Listing Version History + +Use the CLI to inspect version history: + +```bash +feast feature-views list-versions driver_stats +``` + +```text +VERSION TYPE CREATED VERSION_ID +v0 feature_view 2024-01-15 10:30:00 a1b2c3d4-... +v1 feature_view 2024-01-16 14:22:00 e5f6g7h8-... +v2 feature_view 2024-01-20 09:15:00 i9j0k1l2-... +``` + +Or programmatically via the Python SDK: + +```python +store = FeatureStore(repo_path=".") +versions = store.list_feature_view_versions("driver_stats") +for v in versions: + print(f"{v['version']} created at {v['created_timestamp']}") +``` + +## Version-Qualified Feature References + +You can read features from a **specific version** of a feature view by using version-qualified feature references with the `@v` syntax: + +```python +online_features = store.get_online_features( + features=[ + "driver_stats:trips_today", # latest version (default) + "driver_stats@v2:trips_today", # specific version + "driver_stats@latest:trips_today", # explicit latest + ], + entity_rows=[{"driver_id": 1001}], +) +``` + +**How it works:** + +* `driver_stats:trips_today` is equivalent to `driver_stats@latest:trips_today` — it reads from the currently active version +* `driver_stats@v2:trips_today` reads from the v2 snapshot stored in version history, using a version-specific online store table +* Multiple versions of the same feature view can be queried in a single request (e.g., `driver_stats@v1:trips` and `driver_stats@v2:trips_daily`) + +**Backward compatibility:** + +* The unversioned online store table (e.g., `project_driver_stats`) is treated as v0 +* Only versions >= 1 get `_v{N}` suffixed tables (e.g., `project_driver_stats_v1`) +* Pre-versioning users' existing data continues to work without changes — `@latest` resolves to the active version, which for existing unversioned FVs is v0 + +**Materialization:** Each version requires its own materialization. After applying a new version, run `feast materialize` to populate the versioned table before querying it with `@v`. + +## Supported Feature View Types + +Versioning is supported on all three feature view types: + +* `FeatureView` (and `BatchFeatureView`) +* `StreamFeatureView` +* `OnDemandFeatureView` + +## Online Store Support + +{% hint style="info" %} +**Currently, version-qualified online reads (`@v`) are only supported with the SQLite online store.** Support for additional online stores (Redis, DynamoDB, Bigtable, Postgres, etc.) will be added based on community priority. + +If you need versioned online reads for a specific online store, please [open a GitHub issue](https://github.com/feast-dev/feast/issues/new) describing your use case and which store you need. This helps us prioritize development. +{% endhint %} + +Version history tracking in the registry (listing versions, pinning, `--no-promote`) works with **all** registry backends (file, SQL, Snowflake). + +## Full Details + +For the complete design, concurrency semantics, and feature service interactions, see the [Feature View Versioning RFC](../adr/feature-view-versioning.md). + +## Naming Restrictions + +Feature references use a structured format: `feature_view_name@v:feature_name`. To avoid +ambiguity, the following characters are reserved and must not appear in feature view or feature names: + +- **`@`** — Reserved as the version delimiter (e.g., `driver_stats@v2:trips_today`). `feast apply` + will reject feature views with `@` in their name. If you have existing feature views with `@` in + their names, they will continue to work for unversioned reads, but we recommend renaming them to + avoid ambiguity with the `@v` syntax. +- **`:`** — Reserved as the separator between feature view name and feature name in fully qualified + feature references (e.g., `driver_stats:trips_today`). + +## Known Limitations + +- **Online store coverage** — Version-qualified reads (`@v`) are SQLite-only today. Other online stores are follow-up work. +- **Offline store versioning** — Versioned historical retrieval is not yet supported. +- **Version deletion** — There is no mechanism to prune old versions from the registry. +- **Cross-version joins** — Joining features from different versions of the same feature view in `get_historical_features` is not supported. +- **Feature services** — Feature services always resolve to the active (promoted) version. `--no-promote` versions are not served until promoted. diff --git a/docs/reference/alpha-vector-database.md b/docs/reference/alpha-vector-database.md index 861c3fcb114..61da02ce6f4 100644 --- a/docs/reference/alpha-vector-database.md +++ b/docs/reference/alpha-vector-database.md @@ -15,6 +15,7 @@ Below are supported vector databases and implemented features: | Faiss | [ ] | [ ] | [] | [] | | SQLite | [x] | [ ] | [x] | [x] | | Qdrant | [x] | [x] | [] | [] | +| ScyllaDB | [x] | [x] | [x] | [x] | *Note: V2 Support means the SDK supports retrieval of features along with vector embeddings from vector similarity search. @@ -30,7 +31,241 @@ Beyond that, we will then have `retrieve_online_documents` and `retrieve_online_ backwards compatibility and the adopt industry standard naming conventions. {% endhint %} -**Note**: Milvus and SQLite implement the v2 `retrieve_online_documents_v2` method in the SDK. This will be the longer-term solution so that Data Scientists can easily enable vector similarity search by just flipping a flag. +**Note**: Milvus, SQLite, and ScyllaDB implement the v2 `retrieve_online_documents_v2` method in the SDK. This will be the longer-term solution so that Data Scientists can easily enable vector similarity search by just flipping a flag. + +## Feature server search endpoints + +| Endpoint | Use when | +|----------|----------| +| `POST /search` | You have an embedding vector (or use `api_version: 2` with `query_string`) and want Feast's native online-features response format. | +| `GET /v1/vector_stores` | You want to discover available vector stores and their `vs_{hash}` IDs (OpenAI-compatible). | +| `GET /v1/vector_stores/{id}` | You want metadata for a specific vector store (OpenAI-compatible). | +| `POST /v1/vector_stores/{id}/search` | You want plain-text queries with server-side embedding and an OpenAI-compatible response. | + +`POST /retrieve-online-documents` is deprecated; use `POST /search` instead. + +## [Alpha] OpenAI-Compatible Vector Store API + +{% hint style="warning" %} +**Alpha feature.** This API surface is functional and tested, but may change in future releases. Feedback and contributions are welcome. +{% endhint %} + +Feast exposes a set of [OpenAI-compatible vector store endpoints](https://platform.openai.com/docs/api-reference/vector-stores) that let clients discover, inspect, and search vector stores using plain text queries with server-side embedding. This enables integration with AI agents, LLM tool-calling frameworks, and any OpenAI-compatible client without requiring the caller to produce raw embedding vectors. + +### Vector store IDs + +Each feature view with at least one `vector_index=True` field is automatically assigned a deterministic identifier of the form `vs_{hash}`, where `{hash}` is the first 24 characters of `SHA-256(project + ":" + feature_view_name)`. These IDs are stable across server restarts and registry refreshes. + +For example, a feature view named `product_catalog` in project `my_project` always maps to the same `vs_...` identifier. The listing endpoints return these IDs so clients can discover stores at runtime. + +### Endpoints + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| `GET` | `/v1/vector_stores` | `DESCRIBE` | List all vector stores the caller has access to | +| `GET` | `/v1/vector_stores/{vector_store_id}` | `DESCRIBE` | Get metadata for a single vector store | +| `POST` | `/v1/vector_stores/{vector_store_id}/search` | `READ_ONLINE` | Search a vector store with a plain text query | + +All endpoints enforce RBAC when authentication is configured. The listing endpoint filters out stores the caller cannot `DESCRIBE`. + +### Requirements + +1. **Embedding model** — an `embedding_model` section in `feature_store.yaml`. Feast uses [Sentence Transformers](https://www.sbert.net/) by default for local embedding — no external API key required (`pip install sentence-transformers`): + + ```yaml + embedding_model: + provider: sentence_transformers # default; can be omitted + model: all-MiniLM-L6-v2 + ``` + +2. **Vector-indexed feature view** — at least one feature view with `vector_index=True` on a vector field, materialized to an online store that supports vector search. + +3. **Numeric filtering (optional)** — for metadata filters that use numeric or boolean comparisons, set `enable_openai_compatible_store: true` on your online store config and run `feast apply` to add the required `value_num` column. + +### Custom embedding providers + +The built-in Sentence Transformers provider works for most use cases. To use a different embedding backend (OpenAI, Cohere, a custom model, etc.), implement the `EmbeddingProvider` protocol and pass an instance to `FeatureStore`: + +```python +from feast.embedder import EmbeddingProvider + +class MyEmbeddingProvider: + def embed(self, texts: list[str]) -> list[list[float]]: + # Call your embedding API here + return my_model.encode(texts) + + async def aembed(self, texts: list[str]) -> list[list[float]]: + return await my_model.aencode(texts) + +store = FeatureStore( + repo_path=".", + embedding_provider=MyEmbeddingProvider(), +) +``` + +### Numeric storage (`enable_openai_compatible_store`) + +By default, feature values are stored as text in the online store. This means string-ordered comparisons apply (e.g., `'9' > '100'` is `true`). When `enable_openai_compatible_store: true` is set on the online store config, Feast adds a `value_num` column that stores `int`, `float`, `double`, and `bool` values natively so that numeric filters produce correct results. + +```yaml +online_store: + type: postgres # or sqlite + # ... connection settings ... + enable_openai_compatible_store: true +``` + +After changing this setting, run `feast apply` to update the database schema. + +### List vector stores + +```bash +curl http://localhost:6566/v1/vector_stores +``` + +```json +{ + "object": "list", + "data": [ + { + "id": "vs_a1b2c3d4e5f6a1b2c3d4e5f6", + "object": "vector_store", + "name": "product_catalog", + "status": "completed", + "created_at": 1717200000 + } + ] +} +``` + +### Get a single vector store + +```bash +curl http://localhost:6566/v1/vector_stores/vs_a1b2c3d4e5f6a1b2c3d4e5f6 +``` + +Returns the same object shape as a single entry in the list response. Returns `404` if the ID does not match any vector-indexed feature view. + +### Search + +Start the feature server with `feast serve`, then send a search request: + +```bash +curl -X POST http://localhost:6566/v1/vector_stores/vs_a1b2c3d4e5f6a1b2c3d4e5f6/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "wireless noise-cancelling headphones", + "max_num_results": 5 + }' +``` + +#### Request fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `query` | `string` or `list[string]` | (required) | Plain text search query. Lists are joined with spaces before embedding. | +| `max_num_results` | `int` | `10` | Maximum number of results to return. | +| `filters` | `object` | `null` | OpenAI-style filters (see below). | +| `ranking_options` | `object` | `null` | Accepted for forward compatibility, but currently ignored. Setting `score_threshold` or `ranker` inside it will return a 422 error. | +| `rewrite_query` | `bool` | `null` | `false` (the default/no-op) is accepted. `true` is not yet supported and will return a 422 error. | +| `metadata` | `object` | `null` | Optional. `metadata.features_to_retrieve` selects specific features. | + +### Filters + +The endpoint supports OpenAI-style filters for narrowing results beyond vector similarity. + +**Comparison operators:** `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` + +```json +{"type": "eq", "key": "category", "value": "Electronics"} +``` + +**Compound operators:** `and`, `or` (nest to arbitrary depth) + +```json +{ + "type": "and", + "filters": [ + {"type": "eq", "key": "category", "value": "Electronics"}, + {"type": "gte", "key": "rating", "value": 4.5} + ] +} +``` + +For Postgres and SQLite backends, all filtering (including string equality) requires `enable_openai_compatible_store: true` in the online store config. After enabling, run `feast apply` to update the database schema. + +ScyllaDB supports vector retrieval via `retrieve_online_documents_v2`, but OpenAI-style metadata filtering is not implemented yet. Passing `filters` raises `NotImplementedError`. + +### Response format + +Responses follow the OpenAI `vector_store.search_results.page` schema: + +```json +{ + "object": "vector_store.search_results.page", + "search_query": ["wireless noise-cancelling headphones"], + "data": [ + { + "file_id": "vs_a1b2c3d4e5f6a1b2c3d4e5f6_42", + "filename": "vs_a1b2c3d4e5f6a1b2c3d4e5f6", + "score": 0.92, + "attributes": {"name": "...", "category": "..."}, + "content": [ + {"type": "text", "text": "..."} + ] + } + ], + "has_more": false, + "next_page": null +} +``` + +The `file_id` and `filename` fields use the `vs_{hash}` identifier, not raw feature view names. + +The `score` field is a higher-is-better relevance score derived from the raw vector distance using a metric-dependent conversion: + +| Distance metric | Conversion | Range | +|----------------|------------|-------| +| L2 (default) | `1 / (1 + distance)` | (0, 1] | +| Cosine | `1 - distance` | [0, 1] | +| Inner product / dot | `-distance` | varies | + +The metric is determined by `vector_search_metric` on the feature view's vector field, not by an API parameter. When `features_to_retrieve` is omitted, all non-vector features are returned by default (vector embedding columns are excluded). + +Pagination is not yet implemented; `has_more` is always `false`. + +### SDK usage + +The OpenAI-compatible search is also available directly via the Python SDK: + +```python +import asyncio +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +result = asyncio.run(store.openai_search( + vector_store_id="product_catalog", + query="wireless noise-cancelling headphones", + max_num_results=5, + filters={"type": "eq", "key": "category", "value": "Electronics"}, +)) + +for item in result["data"]: + print(f"{item['score']:.3f} {item['attributes']}") +``` + +### Supported online stores + +The OpenAI-compatible filtering has been implemented for the following online stores: + +| Online Store | Vector Search | Metadata Filtering | Notes | +|-------------|--------------|-------------------|-------| +| Milvus | Yes | Yes | Boolean expressions | +| Elasticsearch | Yes | Yes | Query DSL clauses | +| Postgres (pgvector) | Yes | Yes | Requires `enable_openai_compatible_store: true` | +| SQLite (sqlite-vec) | Yes | Yes | Requires `enable_openai_compatible_store: true` | +| MongoDB | Yes | Yes | Aggregation pipeline | +| ScyllaDB | Yes | No | Vector search only; metadata filters are not supported yet | ## Examples diff --git a/docs/reference/alpha-web-ui.md b/docs/reference/alpha-web-ui.md index 80c5b824c5a..3fe8ce052a8 100644 --- a/docs/reference/alpha-web-ui.md +++ b/docs/reference/alpha-web-ui.md @@ -35,6 +35,18 @@ Options: This will spin up a Web UI on localhost which automatically refreshes its view of the registry every `registry_ttl_sec` +#### Curl Generator Feature Server URL + +The Curl Generator uses a default Feature Server URL when building the example curl command. You can configure +this default at build time using: + +```bash +REACT_APP_FEAST_FEATURE_SERVER_URL="http://your-server:6566" +``` + +If this environment variable is not set, the UI falls back to `http://localhost:6566`. A user-edited value in +the UI is still stored in localStorage and will take precedence for that browser. + ### Importing as a module to integrate with an existing React App This is the recommended way to use Feast UI for teams maintaining their own internal UI for their deployment of Feast. @@ -141,3 +153,12 @@ const tabsRegistry = { ``` Examples of custom tabs can be found in the `ui/custom-tabs` folder. + +## Refreshing the registry + +The Feast UI caches registry data (projects, feature views, entities, etc.) using the registry cache. After running `feast apply` to make changes, it may take up to `cache_ttl_seconds` before the updates appear in the UI. + +To see changes faster: + +- **Lower the TTL**: Set `cache_ttl_seconds: 10` (or similar) in your `feature_store.yaml` registry config. This makes all registry consumers — including the UI — pick up changes within 10 seconds. +- **Refresh on demand**: The UI has a **Refresh** button that explicitly invalidates the server-side registry cache (`POST /api/v1/registry/refresh`) and reloads the UI without a full page refresh. diff --git a/docs/reference/beta-on-demand-feature-view.md b/docs/reference/beta-on-demand-feature-view.md index efb7023d567..684bc0ac4b2 100644 --- a/docs/reference/beta-on-demand-feature-view.md +++ b/docs/reference/beta-on-demand-feature-view.md @@ -69,6 +69,42 @@ def driver_aggregated_stats(inputs): Aggregated columns are automatically named using the pattern `{function}_{column}` (e.g., `sum_trips`, `mean_rating`). +### Using `input_schema` with Aggregations + +When the input data is not already stored as a feature view, use `input_schema` instead of `sources` to describe the fields that will be passed at request time. Feast will create an internal `RequestSource` automatically. + +```python +from datetime import timedelta +from feast import Field, on_demand_feature_view +from feast.aggregation import Aggregation +from feast.types import Float64, Int64 + +@on_demand_feature_view( + input_schema=[ + Field(name="txn_amount", dtype=Float64), + ], + schema=[ + Field(name="txn_count", dtype=Int64), + Field(name="total_txn_amount", dtype=Float64), + Field(name="avg_txn_amount", dtype=Float64), + ], + aggregations=[ + Aggregation(column="txn_amount", function="count", name="txn_count", + time_window=timedelta(days=30)), + Aggregation(column="txn_amount", function="sum", name="total_txn_amount", + time_window=timedelta(days=30)), + Aggregation(column="txn_amount", function="mean", name="avg_txn_amount", + time_window=timedelta(days=30)), + ], + entities=[user], +) +def user_transaction_stats(inputs): + # Aggregations replace the transformation function — no body needed. + pass +``` + +`input_schema` also accepts fields that are not aggregation columns — for example, thresholds, currency codes, or other contextual values passed at request time that your UDF needs but that are not stored as features. + ## Example See [https://github.com/feast-dev/on-demand-feature-views-demo](https://github.com/feast-dev/on-demand-feature-views-demo) for an example on how to use on demand feature views. diff --git a/docs/reference/codebase-structure.md b/docs/reference/codebase-structure.md index 80608b5929a..4783773c270 100644 --- a/docs/reference/codebase-structure.md +++ b/docs/reference/codebase-structure.md @@ -28,7 +28,7 @@ The majority of Feast logic lives in these Python files: There are also several important submodules: * `infra/` contains all the infrastructure components, such as the provider, offline store, online store, batch materialization engine, and registry. -* `dqm/` covers data quality monitoring, such as the dataset profiler. +* `dqm/` covers data quality monitoring. See [`monitoring/`](../../sdk/python/feast/monitoring/) for the built-in monitoring system. * `diff/` covers the logic for determining how to apply infrastructure changes upon feature repo changes (e.g. the output of `feast plan` and `feast apply`). * `embedded_go/` covers the Go feature server. * `ui/` contains the embedded Web UI, to be launched on the `feast ui` command. diff --git a/docs/reference/compute-engine/README.md b/docs/reference/compute-engine/README.md index dad2ede75a6..a570e5688ed 100644 --- a/docs/reference/compute-engine/README.md +++ b/docs/reference/compute-engine/README.md @@ -57,6 +57,22 @@ An example of built output from FeatureBuilder: - Supports point-in-time joins and large-scale materialization - Integrates with `SparkOfflineStore` and `SparkMaterializationJob` +### ☸️ SparkApplicationComputeEngine + +{% page-ref page="spark_application.md" %} + +- Batch materialization via Kubeflow Spark Operator `SparkApplication` CRs +- One SparkApplication per materialize call (multi–feature-view batching) +- Requires network-accessible online/offline/registry stores (no file-based backends) + +### 🌊 FlinkComputeEngine + +{% page-ref page="flink.md" %} + +- Distributed DAG execution through Apache Flink's PyFlink Table API +- Supports materialization and historical retrieval with Feast offline stores +- Integrates with `FlinkMaterializationJob` and `FlinkDAGRetrievalJob` + ### ⚡ RayComputeEngine (contrib) - Distributed DAG execution via Ray diff --git a/docs/reference/compute-engine/flink.md b/docs/reference/compute-engine/flink.md new file mode 100644 index 00000000000..0dd5560f70e --- /dev/null +++ b/docs/reference/compute-engine/flink.md @@ -0,0 +1,124 @@ +# Apache Flink + +## Description + +The Apache Flink compute engine provides a distributed execution engine for +feature pipelines through the PyFlink Table API. It implements Feast's unified +`ComputeEngine` interface and can be used for batch materialization operations +(`materialize` and `materialize-incremental`) and historical retrieval +(`get_historical_features`). + +The engine reads data through the configured Feast offline store and executes +the Feast DAG as PyFlink tables. Offline stores that expose a native +`to_flink_table(table_env)` retrieval job hand Flink tables directly to the +engine. Retrieval jobs that only expose the standard Arrow path are also +supported and are converted into Flink tables by the engine. The engine then +uses Flink Table/SQL operations for join, filter, aggregate, dedupe, and +projection steps, and writes materialization results to the configured online +and/or offline store. + +## Configuration + +Install the Flink extra from a Feast source checkout with `uv` before using the +engine: + +```bash +uv sync --extra flink --no-dev +``` + +The `flink` extra installs PyFlink directly. PyFlink currently requires +`pyarrow<21`, while the default Feast install keeps `pyarrow>=21`; Feast's uv +lock resolves the Flink extra in a separate dependency fork so normal Feast +installs do not downgrade Arrow. + +Configure the engine in `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: file +online_store: + type: sqlite + path: data/online_store.db +batch_engine: + type: flink.engine + execution_mode: batch + parallelism: 4 + table_config: + pipeline.name: "Feast Flink Compute Engine" + pandas_split_num: 4 +``` + +## Configuration Options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `type` | string | `flink.engine` | Must be `flink.engine`. | +| `execution_mode` | string | `batch` | PyFlink execution mode: `batch` or `streaming`. | +| `parallelism` | integer | `null` | Default Flink parallelism for jobs created by the engine. | +| `table_config` | map | `null` | Additional PyFlink table configuration entries. | +| `pandas_split_num` | integer | `1` | Number of PyFlink Arrow source splits when converting pandas entity DataFrames into Flink tables. | + +## Flink Transformations + +Use `mode="flink"` when a `BatchFeatureView` transformation should receive and +return PyFlink table objects: + +```python +from feast import BatchFeatureView, Field +from feast.types import Float32 + + +def double_rates(table): + # In production this can use PyFlink Table API operations and return a table. + return table + + +driver_stats = BatchFeatureView( + name="driver_stats", + entities=[driver], + mode="flink", + udf=double_rates, + schema=[Field(name="conv_rate", dtype=Float32)], + source=driver_stats_source, + online=True, +) +``` + +Flink transformations must return PyFlink table objects. pandas-returning UDFs +are not accepted by the Flink compute engine. + +## DAG Support + +The Flink engine implements Feast's compute DAG with Flink-specific nodes: + +- Source reads from Feast offline stores, preferring native Flink tables when a + retrieval job supports `to_flink_table(table_env)` and otherwise converting + Arrow results into Flink tables. +- Transform nodes pass PyFlink tables to `mode="flink"` UDFs and preserve native + Flink table outputs. +- Join nodes use Flink SQL temporary views for feature joins and entity joins. +- Filter nodes apply point-in-time, TTL, and custom filter expressions in Flink + SQL. +- Aggregate nodes support non-windowed Feast aggregations using Flink SQL + aggregate functions. +- Dedupe nodes use `ROW_NUMBER()` over entity keys or internal entity-row ids so + historical retrieval keeps one latest feature row per entity row. +- Validation nodes check required output columns. JSON value validation must be + handled upstream in Flink SQL. +- Output nodes write only for materialization tasks; historical retrieval is + read-only. +- Historical retrieval accepts pandas entity DataFrames and SQL-string entity + DataFrames. SQL strings are interpreted as Flink SQL queries against the + configured TableEnvironment/catalog and must select an `event_timestamp` + column. + +## Current Limitations + +- Windowed aggregations are not yet implemented in the Flink compute engine. Use + non-windowed Feast aggregations or pre-window upstream in Flink. +- JSON value validation is not implemented inside the Flink compute engine + because the engine does not collect intermediate data out of Flink for + validation. diff --git a/docs/reference/compute-engine/ray.md b/docs/reference/compute-engine/ray.md index 22b1e1a4700..19604e21788 100644 --- a/docs/reference/compute-engine/ray.md +++ b/docs/reference/compute-engine/ray.md @@ -28,6 +28,7 @@ The Ray compute engine provides: - **Lazy Evaluation**: Deferred execution for optimal performance - **Resource Management**: Automatic scaling and resource optimization - **Point-in-Time Joins**: Efficient temporal joins for historical feature retrieval +- **GPU Support**: Schedule transformation workers on GPU nodes via `num_gpus` config (all modes including KubeRay) ## Architecture @@ -87,6 +88,9 @@ batch_engine: | `enable_distributed_joins` | boolean | true | Enable distributed joins for large datasets | | `staging_location` | string | None | Remote path for batch materialization jobs | | `ray_conf` | dict | None | Ray configuration parameters (memory, CPU limits) | +| `num_gpus` | float | None | Number of GPUs to request per worker task. Requires GPU nodes in the Ray cluster. Fractional values (e.g. `0.5`) are supported. Supported in all modes including KubeRay. | +| `gpu_batch_format` | string | `"pandas"` | Batch format for `map_batches` when `num_gpus` is set. Use `"numpy"` or `"pyarrow"` for GPU-native libraries (e.g. cuDF, PyTorch). | +| `worker_task_options` | dict | None | Arbitrary Ray `.options()` kwargs applied to every worker task. See [Worker Resource Scheduling](#worker-resource-scheduling) for the full reference. | ### Mode Detection Precedence @@ -344,6 +348,7 @@ import ray # Check cluster resources resources = ray.cluster_resources() print(f"Available CPUs: {resources.get('CPU', 0)}") +print(f"Available GPUs: {resources.get('GPU', 0)}") print(f"Available memory: {resources.get('memory', 0) / 1e9:.2f} GB") # Monitor job progress @@ -351,6 +356,89 @@ job = store.get_historical_features(...) # Ray compute engine provides built-in progress tracking ``` +## Worker Resource Scheduling + +`worker_task_options` is a passthrough dict of [Ray `.options()` kwargs](https://docs.ray.io/en/latest/ray-core/api/doc/ray.remote_function.RemoteFunction.options.html) applied to every worker task Feast dispatches. It pairs with `ray_conf` (cluster-level `ray.init` options) — `worker_task_options` targets individual worker tasks. Options are forwarded at two levels so Ray schedules correctly: + +1. On the `@ray.remote` orchestration task via `.options(**worker_task_options)` — controls node selection. +2. Inside `map_batches` for the scheduling-relevant subset (`num_gpus`, `num_cpus`, `accelerator_type`, `resources`) — controls which nodes run the data workers. + +This is supported across **all execution modes**: local, remote, and KubeRay. + +### Common `worker_task_options` keys + +| Key | Type | Description | +|-----|------|-------------| +| `num_cpus` | float | CPUs per task (default: 1). Fractional values supported. | +| `memory` | int | Heap memory in **bytes** (e.g. `8589934592` for 8 GB). | +| `accelerator_type` | string | Pin tasks to a specific GPU model — `"A100"`, `"T4"`, `"V100"`, etc. Useful on KubeRay clusters with mixed GPU node pools. | +| `resources` | dict | Custom/Kubernetes extended resource labels, e.g. `{"intel.com/gpu": 1}`. | +| `runtime_env` | dict | Per-task [Ray runtime environment](https://docs.ray.io/en/latest/ray-core/handling-dependencies.html) — `pip`, `conda`, `env_vars`, `working_dir`, etc. For KubeRay, use this to install packages on worker pods without rebuilding images. | +| `max_retries` | int | Task retry count on worker failure (default: 3). | +| `scheduling_strategy` | string | `"DEFAULT"`, `"SPREAD"`, or a placement group strategy. | + +> For the full list of supported keys see the [Ray RemoteFunction.options() API docs](https://docs.ray.io/en/latest/ray-core/api/doc/ray.remote_function.RemoteFunction.options.html). + +### GPU support + +`num_gpus` is the only first-class GPU field because it also drives `gpu_batch_format` selection inside Feast. Set it directly rather than inside `worker_task_options`: + +```yaml +batch_engine: + type: ray.engine + num_gpus: 1 # GPUs per task (fractional values like 0.5 supported) + gpu_batch_format: numpy # numpy/pyarrow for GPU-native libs (cuDF, PyTorch) +``` + +When `num_gpus` is set your transformation UDF runs on a GPU worker: + +```python +import cudf # RAPIDS cuDF – GPU-accelerated DataFrame library + +def gpu_transform(batch): + gpu_df = cudf.from_pandas(batch) + gpu_df["score"] = gpu_df["raw_value"] * 2.0 + return gpu_df.to_pandas() +``` + +### Full example — KubeRay with GPU + all common options + +```yaml +batch_engine: + type: ray.engine + use_kuberay: true + kuberay_conf: + cluster_name: "feast-gpu-cluster" + namespace: "feast-system" + auth_token: "${RAY_AUTH_TOKEN}" + auth_server: "https://api.openshift.com:6443" + num_gpus: 1 + gpu_batch_format: numpy + worker_task_options: + num_cpus: 4 + memory: 8589934592 # 8 GB + accelerator_type: "A100" # pin to A100 nodes on mixed GPU pool + max_retries: 5 + runtime_env: + pip: + - cudf-cu12==24.10.0 + - torch==2.4.0 + env_vars: + CUDA_VISIBLE_DEVICES: "0" +``` + +### Checking cluster resources + +```python +import ray + +ray.init(address="auto") +resources = ray.cluster_resources() +print(f"Available CPUs: {resources.get('CPU', 0)}") +print(f"Available GPUs: {resources.get('GPU', 0)}") +print(f"Available memory: {resources.get('memory', 0) / 1e9:.2f} GB") +``` + ## Integration Examples ### With Spark Offline Store diff --git a/docs/reference/compute-engine/snowflake.md b/docs/reference/compute-engine/snowflake.md index e7b0dc5bd63..f6c633a4e40 100644 --- a/docs/reference/compute-engine/snowflake.md +++ b/docs/reference/compute-engine/snowflake.md @@ -24,5 +24,10 @@ batch_engine: role: sysadmin warehouse: demo_wh database: FEAST + python_udf_runtime_version: "3.10" ``` {% endcode %} + +## Configuration + +* `python_udf_runtime_version` *(optional, default: `"3.10"`)* -- The Snowflake Python UDF `RUNTIME_VERSION` used when Feast deploys its materialization UDFs. Snowflake periodically decommissions old Python UDF runtimes (for example, the 3.9 runtime was decommissioned, requiring Feast to bump its default to 3.10 -- see [#6606](https://github.com/feast-dev/feast/issues/6606)). If Snowflake decommissions the 3.10 runtime in the future, set this field to a still-supported version (e.g. `"3.11"`) instead of waiting for a new Feast release. diff --git a/docs/reference/compute-engine/spark_application.md b/docs/reference/compute-engine/spark_application.md new file mode 100644 index 00000000000..0c071d1ed4f --- /dev/null +++ b/docs/reference/compute-engine/spark_application.md @@ -0,0 +1,182 @@ +# SparkApplication Compute Engine + +## Description + +The **SparkApplication** compute engine runs Feast **batch materialization** on Kubernetes by creating a [Kubeflow Spark Operator](https://github.com/kubeflow/spark-operator) `SparkApplication` custom resource for each materialization job. + +Unlike the in-process [`spark.engine`](spark.md) compute engine (which uses a Spark session inside the Feast process), `spark_application` submits work to the Spark Operator. The operator starts a driver pod and executors from your configured image; Feast polls the SparkApplication until it completes. + +| Capability | Supported | +|------------|-----------| +| `materialize` / `materialize-incremental` | Yes | +| Multiple feature views in one job | Yes — one SparkApplication per materialize call | +| `get_historical_features` | Not yet | +| SparkConnect | Separate approach — not this engine | + +### Design + +1. Feast creates a ConfigMap with job tasks and a driver copy of `feature_store.yaml`. +2. Feast creates a `SparkApplication` CR pointing at the driver entrypoint (`main.py` in the image). +3. Inside the pod, the batch engine type is rewritten to `spark.engine` so materialization uses the Spark session created by `spark-submit` (avoids recursive SparkApplication creation). +4. The driver writes features to your configured **online store** and updates the **registry** (same network backends as the server). + +### Requirements + +- Kubeflow Spark Operator installed and watching the target namespace. +- A container **image** that includes the Feast SDK, PySpark, and clients for your stores. See the reference [Dockerfile](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/infra/compute_engines/spark_application/Dockerfile). +- **Network-accessible** online store, offline store, and registry. File-based backends are rejected because Spark pods have an ephemeral filesystem: + +| Rejected | Examples | Use instead | +|----------|----------|-------------| +| File online | `sqlite`, `faiss` | Redis, remote online, etc. | +| File offline | `dask`, `file`, `duckdb` | `spark`, Postgres, Snowflake, BigQuery, etc. | +| File registry | `file` | SQL registry, Snowflake | + +For distributed reads, configure `offline_store.type: spark` (or another store Spark can read efficiently). + +### Kubernetes / Feast Operator notes + +When using the Feast Operator: + +- Point `spec.batchEngine.configMapRef` at a ConfigMap whose `type` is `spark_application` (see [Guide 6 — Batch Engine & Scheduled Jobs](../../how-to-guides/feast-operator/06-batch-and-jobs.md)). +- The operator auto-creates RBAC for the `spark_application` batch engine (server and driver service accounts). +- Set `spec.services.initImage` if init / `feast-apply` containers need the Spark-capable image. + +--- + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_project +registry: + registry_type: sql + path: postgresql+psycopg://feast:****@postgres:5432/feast +online_store: + type: redis + connection_string: redis:6379 +offline_store: + type: spark + spark_conf: + spark.master: local[*] +batch_engine: + type: spark_application + image: my-registry.example.com/feast-spark-driver:latest + namespace: feast + spark_version: "4.0.1" + driver_cores: 1 + driver_memory: "2g" + executor_instances: 2 + executor_cores: 1 + executor_memory: "2g" + spark_conf: + spark.sql.shuffle.partitions: "100" +``` +{% endcode %} + +### Feast Operator ConfigMap + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: feast-spark-batch-engine + namespace: feast +data: + config: | + type: spark_application + image: my-registry.example.com/feast-spark-driver:latest + namespace: feast + executor_instances: 2 + driver_memory: "2g" + executor_memory: "2g" +--- +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: feast + namespace: feast +spec: + feastProject: my_project + batchEngine: + configMapRef: + name: feast-spark-batch-engine + configMapKey: config +``` + +--- + +## Remote materialization + +If the client uses a **remote** online store (`online_store.type: remote`), `FeatureStore.materialize()` delegates to the feature server HTTP API. The server runs the SparkApplication engine. + +- Default (`run_async=False`): block until the server finishes sync materialization. +- `run_async=True`: accept asynchronously (`?async=true`); poll feature-view state in the registry for completion. +- `force=True` (with `run_async=True`): override stuck `MATERIALIZING` state on the server. + +```python +from datetime import datetime, timedelta +from feast import FeatureStore + +store = FeatureStore(repo_path=".") # client feature_store.yaml with online_store.type: remote + +store.materialize( + start_date=datetime.utcnow() - timedelta(days=1), + end_date=datetime.utcnow(), +) +``` + +--- + +## Configuration reference + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `type` | string | `spark_application` | Engine type key | +| `image` | string | **required** | Container image for the Spark driver/executors | +| `image_pull_secrets` | list[str] | `[]` | Image pull secret names | +| `namespace` | string | `default` | Namespace for SparkApplication and ConfigMap | +| `service_account` | string | `""` | Driver service account; empty uses platform/operator default | +| `spark_version` | string | `4.0.1` | Spark version for the CR | +| `driver_cores` | int | `1` | Driver cores | +| `driver_memory` | string | `1g` | Driver memory | +| `executor_instances` | int | `1` | Number of executors | +| `executor_cores` | int | `1` | Cores per executor | +| `executor_memory` | string | `1g` | Memory per executor | +| `spark_conf` | dict | `null` | Extra Spark configuration | +| `hadoop_conf` | dict | `null` | Extra Hadoop configuration | +| `env` | list[dict] | `[]` | Driver env vars (`name` + `value` or `valueFrom`) | +| `env_from` | list[dict] | `[]` | EnvFrom sources | +| `queue_name` | string | `null` | Optional queue / Kueue label | +| `job_timeout_seconds` | int | `3600` | Max wait for SparkApplication completion | +| `poll_interval_seconds` | int | `10` | Status poll interval | +| `ttl_seconds_after_finished` | int | `3600` | CR TTL after finish | +| `restart_policy` | string | `Never` | SparkApplication restart policy | +| `max_retries` | int | `3` | Retries when restart policy allows | +| `concurrency` | int | `1` | Parallel feature views inside one driver | +| `labels` | dict | `{}` | Extra labels on the CR | +| `volumes` / `volume_mounts` | list | `[]` | Extra volumes for the driver | +| `py_files` | list[str] | `[]` | Additional Python files for Spark | +| `node_selector` | dict | `null` | Pod node selector | +| `tolerations` | list | `[]` | Pod tolerations | +| `staging_location` | string | `null` | Reserved for historical retrieval (ignored for materialize) | + +--- + +## Troubleshooting + +| Symptom | What to check | +|---------|----------------| +| SparkApplication Pending / insufficient CPU | Lower resource requests via `spark_conf` (for example `spark.kubernetes.driver.request.cores`) or free cluster capacity | +| ImagePullBackOff | Image name, tag, and `image_pull_secrets` | +| 403 on ConfigMap or SparkApplication | RBAC for the Feast server and Spark driver service accounts | +| Init `ValueError` about file-based stores | Switch online/offline/registry to network backends | +| Init / feast-apply failures missing Spark deps | Use a Spark-capable image (`initImage` with the Feast Operator) | + +--- + +## Related + +- [Spark compute engine (in-process)](spark.md) +- [Feast Operator — batch engine ConfigMap](../../how-to-guides/feast-operator/06-batch-and-jobs.md) +- [Creating a custom compute engine](../../how-to-guides/customizing-feast/creating-a-custom-compute-engine.md) diff --git a/docs/reference/data-sources/README.md b/docs/reference/data-sources/README.md index 151a948d0af..33e47672dcc 100644 --- a/docs/reference/data-sources/README.md +++ b/docs/reference/data-sources/README.md @@ -42,6 +42,10 @@ Please see [Data Source](../../getting-started/concepts/data-ingestion.md) for a [spark.md](spark.md) {% endcontent-ref %} +{% content-ref url="iceberg.md" %} +[iceberg.md](iceberg.md) +{% endcontent-ref %} + {% content-ref url="postgres.md" %} [postgres.md](postgres.md) {% endcontent-ref %} @@ -57,3 +61,19 @@ Please see [Data Source](../../getting-started/concepts/data-ingestion.md) for a {% content-ref url="clickhouse.md" %} [clickhouse.md](clickhouse.md) {% endcontent-ref %} + +{% content-ref url="athena.md" %} +[athena.md](athena.md) +{% endcontent-ref %} + +{% content-ref url="oracle.md" %} +[oracle.md](oracle.md) +{% endcontent-ref %} + +{% content-ref url="ray.md" %} +[ray.md](ray.md) +{% endcontent-ref %} + +{% content-ref url="mongodb.md" %} +[mongodb.md](mongodb.md) +{% endcontent-ref %} diff --git a/docs/reference/data-sources/athena.md b/docs/reference/data-sources/athena.md new file mode 100644 index 00000000000..d3ca67dcb10 --- /dev/null +++ b/docs/reference/data-sources/athena.md @@ -0,0 +1,37 @@ +# Athena source (contrib) + +## Description + +Athena data sources are AWS Athena tables or views. +These can be specified either by a table reference or a SQL query. + +## Disclaimer + +The Athena data source does not achieve full test coverage. +Please do not assume complete stability. + +## Examples + +Defining an Athena source: + +```python +from feast.infra.offline_stores.contrib.athena_offline_store.athena_source import ( + AthenaSource, +) + +driver_stats_source = AthenaSource( + name="driver_hourly_stats", + table="driver_hourly_stats", + database="my_database", + data_source="AwsDataCatalog", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) +``` + +The full set of configuration options is available [here](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.athena_offline_store.athena_source.AthenaSource). + +## Supported Types + +Athena data sources support standard Athena types mapped through the AWS Athena API. +For a comparison against other batch data sources, please see [here](overview.md#functionality-matrix). diff --git a/docs/reference/data-sources/iceberg.md b/docs/reference/data-sources/iceberg.md new file mode 100644 index 00000000000..6402a7ec419 --- /dev/null +++ b/docs/reference/data-sources/iceberg.md @@ -0,0 +1,161 @@ +# Iceberg source (contrib) + +## Description + +Iceberg data sources are tables managed by any supported Iceberg catalog. The `IcebergSource` class provides a unified interface with a configurable `catalog_type` parameter: + +- **`"rest"`** (default): [Apache Iceberg REST Catalog specification](https://iceberg.apache.org/concepts/catalog/#decoupling-using-the-rest-catalog) — Unity Catalog, Apache Polaris, Nessie, Snowflake Open Catalog +- **`"hive"`**: Hive Metastore catalog +- **`"glue"`**: AWS Glue catalog +- **`"sql"`**: SQL-based (JDBC) catalog +- **`"dynamodb"`**: DynamoDB-based catalog + +The data source carries catalog connection details (catalog_type, endpoint, warehouse, namespace, table, authentication). When the offline store (DuckDB, Spark) encounters this source, it resolves table metadata and credentials via the configured catalog at query time. + +## Examples + +### IcebergSource (REST catalog) + +Works with any Iceberg REST Catalog: + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import IcebergSource + +my_source = IcebergSource( + catalog_type="rest", # default + endpoint="http://localhost:8081/api/2.1/unity-catalog/iceberg", + warehouse="unity", + namespace="default", + table="driver_features", + timestamp_field="event_timestamp", + token_env_var="UC_TOKEN", +) +``` + +### IcebergSource (Hive Metastore) + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import IcebergSource + +my_source = IcebergSource( + catalog_type="hive", + catalog_properties={"uri": "thrift://metastore:9083"}, + warehouse="my_warehouse", + namespace="default", + table="driver_features", + timestamp_field="event_timestamp", +) +``` + +### IcebergSource (AWS Glue) + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import IcebergSource + +my_source = IcebergSource( + catalog_type="glue", + catalog_properties={"region_name": "us-east-1"}, + warehouse="my_account", + namespace="my_database", + table="driver_features", + timestamp_field="event_timestamp", +) +``` + +### UnityCatalogSource (with governance) {#unity-catalog-source} + +Extends `IcebergSource` with Unity Catalog governance: + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import ( + UnityCatalogSource, +) + +my_uc_source = UnityCatalogSource( + warehouse="production", + namespace="ml_features", + table="driver_stats", + timestamp_field="event_timestamp", + register_as_feature_table=True, # Register in UC on feast apply + sync_lineage=True, # Record lineage in UC +) +``` + +When `endpoint` is omitted, it defaults to `{DATABRICKS_HOST}/api/2.1/unity-catalog/iceberg`. +When `token_env_var` is omitted, it defaults to `DATABRICKS_TOKEN`. + +### Full Feature View Example + +```python +from datetime import timedelta + +from feast import Entity, FeatureView, Field +from feast.types import Float64, Int64 + +from feast.infra.data_sources.contrib.iceberg_catalog import ( + UnityCatalogSource, +) + +driver = Entity(name="driver_id", join_keys=["driver_id"]) + +driver_stats_source = UnityCatalogSource( + warehouse="production", + namespace="ml_features", + table="driver_hourly_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + source=driver_stats_source, + schema=[ + Field(name="conv_rate", dtype=Float64), + Field(name="acc_rate", dtype=Float64), + Field(name="avg_daily_trips", dtype=Int64), + ], + ttl=timedelta(days=1), + online=True, +) +``` + +## Configuration Reference + +### IcebergSource + +| Parameter | Type | Description | +| :--- | :--- | :--- | +| `catalog_type` | `str` | Catalog backend: `"rest"` (default), `"hive"`, `"glue"`, `"sql"`, `"dynamodb"` | +| `endpoint` | `str` | Catalog endpoint URL (required for `"rest"`, optional for others) | +| `warehouse` | `str` | Catalog/warehouse name | +| `namespace` | `str` | Schema/namespace within the catalog | +| `table` | `str` | Table name | +| `catalog_properties` | `dict` | Additional catalog-specific properties passed to PyIceberg | +| `timestamp_field` | `str` | Event timestamp column for point-in-time joins | +| `created_timestamp_column` | `str` | Optional column indicating row creation time | +| `token_env_var` | `str` | Environment variable name holding the auth token | +| `credential_vending` | `bool` | Whether to request scoped credentials (default: `True`) | +| `field_mapping` | `dict` | Column name mapping from source to feature names | + +### UnityCatalogSource (additional parameters) + +| Parameter | Type | Description | +| :--- | :--- | :--- | +| `register_as_feature_table` | `bool` | Register as UC feature table on `feast apply` (default: `True`) | +| `sync_lineage` | `bool` | Sync lineage metadata to Unity Catalog (default: `True`) | + +## Supported Types + +| Iceberg Type | Feast Type | +| :--- | :--- | +| `boolean` | `BOOL` | +| `int` | `INT32` | +| `long` | `INT64` | +| `float` | `FLOAT` | +| `double` | `DOUBLE` | +| `string` | `STRING` | +| `binary` | `BYTES` | +| `timestamp` / `timestamptz` | `INT64` | +| `decimal` | `DOUBLE` | +| `uuid` | `STRING` | diff --git a/docs/reference/data-sources/kafka.md b/docs/reference/data-sources/kafka.md index 8794c7a1e81..dd7203a6149 100644 --- a/docs/reference/data-sources/kafka.md +++ b/docs/reference/data-sources/kafka.md @@ -72,4 +72,4 @@ def driver_hourly_stats_stream(df: DataFrame): ``` ### Ingesting data -See [here](https://github.com/feast-dev/streaming-tutorial) for a example of how to ingest data from a Kafka source into Feast. +See [here](https://github.com/feast-dev/streaming-tutorial) for an example of how to ingest data from a Kafka source into Feast. diff --git a/docs/reference/data-sources/kinesis.md b/docs/reference/data-sources/kinesis.md index f2adadfec03..09706617da9 100644 --- a/docs/reference/data-sources/kinesis.md +++ b/docs/reference/data-sources/kinesis.md @@ -71,4 +71,4 @@ def driver_hourly_stats_stream(df: DataFrame): ``` ### Ingesting data -See [here](https://github.com/feast-dev/streaming-tutorial) for a example of how to ingest data from a Kafka source into Feast. The approach used in the tutorial can be easily adapted to work for Kinesis as well. +See [here](https://github.com/feast-dev/streaming-tutorial) for an example of how to ingest data from a Kafka source into Feast. The approach used in the tutorial can be easily adapted to work for Kinesis as well. diff --git a/docs/reference/data-sources/mongodb.md b/docs/reference/data-sources/mongodb.md new file mode 100644 index 00000000000..8902affd3c5 --- /dev/null +++ b/docs/reference/data-sources/mongodb.md @@ -0,0 +1,103 @@ +# MongoDB source (contrib) + +## Description + +MongoDB data sources are [MongoDB](https://www.mongodb.com/) collections that can be used as a source for feature data. The `MongoDBSource` points at a MongoDB collection and provides the metadata Feast needs to read historical features from the offline store's collection. + +## Examples + +Defining a MongoDB source: + +```python +from feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb import ( + MongoDBSource, +) + +driver_stats_source = MongoDBSource( + name="driver_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created_at", +) +``` + +The `name` field becomes the `feature_view` discriminator stored in every document in the `feature_history` collection. + +Configuration options such as `connection_string`, `database`, and `collection` are inherited from the offline store configuration in `feature_store.yaml`. + +The full set of configuration options is available [here](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBSource). + +## Vector Search + +The MongoDB online store supports [MongoDB Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/), enabling similarity search over feature embeddings stored in MongoDB. This is powered by the `$vectorSearch` aggregation stage and supports MongoDB Atlas, self-hosted MongoDB with Atlas Search indexes, and the `mongodb/mongodb-atlas-local` Docker image for local development. + +### Configuration + +Enable vector search in your `feature_store.yaml`: + +```yaml +project: my_project +provider: local +online_store: + type: mongodb + connection_string: mongodb+srv://:@cluster.mongodb.net # pragma: allowlist secret + vector_enabled: true + similarity: cosine # cosine | euclidean | dotProduct + vector_index_wait_timeout: 60 # seconds to wait for index to become queryable + vector_index_wait_poll_interval: 1.0 # seconds between polls +``` + +### Defining a Feature View with Vector Index + +Mark embedding fields with `vector_index=True` and specify `vector_length`: + +```python +from feast import Entity, FeatureView, Field, FileSource +from feast.types import Array, Float32, Int64, String +from datetime import timedelta + +item_embeddings = FeatureView( + name="item_embeddings", + entities=[Entity(name="item_id", join_keys=["item_id"])], + schema=[ + Field( + name="embedding", + dtype=Array(Float32), + vector_index=True, + vector_length=384, + vector_search_metric="cosine", + ), + Field(name="title", dtype=String), + Field(name="item_id", dtype=Int64), + ], + source=FileSource(path="items.parquet", timestamp_field="event_timestamp"), + ttl=timedelta(hours=24), +) +``` + +When `feast apply` (or `store.update()`) runs with `vector_enabled=True`, MongoDB vector search indexes are automatically created for any field with `vector_index=True`. Indexes are also automatically dropped when feature views are removed. + +### Retrieving Documents via Vector Search + +Use `retrieve_online_documents_v2()` to perform similarity search: + +```python +store = FeatureStore(repo_path=".") +results = store.retrieve_online_documents_v2( + features=["item_embeddings:embedding", "item_embeddings:title"], + query=[0.1, 0.2, ...], # query vector + top_k=5, +) +``` + +### How It Works + +- **Index creation**: `update()` creates a MongoDB vector search index named `____vs_index` for each vector-indexed field. It waits for the index to reach `READY` status before proceeding. +- **Query execution**: `retrieve_online_documents_v2()` builds a `$vectorSearch` aggregation pipeline with `numCandidates = max(top_k * 10, 100)` and the specified `limit`. +- **Score**: Results include a `distance` field populated from `$meta: "vectorSearchScore"`. +- **BSON compatibility**: Query vectors are coerced to native Python floats to avoid numpy serialization issues. +- **Idempotency**: Calling `update()` multiple times will not duplicate indexes. + +## Supported Types + +MongoDB data sources support all eight primitive types (`bytes`, `string`, `int32`, `int64`, `float32`, `float64`, `bool`, `timestamp`) and their corresponding array types. Complex types such as `Map` and `Struct` are preserved through the MongoDB document model. +For a comparison against other batch data sources, please see [here](overview.md#functionality-matrix). diff --git a/docs/reference/data-sources/oracle.md b/docs/reference/data-sources/oracle.md new file mode 100644 index 00000000000..48c249ba3b8 --- /dev/null +++ b/docs/reference/data-sources/oracle.md @@ -0,0 +1,36 @@ +# Oracle source (contrib) + +## Description + +Oracle data sources are Oracle database tables. +These are specified by a table reference (e.g. `"TRANSACTION_FEATURES"` or `"SCHEMA.TABLE"`). + +## Disclaimer + +The Oracle data source does not achieve full test coverage. +Please do not assume complete stability. + +## Examples + +Defining an Oracle source: + +```python +from feast.infra.offline_stores.contrib.oracle_offline_store.oracle_source import ( + OracleSource, +) + +driver_stats_source = OracleSource( + name="driver_hourly_stats", + table_ref="DRIVER_HOURLY_STATS", + event_timestamp_column="EVENT_TIMESTAMP", + created_timestamp_column="CREATED", +) +``` + +**Note:** Oracle stores unquoted identifiers in uppercase. Reference columns using the casing shown by Oracle (e.g. `USER_ID` for unquoted identifiers). + +## Supported Types + +Oracle data sources support standard Oracle numeric, string, date, and timestamp types mapped through the ibis Oracle backend. +For a comparison against other batch data sources, please see [here](overview.md#functionality-matrix). + diff --git a/docs/reference/data-sources/overview.md b/docs/reference/data-sources/overview.md index 7cffc4154dd..5cc5285f77e 100644 --- a/docs/reference/data-sources/overview.md +++ b/docs/reference/data-sources/overview.md @@ -5,7 +5,7 @@ In Feast, each batch data source is associated with corresponding offline stores. For example, a `SnowflakeSource` can only be processed by the Snowflake offline store, while a `FileSource` can be processed by both File and DuckDB offline stores. Otherwise, the primary difference between batch data sources is the set of supported types. -Feast has an internal type system, and aims to support eight primitive types (`bytes`, `string`, `int32`, `int64`, `float32`, `float64`, `bool`, and `timestamp`) along with the corresponding array types. +Feast has an internal type system that supports primitive types (`bytes`, `string`, `int32`, `int64`, `float32`, `float64`, `bool`, `timestamp`), array types, set types, map/JSON types, and struct types. However, not every batch data source supports all of these types. For more details on the Feast type system, see [here](../type-system.md). @@ -29,3 +29,9 @@ Below is a matrix indicating which data sources support which types. | `bool` | yes | yes | yes | yes | yes | yes | yes | yes | | `timestamp` | yes | yes | yes | yes | yes | yes | yes | yes | | array types | yes | yes | yes | no | yes | yes | yes | no | +| `Map` | yes | no | yes | yes | yes | yes | yes | no | +| `Json` | yes | yes | yes | yes | yes | no | no | no | +| `Struct` | yes | yes | no | no | yes | yes | no | no | +| set types | yes* | no | no | no | no | no | no | no | + +\* **Set types** are defined in Feast's proto and Python type system but are **not inferred** by any backend. They must be explicitly declared in the feature view schema and are best suited for online serving use cases. See [Type System](../type-system.md#set-types) for details. diff --git a/docs/reference/data-sources/ray.md b/docs/reference/data-sources/ray.md new file mode 100644 index 00000000000..30d4880a58a --- /dev/null +++ b/docs/reference/data-sources/ray.md @@ -0,0 +1,233 @@ +# Ray Data Source (contrib) + +> **⚠️ Contrib Plugin:** +> `RaySource` is a contributed plugin shipped alongside the [Ray offline store](../offline-stores/ray.md). It may not be as stable or fully supported as core data sources. + +`RaySource` is a pure-metadata descriptor that tells Feast **how** to load a +[Ray Dataset](https://docs.ray.io/en/latest/data/api/dataset.html) from any +source that Ray Data supports natively — Parquet, CSV, JSON, HuggingFace +Datasets, MongoDB, binary files, images, TFRecords, and more. + +It is the recommended data source when using the +[Ray offline store](../offline-stores/ray.md) and replaces the need for +`FileSource` for all non-Parquet and non-file-based data. + +--- + +## When to use RaySource vs FileSource + +| Scenario | Recommended source | +|---|---| +| Parquet files on disk / S3 / GCS (existing setup) | `FileSource` (backward compatible) | +| Parquet via Ray reader (pipelines, remote auth) | `RaySource(reader_type="parquet")` | +| CSV, JSON, text, images via Ray | `RaySource` | +| HuggingFace `datasets` library | `RaySource(reader_type="huggingface")` | +| MongoDB, SQL, TFRecords, WebDataset | `RaySource` | + +--- + +## Installation + +`RaySource` is bundled with the Ray offline store contrib package: + +```bash +pip install 'feast[ray]' +``` + +--- + +## Supported `reader_type` values + +| `reader_type` | Underlying Ray API | Notes | +|---|---|---| +| `parquet` | `ray.data.read_parquet` | S3, GCS, HDFS, local | +| `csv` | `ray.data.read_csv` | | +| `json` | `ray.data.read_json` | | +| `text` | `ray.data.read_text` | | +| `images` | `ray.data.read_images` | | +| `binary_files` | `ray.data.read_binary_files` | | +| `tfrecords` | `ray.data.read_tfrecords` | | +| `webdataset` | `ray.data.read_webdataset` | | +| `huggingface` | `ray.data.from_huggingface` | Wraps `datasets.load_dataset` | +| `mongo` | `ray.data.read_mongo` | | +| `sql` | `ray.data.read_sql` | Pass `connection_url` in `reader_options` | + +--- + +## Configuration + +### Parameters + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `name` | `str` | Yes | Unique name for this data source | +| `reader_type` | `str` | Yes | One of the supported reader types above | +| `path` | `str` | No | File or directory path (required for file-based readers) | +| `reader_options` | `dict` | No | Extra keyword arguments forwarded to the Ray reader | +| `timestamp_field` | `str` | No | Column containing event timestamps | +| `created_timestamp_column` | `str` | No | Column containing row creation timestamps | +| `tags` | `dict` | No | Arbitrary key-value metadata | +| `description` | `str` | No | Human-readable description | +| `owner` | `str` | No | Owning team or contact | + +--- + +## Usage examples + +### Parquet on S3 + +```python +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +driver_stats = RaySource( + name="driver_stats_parquet", + reader_type="parquet", + path="s3://my-bucket/driver_stats/", + timestamp_field="event_timestamp", +) +``` + +### CSV + +```python +sensor_readings = RaySource( + name="sensor_readings_csv", + reader_type="csv", + path="/data/sensors/", + timestamp_field="ts", +) +``` + +### HuggingFace dataset + +Load a dataset from the [HuggingFace Hub](https://huggingface.co/datasets) +directly into Feast. + +```python +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +cheque_images = RaySource( + name="cheque_images_hf", + reader_type="huggingface", + reader_options={ + "dataset_name": "cheques_sample_data", + "split": "train", + }, + timestamp_field="event_timestamp", +) +``` + +### MongoDB + +```python +transaction_log = RaySource( + name="transactions_mongo", + reader_type="mongo", + reader_options={ + "uri": "mongodb://localhost:27017", + "database": "featuredb", + "collection": "transactions", + }, + timestamp_field="created_at", +) +``` + +### SQL (via connection URL) + +```python +user_features = RaySource( + name="user_features_sql", + reader_type="sql", + reader_options={ + "connection_url": "postgresql+psycopg2://user:password@host:5432/db", # pragma: allowlist secret + "query": "SELECT * FROM user_features", + }, + timestamp_field="event_timestamp", +) +``` + +--- + +## Using RaySource in a BatchFeatureView + +```python +from datetime import timedelta +from feast import BatchFeatureView, Entity, Field +from feast.types import Float32, Int64, String +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +cheque = Entity(name="cheque_id", description="Unique cheque identifier") + +cheque_source = RaySource( + name="cheque_images_hf", + reader_type="huggingface", + reader_options={ + "dataset_name": "cheques_sample_data", + "split": "train", + }, + timestamp_field="event_timestamp", +) + +cheque_ocr_fv = BatchFeatureView( + name="cheque_ocr_features", + entities=[cheque], + ttl=timedelta(days=365), + schema=[ + Field(name="cheque_id", dtype=Int64), + Field(name="payee_name", dtype=String), + Field(name="amount", dtype=String), + Field(name="bank_name", dtype=String), + Field(name="raw_text", dtype=String), + ], + source=cheque_source, +) +``` + +--- + +## Retrieving data as a Ray Dataset + +Once the feature view is materialised you can retrieve the offline features +directly as a Ray Dataset using the first-class `to_ray_dataset()` method: + +```python +from feast import FeatureStore + +store = FeatureStore(".") + +# Chain directly on the retrieval job — to_ray_dataset() is a first-class +# method on every RetrievalJobs. +ds = store.get_historical_features( + features=["cheque_ocr_features:payee_name", "cheque_ocr_features:amount"], + entity_df=entity_df, +).to_ray_dataset() + +# Use the dataset downstream in Ray or ML pipelines +ds.show(3) +``` + +--- + +## Proto serialisation + +`RaySource` is fully serialisable to Feast's protobuf registry format. The +`reader_type`, `path`, and `reader_options` dict are all persisted and can be +round-tripped via `to_proto()` / `from_proto()`. + +--- + +## Limitations + +* The Ray offline store (and therefore `RaySource`) requires `feast[ray]`. +* `reader_type="sql"` requires a serialisable `connection_url`; raw + `sqlalchemy.engine.Engine` objects cannot be pickled across Ray workers. +* Streaming sources (Kafka, Kinesis) are not supported via `RaySource`; use + the dedicated [Kafka](kafka.md) or [Kinesis](kinesis.md) data sources. + +--- + +## Related pages + +* [Ray Offline Store](../offline-stores/ray.md) +* [Ray Compute Engine](../compute-engine/ray.md) +* [Feature Retrieval](../../getting-started/concepts/feature-retrieval.md) diff --git a/docs/reference/dqm.md b/docs/reference/dqm.md index 5a02413e534..47090b5dd1c 100644 --- a/docs/reference/dqm.md +++ b/docs/reference/dqm.md @@ -1,77 +1,81 @@ # Data Quality Monitoring -Data Quality Monitoring (DQM) is a Feast module aimed to help users to validate their data with the user-curated set of rules. -Validation could be applied during: -* Historical retrieval (training dataset generation) -* [planned] Writing features into an online store -* [planned] Reading features from an online store +Feast's Data Quality Monitoring (DQM) system computes, stores, and serves statistical metrics for every registered feature. It gives you visibility into feature health — distributions, null rates, percentiles, histograms — across batch data and feature serving logs. -Its goal is to address several complex data problems, namely: -* Data consistency - new training datasets can be significantly different from previous datasets. This might require a change in model architecture. -* Issues/bugs in the upstream pipeline - bugs in upstream pipelines can cause invalid values to overwrite existing valid values in an online store. -* Training/serving skew - distribution shift could significantly decrease the performance of the model. +Its goal is to address several complex data problems: -> To monitor data quality, we check that the characteristics of the tested dataset (aka the tested dataset's profile) are "equivalent" to the characteristics of the reference dataset. -> How exactly profile equivalency should be measured is up to the user. +* **Data consistency** — new training datasets can differ significantly from previous datasets, potentially requiring changes in model architecture. +* **Upstream pipeline bugs** — bugs in upstream pipelines can cause invalid values to overwrite existing valid values in an online store. +* **Training/serving skew** — distribution shift between training and serving data can decrease model performance. ### Overview -The validation process consists of the following steps: -1. User prepares reference dataset (currently only [saved datasets](../getting-started/concepts/dataset.md) from historical retrieval are supported). -2. User defines profiler function, which should produce profile by given dataset (currently only profilers based on [Great Expectations](https://docs.greatexpectations.io) are allowed). -3. Validation of tested dataset is performed with reference dataset and profiler provided as parameters. +Feast's DQM system works natively with your configured offline store — no additional infrastructure or external dependencies are required. The workflow is: -### Preparations -Feast with Great Expectations support can be installed via -```shell -pip install 'feast[ge]' +1. **Register features** — run `feast apply` to register feature views. If `auto_baseline: true` is configured, baseline metrics are computed automatically. +2. **Schedule monitoring** — run `feast monitor run` on a schedule (daily recommended) to compute metrics across multiple time windows. +3. **Read metrics** — query metrics via the REST API or view them in the Feast UI. + +### Configuration + +Enable DQM in your `feature_store.yaml`: + +```yaml +data_quality_monitoring: + auto_baseline: true ``` -### Dataset profile -Currently, Feast supports only [Great Expectation's](https://greatexpectations.io/) [ExpectationSuite](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/core/expectation_suite/index.html#great_expectations.core.expectation_suite.ExpectationSuite) -as dataset's profile. Hence, the user needs to define a function (profiler) that would receive a dataset and return an [ExpectationSuite](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/core/expectation_suite/index.html#great_expectations.core.expectation_suite.ExpectationSuite). +### Computing Metrics -Great Expectations supports automatic profiling as well as manually specifying expectations: -```python -from great_expectations.dataset import Dataset -from great_expectations.core.expectation_suite import ExpectationSuite +**Auto mode (recommended for production):** -from feast.dqm.profilers.ge_profiler import ge_profiler +```bash +feast monitor run +``` -@ge_profiler -def automatic_profiler(dataset: Dataset) -> ExpectationSuite: - from great_expectations.profile.user_configurable_profiler import UserConfigurableProfiler +This detects the latest event timestamp in the source data and computes metrics for 5 time windows: daily, weekly, biweekly, monthly, and quarterly. - return UserConfigurableProfiler( - profile_dataset=dataset, - ignored_columns=['conv_rate'], - value_set_threshold='few' - ).build_suite() +**Target a specific feature view:** + +```bash +feast monitor run --feature-view driver_stats ``` -However, from our experience capabilities of automatic profiler are quite limited. So we would recommend crafting your own expectations: -```python -@ge_profiler -def manual_profiler(dataset: Dataset) -> ExpectationSuite: - dataset.expect_column_max_to_be_between("column", 1, 2) - return dataset.get_expectation_suite() + +**Explicit date range:** + +```bash +feast monitor run \ + --feature-view driver_stats \ + --start-date 2025-01-01 \ + --end-date 2025-01-07 \ + --granularity weekly ``` +**Set a manual baseline:** +```bash +feast monitor run \ + --feature-view driver_stats \ + --start-date 2025-01-01 \ + --end-date 2025-03-31 \ + --granularity daily \ + --set-baseline +``` + +### Monitoring Feature Serving Logs + +If your feature services have logging configured, you can compute metrics from the actual features served to models in production: -### Validating Training Dataset -During retrieval of historical features, `validation_reference` can be passed as a parameter to methods `.to_df(validation_reference=...)` or `.to_arrow(validation_reference=...)` of RetrievalJob. -If parameter is provided Feast will run validation once dataset is materialized. In case if validation successful materialized dataset is returned. -Otherwise, `feast.dqm.errors.ValidationFailed` exception would be raised. It will consist of all details for expectations that didn't pass. +```bash +feast monitor run --source-type log +``` -```python -from feast import FeatureStore +### Reading Metrics -fs = FeatureStore(".") +Metrics are accessible via the REST API: -job = fs.get_historical_features(...) -job.to_df( - validation_reference=fs - .get_saved_dataset("my_reference_dataset") - .as_reference(profiler=manual_profiler) -) ``` +GET /monitoring/metrics/features?project=my_project&feature_view_name=driver_stats&granularity=daily +``` + +See the [Feature Quality Monitoring guide](../how-to-guides/feature-monitoring.md) for full API reference, UI integration, and orchestrator examples. diff --git a/docs/reference/feast-cli-commands.md b/docs/reference/feast-cli-commands.md index eb6fa90d280..85781f6abc2 100644 --- a/docs/reference/feast-cli-commands.md +++ b/docs/reference/feast-cli-commands.md @@ -21,12 +21,14 @@ Commands: apply Create or update a feature store deployment configuration Display Feast configuration delete Delete a Feast object from the registry + demo-notebooks Generate demo Jupyter notebooks for the project entities Access entities feature-views Access feature views init Create a new Feast repository materialize Run a (non-incremental) materialization job to... materialize-incremental Run an incremental materialization job to ingest... permissions Access permissions + registry Manage the feature registry registry-dump Print contents of the metadata registry teardown Tear down deployed feature store infrastructure version Display Feast SDK version @@ -142,6 +144,47 @@ The delete operation is permanent and will remove the object from the registry. If multiple objects have the same name across different types, `feast delete` will delete the first one it finds. For programmatic deletion with more control, use the Python SDK methods like `store.delete_feature_view()`, `store.delete_feature_service()`, etc. {% endhint %} +## Demo Notebooks + +Generate tailored demo Jupyter notebooks for each Feast project found in the current directory. + +```bash +feast demo-notebooks +``` + +The command searches for `feature_store.yaml` in the current directory and every file inside the `feast-config/` directory. Each file is treated as a separate project config, and notebooks are created under `./feast-demo-notebooks//`. + +The generated notebooks adapt to your project configuration (online/offline store types, authentication, vector search) and cover: + +* **Feature store overview** — explore registered entities, feature views, and services. +* **Historical feature retrieval** — build training datasets with point-in-time correct joins. +* **Online feature serving** — materialize features and retrieve them at low latency. + +**Options:** + +* `-o, --output-dir` — Directory where the notebooks are written. Default: `./feast-demo-notebooks`. +* `--overwrite` — Overwrite existing notebooks if the output directory already exists. + +```bash +feast demo-notebooks -o ./my-notebooks --overwrite +``` + +You can also use the `--chdir` global option to point at a different feature repository: + +```bash +feast -c /path/to/feature_repo demo-notebooks +``` + +The same functionality is available via the Python SDK: + +```python +from feast import copy_demo_notebooks + +copy_demo_notebooks(output_dir="./feast-demo-notebooks", repo_path=".") +``` + +For more details see the [Demo Notebooks tutorial](../tutorials/demo-notebooks.md). + ## Entities List all registered entities @@ -176,6 +219,18 @@ NAME ENTITIES TYPE driver_hourly_stats {'driver'} FeatureView ``` +List version history for a feature view + +```text +feast feature-views list-versions FEATURE_VIEW_NAME +``` + +```text +VERSION TYPE CREATED VERSION_ID +v0 feature_view 2024-01-15 10:30:00 a1b2c3d4-... +v1 feature_view 2024-01-16 14:22:00 e5f6g7h8-... +``` + ## Init Creates a new feature repository @@ -429,6 +484,18 @@ reader driver_hourly_stats_fresh FeatureView DESCRIBE ``` +## Registry + +### create-schema + +Pre-create the SQL registry schema so the application does not need DDL privileges at runtime. Use this with `schema_mode: verify` or `schema_mode: skip` in your `feature_store.yaml`. + +```text +feast registry create-schema +``` + +This command only applies to SQL-based registries (`registry_type: sql`). It is safe to run multiple times — existing tables are not modified. + ## Teardown Tear down deployed feature store infrastructure diff --git a/docs/reference/feature-repository/README.md b/docs/reference/feature-repository/README.md index 2c1b112a783..38968825c1d 100644 --- a/docs/reference/feature-repository/README.md +++ b/docs/reference/feature-repository/README.md @@ -127,4 +127,4 @@ To declare new feature definitions, just add code to the feature repository, eit ### Next steps * See [Create a feature repository](../../how-to-guides/feast-snowflake-gcp-aws/create-a-feature-repository.md) to get started with an example feature repository. -* See [feature_store.yaml](feature-store-yaml.md), [.feastignore](feast-ignore.md), or [Feature Views](../../getting-started/concepts/feature-view.md) for more information on the configuration files that live in a feature registry. +* See [feature_store.yaml](feature-store-yaml.md), [.feastignore](feast-ignore.md), [Registration inferencing](registration-inferencing.md), or [Feature Views](../../getting-started/concepts/feature-view.md) for more information on the configuration files that live in a feature registry. diff --git a/docs/reference/feature-repository/feature-store-yaml.md b/docs/reference/feature-repository/feature-store-yaml.md index a87e09ba43e..aec6082b383 100644 --- a/docs/reference/feature-repository/feature-store-yaml.md +++ b/docs/reference/feature-repository/feature-store-yaml.md @@ -25,5 +25,69 @@ The following top-level configuration options exist in the `feature_store.yaml` * **offline_store** — Configures the offline store. * **project** — Defines a namespace for the entire feature store. Can be used to isolate multiple deployments in a single installation of Feast. Should only contain letters, numbers, and underscores. * **engine** - Configures the batch materialization engine. +* **materialization** - Configures materialization behavior (write batching, feature pull strategy). See below. Please see the [RepoConfig](https://rtd.feast.dev/en/latest/#feast.repo_config.RepoConfig) API reference for the full list of configuration options. + +--- + +## `materialization` configuration + +The `materialization` block controls how Feast reads from the offline store and writes to the online store during `feast materialize` / `feast materialize-incremental` runs. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: redis + connection_string: "localhost:6379" +materialization: + online_write_batch_size: 10000 # write rows in chunks of 10 000 + pull_latest_features: false # pull full time range (default) +``` +{% endcode %} + +### `online_write_batch_size` + +| Field | Type | Default | Supported engines | +| --- | --- | --- | --- | +| `online_write_batch_size` | `int` (positive) | `null` | local, spark, ray | + +Controls how many rows are converted to protobuf and written to the online store per batch during materialization. + +**Default behaviour (`null`):** All rows fetched from the offline store are converted to protobuf in a single in-memory operation before writing. This is fast but can exhaust memory for large datasets — every row must be held as a Python proto object simultaneously. + +**With `online_write_batch_size` set:** The Arrow table returned by the offline store is split into chunks of at most `online_write_batch_size` rows. Each chunk is converted and written independently, keeping peak memory proportional to the batch size rather than the full dataset size. + +```yaml +# Recommended for datasets > a few million rows or memory-constrained workers +materialization: + online_write_batch_size: 10000 +``` + +**Choosing a value:** + +| Dataset size | Worker memory | Recommended batch size | +| --- | --- | --- | +| < 1 M rows | Any | `null` (default — single batch is fine) | +| 1–10 M rows | ≥ 4 GB | `50000` | +| 10–100 M rows | ≥ 8 GB | `10000` | +| > 100 M rows | Any | `5000`–`10000` | + +A smaller batch size reduces peak memory at the cost of more `online_write_batch` calls to the online store. For Redis, each call is a pipelined batch, so the overhead is low. For stores with higher per-call latency (e.g. DynamoDB), prefer larger batch sizes. + +{% hint style="info" %} +`online_write_batch_size` is applied **per feature view** within a single materialization job. If you materialize five feature views in parallel, peak memory is `5 × batch_size × bytes_per_row`. +{% endhint %} + +### `pull_latest_features` + +| Field | Type | Default | +| --- | --- | --- | +| `pull_latest_features` | `bool` | `false` | + +When `false` (default), the offline store retrieves **all** feature values within the requested time range for each entity. + +When `true`, only the **latest** value per entity is retrieved. This reduces I/O and memory for feature views where historical values are not needed (e.g., slowly changing dimensions). It is equivalent to running a `GROUP BY entity, MAX(event_timestamp)` on the offline data before writing. diff --git a/docs/reference/feature-servers/mcp-feature-server.md b/docs/reference/feature-servers/mcp-feature-server.md new file mode 100644 index 00000000000..8bfc96b1891 --- /dev/null +++ b/docs/reference/feature-servers/mcp-feature-server.md @@ -0,0 +1,51 @@ +# MCP Feature Server + +## Overview + +Feast can expose the Python Feature Server as an MCP (Model Context Protocol) server using `fastapi_mcp`. When enabled, MCP clients can discover and call Feast tools such as online feature retrieval. + +## Installation + +```bash +pip install feast[mcp] +``` + +## Configuration + +Add an MCP `feature_server` block to your `feature_store.yaml`: + +```yaml +feature_server: + type: mcp + enabled: true + mcp_enabled: true + mcp_transport: http + mcp_server_name: "feast-feature-store" + mcp_server_version: "1.0.0" +``` + +### mcp_transport + +`mcp_transport` controls how MCP is mounted into the Feature Server: + +- `sse`: SSE-based transport. This is the default for backward compatibility. +- `http`: Streamable HTTP transport. This is recommended for improved compatibility with some MCP clients. + +If `mcp_transport: http` is configured but your installed `fastapi_mcp` version does not support Streamable HTTP mounting, Feast will fail fast with an error asking you to upgrade `fastapi_mcp` (or reinstall `feast[mcp]`). + +## Endpoints + +MCP is mounted at: + +- `/mcp` + +## Connecting an MCP client + +Use your MCP client’s “HTTP” configuration and point it to the Feature Server base URL. For example, if your Feature Server runs at `http://localhost:6566`, use: + +- `http://localhost:6566/mcp` + +## Troubleshooting + +- If you see a deprecation warning about `mount()` at runtime, upgrade `fastapi_mcp` and use `mcp_transport: http` or `mcp_transport: sse`. +- If your MCP client has intermittent connectivity issues with `mcp_transport: sse`, switch to `mcp_transport: http`. diff --git a/docs/reference/feature-servers/python-feature-server.md b/docs/reference/feature-servers/python-feature-server.md index 2e5792b0a6f..b1b873cc7d2 100644 --- a/docs/reference/feature-servers/python-feature-server.md +++ b/docs/reference/feature-servers/python-feature-server.md @@ -311,6 +311,258 @@ requests.post( data=json.dumps(materialize_data)) ``` +## Prometheus Metrics + +The Python feature server can expose Prometheus-compatible metrics on a dedicated +HTTP endpoint (default port `8000`). Metrics are **opt-in** and carry zero overhead +when disabled. + +### Enabling metrics + +**Option 1 — CLI flag** (useful for one-off runs): + +```bash +feast serve --metrics +``` + +**Option 2 — `feature_store.yaml`** (recommended for production): + +```yaml +feature_server: + type: local + metrics: + enabled: true +``` + +Either option is sufficient. When both are set, metrics are enabled. + +### Per-category control + +By default, enabling metrics turns on **all** categories. You can selectively +disable individual categories within the same `metrics` block: + +```yaml +feature_server: + type: local + metrics: + enabled: true + resource: true # CPU / memory gauges + request: false # disable endpoint latency & request counters + online_features: true # online feature retrieval counters + push: true # push request counters + materialization: true # materialization counters & duration + freshness: true # feature freshness gauges + offline_features: true # offline store retrieval counters & latency + audit_logging: false # structured JSON audit logs (see below) +``` + +Any category set to `false` will emit no metrics and start no background +threads (e.g., setting `freshness: false` prevents the registry polling +thread from starting). All categories default to `true` except +`audit_logging`, which defaults to `false`. + +### Available metrics + +| Metric | Type | Labels | Category | Description | +|--------|------|--------|----------|-------------| +| `feast_feature_server_cpu_usage` | Gauge | — | `resource` | Process CPU usage % | +| `feast_feature_server_memory_usage` | Gauge | — | `resource` | Process memory usage % | +| `feast_feature_server_request_total` | Counter | `endpoint`, `status` | `request` | Total requests per endpoint | +| `feast_feature_server_request_latency_seconds` | Histogram | `endpoint`, `feature_count`, `feature_view_count` | `request` | Request latency with p50/p95/p99 support | +| `feast_online_features_request_total` | Counter | — | `online_features` | Total online feature retrieval requests | +| `feast_online_features_entity_count` | Histogram | — | `online_features` | Entity rows per online feature request | +| `feast_feature_server_online_store_read_duration_seconds` | Histogram | — | `online_features` | Online store read phase duration (sync and async) | +| `feast_feature_server_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | `online_features` | ODFV read-path transformation duration (requires `track_metrics=True` on the ODFV) | +| `feast_feature_server_write_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | `online_features` | ODFV write-path transformation duration (requires `track_metrics=True` on the ODFV) | +| `feast_push_request_total` | Counter | `push_source`, `mode` | `push` | Push requests by source and mode | +| `feast_materialization_result_total` | Counter | `feature_view`, `status` | `materialization` | Materialization runs (success/failure) | +| `feast_materialization_duration_seconds` | Histogram | `feature_view` | `materialization` | Materialization duration per feature view | +| `feast_feature_freshness_seconds` | Gauge | `feature_view`, `project` | `freshness` | Seconds since last materialization | +| `feast_offline_store_request_total` | Counter | `method`, `status` | `offline_features` | Total offline store retrieval requests | +| `feast_offline_store_request_latency_seconds` | Histogram | `method` | `offline_features` | Latency of offline store retrieval operations | +| `feast_offline_store_row_count` | Histogram | `method` | `offline_features` | Rows returned by offline store retrieval | + +### Per-ODFV transformation metrics + +The `transformation_duration_seconds` and `write_transformation_duration_seconds` +metrics are gated behind **two** conditions — both must be true for any +instrumentation to run: + +1. **Server-level**: the `online_features` category must be enabled in the + metrics configuration. +2. **ODFV-level**: the `OnDemandFeatureView` must have `track_metrics=True`. + +This defaults to `False`, so no ODFV incurs timing overhead unless explicitly +opted in: + +```python +from feast.on_demand_feature_view import on_demand_feature_view + +@on_demand_feature_view( + sources=[my_feature_view, my_request_source], + schema=[Field(name="output", dtype=Float64)], + track_metrics=True, # opt in to transformation timing +) +def my_transform(inputs: pd.DataFrame) -> pd.DataFrame: + ... +``` + +The `odfv_name` label lets you filter or group by individual ODFV, +and the `mode` label (`python`, `pandas`, `substrait`) lets you compare +transformation engines. + +### Audit logging + +Feast can emit structured JSON audit log entries for every online and offline +feature retrieval. These are written via the standard `feast.audit` Python +logger, so you can route them to a dedicated file, SIEM, or log aggregator +independently of application logs. + +Audit logging is **disabled by default**. Enable it in `feature_store.yaml`: + +```yaml +feature_server: + type: local + metrics: + enabled: true + audit_logging: true +``` + +**Online audit log** (emitted per `/get-online-features` call): + +```json +{ + "event": "online_feature_request", + "timestamp": "2026-05-11T08:30:00.123456+00:00", + "requestor_id": "user@example.com", + "entity_keys": ["driver_id"], + "entity_count": 3, + "feature_views": ["driver_hourly_stats"], + "feature_count": 3, + "status": "success", + "latency_ms": 12.34 +} +``` + +**Offline audit log** (emitted per `RetrievalJob.to_arrow()` call): + +```json +{ + "event": "offline_feature_retrieval", + "timestamp": "2026-05-11T08:31:00.456789+00:00", + "method": "to_arrow", + "start_time": "2026-05-11T08:30:59.226789+00:00", + "end_time": "2026-05-11T08:31:00.456789+00:00", + "feature_views": ["driver_hourly_stats"], + "feature_count": 3, + "row_count": 500, + "status": "success", + "duration_ms": 1230.0 +} +``` + +The `requestor_id` field in online audit logs is populated from the +security manager's current user when authentication is configured, and +falls back to `"anonymous"` otherwise. + +To route audit logs to a separate file: + +```python +import logging + +handler = logging.FileHandler("/var/log/feast/audit.log") +handler.setFormatter(logging.Formatter("%(message)s")) +logging.getLogger("feast.audit").addHandler(handler) +``` + +### Scraping with Prometheus + +```yaml +scrape_configs: + - job_name: feast + static_configs: + - targets: ["localhost:8000"] +``` + +### Kubernetes / Feast Operator + +Set `metrics: true` in your FeatureStore CR: + +```yaml +spec: + services: + onlineStore: + server: + metrics: true +``` + +The operator automatically exposes port 8000 and creates the corresponding +Service port so Prometheus can discover it. + +### Multi-worker and multi-replica (HPA) support + +Feast uses Prometheus **multiprocess mode** so that metrics are correct +regardless of the number of Gunicorn workers or Kubernetes replicas. + +**How it works:** + +* Each Gunicorn worker writes metric values to shared files in a + temporary directory (`PROMETHEUS_MULTIPROCESS_DIR`). Feast creates + this directory automatically; you can override it by setting the + environment variable yourself. +* The metrics HTTP server on port 8000 aggregates all workers' + metric files using `MultiProcessCollector`, so a single scrape + returns accurate totals. +* Gunicorn hooks clean up dead-worker files automatically + (`child_exit` → `mark_process_dead`). +* CPU and memory gauges use `multiprocess_mode=liveall` — Prometheus + shows per-worker values distinguished by a `pid` label. +* Feature freshness gauges use `multiprocess_mode=max` — Prometheus + shows the worst-case staleness (all workers compute the same value). +* Counters and histograms (request counts, latency, materialization) + are automatically summed across workers. + +**Multiple replicas (HPA):** Each pod runs its own metrics endpoint. +Prometheus adds an `instance` label per pod, so there is no +duplication. Use `sum(rate(...))` or `histogram_quantile(...)` across +instances as usual. + +## Vector Search (`POST /search`) + +The feature server exposes `POST /search` for vector similarity search against online document embeddings. Pass a pre-computed embedding in `query`, or use `api_version: 2` with `query_string` for text-based search when the online store supports it. + +`POST /retrieve-online-documents` is a deprecated alias with the same request body and response; new integrations should use `/search`. + +## [Alpha] OpenAI-Compatible Vector Store API + +{% hint style="warning" %} +**Alpha feature.** This API surface is functional and tested, but may change in future releases. +{% endhint %} + +The feature server exposes OpenAI-compatible vector store endpoints. This allows clients (including LLM agents and tool-calling frameworks) to discover and search vector data with plain text queries, without computing embeddings client-side. + +Each feature view with vector-indexed fields gets a deterministic `vs_{hash}` identifier derived from `SHA-256(project + ":" + feature_view_name)`. These IDs are stable across server restarts. + +### Endpoints + +| Method | Path | RBAC | Description | +|---|---|---|---| +| `GET` | `/v1/vector_stores` | `DESCRIBE` | List all vector stores (filtered by caller permissions) | +| `GET` | `/v1/vector_stores/{vector_store_id}` | `DESCRIBE` | Get metadata for a single vector store | +| `POST` | `/v1/vector_stores/{vector_store_id}/search` | `READ_ONLINE` | Search a vector store with server-side embedding | + +### Configuration + +Add an `embedding_model` section to your `feature_store.yaml`: + +```yaml +embedding_model: + provider: sentence_transformers # default; can be omitted + model: all-MiniLM-L6-v2 +``` + +Feast uses **Sentence Transformers** (default) for local embedding inference — no external API key required. Custom embedding providers can be plugged in by implementing the `EmbeddingProvider` protocol. See [\[Alpha\] Vector Database](../alpha-vector-database.md#alpha-openai-compatible-vector-store-api) for full configuration, custom providers, filter details, and SDK usage. + ## Starting the feature server in TLS(SSL) mode Enabling TLS mode ensures that data between the Feast client and server is transmitted securely. For an ideal production environment, it is recommended to start the feature server in TLS mode. @@ -382,7 +634,11 @@ The [PyTorch NLP template](https://github.com/feast-dev/feast/tree/main/sdk/pyth | Endpoint | Resource Type | Permission | Description | |----------------------------|---------------------------------|-------------------------------------------------------|----------------------------------------------------------------| | /get-online-features | FeatureView,OnDemandFeatureView | Read Online | Get online features from the feature store | -| /retrieve-online-documents | FeatureView | Read Online | Retrieve online documents from the feature store for RAG | +| /search | FeatureView | Read Online | Vector similarity search for RAG (embedding vector or text query) | +| /retrieve-online-documents | FeatureView | Read Online | **Deprecated.** Use `/search` instead. | +| /v1/vector_stores | FeatureView | Describe | [Alpha] List all vector stores | +| /v1/vector_stores/{id} | FeatureView | Describe | [Alpha] Get a single vector store | +| /v1/vector_stores/{id}/search | FeatureView | Read Online | [Alpha] OpenAI-compatible vector search with server-side embedding | | /push | FeatureView | Write Online, Write Offline, Write Online and Offline | Push features to the feature store (online, offline, or both) | | /write-to-online-store | FeatureView | Write Online | Write features to the online store | | /materialize | FeatureView | Write Online | Materialize features within a specified time range | diff --git a/docs/reference/feature-servers/registry-server.md b/docs/reference/feature-servers/registry-server.md index 496eaa8badc..4558a10ce63 100644 --- a/docs/reference/feature-servers/registry-server.md +++ b/docs/reference/feature-servers/registry-server.md @@ -214,6 +214,7 @@ Most endpoints support these common query parameters: - `feature` (optional): Filter feature views by feature name - `feature_service` (optional): Filter feature views by feature service name - `data_source` (optional): Filter feature views by data source name + - `updated_since` (optional): Only return feature views updated at or after this ISO-8601 UTC timestamp (e.g. `2024-01-01T00:00:00Z`) - `page` (optional): Page number for pagination - `limit` (optional): Number of items per page - `sort_by` (optional): Field to sort by @@ -223,27 +224,31 @@ Most endpoints support these common query parameters: # Basic list curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project" - + # With pagination and relationships curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&include_relationships=true&page=1&limit=5&sort_by=name" - + # Filter by entity curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&entity=user" - + # Filter by feature curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&feature=age" - + # Filter by data source curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&data_source=user_profile_source" - + # Filter by feature service curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&feature_service=user_service" - + + # Filter by last-updated timestamp + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/feature_views?project=my_project&updated_since=2024-06-01T00:00:00Z" + # Multiple filters combined curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&entity=user&feature=age" diff --git a/docs/reference/feature-store-yaml.md b/docs/reference/feature-store-yaml.md index 820731064fc..1aac166bd8b 100644 --- a/docs/reference/feature-store-yaml.md +++ b/docs/reference/feature-store-yaml.md @@ -36,11 +36,43 @@ An example configuration: ```yaml feature_server: type: local + metrics: # Prometheus metrics configuration. Also achievable via `feast serve --metrics`. + enabled: true # Enable Prometheus metrics server on port 8000 + resource: true # CPU / memory gauges + request: true # endpoint latency histograms & request counters + online_features: true # online feature retrieval counters + store read & ODFV transform timing + push: true # push request counters + materialization: true # materialization counters & duration histograms + freshness: true # per-feature-view freshness gauges offline_push_batching_enabled: true # Enables batching of offline writes processed by /push. Online writes are unaffected. offline_push_batching_batch_size: 100 # Maximum number of buffered rows before writing to the offline store. offline_push_batching_batch_interval_seconds: 5 # Maximum time rows may remain buffered before a forced flush. ``` +### registry + +The `registry` field can be a simple path string or an object with additional +configuration. When using the REST registry server, MCP support can be enabled: + +```yaml +registry: + registry_type: sql + path: postgresql+psycopg://feast:feast@localhost:5432/feast #pragma: allowlist secret + mcp: + enabled: true +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `registry_type` | string | `file` | Registry backend (`file`, `sql`, etc.) | +| `path` | string | — | Connection string or file path | +| `schema_mode` | string | `auto` | SQL registry only. `auto`: create tables on startup; `verify`: check tables exist, error if missing; `skip`: no DDL or verification. See [SQL Registry docs](registries/sql.md#schema-management-schema_mode). | +| `mcp.enabled` | bool | `false` | Enable MCP (Model Context Protocol) on the REST registry server | + +When `registry.mcp.enabled` is `true`, the REST registry server exposes registry +metadata (entities, feature views, feature services) as MCP tool endpoints for +LLM agents. Requires `feast[mcp]` to be installed. + ## Providers The `provider` field defines the environment in which Feast will execute data flows. As a result, it also determines the default values for other fields. diff --git a/docs/reference/mlflow.md b/docs/reference/mlflow.md new file mode 100644 index 00000000000..6522478409c --- /dev/null +++ b/docs/reference/mlflow.md @@ -0,0 +1,347 @@ +# MLflow Integration + +Feast provides **native integration** with [MLflow](https://mlflow.org/) for automatic feature lineage tracking alongside ML experiments. When enabled, every feature retrieval is logged to the active MLflow run. + +## Overview + +- **Which features did this model use?** -- auto-logged on every `get_historical_features()` / `get_online_features()` call +- **Which feature service should I use to serve this model?** -- resolved from model URI via `store.mlflow.resolve_features()` +- **Can I reproduce the exact training data?** -- entity DataFrame saved as an MLflow artifact +- **Which models break if I change a feature view?** -- reverse index via the Feast UI `/api/mlflow-feature-usage` endpoint +- **When was the feature store last updated?** -- `feast apply` and `feast materialize` logged to a separate ops experiment + +### Capabilities + +| Capability | How | +|---|---| +| Auto-log feature metadata | Tags on every retrieval inside an active MLflow run | +| Entity DataFrame archival | `entity_df.parquet` artifact for full reproducibility | +| Model registration with lineage | `feast.feature_service` tag propagated to model versions | +| Training-to-prediction linkage | `store.mlflow.load_model()` links prediction runs back to training runs | +| Model-to-feature resolution | Map any model URI back to its Feast feature service | +| Operation audit trail | `feast apply` / `feast materialize` logged to `{project}-feast-ops` | +| `store.mlflow` API | Single entry point — zero `import mlflow`, zero client objects | +| Feast UI integration | Per-feature-view usage stats and registered model associations | + +## Installation + +MLflow is an optional dependency: + +```bash +pip install feast[mlflow] +``` + +## Configuration + +Add the `mlflow` section to your `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db + +mlflow: + enabled: true + tracking_uri: http://127.0.0.1:5000 # optional, falls back to MLFLOW_TRACKING_URI env var + auto_log: true # default + auto_log_entity_df: false # default + entity_df_max_rows: 100000 # default + log_operations: false # default + ops_experiment_suffix: "-feast-ops" # default +``` + +### Configuration options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | bool | `false` | Master switch for the entire integration | +| `tracking_uri` | string | *(none)* | MLflow tracking server URI. Falls back to `MLFLOW_TRACKING_URI` env var, then MLflow default (`./mlruns`) | +| `auto_log` | bool | `true` | Automatically log feature metadata on every retrieval when an active MLflow run exists | +| `auto_log_entity_df` | bool | `false` | Save the entity DataFrame as `entity_df.parquet` artifact on historical retrieval | +| `entity_df_max_rows` | int | `100000` | Skip entity DataFrame artifact upload for DataFrames exceeding this limit | +| `log_operations` | bool | `false` | Log `feast apply` and `feast materialize` to a separate MLflow experiment | +| `ops_experiment_suffix` | string | `"-feast-ops"` | Suffix appended to project name for the operations experiment | + +### Tracking URI resolution + +The tracking URI is resolved in this order: + +1. `tracking_uri` field in `feature_store.yaml` +2. `MLFLOW_TRACKING_URI` environment variable +3. MLflow's default (`./mlruns` local directory) + +This means you can omit `tracking_uri` from the YAML and set `MLFLOW_TRACKING_URI` in your environment instead, or it would be pulled from `./mlruns` automatically when both are not set. + +## What gets logged + +### Tags on retrieval runs + +When `auto_log: true` and an active MLflow run exists, each `get_historical_features()` or `get_online_features()` call records: + +| Tag | Example | Description | +|-----|---------|-------------| +| `feast.project` | `my_project` | Feast project name | +| `feast.retrieval_type` | `historical` / `online` | Type of feature retrieval | +| `feast.feature_service` | `driver_activity_v1` | Auto-resolved feature service name (if matched) | +| `feast.feature_views` | `driver_hourly_stats` | Comma-separated feature view names | +| `feast.feature_refs` | `driver_hourly_stats:conv_rate,...` | All feature references | +| `feast.entity_count` | `200` | Number of entities in the request | +| `feast.feature_count` | `5` | Number of features retrieved | + +### Metrics + +| Metric | Example | Description | +|--------|---------|-------------| +| `feast.job_submission_sec` | `0.4321` | Feature retrieval duration in seconds | + +### Artifacts + +When `auto_log_entity_df: true` and the entity DataFrame has fewer than `entity_df_max_rows` rows: + +| Artifact | Description | +|----------|-------------| +| `entity_df.parquet` | Full entity DataFrame used in the retrieval | + +When a model is logged via `store.mlflow.log_model()`: + +| Artifact | Description | +|----------|-------------| +| `feast_features.json` | JSON list of feature references the model was trained on | + +### Entity DataFrame metadata + +Regardless of `auto_log_entity_df`, the following metadata is logged when present: + +| Tag / Param | When | Description | +|-------------|------|-------------| +| `feast.entity_df_type` | Always | `dataframe`, `sql`, or `range` | +| `feast.entity_df_rows` | DataFrame input | Row count | +| `feast.entity_df_columns` | DataFrame input | Column names | +| `feast.entity_df_query` | SQL input | The SQL query string | +| `feast.start_date` / `feast.end_date` | Range-based input | Date range | + +### Operation logs + +When `log_operations: true`, `feast apply` and `feast materialize` create self-contained runs in the `{project}{ops_experiment_suffix}` experiment (default: `my_project-feast-ops`): + +**Apply runs:** + +| Tag / Metric | Example | +|--------------|---------| +| `feast.operation` | `apply` | +| `feast.project` | `my_project` | +| `feast.feature_views_changed` | `driver_hourly_stats,order_stats` | +| `feast.feature_services_changed` | `driver_activity_v1` | +| `feast.entities_changed` | `driver,restaurant` | +| `feast.apply.feature_views_count` | `2` | +| `feast.apply.feature_services_count` | `1` | +| `feast.apply.entities_count` | `2` | + +**Materialize runs:** + +| Tag / Metric | Example | +|--------------|---------| +| `feast.operation` | `materialize` / `materialize_incremental` | +| `feast.project` | `my_project` | +| `feast.materialize.feature_views` | `driver_hourly_stats` | +| `feast.materialize.start_date` | `2024-01-01T00:00:00` | +| `feast.materialize.end_date` | `2024-01-02T00:00:00` | +| `feast.materialize.duration_sec` | `12.3456` | + +## Usage + +### Automatic logging (zero code) + +With the configuration above, feature metadata is logged automatically whenever there is an active MLflow run. No explicit `import mlflow` is needed — just use `store.mlflow`: + +```python +from feast import FeatureStore + +store = FeatureStore(".") + +with store.mlflow.start_run(run_name="my_training"): + training_df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + # The run is now tagged with feast.feature_refs, feast.feature_views, etc. + + model = train(training_df) + store.mlflow.log_model(model, "model") +``` + +No extra code needed — the tags are written automatically. + +### `store.mlflow` API (recommended) + +`store.mlflow` is the primary way to interact with the Feast–MLflow integration. It provides Feast-enhanced versions of common MLflow operations, and delegates everything else to the raw `mlflow` module: + +```python +from feast import FeatureStore +from sklearn.linear_model import LogisticRegression + +store = FeatureStore(".") + +# Training +with store.mlflow.start_run(run_name="v1_training"): + df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + model = LogisticRegression().fit(X, y) + store.mlflow.log_model(model, "model") # Feast-enhanced: saves feast_features.json + train_run_id = store.mlflow.active_run_id + +# Register model (auto-tags version with feast.feature_service) +store.mlflow.register_model(f"runs:/{train_run_id}/model", "driver_model") + +# Prediction (auto-links to training run) +with store.mlflow.start_run(run_name="prediction"): + model = store.mlflow.load_model("models:/driver_model/1") + online_features = store.get_online_features( + features=store.get_feature_service("driver_activity_v1"), + entity_rows=[{"driver_id": 1001}], + ) + predictions = model.predict(...) +``` + +### `feast.mlflow` module API (alternative) + +For users who prefer a module-level import, `feast.mlflow` is a **drop-in replacement for `import mlflow`** that delegates to the same `store.mlflow` client under the hood: + +```python +import feast.mlflow +from feast import FeatureStore + +store = FeatureStore(".") # auto-registers with feast.mlflow + +with feast.mlflow.start_run(run_name="training"): + df = store.get_historical_features(...).to_df() + feast.mlflow.log_params({"lr": "0.01"}) # plain passthrough + feast.mlflow.log_metrics({"f1": 0.85}) # plain passthrough + feast.mlflow.log_model(model, "model") # Feast-enhanced +``` + +#### Store resolution + +`feast.mlflow` resolves its `FeatureStore` in this order: + +1. **Explicit `feast.mlflow.init(store)`** — if called, overrides everything +2. **Auto-registered** — the most recently created `FeatureStore` with `mlflow.enabled=true` registers itself automatically +3. **Auto-discovery** — falls back to `FeatureStore(".")` from the current directory + +In most cases, simply creating a `FeatureStore(...)` is enough — no `init()` needed. + +#### Error handling + +`feast.mlflow` raises clear errors on first use if something is misconfigured: + +| Condition | Error | +|-----------|-------| +| No `feature_store.yaml` in cwd and no store created | `RuntimeError` with guidance to call `feast.mlflow.init(store)` | +| `mlflow.enabled` is not set to `true` | `RuntimeError` with guidance to set `mlflow.enabled=true` | +| `mlflow` pip package not installed | `ImportError` with guidance to run `pip install feast[mlflow]` | + +When `mlflow.enabled` is `false` (or omitted), `store.mlflow` returns `None`, allowing callers to guard with `if store.mlflow:`. The `feast.mlflow` module raises `RuntimeError` only when you attempt to use it without an enabled store. + +### Feast-enhanced functions + +These functions add automatic Feast tagging and lineage on top of their MLflow counterparts: + +| Function | Enhancement | +|----------|-------------| +| `store.mlflow.start_run(run_name, tags)` | Auto-tags run with `feast.project` | +| `store.mlflow.log_model(model, path, flavor)` | Auto-attaches `feast_features.json` artifact | +| `store.mlflow.register_model(model_uri, name)` | Auto-tags model version with `feast.feature_service` | +| `store.mlflow.load_model(model_uri)` | Auto-tags prediction run with training lineage | + +**Supported model flavors for `log_model()`:** `sklearn`, `pytorch`, `xgboost`, `lightgbm`, `tensorflow`, `keras`, `pyfunc`. + +### Feast-only functions + +These are unique to the Feast integration and have no `mlflow` equivalent: + +| Function | Description | +|----------|-------------| +| `store.mlflow.resolve_features(model_uri)` | Resolve model URI to Feast feature service name | +| `store.mlflow.get_training_entity_df(run_id, ...)` | Recover entity DataFrame from a past MLflow run | +| `store.mlflow.log_training_dataset(df, dataset_name)` | Log a training DataFrame as an MLflow dataset input | +| `store.mlflow.active_run_id` | Current active MLflow run ID (or `None`) | +| `store.mlflow.client` | The underlying `MlflowClient` instance for advanced queries | +| `feast.mlflow.init(store)` | Explicitly bind `feast.mlflow` module to a `FeatureStore` (optional) | + +### Passthrough behavior + +The `feast.mlflow` module delegates any attribute not listed above to the raw `mlflow` module. This means you can use `feast.mlflow` as a drop-in replacement for `import mlflow`: + +```python +feast.mlflow.log_params(params) # passes through to mlflow.log_params +feast.mlflow.log_metrics(metrics) +feast.mlflow.set_tag("env", "staging") +feast.mlflow.MlflowClient() +``` + +`store.mlflow` does **not** have this passthrough — it only exposes the Feast-enhanced and Feast-only methods listed above. To access raw `mlflow` functions from `store.mlflow`, use the escape hatches: + +```python +store.mlflow.client.log_param(run_id, "lr", "0.01") # via MlflowClient instance +store.mlflow.mlflow.log_params(params) # via raw mlflow module +``` + +### Resolve a model back to its feature service + +```python +from feast import FeatureStore + +store = FeatureStore(".") +fs_name = store.mlflow.resolve_features("models:/driver_model/1") +# Returns: "driver_activity_v1" +``` + +Resolution order: +1. Model version tag `feast.feature_service` (set by `register_model()`) +2. Training run tag `feast.feature_service` (set by auto-logging) + +### Reproduce training from a past run + +```python +from feast import FeatureStore + +store = FeatureStore(".") + +entity_df = store.mlflow.get_training_entity_df(run_id="abc123") + +with store.mlflow.start_run(run_name="retrain_v2"): + new_df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + model = train(new_df) + store.mlflow.log_model(model, "model") +``` + +This requires `auto_log_entity_df: true` to have been enabled when the original run was recorded. + +## Feast UI integration + +The Feast UI server exposes three API endpoints that aggregate data from MLflow: + +| Endpoint | Description | +|----------|-------------| +| `/api/mlflow-runs` | All Feast-tagged MLflow runs with linked registered models | +| `/api/mlflow-feature-usage` | Per-feature-view usage stats (run count, last used, associated models) | +| `/api/mlflow-feature-models` | Reverse index of feature refs to registered models | + +The feature view detail page in the Feast UI displays: +- **MLflow Training Runs** count and **Last Used** date in the header stats +- An **MLflow Usage** panel showing training run count, relative last-used time, and a table of registered models that depend on the feature view + +Start the Feast UI with: + +```bash +feast ui --host 127.0.0.1 --port 8888 +``` diff --git a/docs/reference/offline-stores/README.md b/docs/reference/offline-stores/README.md index b5e2bccbdd1..1c0d24c8d07 100644 --- a/docs/reference/offline-stores/README.md +++ b/docs/reference/offline-stores/README.md @@ -49,3 +49,27 @@ Please see [Offline Store](../../getting-started/components/offline-store.md) fo {% content-ref url="ray.md" %} [ray.md](ray.md) {% endcontent-ref %} + +{% content-ref url="oracle.md" %} +[oracle.md](oracle.md) +{% endcontent-ref %} + +{% content-ref url="athena.md" %} +[athena.md](athena.md) +{% endcontent-ref %} + +{% content-ref url="clickhouse.md" %} +[clickhouse.md](clickhouse.md) +{% endcontent-ref %} + +{% content-ref url="mongodb.md" %} +[mongodb.md](mongodb.md) +{% endcontent-ref %} + +{% content-ref url="remote-offline-store.md" %} +[remote-offline-store.md](remote-offline-store.md) +{% endcontent-ref %} + +{% content-ref url="hybrid.md" %} +[hybrid.md](hybrid.md) +{% endcontent-ref %} diff --git a/docs/reference/offline-stores/athena.md b/docs/reference/offline-stores/athena.md new file mode 100644 index 00000000000..3fbe77681ee --- /dev/null +++ b/docs/reference/offline-stores/athena.md @@ -0,0 +1,66 @@ +# Athena offline store (contrib) + +## Description + +The Athena offline store provides support for reading [AthenaSources](../data-sources/athena.md). +* Entity dataframes can be provided as a SQL query or can be provided as a Pandas dataframe. + +## Disclaimer + +The Athena offline store does not achieve full test coverage. +Please do not assume complete stability. + +## Getting started +In order to use this offline store, you'll need to run `pip install 'feast[aws]'`. + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: athena + data_source: AwsDataCatalog + region: us-east-1 + database: my_database + workgroup: primary +online_store: + path: data/online_store.db +``` +{% endcode %} + +The full set of configuration options is available in [AthenaOfflineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.athena_offline_store.athena.AthenaOfflineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by offline stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Athena offline store. + +| | Athena | +| :----------------------------------------------------------------- |:-------| +| `get_historical_features` (point-in-time correct join) | yes | +| `pull_latest_from_table_or_query` (retrieve latest feature values) | yes | +| `pull_all_from_table_or_query` (retrieve a saved dataset) | yes | +| `offline_write_batch` (persist dataframes to offline store) | no | +| `write_logged_features` (persist logged features to offline store) | yes | + +Below is a matrix indicating which functionality is supported by `AthenaRetrievalJob`. + +| | Athena | +| ----------------------------------------------------- |--------| +| export to dataframe | yes | +| export to arrow table | yes | +| export to arrow batches | no | +| export to SQL | yes | +| export to data lake (S3, GCS, etc.) | no | +| export to data warehouse | no | +| export as Spark dataframe | no | +| local execution of Python-based on-demand transforms | yes | +| remote execution of Python-based on-demand transforms | no | +| persist results in the offline store | yes | +| preview the query plan before execution | yes | +| read partitioned data | yes | + +To compare this set of functionality against other offline stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/offline-stores/mongodb.md b/docs/reference/offline-stores/mongodb.md new file mode 100644 index 00000000000..a41d43ca676 --- /dev/null +++ b/docs/reference/offline-stores/mongodb.md @@ -0,0 +1,101 @@ +# MongoDB offline store (contrib) + +## Description + +The MongoDB offline store provides support for reading [MongoDBSource](../data-sources/mongodb.md). + +## Getting started + +In order to use this offline store, you'll need to run `pip install 'feast[mongodb]'`. + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBOfflineStore + connection_string: "mongodb+srv://user:pass@cluster.mongodb.net" # pragma: allowlist secret + database: feast + collection: feature_history +online_store: + type: mongodb + connection_string: "mongodb+srv://user:pass@cluster.mongodb.net" # pragma: allowlist secret + database_name: feast_online_store + collection_suffix: latest + client_kwargs: {} +``` +{% endcode %} + +The full set of configuration options is available in [MongoDBOfflineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBOfflineStoreConfig). + +## Data Model + +The offline store uses a single shared collection (by default `feature_history`) that stores append-only historical feature rows for all feature views. Each document represents one observation of one entity for one FeatureView at a specific event timestamp: + +```json +{ + "entity_id": "Binary(...)", + "feature_view": "driver_stats", + "event_timestamp": "ISODate(2024-01-15T12:00:00Z)", + "created_at": "ISODate(2024-01-15T12:01:00Z)", + "features": { + "conv_rate": 0.72, + "acc_rate": 0.91, + "avg_daily_trips": 14 + } +} +``` + +Key properties: + +* **Append-only**: Historical data is treated as immutable; corrections are written as new rows with newer `created_at` timestamps rather than in-place updates. +* **Time-series friendly**: `event_timestamp` represents when the feature value was observed; `created_at` is used as a tie-breaker when multiple observations share the same event timestamp. +* **Feature grouping by FeatureView**: `feature_view` identifies which FeatureView the row belongs to, so a single collection can host multiple FVs. + +A single compound index supports all major query patterns: + +``` +(entity_id ASC, feature_view ASC, event_timestamp DESC, created_at DESC) +``` + +This index enables efficient range scans over entities and feature views, while ensuring that the most recent observation per `(entity_id, feature_view)` is seen first during aggregation. The index is created lazily on first use and cached per connection string. + +## Key Optimizations + +* **Scoring vs. training paths**: When each entity appears only once in `entity_df` (scoring/inference — one feature lookup per entity), server-side `$group $first` efficiently returns the single latest value per entity. When the same entity appears at multiple timestamps (training — building a dataset with many historical snapshots per entity), the store retrieves all candidate rows and uses `pd.merge_asof` to select the correct point-in-time value for each request timestamp. +* **Two-level chunking**: `CHUNK_SIZE` (50,000 rows) controls the size of intermediate DataFrames in memory; `MONGO_BATCH_SIZE` (10,000 entity IDs) limits the query size sent to MongoDB. + +## Functionality Matrix + +The set of functionality supported by offline stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the MongoDB offline store. + +| | MongoDB | +| :----------------------------------------------------------------- | :------ | +| `get_historical_features` (point-in-time correct join) | yes | +| `pull_latest_from_table_or_query` (retrieve latest feature values) | yes | +| `pull_all_from_table_or_query` (retrieve a saved dataset) | yes | +| `offline_write_batch` (persist dataframes to offline store) | yes | +| `write_logged_features` (persist logged features to offline store) | no | + +Below is a matrix indicating which functionality is supported by `MongoDBRetrievalJob`. + +| | MongoDB | +| ----------------------------------------------------- | ------- | +| export to dataframe | yes | +| export to arrow table | yes | +| export to arrow batches | no | +| export to SQL | no | +| export to data lake (S3, GCS, etc.) | no | +| export to data warehouse | no | +| export as Spark dataframe | no | +| local execution of Python-based on-demand transforms | yes | +| remote execution of Python-based on-demand transforms | no | +| persist results in the offline store | yes | +| preview the query plan before execution | no | +| read partitioned data | no | + +To compare this set of functionality against other offline stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/offline-stores/oracle.md b/docs/reference/offline-stores/oracle.md new file mode 100644 index 00000000000..12f5b761ce6 --- /dev/null +++ b/docs/reference/offline-stores/oracle.md @@ -0,0 +1,109 @@ +# Oracle offline store (contrib) + +## Description + +The Oracle offline store provides support for reading [OracleSources](../data-sources/oracle.md). +* Entity dataframes can be provided as a SQL query or as a Pandas dataframe. +* Uses the [ibis](https://ibis-project.org/) Oracle backend (`ibis.oracle`) for all database interactions. +* Only one of `service_name`, `sid`, or `dsn` may be set in the configuration. + +## Disclaimer + +The Oracle offline store does not achieve full test coverage. +Please do not assume complete stability. + +## Getting started + +Install the Oracle extras: + +```bash +pip install 'feast[oracle]' +``` + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: oracle + host: DB_HOST + port: 1521 + user: DB_USERNAME + password: DB_PASSWORD + service_name: ORCL +online_store: + path: data/online_store.db +``` +{% endcode %} + +Connection can alternatively use `sid` or `dsn` instead of `service_name`: + +```yaml +# Using SID +offline_store: + type: oracle + host: DB_HOST + port: 1521 + user: DB_USERNAME + password: DB_PASSWORD + sid: ORCL + +# Using DSN +offline_store: + type: oracle + host: DB_HOST + port: 1521 + user: DB_USERNAME + password: DB_PASSWORD + dsn: "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=DB_HOST)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))" +``` + +### Configuration reference + +| Parameter | Required | Default | Description | +| :------------- | :------- | :---------- | :------------------------------------------------------- | +| `type` | yes | — | Must be set to `oracle` | +| `user` | yes | — | Oracle database user | +| `password` | yes | — | Oracle database password | +| `host` | no | `localhost` | Oracle database host | +| `port` | no | `1521` | Oracle database port | +| `service_name` | no | — | Oracle service name (mutually exclusive with sid and dsn) | +| `sid` | no | — | Oracle SID (mutually exclusive with service_name and dsn) | +| `database` | no | — | Oracle database name | +| `dsn` | no | — | Oracle DSN string (mutually exclusive with service_name and sid) | + +## Functionality Matrix + +The set of functionality supported by offline stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Oracle offline store. + +| | Oracle | +| :----------------------------------------------------------------- | :----- | +| `get_historical_features` (point-in-time correct join) | yes | +| `pull_latest_from_table_or_query` (retrieve latest feature values) | yes | +| `pull_all_from_table_or_query` (retrieve a saved dataset) | yes | +| `offline_write_batch` (persist dataframes to offline store) | yes | +| `write_logged_features` (persist logged features to offline store) | yes | + +Below is a matrix indicating which functionality is supported by `OracleRetrievalJob`. + +| | Oracle | +| ----------------------------------------------------- | ------ | +| export to dataframe | yes | +| export to arrow table | yes | +| export to arrow batches | no | +| export to SQL | no | +| export to data lake (S3, GCS, etc.) | no | +| export to data warehouse | no | +| export as Spark dataframe | no | +| local execution of Python-based on-demand transforms | yes | +| remote execution of Python-based on-demand transforms | no | +| persist results in the offline store | yes | +| preview the query plan before execution | no | +| read partitioned data | no | + +To compare this set of functionality against other offline stores, please see the full [functionality matrix](overview.md#functionality-matrix). + diff --git a/docs/reference/offline-stores/postgres.md b/docs/reference/offline-stores/postgres.md index 321ddcf25e7..7e9f112b3b3 100644 --- a/docs/reference/offline-stores/postgres.md +++ b/docs/reference/offline-stores/postgres.md @@ -38,7 +38,7 @@ online_store: ``` {% endcode %} -Note that `sslmode`, `sslkey_path`, `sslcert_path`, and `sslrootcert_path` are optional parameters. +Note that `sslmode` defaults to `require`, which encrypts the connection without certificate verification. To disable SSL (e.g. for local development), set `sslmode: disable`. For certificate verification, set `sslmode` to `verify-ca` or `verify-full` and provide the corresponding `sslrootcert_path` (and optionally `sslcert_path` and `sslkey_path` for mutual TLS). The full set of configuration options is available in [PostgreSQLOfflineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.postgres_offline_store.postgres.PostgreSQLOfflineStoreConfig). Additionally, a new optional parameter `entity_select_mode` was added to tell how Postgres should load the entity data. By default(`temp_table`), a temporary table is created and the entity data frame or sql is loaded into that table. A new value of `embed_query` was added to allow directly loading the SQL query into a CTE, providing improved performance and skipping the need to CREATE and DROP the temporary table. diff --git a/docs/reference/offline-stores/ray.md b/docs/reference/offline-stores/ray.md index 063ffcb209a..a9d8cd7ef08 100644 --- a/docs/reference/offline-stores/ray.md +++ b/docs/reference/offline-stores/ray.md @@ -31,6 +31,7 @@ The Ray offline store provides: - Efficient data filtering and column selection - Timestamp-based data processing with timezone awareness - Enterprise-ready KubeRay cluster support via CodeFlare SDK +- **GPU support**: schedule worker tasks on GPU nodes via `num_gpus` config (all modes including KubeRay) ## Functionality Matrix @@ -246,6 +247,9 @@ batch_engine: | `max_parallelism_multiplier` | int | 2 | Parallelism as multiple of CPU cores | | `target_partition_size_mb` | int | 64 | Target partition size (MB) | | `window_size_for_joins` | string | "1H" | Time window for distributed joins | +| `num_gpus` | float | None | GPUs per worker task. Supported in all modes. See [Worker Resource Scheduling](../compute-engine/ray.md#worker-resource-scheduling). | +| `gpu_batch_format` | string | `"pandas"` | Batch format for `map_batches` when `num_gpus` is set (`"numpy"` or `"pyarrow"` for GPU-native libs). | +| `worker_task_options` | dict | None | Arbitrary Ray `.options()` kwargs (num_cpus, memory, accelerator_type, resources, runtime_env, …). See [Worker Resource Scheduling](../compute-engine/ray.md#worker-resource-scheduling) for the full reference. | #### Mode Detection Precedence @@ -542,6 +546,12 @@ python your_feast_script.py - Secure communication between client and Ray cluster - Automatic cluster discovery +### GPU Support + +The Ray offline store supports GPU scheduling via the `num_gpus` and `gpu_batch_format` config options. This works across all execution modes (local, remote, and KubeRay). + +For full configuration details, examples, and KubeRay GPU setup, see the [Ray Compute Engine GPU Support](../compute-engine/ray.md#gpu-support) section. + ### Data Source Validation The Ray offline store validates data sources to ensure compatibility: @@ -557,14 +567,45 @@ except Exception as e: print(f"Data source validation failed: {e}") ``` +## Data Sources + +[`RaySource`](../data-sources/ray.md) is the recommended data source for the +Ray offline store. It is a pure-metadata descriptor that tells Feast how to +load a Ray Dataset from any source Ray Data supports — Parquet, CSV, JSON, +HuggingFace datasets, MongoDB, binary files, images, TFRecords, WebDataset, +SQL, and more. + +```python +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +# Load directly from the HuggingFace Hub +cheque_source = RaySource( + name="cheque_images_hf", + reader_type="huggingface", + reader_options={ + "dataset_name": "cheques_sample_data", + "split": "train", + }, + timestamp_field="event_timestamp", +) +``` + +See the [RaySource reference](../data-sources/ray.md) for a full list of +`reader_type` values and configuration options. + +> **Note:** `FileSource` (Parquet) remains supported for backward compatibility +> but `RaySource(reader_type="parquet")` is preferred for new projects. + ## Limitations -The Ray offline store has the following limitations: +The Ray offline store has one known limitation: -1. **File Sources Only**: Currently supports only `FileSource` data sources -2. **No Direct SQL**: Does not support SQL query interfaces -3. **No Online Writes**: Cannot write directly to online stores -4. **No Complex Transformations**: The Ray offline store focuses on data I/O operations. For complex feature transformations (aggregations, joins, custom UDFs), use the [Ray Compute Engine](../compute-engine/ray.md) instead +* **`online_write_batch` not implemented**: The `OfflineStore.online_write_batch()` interface + is not supported by the Ray offline store. This does **not** affect materialization — + `feast materialize` writes to the online store correctly via the + [Ray Compute Engine](../compute-engine/ray.md). The restriction only applies to callers + that invoke `online_write_batch` on the offline store object directly, which is an + uncommon pattern outside of custom tooling. ## Integration with Ray Compute Engine diff --git a/docs/reference/online-stores/README.md b/docs/reference/online-stores/README.md index 5df4710434c..257864b9b30 100644 --- a/docs/reference/online-stores/README.md +++ b/docs/reference/online-stores/README.md @@ -22,8 +22,8 @@ Please see [Online Store](../../getting-started/components/online-store.md) for [dragonfly.md](dragonfly.md) {% endcontent-ref %} -{% content-ref url="ikv.md" %} -[ikv.md](ikv.md) +{% content-ref url="valkey.md" %} +[valkey.md](valkey.md) {% endcontent-ref %} {% content-ref url="datastore.md" %} @@ -35,13 +35,17 @@ Please see [Online Store](../../getting-started/components/online-store.md) for {% endcontent-ref %} {% content-ref url="bigtable.md" %} -[bigtable.md](mysql.md) +[bigtable.md](bigtable.md) {% endcontent-ref %} {% content-ref url="postgres.md" %} [postgres.md](postgres.md) {% endcontent-ref %} +{% content-ref url="hbase.md" %} +[hbase.md](hbase.md) +{% endcontent-ref %} + {% content-ref url="cassandra.md" %} [cassandra.md](cassandra.md) {% endcontent-ref %} @@ -54,6 +58,14 @@ Please see [Online Store](../../getting-started/components/online-store.md) for [mysql.md](mysql.md) {% endcontent-ref %} +{% content-ref url="mongodb.md" %} +[mongodb.md](mongodb.md) +{% endcontent-ref %} + +{% content-ref url="aerospike.md" %} +[aerospike.md](aerospike.md) +{% endcontent-ref %} + {% content-ref url="hazelcast.md" %} [hazelcast.md](hazelcast.md) {% endcontent-ref %} @@ -69,3 +81,23 @@ Please see [Online Store](../../getting-started/components/online-store.md) for {% content-ref url="singlestore.md" %} [singlestore.md](singlestore.md) {% endcontent-ref %} + +{% content-ref url="elasticsearch.md" %} +[elasticsearch.md](elasticsearch.md) +{% endcontent-ref %} + +{% content-ref url="qdrant.md" %} +[qdrant.md](qdrant.md) +{% endcontent-ref %} + +{% content-ref url="milvus.md" %} +[milvus.md](milvus.md) +{% endcontent-ref %} + +{% content-ref url="faiss.md" %} +[faiss.md](faiss.md) +{% endcontent-ref %} + +{% content-ref url="hybrid.md" %} +[hybrid.md](hybrid.md) +{% endcontent-ref %} diff --git a/docs/reference/online-stores/aerospike.md b/docs/reference/online-stores/aerospike.md new file mode 100644 index 00000000000..e5a9754796b --- /dev/null +++ b/docs/reference/online-stores/aerospike.md @@ -0,0 +1,389 @@ +# Aerospike online store (Preview) + +## Description + +The [Aerospike](https://aerospike.com/) online store provides support for materializing feature values into an Aerospike cluster for serving online features. + +{% hint style="warning" %} +The Aerospike online store is currently in **preview**. Some functionality may be unstable, and breaking changes may occur in future releases. +{% endhint %} + +## Features + +* Supports both synchronous and asynchronous read/write paths (`online_read` / `online_read_async`, `online_write_batch` / `online_write_batch_async`). Async methods wrap the blocking client in `run_in_executor`, keeping the event loop responsive in feature-server workloads. +* Partial, server-side upserts via Aerospike Map CDT operations — writing one feature view never clobbers another feature view stored on the same entity. +* Record-level TTL controlled by a single `ttl_seconds` config option (honours the namespace default, a "never expire" sentinel, or an explicit number of seconds). +* Per-feature-view **namespace overrides** and **set overrides** — pin individual feature views to RAM-only or SSD-backed namespaces, or isolate one view in its own set, without splitting projects. +* **Prewriting hook** — a configurable, import-string-resolved callable applied to every write batch for cross-cutting concerns like PII masking, application-side encryption, or value coercion. +* Authentication and TLS options for Aerospike Enterprise Edition passed straight through to the Aerospike Python client. +* `client_kwargs` escape hatch for any advanced client-config field not surfaced on `AerospikeOnlineStoreConfig`. +* Baseline: Aerospike Server **≥ 6.0** (uses batch-write / batch-operate APIs). The store has been developed against CE 8.x. + +## Getting started + +Install the Aerospike extra (alongside the dependency for the offline store of choice): + +```bash +pip install 'feast[aerospike]' +``` + +You can start from any of the standard templates (e.g. `feast init -t local` or `feast init -t aws`) and then swap in Aerospike as the online store as shown below. + +## Examples + +### Basic configuration — local Aerospike CE + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["127.0.0.1", 3000] + namespace: feast +``` +{% endcode %} + +### Multi-node cluster + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike-1.internal", 3000] + - ["aerospike-2.internal", 3000] + - ["aerospike-3.internal", 3000] + namespace: feast + ttl_seconds: 86400 # 24h record-level TTL + read_timeout_ms: 150 # hard deadline for a single-record get + write_timeout_ms: 300 # hard deadline for a single-record put/operate + batch_total_timeout_ms: 500 # hard deadline for online_read / online_write_batch + batch_max_records: 1000 # chunk size for batch_write / batch_operate + socket_timeout_ms: 50 # per-attempt deadline so max_retries can fire + max_retries: 2 +``` +{% endcode %} + +> **Timeout semantics.** The Aerospike client distinguishes per-attempt +> (`socket_timeout`) from total (`total_timeout`) deadlines. `*_timeout_ms` map +> to `total_timeout` — the overall budget for a call including retries. Set +> `socket_timeout_ms` as well so each individual attempt has its own (shorter) +> deadline; without it, `max_retries` effectively never fires because the +> first attempt is allowed to consume the entire total deadline. + +> **Batch chunking.** `online_read` and `online_write_batch` split large +> requests into chunks of at most `batch_max_records` (default `1000`). +> Aerospike enforces a per-node batch limit via the server `batch-max-requests` +> setting (historically `5000`). Lower `batch_max_records` if your cluster cap +> is tighter; raise it only when the server limit and client timeouts allow. + +### Aerospike Enterprise with authentication + +> Requires Aerospike Enterprise Edition. The Community Edition server has no built-in user/security model and will reject these config keys. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike.internal", 3000] + namespace: feast + user: feast_user + password: ${AEROSPIKE_PASSWORD} # pragma: allowlist secret + auth_mode: internal # internal | external | pki +``` +{% endcode %} + +### Aerospike Enterprise with TLS + +> Requires Aerospike Enterprise Edition. The Community Edition server does not implement TLS, so `tls` config is effective only against EE clusters. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike-1.internal", 4333, "aerospike-tls"] + namespace: feast + tls: + enable: true + cafile: /etc/aerospike/certs/ca.pem + certfile: /etc/aerospike/certs/client.pem + keyfile: /etc/aerospike/certs/client.key +``` +{% endcode %} + +### Per-feature-view namespace and set overrides + +Two `Dict[str, str]` config fields — `namespace_overrides` and `set_overrides` — let you place individual feature views on a different Aerospike namespace or set without splitting your project across stores. Anything not listed in either map falls back to the store-level default (`namespace` / `set_name_template`). + +Common reasons to reach for these: + +* A **hot, latency-sensitive view** belongs on a RAM-only namespace; a **wide, cold view** belongs on an SSD-backed namespace. Same project, different storage tiers. +* You want `feast apply` deletions or `truncate` on one feature view to be O(1) without scanning records of the others — give that view its own set. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike.internal", 3000] + namespace: feast # default namespace + set_name_template: "{project}_{collection_suffix}" + namespace_overrides: + driver_realtime_stats: feast_ram # in-memory namespace + driver_history_lookup: feast_ssd # device-backed namespace + set_overrides: + isolated_view: my_feature_repo_isolated +``` +{% endcode %} + +> **Tradeoffs.** +> +> * Every namespace listed in `namespace_overrides` MUST already exist on the cluster — Aerospike cannot create namespaces at runtime, and a missing namespace surfaces as an opaque `AEROSPIKE_ERR_PARAM` on the first read or write. +> * Putting feature views on different sets means a multi-feature-view read for the same entity becomes one Aerospike round trip per set, not one round trip total. Only opt in when the operational isolation is worth that cost. Reads that touch a single feature view are unaffected. +> * Admin operations honour the overrides automatically: `update()` (called by `feast apply`) groups dropped feature views by their resolved `(namespace, set)` and issues one background scan per group; `teardown()` truncates every unique `(namespace, set)` pair the project may have written to (including the store-level default). + +### Prewriting hooks + +`prewriting_hook` is the import path of a callable that is invoked once per `online_write_batch` call, receives the rows about to be written, and returns the rows that actually go on the wire. Use it for cross-cutting write-side concerns that you don't want sprinkled through every materialization job — PII masking, application-side encryption, dual-write fan-out, value coercion, etc. + +Hooks are referenced by import string (rather than as a Python `Callable` value) so the config survives YAML/JSON serialisation and remote-feature-server transport. The resolved callable is cached on the store instance, so import cost is paid once per store lifetime. + +**Hook signature:** + +```python +def hook( + config: RepoConfig, + table: FeatureView, + data: list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + datetime | None, + ] + ], +) -> list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + datetime | None, + ] +]: + ... +``` + +The hook MUST return a row list with the same schema as its input. Returning `[]` short-circuits the write — same path as an empty input, no wire call is issued. Hooks that raise will fail the whole batch; there is no per-row fallback. + +**1. Drop a hook function in your project.** Any module on the `PYTHONPATH` of every process that writes through Feast will do (the materialization workers, the registry CLI host, and the feature server, if you run one). + +{% code title="my_feature_repo/hooks.py" %} +```python +"""Prewriting hooks for the Aerospike online store.""" +from __future__ import annotations + +import hashlib +import os +from datetime import datetime +from typing import Optional + +from feast import FeatureView +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RepoConfig + +# Names of features that must never reach the online store as plaintext. +# Matched by exact feature name; tweak to your project's conventions. +_SENSITIVE_FEATURES = {"email", "phone_number", "ssn"} + + +def hash_pii_string_features( + config: RepoConfig, + table: FeatureView, + data: list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + Optional[datetime], + ] + ], +) -> list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + Optional[datetime], + ] +]: + """Replace any sensitive string feature with a salted SHA-256 hex digest. + + The hash is deterministic (same input → same digest) so downstream lookups + that hash the candidate value the same way still hit. ``FEAST_PII_SALT`` + must be set on every process that materialises features; an unset salt + raises rather than silently falling back to plaintext. + """ + salt = os.environ.get("FEAST_PII_SALT") + if salt is None: + raise RuntimeError( + "FEAST_PII_SALT is not set; refusing to write feature batches " + "without a configured PII salt." + ) + salt_bytes = salt.encode("utf-8") + + def _digest(plaintext: str) -> str: + h = hashlib.sha256() + h.update(salt_bytes) + h.update(plaintext.encode("utf-8")) + return h.hexdigest() + + transformed: list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + Optional[datetime], + ] + ] = [] + for entity_key, values, event_ts, created_ts in data: + new_values = dict(values) + for feature_name in _SENSITIVE_FEATURES.intersection(new_values): + v = new_values[feature_name] + if v.HasField("string_val") and v.string_val: + new_values[feature_name] = ValueProto(string_val=_digest(v.string_val)) + transformed.append((entity_key, new_values, event_ts, created_ts)) + return transformed +``` +{% endcode %} + +**2. Reference the hook from `feature_store.yaml`:** + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike.internal", 3000] + namespace: feast + prewriting_hook: my_feature_repo.hooks.hash_pii_string_features +``` +{% endcode %} + +> **Operational notes.** +> +> * The hook is **only invoked on the write path**; reads pass through the store untouched. If your hook is one-way (e.g. hashing) you have to apply the same transformation to the candidate value at read time yourself. +> * Hooks run inside the same process as the writer — they're not RPCs and not sandboxed. They can read environment variables, open files, call out to KMS, etc. Treat them as part of your trusted code base. +> * A misconfigured `prewriting_hook` (bad import path, missing function, non-callable target) raises `ValueError` / `TypeError` on the *first* `online_write_batch` call, not on store construction. Add a smoke test that writes one row at deploy time so misconfigurations surface before a real batch. + +The full set of configuration options is available in [`AerospikeOnlineStoreConfig`](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.aerospike_online_store.aerospike.AerospikeOnlineStoreConfig). + +## Data Model + +The Aerospike online store uses a **single set per project** with entity-key collocation. Features from multiple feature views for the same entity are stored together on a single Aerospike record, analogous to the MongoDB online store's "one document per entity" layout. + +| Aerospike concept | Feast mapping | +| :---------------- | :---------------------------------------------------------------------------- | +| Namespace | `online_store.namespace` (must be pre-configured on the cluster); per-feature-view override via `online_store.namespace_overrides` | +| Set | `online_store.set_name_template` → `"{project}_{collection_suffix}"` by default; per-feature-view override via `online_store.set_overrides` | +| Key | `serialize_entity_key(entity_key)` as `bytearray` user key | +| Bin `features` | Map CDT keyed by feature-view name, each value a map of `feature → native` | +| Bin `event_ts` | Map CDT keyed by feature-view name, each value an int64 epoch-ms timestamp | +| Bin `created_ts` | Top-level int64 epoch-ms timestamp (last `feast materialize`) | + +### Example record + +For a single entity carrying features from two feature views (`driver_stats` and `pricing`): + +```text +key: (ns="feast", set="my_feature_repo_latest", user_key=) +bins: + features: + driver_stats: + rating: 4.91 + trips_last_7d: 132 + pricing: + surge_multiplier: 1.2 + event_ts: + driver_stats: 1737374400000 # 2025-01-20T12:00:00Z + pricing: 1737447000000 # 2025-01-21T08:30:00Z + created_ts: 1737460805000 # 2025-01-21T12:00:05Z +``` + +### Key design decisions + +* **Record per entity, bin per concept.** `features` and `event_ts` are Aerospike Map CDT bins, not dynamic bins, which keeps the store within the 15-byte Aerospike bin-name limit regardless of how many feature views a project has. +* **Partial upserts via Map CDT ops.** Writes use `batch_write` with `map_put_items("features", {: {...}})` and `map_put("event_ts", , )`. Concurrent writes to different feature views on the same entity never clobber each other — each write mutates only its own map keys. +* **Entity-key bytes as the Aerospike user key.** Feast's `serialize_entity_key` output is passed as a `bytearray` user key (not `bytes` — the Python client hashes only the first byte of `bytes` keys, which would collapse distinct entities). +* **Timestamps as int64 epoch milliseconds.** Aerospike has no native datetime type; tz-naive timestamps are treated as UTC per the `OnlineStore` contract. + +### TTL and expiry + +`ttl_seconds` is written as record-level metadata on every `online_write_batch` call: + +| `ttl_seconds` | Aerospike TTL | Effect | +| :------------ | :-------------------------------- | :---------------------------------------------------------- | +| not set / `null` | `TTL_NAMESPACE_DEFAULT` | Record inherits the namespace's configured `default-ttl`. | +| `0` | `TTL_NEVER_EXPIRE` | Record is kept until explicitly deleted. | +| `>0` | that many seconds | Record is evicted by the server's `nsup` thread. | + +There is no per-feature-view TTL override in this version — the setting is applied uniformly for every write made by the online store. + +### Indexes + +No secondary indexes are created. All access goes through the primary key, which is the serialized entity key. + +## Async support + +Async read/write are provided by running the Aerospike Python client's blocking calls on the default thread-pool executor (`loop.run_in_executor`). The underlying C client releases the GIL during network I/O, so `await store.online_read_async(...)` keeps the event loop responsive. A native asyncio Aerospike client is not currently used. + +Both sync and async methods are fully supported: + +* `online_read` / `online_read_async` +* `online_write_batch` / `online_write_batch_async` +* `initialize` / `close` — `initialize(config)` eagerly opens the connection so feature servers pay the TCP/handshake cost at startup; `close()` releases the cached client. + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Aerospike online store. + +| | Aerospike | +| :-------------------------------------------------------- | :-------- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | yes | +| support for ttl (time to live) at retrieval | yes | +| support for deleting expired data | yes | +| collocated by feature view | no | +| collocated by feature service | no | +| collocated by entity key | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/cassandra.md b/docs/reference/online-stores/cassandra.md index 198f15ca47f..5d95e526421 100644 --- a/docs/reference/online-stores/cassandra.md +++ b/docs/reference/online-stores/cassandra.md @@ -37,6 +37,51 @@ online_store: ``` {% endcode %} +### Example (Cassandra — multi-DC) + +Use `datacenters` instead of `hosts` when your cluster spans multiple datacenters. +Each entry gets a named Cassandra **execution profile** keyed by its `name` field, +enabling per-DC routing. The default profile is determined by `load_balancing.local_dc` +(or the first datacenter entry when `load_balancing` is absent). Use the optional +`routing` block to direct reads and writes to specific datacenters. The keyspace must +already exist; Feast does not create it automatically. + +`datacenters` is mutually exclusive with `hosts` and `secure_bundle_path`. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: cassandra + keyspace: KeyspaceName + datacenters: + - name: dc1 + hosts: + - 192.168.1.1 + - 192.168.1.2 + replication_factor: 3 # optional, informational only + replication_strategy: NetworkTopologyStrategy # optional, informational only + - name: dc2 + hosts: + - 10.0.0.1 + replication_factor: 2 # optional, informational only + routing: # optional + read_dc: dc2 # DC to use for reads (default: load_balancing.local_dc) + write_dc: dc1 # DC to use for writes (default: load_balancing.local_dc) + port: 9042 # optional + username: user # optional + password: secret # optional + protocol_version: 5 # optional + load_balancing: # optional + local_dc: 'dc1' # sets the default execution profile + load_balancing_policy: 'TokenAwarePolicy(DCAwareRoundRobinPolicy)' # optional + read_concurrency: 100 # optional + write_concurrency: 100 # optional +``` +{% endcode %} + ### Example (Astra DB) {% code title="feature_store.yaml" %} diff --git a/docs/reference/online-stores/dynamodb.md b/docs/reference/online-stores/dynamodb.md index 344caccac1d..a7f6b7392c9 100644 --- a/docs/reference/online-stores/dynamodb.md +++ b/docs/reference/online-stores/dynamodb.md @@ -22,13 +22,54 @@ online_store: The full set of configuration options is available in [DynamoDBOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.dynamodb.DynamoDBOnlineStoreConfig). +## Configuration + +Below is an example with performance tuning options: + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: aws +online_store: + type: dynamodb + region: us-west-2 + batch_size: 100 + max_read_workers: 10 + consistent_reads: false +``` +{% endcode %} + +### Configuration Options + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| `region` | string | | AWS region for DynamoDB | +| `table_name_template` | string | `{project}.{table_name}` | Template for table names | +| `batch_size` | int | `100` | Number of items per BatchGetItem/BatchWriteItem request (max 100) | +| `max_read_workers` | int | `10` | Maximum parallel threads for batch read operations. Higher values improve throughput for large batch reads but increase resource usage | +| `consistent_reads` | bool | `false` | Whether to use strongly consistent reads (higher latency, guaranteed latest data) | +| `tags` | dict | `null` | AWS resource tags added to each table | +| `session_based_auth` | bool | `false` | Use AWS session-based client authentication | + +### Performance Tuning + +**Parallel Batch Reads**: When reading features for many entities, DynamoDB's BatchGetItem is limited to 100 items per request. For 500 entities, this requires 5 batch requests. The `max_read_workers` option controls how many of these batches execute in parallel: + +- **Sequential (old behavior)**: 5 batches × 10ms = 50ms total +- **Parallel (with `max_read_workers: 10`)**: 5 batches in parallel ≈ 10ms total + +For high-throughput workloads with large entity counts, increase `max_read_workers` (up to 20-30) based on your DynamoDB capacity and network conditions. + +**Batch Size**: Increase `batch_size` up to 100 to reduce the number of API calls. However, larger batches may hit DynamoDB's 16MB response limit for tables with large feature values. + ## Permissions Feast requires the following permissions in order to execute commands for DynamoDB online store: | **Command** | Permissions | Resources | | ----------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------- | -| **Apply** |

dynamodb:CreateTable

dynamodb:DescribeTable

dynamodb:DeleteTable

| arn:aws:dynamodb:\:\:table/\* | +| **Apply** |

dynamodb:CreateTable

dynamodb:DescribeTable

dynamodb:DeleteTable

dynamodb:TagResource

| arn:aws:dynamodb:\:\:table/\* | | **Materialize** | dynamodb.BatchWriteItem | arn:aws:dynamodb:\:\:table/\* | | **Get Online Features** | dynamodb.BatchGetItem | arn:aws:dynamodb:\:\:table/\* | @@ -42,6 +83,7 @@ The following inline policy can be used to grant Feast the necessary permissions "dynamodb:CreateTable", "dynamodb:DescribeTable", "dynamodb:DeleteTable", + "dynamodb:TagResource", "dynamodb:BatchWriteItem", "dynamodb:BatchGetItem" ], diff --git a/docs/reference/online-stores/faiss.md b/docs/reference/online-stores/faiss.md new file mode 100644 index 00000000000..32c9242dd60 --- /dev/null +++ b/docs/reference/online-stores/faiss.md @@ -0,0 +1,57 @@ +# Faiss online store + +## Description + +The [Faiss](https://github.com/facebookresearch/faiss) online store provides support for materializing feature values and performing vector similarity search using Facebook AI Similarity Search (Faiss). Faiss is a library for efficient similarity search and clustering of dense vectors, making it well-suited for use cases involving embeddings and nearest-neighbor lookups. + +## Getting started +In order to use this online store, you'll need to install the Faiss dependency. E.g. + +`pip install 'feast[faiss]'` + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: feast.infra.online_stores.faiss_online_store.FaissOnlineStore + dimension: 128 + index_path: data/faiss_index + index_type: IVFFlat # optional, default: IVFFlat + nlist: 100 # optional, default: 100 +``` +{% endcode %} + +**Note:** Faiss is not registered as a named online store type. You must use the fully qualified class path as the `type` value. + +The full set of configuration options is available in [FaissOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.faiss_online_store.FaissOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Faiss online store. + +| | Faiss | +|:----------------------------------------------------------|:------| +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | +| vector similarity search | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/hbase.md b/docs/reference/online-stores/hbase.md new file mode 100644 index 00000000000..b53bfed5fe5 --- /dev/null +++ b/docs/reference/online-stores/hbase.md @@ -0,0 +1,56 @@ +# HBase online store + +## Description + +The [HBase](https://hbase.apache.org/) online store provides support for materializing feature values into an Apache HBase database for serving online features in real-time. + +* Each feature view is mapped to an HBase table +* Connects to HBase via the Thrift server using [happybase](https://happybase.readthedocs.io/) + +## Getting started +In order to use this online store, you'll need to run `pip install 'feast[hbase]'`. + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: hbase + host: localhost + port: "9090" + connection_pool_size: 4 # optional + protocol: binary # optional + transport: buffered # optional +``` +{% endcode %} + +The full set of configuration options is available in [HbaseOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.hbase_online_store.hbase.HbaseOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the HBase online store. + +| | HBase | +| :-------------------------------------------------------- | :---- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/ikv.md b/docs/reference/online-stores/ikv.md deleted file mode 100644 index 79f21d17797..00000000000 --- a/docs/reference/online-stores/ikv.md +++ /dev/null @@ -1,69 +0,0 @@ -# IKV (Inlined Key-Value Store) online store - -## Description - -[IKV](https://github.com/inlinedio/ikv-store) is a fully-managed embedded key-value store, primarily designed for storing ML features. Most key-value stores (think Redis or Cassandra) need a remote database cluster, whereas IKV allows you to utilize your existing application infrastructure to store data (cost efficient) and access it without any network calls (better performance). See detailed performance benchmarks and cost comparison with Redis on [https://inlined.io](https://inlined.io). IKV can be used as an online-store in Feast, the rest of this guide goes over the setup. - -## Getting started -Make sure you have Python and `pip` installed. - -Install the Feast SDK and CLI: `pip install feast` - -In order to use this online store, you'll need to install the IKV extra (along with the dependency needed for the offline store of choice). E.g. -- `pip install 'feast[gcp, ikv]'` -- `pip install 'feast[snowflake, ikv]'` -- `pip install 'feast[aws, ikv]'` -- `pip install 'feast[azure, ikv]'` - -You can get started by using any of the other templates (e.g. `feast init -t gcp` or `feast init -t snowflake` or `feast init -t aws`), and then swapping in IKV as the online store as seen below in the examples. - -### 1. Provision an IKV store -Go to [https://inlined.io](https://inlined.io) or email onboarding[at]inlined.io - -### 2. Configure - -Update `my_feature_repo/feature_store.yaml` with the below contents: - -{% code title="feature_store.yaml" %} -```yaml -project: my_feature_repo -registry: data/registry.db -provider: local -online_store: - type: ikv - account_id: secret - account_passkey: secret - store_name: your-store-name - mount_directory: /absolute/path/on/disk/for/ikv/embedded/index -``` -{% endcode %} - -After provisioning an IKV account/store, you should have an account id, passkey and store-name. Additionally you must specify a mount-directory - where IKV will pull/update (maintain) a copy of the index for online reads (IKV is an embedded database). It can be skipped only if you don't plan to read any data from this container. The mount directory path usually points to a location on local/remote disk. - -The full set of configuration options is available in IKVOnlineStoreConfig at `sdk/python/feast/infra/online_stores/contrib/ikv_online_store/ikv.py` - -## Functionality Matrix - -The set of functionality supported by online stores is described in detail [here](overview.md#functionality). -Below is a matrix indicating which functionality is supported by the IKV online store. - -| | IKV | -| :-------------------------------------------------------- | :---- | -| write feature values to the online store | yes | -| read feature values from the online store | yes | -| update infrastructure (e.g. tables) in the online store | yes | -| teardown infrastructure (e.g. tables) in the online store | yes | -| generate a plan of infrastructure changes | no | -| support for on-demand transforms | yes | -| readable by Python SDK | yes | -| readable by Java | no | -| readable by Go | no | -| support for entityless feature views | yes | -| support for concurrent writing to the same key | yes | -| support for ttl (time to live) at retrieval | no | -| support for deleting expired data | no | -| collocated by feature view | no | -| collocated by feature service | no | -| collocated by entity key | yes | - -To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/milvus.md b/docs/reference/online-stores/milvus.md index 014c7bd68a5..58f7dbd167a 100644 --- a/docs/reference/online-stores/milvus.md +++ b/docs/reference/online-stores/milvus.md @@ -11,6 +11,14 @@ In order to use this online store, you'll need to install the Milvus extra (alon `pip install 'feast[milvus]'` +{% hint style="warning" %} +**Upgrading to milvus-lite 3.0.0+** + +Feast supports both milvus-lite 2.x and 3.x. However, if you upgrade from milvus-lite 2.x.x to 3.0.0+, the `.db` files created by the original storage format are **not compatible** with the milvus-lite 3.0.0+ engine. You will need to re-import your data into a new database — automatic migration is not available. + +See the [milvus-lite GitHub page](https://github.com/milvus-io/milvus-lite) for more details. +{% endhint %} + You can get started by using any of the other templates (e.g. `feast init -t gcp` or `feast init -t snowflake` or `feast init -t aws`), and then swapping in Redis as the online store as seen below in the examples. ## Examples diff --git a/docs/reference/online-stores/mongodb.md b/docs/reference/online-stores/mongodb.md new file mode 100644 index 00000000000..e5251747e77 --- /dev/null +++ b/docs/reference/online-stores/mongodb.md @@ -0,0 +1,177 @@ +# MongoDB online store + +## Description + +The [MongoDB](https://www.mongodb.com/) online store provides support for materializing feature values into MongoDB for serving online features. + +## Features + +* Supports both synchronous and asynchronous operations for high-performance feature retrieval +* Native async support uses PyMongo's `AsyncMongoClient` (no Motor dependency required) +* Flexible connection options supporting MongoDB Atlas, self-hosted MongoDB, and MongoDB replica sets +* Automatic index creation for optimized query performance +* Entity key collocation for efficient feature retrieval + +## Getting started + +In order to use this online store, you'll need to install the MongoDB extra (along with the dependency needed for the offline store of choice): + +```bash +pip install 'feast[mongodb]' +``` + +You can get started by using any of the other templates (e.g. `feast init -t gcp` or `feast init -t snowflake` or `feast init -t aws`), and then swapping in MongoDB as the online store as seen below in the examples. + +## Examples + +### Basic configuration with MongoDB Atlas + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mongodb + connection_string: "mongodb+srv://username:password@cluster.mongodb.net/" # pragma: allowlist secret + database_name: feast_online_store +``` +{% endcode %} + +### Self-hosted MongoDB with authentication + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mongodb + connection_string: "mongodb://username:password@localhost:27017/" # pragma: allowlist secret + database_name: feast_online_store + collection_suffix: features +``` +{% endcode %} + +### MongoDB replica set configuration + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mongodb + connection_string: "mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplicaSet" + database_name: feast_online_store + client_kwargs: + retryWrites: true + w: majority +``` +{% endcode %} + +### Advanced configuration with custom client options + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mongodb + connection_string: "mongodb+srv://cluster.mongodb.net/" + database_name: feast_online_store + collection_suffix: features + client_kwargs: + maxPoolSize: 50 + minPoolSize: 10 + serverSelectionTimeoutMS: 5000 + connectTimeoutMS: 10000 +``` +{% endcode %} + +The full set of configuration options is available in [MongoDBOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.mongodb_online_store.mongodb.MongoDBOnlineStoreConfig). + +## Data Model + +The MongoDB online store uses a **single collection per project** with entity key collocation. Features from multiple feature views for the same entity are stored together in a single document. + +### Example Document Schema + +The example shows a single entity. It contains 3 features from 2 feature views: "rating" and "trips_last7d" from Feature +View "driver_stats", and "surge_multiplier" from "pricing" view. +Each feature view has its own event timestamp. +The "created_timestamp" marks when the entity was materialized. + +```javascript +{ + "_id": "", // Binary entity key (bytes) + "features": { + "driver_stats": { + "rating": 4.91, + "trips_last_7d": 132 + }, + "pricing": { + "surge_multiplier": 1.2 + } + }, + "event_timestamps": { + "driver_stats": ISODate("2026-01-20T12:00:00Z"), + "pricing": ISODate("2026-01-21T08:30:00Z") + }, + "created_timestamp": ISODate("2026-01-21T12:00:05Z") +} +``` + +### Key Design Decisions + +* **`_id` field**: Uses the serialized entity key (bytes) as the primary key for efficient lookups +* **Nested features**: Features are organized by feature view name, allowing multiple feature views per entity +* **Event timestamps**: Stored per feature view to track when each feature set was last updated +* **Created timestamp**: Global timestamp for the entire document + +### Indexes + +The online store automatically creates the following index: +* Primary key index on `_id` (automatic in MongoDB), set to the serialized entity key. + +No additional indexes are required for the online store operations. + +## Async Support + +The MongoDB online store provides native async support using PyMongo 4.13+'s stable `AsyncMongoClient`. This enables: + +* **High concurrency**: Handle thousands of concurrent feature requests without thread pool limitations +* **True async I/O**: Non-blocking operations for better performance in async applications +* **10-20x performance improvement**: For concurrent workloads compared to sequential sync operations + +Both sync and async methods are fully supported: +* `online_read` / `online_read_async` +* `online_write_batch` / `online_write_batch_async` + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the MongoDB online store. + +| | MongoDB | +| :-------------------------------------------------------- | :------ | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | yes | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | no | +| collocated by feature service | no | +| collocated by entity key | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). + diff --git a/docs/reference/online-stores/overview.md b/docs/reference/online-stores/overview.md index b54329ad613..663a48836dc 100644 --- a/docs/reference/online-stores/overview.md +++ b/docs/reference/online-stores/overview.md @@ -29,26 +29,26 @@ See this [issue](https://github.com/feast-dev/feast/issues/2254) for a discussio ## Functionality Matrix There are currently five core online store implementations: `SqliteOnlineStore`, `RedisOnlineStore`, `DynamoDBOnlineStore`, `SnowflakeOnlineStore`, and `DatastoreOnlineStore`. -There are several additional implementations contributed by the Feast community (`PostgreSQLOnlineStore`, `HbaseOnlineStore`, `CassandraOnlineStore` and `IKVOnlineStore`), which are not guaranteed to be stable or to match the functionality of the core implementations. +There are several additional implementations contributed by the Feast community (`PostgreSQLOnlineStore`, `HbaseOnlineStore`, `CassandraOnlineStore` and `ScyllaDBOnlineStore`), which are not guaranteed to be stable or to match the functionality of the core implementations. Details for each specific online store, such as how to configure it in a `feature_store.yaml`, can be found [here](README.md). Below is a matrix indicating which online stores support what functionality. -| | Sqlite | Redis | DynamoDB | Snowflake | Datastore | Postgres | Hbase | [[Cassandra](https://cassandra.apache.org/_/index.html) / [Astra DB](https://www.datastax.com/products/datastax-astra?utm_source=feast)] | [IKV](https://inlined.io) | Milvus | -| :-------------------------------------------------------- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- |:-------| -| write feature values to the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| read feature values from the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| update infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| teardown infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| generate a plan of infrastructure changes | yes | no | no | no | no | no | no | yes | no | no | -| support for on-demand transforms | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| readable by Python SDK | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| readable by Java | no | yes | no | no | no | no | no | no | no | no | -| readable by Go | yes | yes | no | no | no | no | no | no | no | no | -| support for entityless feature views | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | -| support for concurrent writing to the same key | no | yes | no | no | no | no | no | no | yes | no | -| support for ttl (time to live) at retrieval | no | yes | no | no | no | no | no | no | no | no | -| support for deleting expired data | no | yes | no | no | no | no | no | no | no | no | -| collocated by feature view | yes | no | yes | yes | yes | yes | yes | yes | no | no | +| | Sqlite | Redis | DynamoDB | Snowflake | Datastore | Postgres | Hbase | [[Cassandra](https://cassandra.apache.org/_/index.html) / [Astra DB](https://www.datastax.com/products/datastax-astra?utm_source=feast)] | Milvus | ScyllaDB | +| :-------------------------------------------------------- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- |:----| :-- | +| write feature values to the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| read feature values from the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| update infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| generate a plan of infrastructure changes | yes | no | no | no | no | no | no | yes | no | no | +| support for on-demand transforms | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| readable by Python SDK | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| readable by Java | no | yes | no | no | no | no | no | no | no | no | +| readable by Go | yes | yes | no | no | no | no | no | no | no | no | +| support for entityless feature views | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| support for concurrent writing to the same key | no | yes | no | no | no | no | no | no | yes | no | +| support for ttl (time to live) at retrieval | no | yes | no | no | no | no | no | no | no | yes | +| support for deleting expired data | no | yes | no | no | no | no | no | no | no | yes | +| collocated by feature view | yes | no | yes | yes | yes | yes | yes | yes | no | yes | | collocated by feature service | no | no | no | no | no | no | no | no | no | no | -| collocated by entity key | no | yes | no | no | no | no | no | no | yes | no | +| collocated by entity key | no | yes | no | no | no | no | no | no | yes | no | diff --git a/docs/reference/online-stores/postgres.md b/docs/reference/online-stores/postgres.md index fb2253d043e..10f3f871710 100644 --- a/docs/reference/online-stores/postgres.md +++ b/docs/reference/online-stores/postgres.md @@ -6,7 +6,7 @@ The PostgreSQL online store provides support for materializing feature values in * Only the latest feature values are persisted -* sslmode, sslkey_path, sslcert_path, and sslrootcert_path are optional +* `sslmode` defaults to `require`, which encrypts the connection without certificate verification. To disable SSL (e.g. for local development), set `sslmode: disable`. For certificate verification, set `sslmode` to `verify-ca` or `verify-full` and provide the corresponding `sslrootcert_path` (and optionally `sslcert_path` and `sslkey_path` for mutual TLS) ## Getting started In order to use this online store, you'll need to run `pip install 'feast[postgres]'`. You can get started by then running `feast init -t postgres`. diff --git a/docs/reference/online-stores/redis.md b/docs/reference/online-stores/redis.md index ae7f8b4c5ca..f212f03d943 100644 --- a/docs/reference/online-stores/redis.md +++ b/docs/reference/online-stores/redis.md @@ -7,7 +7,12 @@ The [Redis](https://redis.io) online store provides support for materializing fe * Both Redis and Redis Cluster are supported. * The data model used to store feature values in Redis is described in more detail [here](../../specs/online\_store\_format.md). +**Data model:** All feature views that share the same entity key are stored in a single Redis hash. The hash key is derived from the serialized entity key and the project name. Each feature's value is a hash field keyed by a murmur3 hash of `"feature_view_name:feature_name"`, and a separate `_ts:` field stores the event timestamp per feature view. + +This collocated-by-entity design enables an important performance optimization: `get_online_features()` requests that span multiple feature views for the same entity can issue all `HMGET` commands in a **single Redis pipeline execution**, regardless of how many feature views are requested. See [Performance characteristics](#performance-characteristics) below. + ## Getting started + In order to use this online store, you'll need to install the redis extra (along with the dependency needed for the offline store of choice). E.g. - `pip install 'feast[gcp, redis]'` - `pip install 'feast[snowflake, redis]'` @@ -60,22 +65,85 @@ online_store: ``` {% endcode %} -Additionally, the redis online store also supports automatically deleting data via a TTL mechanism. -The TTL is applied at the entity level, so feature values from any associated feature views for an entity are removed together. -This TTL can be set in the `feature_store.yaml`, using the `key_ttl_seconds` field in the online store. For example: +## TTL configuration + +The Redis online store supports two complementary TTL mechanisms: + +### Key-level TTL (`key_ttl_seconds`) + +Sets a Redis `EXPIRE` on the entire entity hash key. When the TTL elapses, Redis automatically deletes all feature values for that entity across **all** feature views that share the same key. Use this to bound memory usage and automatically evict stale entity data. -{% code title="feature_store.yaml" %} ```yaml -project: my_feature_repo -registry: data/registry.db -provider: local online_store: type: redis - key_ttl_seconds: 604800 + key_ttl_seconds: 604800 # 7 days connection_string: "localhost:6379" ``` -{% endcode %} +{% hint style="warning" %} +Because all feature views for the same entity share one Redis hash key, `key_ttl_seconds` uses the **entity** as the expiry unit, not the feature view. Writing any feature view for an entity resets the TTL for the whole hash. This means a frequently written feature view can keep a stale, infrequently written feature view alive beyond its intended TTL. +{% endhint %} + +{% hint style="info" %} +`FeatureView.ttl` defines the **offline retrieval window** (how far back in time point-in-time joins look in the offline store). It does **not** filter online store reads. To control online data expiry, use `key_ttl_seconds`. +{% endhint %} + +## Performance characteristics + +### Batched multi-feature-view reads + +Unlike most online stores, the Redis implementation overrides `get_online_features()` to issue a **single pipeline execution** for all feature views in the request. Because all feature views for the same entity live in the same Redis hash, all `HMGET` commands across every feature view are batched into one `pipeline.execute()` call. + +| Feature views in request | Redis round trips (before) | Redis round trips (after) | +| :---: | :---: | :---: | +| 1 | 1 | 1 | +| 5 | 5 | 1 | +| 10 | 10 | 1 | +| 20 | 20 | 1 | + +Benchmark results against Redis 8.6.2 (localhost, 50 entities, 3 features/FV, 300 rounds): + +| Feature views | Master (per-FV pipeline) | Improved (batched pipeline) | Speedup | +| :---: | ---: | ---: | :---: | +| 1 | 1.57 ms | 1.32 ms | 1.19× | +| 5 | 7.27 ms | 5.63 ms | 1.29× | +| 10 | 15.64 ms | 10.65 ms | 1.47× | +| 20 | 36.33 ms | 21.21 ms | **1.71×** | + +The speedup grows with the number of feature views and is most pronounced in production environments with non-trivial network RTT to Redis. + +### Write path: `skip_dedup` for bulk loads + +By default, `online_write_batch()` checks existing timestamps before writing (to avoid overwriting newer data with older data). This requires two pipeline round trips per batch: one to read existing timestamps, one to write new values. + +For initial bulk loads or append-only pipelines where out-of-order writes are not a concern, set `skip_dedup: true` to write in a **single pipeline round trip**: + +```yaml +online_store: + type: redis + connection_string: "localhost:6379" + skip_dedup: true +``` + +{% hint style="warning" %} +With `skip_dedup: true`, writes always overwrite existing data regardless of timestamp order. Under concurrent writers, an older record can overwrite a newer one. Use only for controlled bulk loads or pipelines that guarantee ordered delivery. +{% endhint %} + +### Async write support + +The Redis online store implements `online_write_batch_async()` using the async Redis client. This enables non-blocking batch writes in async serving frameworks. `skip_dedup` is also respected in the async path. + +## Configuration reference + +| Parameter | Default | Description | +| --- | --- | --- | +| `type` | `redis` | Online store type selector | +| `redis_type` | `redis` | Connection type: `redis`, `redis_cluster`, or `redis_sentinel` | +| `connection_string` | `localhost:6379` | Host:port and optional parameters. For cluster: `redis1:6379,redis2:6379,ssl=true,password=...` | +| `sentinel_master` | `mymaster` | Sentinel master name (only used when `redis_type: redis_sentinel`) | +| `key_ttl_seconds` | `null` | Redis `EXPIRE` TTL in seconds applied to the entity hash key after each write. Expires all feature views for that entity together. | +| `full_scan_for_deletion` | `true` | When `true`, deleting or renaming a feature view scans Redis to remove its hash fields. Set `false` to skip deletion scans (faster `feast apply`, but leaves orphaned data). | +| `skip_dedup` | `false` | When `true`, skips the existing-timestamp read before each write, halving write round trips. Suitable for initial bulk loads; may cause older values to overwrite newer ones under concurrent writers. | The full set of configuration options is available in [RedisOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.redis.RedisOnlineStoreConfig). @@ -102,5 +170,7 @@ Below is a matrix indicating which functionality is supported by the Redis onlin | collocated by feature view | no | | collocated by feature service | no | | collocated by entity key | yes | +| async batch writes | yes | +| batched multi-feature-view reads (single pipeline) | yes | To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/scylladb.md b/docs/reference/online-stores/scylladb.md index c8583ac101a..98dc03b24cb 100644 --- a/docs/reference/online-stores/scylladb.md +++ b/docs/reference/online-stores/scylladb.md @@ -2,20 +2,15 @@ ## Description -ScyllaDB is a low-latency and high-performance Cassandra-compatible (uses CQL) database. You can use the existing Cassandra connector to use ScyllaDB as an online store in Feast. - -The [ScyllaDB](https://www.scylladb.com/) online store provides support for materializing feature values into a ScyllaDB or [ScyllaDB Cloud](https://www.scylladb.com/product/scylla-cloud/) cluster for serving online features real-time. +[ScyllaDB](https://www.scylladb.com/) is a distributed real-time NoSQL database with vector search support. +This integration uses the native **`scylla-driver`** Python driver for optimised performance and supports materializing feature values into a [ScyllaDB Cloud](https://www.scylladb.com/product/scylla-cloud/) cluster for real-time online feature serving. ## Getting started -Install Feast with Cassandra support: -```bash -pip install "feast[cassandra]" -``` +Install Feast with the `scylladb` extra, which pulls in `scylla-driver` automatically: -Create a new Feast project: ```bash -feast init REPO_NAME -t cassandra +pip install feast[scylladb] ``` ### Example (ScyllaDB) @@ -26,7 +21,7 @@ project: scylla_feature_repo registry: data/registry.db provider: local online_store: - type: cassandra + type: scylladb hosts: - 172.17.0.2 keyspace: feast @@ -43,44 +38,106 @@ project: scylla_feature_repo registry: data/registry.db provider: local online_store: - type: cassandra + type: scylladb hosts: - node-0.aws_us_east_1.xxxxxxxx.clusters.scylla.cloud - node-1.aws_us_east_1.xxxxxxxx.clusters.scylla.cloud - node-2.aws_us_east_1.xxxxxxxx.clusters.scylla.cloud keyspace: feast username: scylla - password: password + password: xxxxxx + local_dc: AWS_US_EAST_1 ``` {% endcode %} - -The full set of configuration options is available in [CassandraOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.cassandra_online_store.cassandra_online_store.CassandraOnlineStoreConfig). -For a full explanation of configuration options please look at file -`sdk/python/feast/infra/online_stores/contrib/cassandra_online_store/README.md`. +## Configuration options + +| Parameter | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| `hosts` | list[str] | *(required)* | Contact-point host addresses. | +| `port` | int | `9042` | CQL port. | +| `keyspace` | str | `feast_keyspace` | Target ScyllaDB keyspace. | +| `username` | str | `None` | Auth username. | +| `password` | str | `None` | Auth password. | +| `local_dc` | str | `None` | Local datacenter name for DC-aware load balancing. | +| `request_timeout` | float | `None` | Driver request timeout in seconds. | +| `read_concurrency` | int | `100` | `concurrency` argument passed to the driver's `execute_concurrent_with_args` for reads. Controls how many CQL statements are in-flight at once. | +| `write_concurrency` | int | `100` | `concurrency` argument passed to the driver's `execute_concurrent_with_args` for writes. Controls how many CQL statements are in-flight at once. | +| `vector_similarity_function` | str | `COSINE` | Default similarity function for vector indexes. Supported: `COSINE`, `DOT_PRODUCT`, `EUCLIDEAN`. Can be overridden per-feature via the `similarity_function` Field tag. | Storage specifications can be found at `docs/specs/online_store_format.md`. +## Vector Search + +ScyllaDB Cloud supports approximate nearest-neighbour (ANN) vector search. +To enable it for a feature view, tag the embedding `Field` with `vector_index=true` and specify the number of dimensions: + +{% code title="feature_definitions.py" %} +```python +from feast import FeatureView, Field +from feast.types import Array, Float32, String + +documents_fv = FeatureView( + name="documents", + entities=[item], + schema=[ + Field(name="text", dtype=String), + Field( + name="embedding", + dtype=Array(Float32), + tags={ + "vector_index": "true", + "dimensions": "768", + "similarity_function": "COSINE", # COSINE | DOT_PRODUCT | EUCLIDEAN + }, + ), + ], + online=True, + source=push_source, +) +``` +{% endcode %} + +When `feast apply` runs, the store automatically creates the necessary tables and HNSW ANN index for any feature view with vector-tagged fields. + +To query the top-k most similar documents: + +```python +result = store.retrieve_online_documents_v2( + features=["documents:text", "documents:embedding"], + query=[0.1, 0.2, ...], # your query embedding + top_k=10, + distance_metric="COSINE", +) +``` + +### Metadata filtering (OpenAI-compatible) + +ScyllaDB supports vector similarity search, but OpenAI-style metadata filtering is **not supported yet**. +Passing `filters` to `retrieve_online_documents_v2` or the OpenAI-compatible search endpoint raises `NotImplementedError`. + +For filtered vector search today, use one of the backends that implement metadata filters (for example Milvus, Elasticsearch, Postgres, SQLite, or MongoDB). See [Alpha Vector Database](../alpha-vector-database.md#supported-online-stores). + ## Functionality Matrix The set of functionality supported by online stores is described in detail [here](overview.md#functionality). -Below is a matrix indicating which functionality is supported by the Cassandra plugin. +Below is a matrix indicating which functionality is supported by the ScyllaDB online store. -| | Cassandra | +| | ScyllaDB | | :-------------------------------------------------------- | :-------- | | write feature values to the online store | yes | | read feature values from the online store | yes | | update infrastructure (e.g. tables) in the online store | yes | | teardown infrastructure (e.g. tables) in the online store | yes | -| generate a plan of infrastructure changes | yes | +| generate a plan of infrastructure changes | no | | support for on-demand transforms | yes | | readable by Python SDK | yes | | readable by Java | no | | readable by Go | no | | support for entityless feature views | yes | | support for concurrent writing to the same key | no | -| support for ttl (time to live) at retrieval | no | -| support for deleting expired data | no | +| support for ttl (time to live) at retrieval | yes | +| support for deleting expired data | yes | | collocated by feature view | yes | | collocated by feature service | no | | collocated by entity key | no | @@ -89,6 +146,6 @@ To compare this set of functionality against other online stores, please see the ## Resources -* [Sample application with ScyllaDB](https://feature-store.scylladb.com/stable/) +* [ScyllaDB Vector Search documentation](https://cloud.docs.scylladb.com/stable/vector-search/) * [ScyllaDB website](https://www.scylladb.com/) * [ScyllaDB Cloud documentation](https://cloud.docs.scylladb.com/stable/) diff --git a/docs/reference/online-stores/valkey.md b/docs/reference/online-stores/valkey.md new file mode 100644 index 00000000000..4ede3f65b5d --- /dev/null +++ b/docs/reference/online-stores/valkey.md @@ -0,0 +1,94 @@ +# Valkey online store + +## Description + +[Valkey](https://valkey.io/) is an open source (BSD-3-Clause), high-performance key/value datastore hosted by the Linux Foundation, created as a community fork of Redis. It maintains compatibility with the Redis wire protocol, so it can act as a drop-in replacement for Redis. Valkey is also offered as a managed engine by major cloud providers (for example, Amazon ElastiCache for Valkey). + +Similar to Redis and [Dragonfly](dragonfly.md), Valkey can be used as an online feature store for Feast: Feast's Redis online store only issues core commands (hash reads/writes, scans, key expiry, pipelines), all of which Valkey implements. + +Feast's standard online store operations have been verified against Valkey 8.1: `feast apply`, `feast materialize`, online retrieval via `get_online_features`, `feast teardown`, and key expiry via the `key_ttl_seconds` option. Features that depend on Redis modules (such as vector search) are outside the scope of this page. + +## Using Valkey as a drop-in Feast online store instead of Redis + +Make sure you have Python and `pip` installed. + +Install the Feast SDK and CLI + +`pip install feast` + +In order to use Valkey as the online store, you'll need to install the redis extra: + +`pip install 'feast[redis]'` + +### 1. Create a feature repository + +Bootstrap a new feature repository: + +``` +feast init feast_valkey +cd feast_valkey/feature_repo +``` + +Update `feature_repo/feature_store.yaml` with the below contents: + +``` +project: feast_valkey +registry: data/registry.db +provider: local +online_store: + type: redis + connection_string: "localhost:6379" +``` + +Note that the online store `type` remains `redis`: Feast talks to Valkey over the Redis protocol, and all options of the [Redis online store](redis.md) (such as `key_ttl_seconds`) apply unchanged. + +### 2. Start Valkey + +There are several options available to get Valkey up and running quickly. We will be using Docker for this tutorial. + +`docker run -d -p 6379:6379 valkey/valkey:8.1` + +### 3. Register feature definitions and deploy your feature store + +`feast apply` + +The `apply` command scans python files in the current directory for feature view/entity definitions, registers the objects, and deploys infrastructure. +You should see the following output: + +``` +.... +Created entity driver +Created feature view driver_hourly_stats_fresh +Created feature view driver_hourly_stats +Created on demand feature view transformed_conv_rate +Created on demand feature view transformed_conv_rate_fresh +Created feature service driver_activity_v1 +Created feature service driver_activity_v3 +Created feature service driver_activity_v2 +``` + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Redis online store, which Feast uses to communicate with Valkey. + +| | Redis | +| :-------------------------------------------------------- | :---- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | yes | +| readable by Go | yes | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | yes | +| support for ttl (time to live) at retrieval | yes | +| support for deleting expired data | yes | +| collocated by feature view | no | +| collocated by feature service | no | +| collocated by entity key | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/openlineage.md b/docs/reference/openlineage.md index 01837c9936a..bf9e18750ed 100644 --- a/docs/reference/openlineage.md +++ b/docs/reference/openlineage.md @@ -88,7 +88,7 @@ fs.materialize( | Option | Default | Description | |--------|---------|-------------| | `enabled` | `false` | Enable/disable OpenLineage integration | -| `transport_type` | `http` | Transport type: `http`, `file`, `kafka` | +| `transport_type` | `None` | Transport type: `http`, `console`, `file`, `kafka`. When unset, defers to OpenLineage SDK defaults. | | `transport_url` | - | URL for HTTP transport (required) | | `transport_endpoint` | `api/v1/lineage` | API endpoint for HTTP transport | | `api_key` | - | Optional API key for authentication | @@ -186,6 +186,12 @@ Captures materialization run metadata: ## Lineage Visualization +### Option 1: Feast UI (Built-in) + +Feast includes a built-in OpenLineage consumer that can receive, store, and visualize lineage from **all** OpenLineage producers (Airflow, Spark, dbt, Feast itself, etc.) directly in the Feast UI. See the [OpenLineage Consumer](#openlineage-consumer) section below. + +### Option 2: Marquez + Use [Marquez](https://marquezproject.ai/) to visualize your Feast lineage: ```bash @@ -216,3 +222,257 @@ Then access the Marquez UI at http://localhost:3000 to see your feature lineage. | Entity | InputDataset | | FeatureService | OutputDataset | | Materialization | RunEvent (START/COMPLETE/FAIL) | + +--- + +## OpenLineage Consumer + +Feast can act as an **OpenLineage consumer**, receiving lineage events from any OpenLineage-compatible producer and displaying them in the Feast UI. This eliminates the need for a separate Marquez deployment when you want to visualize cross-system data lineage alongside your feature store. + +### Consumer Architecture + +``` +Producers (Airflow, Spark, dbt, Feast, Flink, …) + │ + ▼ + POST /api/v1/lineage ──→ Event Processor ──→ Lineage Store (SQL) + │ + ▼ + Feast UI + ┌──────────────────────────┐ + │ Lineage tab │ + │ ├─ OpenLineage Graph │ + │ │ (all producers) │ + │ └─ ☐ Feast Only Lineage │ + │ (registry view) │ + │ │ + │ Events tab │ + │ └─ Event browser │ + └──────────────────────────┘ +``` + +When the consumer is **not** enabled, the Feast UI shows only the original registry-based lineage view — no tabs are added. + +### Enabling the Consumer + +Add the `consumer` section under `openlineage` in your `feature_store.yaml`: + +```yaml +project: my_project +registry: + registry_type: sql + path: postgresql://user:****@host:5432/feast # pragma: allowlist secret + +openlineage: + enabled: true + namespace: my_project + consumer: + enabled: true + store_type: sql + # Optional: separate database for lineage storage. + # If omitted, the SQL registry database is reused. + # connection_string: postgresql://user:****@host:5432/feast_lineage + api_key: "change-me" # pragma: allowlist secret + namespace_mapping: + airflow_ns: my_project + spark_ns: my_project +``` + +Or via environment variables: + +```bash +export FEAST_OPENLINEAGE_CONSUMER_ENABLED=true +export FEAST_OPENLINEAGE_CONSUMER_STORE_TYPE=sql +export FEAST_OPENLINEAGE_CONSUMER_API_KEY=change-me # pragma: allowlist secret +# Optional separate DB: +# export FEAST_OPENLINEAGE_CONSUMER_CONNECTION_STRING=postgresql://... +``` + +### Consumer Configuration Options + +| Option | Default | Description | +|--------|---------|-------------| +| `consumer.enabled` | `false` | Enable the OpenLineage consumer | +| `consumer.store_type` | `sql` | Storage backend type. Currently only `sql` is supported | +| `consumer.connection_string` | - | Optional separate database connection string. If omitted, reuses the SQL registry database | +| `consumer.api_key` | - | API key that producers must provide when sending events | +| `consumer.namespace_mapping` | `{}` | Maps OpenLineage namespaces to Feast projects for RBAC scoping | + +### Consumer API Endpoints + +When the consumer is enabled, the following endpoints are available on the Feast REST registry server: + +#### Event Receiver (Producer-facing) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/lineage` | `POST` | Receive a single OpenLineage event (or array of events) | +| `/api/v1/lineage/batch` | `POST` | Receive a batch of OpenLineage events | + +Both endpoints require the `X-API-Key` header (or `Authorization: Bearer `) if `consumer.api_key` is configured. + +#### Admin Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/lineage/openlineage/reset` | `DELETE` | Purge all OpenLineage data. Accepts optional `?namespace=X` to delete only a specific namespace. Requires API key. | + +#### OpenLineage Query Endpoints (UI-facing) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/lineage/openlineage/graph` | `GET` | Full lineage graph with all nodes, edges, and symlinks | +| `/lineage/openlineage/graph/{node_type}/{namespace}/{name}` | `GET` | Lineage graph centered on a specific node | +| `/lineage/openlineage/events` | `GET` | Browse stored events with filtering | +| `/lineage/openlineage/jobs` | `GET` | List all known OpenLineage jobs | +| `/lineage/openlineage/datasets` | `GET` | List all known OpenLineage datasets | +| `/lineage/openlineage/runs` | `GET` | List runs with optional `?job_namespace=X&job_name=Y` filtering | +| `/lineage/openlineage/runs/{run_id}` | `GET` | Single run detail with input/output datasets | + +#### Registry Query Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/lineage/registry` | `GET` | Feast registry lineage (entities, feature views, services) | +| `/lineage/registry/all` | `GET` | All registry objects with full metadata | +| `/lineage/objects/{object_type}/{object_name}` | `GET` | Detail for a specific registry object | +| `/lineage/complete` | `GET` | Complete registry lineage with relationships | +| `/lineage/complete/all` | `GET` | Complete registry lineage for all objects | + +### Configuring Producers to Send Events to Feast + +Configure any OpenLineage producer to send events to your Feast instance: + +#### Airflow + +```python +# In airflow.cfg or environment +OPENLINEAGE_URL = "http://feast-registry:8080/api" +OPENLINEAGE_API_KEY = "change-me" # pragma: allowlist secret +``` + +#### Spark + +```properties +spark.openlineage.transport.type=http +spark.openlineage.transport.url=http://feast-registry:8080/api +spark.openlineage.transport.endpoint=/v1/lineage +spark.openlineage.transport.auth.type=api_key +spark.openlineage.transport.auth.apiKey=change-me +``` + +#### dbt + +```yaml +# In profiles.yml or environment +OPENLINEAGE_URL: "http://feast-registry:8080/api" +OPENLINEAGE_API_KEY: "change-me" # pragma: allowlist secret +``` + +#### Feast (Self-reporting) + +When both the OpenLineage producer and consumer are enabled, Feast's own events (from `feast apply`, materialization, etc.) are automatically ingested into the local consumer store — no HTTP transport is needed. + +```yaml +# In feature_store.yaml +openlineage: + enabled: true + namespace: my_project + consumer: + enabled: true + api_key: change-me # pragma: allowlist secret +``` + +### Feast UI Lineage Views + +When the consumer is enabled, the lineage page in the Feast UI shows two tabs: + +**Lineage tab** + +- **OpenLineage Graph** (default) — shows lineage from all OpenLineage producers with cross-producer connectivity. Nodes are color-coded by producer (colors generated dynamically). The graph supports filtering by type, producer, and object name. Clicking a node opens a **detail panel** showing description, schema, tags, features, entities, data quality metrics, data source info, other facets, and **run history** (for job nodes — see [Per-Run Lineage](#per-run-lineage-run-history)). +- **Feast Only Lineage** (checkbox) — switches to the original Feast registry view (DataSource → FeatureView → FeatureService) powered entirely by the Feast registry. + +**Events tab** + +- Browse individual OpenLineage events with filtering by event type, job name, and run ID. Expand any event to inspect the full JSON payload. + +### Cross-Producer Lineage Connectivity + +The consumer automatically links datasets across different producers when they refer to the same physical data. Linking mechanisms: + +1. **Shared namespace + name** — If Airflow writes to `s3://bucket/path` and Spark reads from the same `s3://bucket/path`, the graph connects them automatically. +2. **SymlinksDatasetFacet** — Producers can declare aliases. For example, Feast can declare that its internal `driver_hourly_stats` is a symlink to the Spark output at `s3://bucket/features/driver_hourly_stats/`. +3. **dataSource URI matching** — Datasets with matching `dataSource.uri` facets are linked even if their namespace or name differ. + +Compatible producers include Airflow, Spark, dbt, Flink, Feast, and Dagster. + +### RBAC for Lineage + +The OpenLineage consumer integrates with Feast's existing RBAC: + +- **Write access** (producers sending events): Authenticated via API key in the `X-API-Key` header +- **Read access** (UI viewing lineage): Namespace-based filtering maps OpenLineage namespaces to Feast projects. Users see only lineage data for namespaces they have access to via the `namespace_mapping` configuration + +### Lineage Cleanup / Reset + +Over time the OpenLineage store accumulates historical data. Two mechanisms are provided for cleanup: + +#### Admin Reset Endpoint + +Use the `DELETE /lineage/openlineage/reset` endpoint to purge lineage data. The endpoint requires the same API key used for event ingestion. + +```bash +# Purge ALL OpenLineage data +curl -X DELETE -H "X-API-Key: your-key" \ + http://localhost:8080/api/v1/lineage/openlineage/reset + +# Purge only a specific namespace +curl -X DELETE -H "X-API-Key: your-key" \ + "http://localhost:8080/api/v1/lineage/openlineage/reset?namespace=airflow://prod-cluster" +``` + +A full purge deletes data from all seven `openlineage_*` tables. A namespace-scoped purge deletes jobs, datasets, runs, events, edges, and symlinks associated with that namespace, leaving other namespaces intact. + +#### Feast Teardown Hook + +When you run `feast teardown`, Feast automatically cleans up OpenLineage data for the project's namespace (if the consumer is configured). This ensures that tearing down a Feast project doesn't leave orphaned lineage data behind. + +```bash +# Tears down the Feast project AND its OpenLineage lineage +feast teardown +``` + +### Per-Run Lineage (Run History) + +The consumer tracks individual pipeline runs in the `openlineage_runs` table. When you click on a **job node** in the OpenLineage Graph, the detail panel shows a **Run History** section with: + +- A table of past runs: truncated run ID, status badge (COMPLETE, FAIL, RUNNING, ABORT), start time, and duration +- Click any run to expand its **inputs and outputs** — the specific datasets that run consumed and produced + +#### Run History API + +```bash +# List runs for a specific job +curl "http://localhost:8080/api/v1/lineage/openlineage/runs?job_namespace=spark://emr-cluster&job_name=feature_engineering" + +# Get a single run with its I/O datasets +curl "http://localhost:8080/api/v1/lineage/openlineage/runs/{run_id}" +``` + +The run detail response includes `inputs` and `outputs` arrays, each containing the dataset namespace, name, and any I/O facets recorded by the producer. + +### Database Schema + +The consumer creates the following tables (automatically on first startup): + +| Table | Purpose | +|-------|---------| +| `openlineage_events` | Raw event storage with JSON payloads | +| `openlineage_jobs` | Deduplicated job records with producer, description, and facets | +| `openlineage_datasets` | Deduplicated dataset records with schema, facets, and Feast mapping | +| `openlineage_runs` | Run lifecycle tracking (START/COMPLETE/FAIL) | +| `openlineage_run_io` | Input/output relationships between runs and datasets | +| `openlineage_lineage_edges` | Materialized lineage graph edges for efficient traversal | +| `openlineage_dataset_symlinks` | Cross-producer dataset linking via `SymlinksDatasetFacet` and `dataSource` URI matching | + +By default these tables are created in the **same database** as the SQL registry (hybrid storage). Set `consumer.connection_string` to store them in a separate database instead. diff --git a/docs/reference/registries/metadata.md b/docs/reference/registries/metadata.md index 575f2a5c8b7..371be9b5289 100644 --- a/docs/reference/registries/metadata.md +++ b/docs/reference/registries/metadata.md @@ -20,6 +20,7 @@ The metadata info of Feast `feature_store.yaml` is: | registry.warehouse | N | string | snowflake warehouse name | | registry.database | N | string | snowflake db name | | registry.schema | N | string | snowflake schema name | +| registry.enable_online_feature_view_versioning | N | boolean | enable versioned online store tables and version-qualified reads (default: false). Version history tracking is always active. | | online_store | Y | | | | offline_store | Y | NA | | | | offline_store.type | Y | string | storage type | diff --git a/docs/reference/registries/remote.md b/docs/reference/registries/remote.md index a03e30ac85f..64055304283 100644 --- a/docs/reference/registries/remote.md +++ b/docs/reference/registries/remote.md @@ -18,7 +18,27 @@ registry: {% endcode %} The optional `cert` parameter can be configured as well, it should point to the public certificate path when the Registry Server starts in SSL mode. This may be needed if the Registry Server is started with a self-signed certificate, typically this file ends with *.crt, *.cer, or *.pem. -More info about the `cert` parameter can be found in [feast-client-connecting-to-remote-registry-sever-started-in-tls-mode](../../how-to-guides/starting-feast-servers-tls-mode.md#feast-client-connecting-to-remote-registry-sever-started-in-tls-mode) + +For **mutual TLS (mTLS)**, you can also configure: +* `client_cert` — Path to the client certificate presented to the server. Must be paired with `client_key`. Typically ends with `*.crt` or `*.pem`. +* `client_key` — Path to the client private key. Must be paired with `client_cert`. Typically ends with `*.key` or `*.pem`. + +When connecting through a tunnel or proxy where the connection address differs from the server hostname, set: +* `authority` — Overrides the gRPC `:authority` header so the server certificate is validated against the correct hostname. + +{% code title="feature_store.yaml" %} +```yaml +registry: + registry_type: remote + path: localhost:8443 + cert: /path/to/ca.crt + client_cert: /path/to/tls.crt + client_key: /path/to/tls.key + authority: feature-registry.example.com +``` +{% endcode %} + +More info about TLS configuration can be found in [feast-client-connecting-to-remote-registry-sever-started-in-tls-mode](../../how-to-guides/starting-feast-servers-tls-mode.md#feast-client-connecting-to-remote-registry-sever-started-in-tls-mode) ## How to configure the server diff --git a/docs/reference/registries/sql.md b/docs/reference/registries/sql.md index ef9993c8753..e8d1bcef17a 100644 --- a/docs/reference/registries/sql.md +++ b/docs/reference/registries/sql.md @@ -80,10 +80,114 @@ docker build \ If you are running Feast in Kubernetes, set the `image.repository` and `imagePullSecrets` Helm values accordingly to utilize your custom image. +## Schema management (`schema_mode`) + +By default, the SQL registry creates its tables on every startup (`schema_mode: auto`). In production environments where the application should not have DDL privileges, you can pre-create the schema and configure the registry to only verify it: + +```yaml +registry: + registry_type: sql + path: postgresql://db:5432/feast + schema_mode: verify # or "skip" +``` + +| Value | Behavior | +|---|---| +| `auto` (default) | Creates tables if they don't exist. Current behavior, no breaking change. | +| `verify` | Skips DDL. Checks that all expected tables exist on startup; raises an error listing missing tables if any are absent. When a separate `read_path` is configured, the read replica is also verified — a lagging replica (e.g. mid-migration) will block startup. Note: this is a table-level check only — it does not verify individual columns. A schema created by an older Feast version (missing newer columns) will pass verification but may fail at query time. | +| `skip` | Skips both creation and verification. Use when schema is managed entirely outside Feast (e.g. by a migration tool). | + +### Pre-creating the schema + +When using `verify` or `skip` mode, run the following CLI command with a user that has DDL privileges to create the schema before starting the application: + +```shell +feast registry create-schema +``` + +This reads `feature_store.yaml`, connects to the configured database, and creates all required tables. It is safe to run multiple times — existing tables are not modified. + There are some things to note about how the SQL registry works: -- Once instantiated, the Registry ensures the tables needed to store data exist, and creates them if they do not. -- Upon tearing down the feast project, the registry ensures that the tables are dropped from the database. -- The schema for how data is laid out in tables can be found . It is intentionally simple, storing the serialized protobuf versions of each Feast object keyed by its name. +- When `schema_mode` is `auto` (the default), the Registry ensures the tables needed to store data exist, and creates them if they do not. +- Upon tearing down the feast project, the registry deletes all rows from the registry tables (it does not drop the tables themselves). This runs regardless of `schema_mode` and requires only DML (`DELETE`) privileges, not DDL. +- The schema for how data is laid out in tables can be found in the table definitions in [`sdk/python/feast/infra/registry/sql.py`](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/infra/registry/sql.py). It is intentionally simple, storing the serialized protobuf versions of each Feast object keyed by its name. + +## MySQL: serialized-proto columns use `LONGBLOB` + +The registry stores each Feast object as a serialized protobuf in a binary +column. On MySQL these columns are created as `LONGBLOB` (up to 4 GB). Earlier +versions created them as `BLOB`, which caps at 64 KB — a single `FeatureView` +proto routinely exceeds that, so MySQL would silently truncate the write and the +registry would later fail to load with a protobuf `DecodeError` (for example, +`feast serve` failing to start). Other dialects (PostgreSQL, SQLite) were never +affected. + +New deployments get the correct schema automatically — the registry creates its +tables as `LONGBLOB` on first use. When an existing MySQL/MariaDB registry still +has `BLOB` columns, the registry logs an error at startup listing the affected +columns (it does not refuse to start — a registry whose protos all fit in 64 KB +is unaffected). **Existing deployments are not migrated automatically**: the +registry only creates tables that do not already exist, and it has no +schema-migration step, so previously created `BLOB` columns remain `BLOB`. To +upgrade an existing MySQL registry, alter each serialized-proto column to +`LONGBLOB`, for example: + +> ⚠️ **Run the migration carefully on a live registry.** A `BLOB`→`LONGBLOB` +> change is a column *data-type* change, which MySQL InnoDB performs with +> `ALGORITHM=COPY` — a full table rebuild under a metadata lock that blocks +> readers and writers for the duration (potentially minutes on a large table +> such as `feature_view_version_history`). `ALGORITHM=INPLACE` is **not** +> generally supported for this change and is rejected with +> `ER_ALTER_OPERATION_NOT_SUPPORTED_REASON` on most builds — do not rely on it. +> +> **Before running any `ALTER TABLE`:** +> +> 1. **Stop all `feast apply` and materialization jobs.** This is required, not +> optional — a write of a `>64 KB` proto to a not-yet-widened `BLOB` column +> truncates silently with no error, and concurrent writes also extend the +> `ALTER`'s lock duration. +> 2. Confirm there are no active writers (e.g. `SHOW PROCESSLIST`). +> 3. Verify you have a backup of the registry database. +> +> Then, to minimize the lock window: +> +> - On large tables, or on managed MySQL (AWS RDS, Aurora) without shell access, +> use an online schema-change tool — +> [`pt-online-schema-change`](https://docs.percona.com/percona-toolkit/pt-online-schema-change.html) +> (Percona Toolkit) or [`gh-ost`](https://github.com/github/gh-ost) — which +> rebuild the table without a long-held lock. For small tables a plain +> `ALTER TABLE` in the maintenance window is fine. +> - Apply one table at a time so a failure is easy to isolate and re-run. +> - Resume jobs only after all `ALTER TABLE` statements complete successfully. +> - Rollback is safe (revert `MODIFY ... BLOB`) **only** while no stored proto +> exceeds 64 KB; otherwise a revert re-introduces truncation. + +```sql +ALTER TABLE projects MODIFY project_proto LONGBLOB NOT NULL; +ALTER TABLE entities MODIFY entity_proto LONGBLOB NOT NULL; +ALTER TABLE data_sources MODIFY data_source_proto LONGBLOB NOT NULL; +ALTER TABLE feature_views MODIFY materialized_intervals LONGBLOB, + MODIFY feature_view_proto LONGBLOB NOT NULL, + MODIFY user_metadata LONGBLOB; +ALTER TABLE stream_feature_views MODIFY feature_view_proto LONGBLOB NOT NULL, + MODIFY user_metadata LONGBLOB; +ALTER TABLE on_demand_feature_views MODIFY feature_view_proto LONGBLOB NOT NULL, + MODIFY user_metadata LONGBLOB; +ALTER TABLE label_views MODIFY feature_view_proto LONGBLOB NOT NULL, + MODIFY user_metadata LONGBLOB; +ALTER TABLE feature_services MODIFY feature_service_proto LONGBLOB NOT NULL; +ALTER TABLE saved_datasets MODIFY saved_dataset_proto LONGBLOB NOT NULL; +ALTER TABLE validation_references MODIFY validation_reference_proto LONGBLOB NOT NULL; +ALTER TABLE managed_infra MODIFY infra_proto LONGBLOB NOT NULL; +ALTER TABLE permissions MODIFY permission_proto LONGBLOB NOT NULL; +-- LARGE TABLE: one row per versioned apply — likely the slowest ALTER. Use +-- pt-online-schema-change or gh-ost if this registry has significant history. +ALTER TABLE feature_view_version_history MODIFY feature_view_proto LONGBLOB NOT NULL; +``` + +Any object whose proto already exceeded 64 KB before the upgrade may have been +stored truncated; re-run `feast apply` for those objects after altering the +columns so the full proto is rewritten. ## Example Usage: Concurrent materialization The SQL Registry should be used when materializing feature views concurrently to ensure correctness of data in the registry. This can be achieved by simply running feast materialize or feature_store.materialize multiple times using a correctly configured feature_store.yaml. This will make each materialization process talk to the registry database concurrently, and ensure the metadata updates are serialized. diff --git a/docs/reference/type-system.md b/docs/reference/type-system.md index 9f36f6eeaff..97cc6036dc8 100644 --- a/docs/reference/type-system.md +++ b/docs/reference/type-system.md @@ -3,7 +3,7 @@ ## Motivation Feast uses an internal type system to provide guarantees on training and serving data. -Feast supports primitive types, array types, set types, and map types for feature values. +Feast supports primitive types, array types, set types, map types, JSON, and struct types for feature values. Null types are not supported, although the `UNIX_TIMESTAMP` type is nullable. The type system is controlled by [`Value.proto`](https://github.com/feast-dev/feast/blob/master/protos/feast/types/Value.proto) in protobuf and by [`types.py`](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/types.py) in Python. Type conversion logic can be found in [`type_map.py`](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/type_map.py). @@ -24,6 +24,23 @@ Feast supports the following data types: | `Bytes` | `bytes` | Binary data | | `Bool` | `bool` | Boolean value | | `UnixTimestamp` | `datetime` | Unix timestamp (nullable) | +| `ZonedTimestamp` | `datetime` | Timezone-aware datetime preserving its source zone (nullable) | +| `Uuid` | `uuid.UUID` | UUID (any version) | +| `TimeUuid` | `uuid.UUID` | Time-based UUID (version 1) | +| `Decimal` | `decimal.Decimal` | Arbitrary-precision decimal number | + +### Domain-Specific Primitive Types + +These types are semantic aliases over `Bytes` for domain-specific use cases (e.g., RAG pipelines, image processing). They are stored as `bytes` at the proto level. + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `PdfBytes` | `bytes` | PDF document binary data (used in RAG / document processing pipelines) | +| `ImageBytes` | `bytes` | Image binary data (used in image processing / multimodal pipelines) | + +{% hint style="warning" %} +`PdfBytes` and `ImageBytes` are not natively supported by any backend's type inference. You must explicitly declare them in your feature view schema. Backend storage treats them as raw `bytes`. +{% endhint %} ### Array Types @@ -39,10 +56,13 @@ All primitive types have corresponding array (list) types: | `Array(Bytes)` | `List[bytes]` | List of binary data | | `Array(Bool)` | `List[bool]` | List of booleans | | `Array(UnixTimestamp)` | `List[datetime]` | List of timestamps | +| `Array(Uuid)` | `List[uuid.UUID]` | List of UUIDs | +| `Array(TimeUuid)` | `List[uuid.UUID]` | List of time-based UUIDs | +| `Array(Decimal)` | `List[decimal.Decimal]` | List of arbitrary-precision decimals | ### Set Types -All primitive types (except Map) have corresponding set types for storing unique values: +All primitive types (except `Map` and `Json`) have corresponding set types for storing unique values: | Feast Type | Python Type | Description | |------------|-------------|-------------| @@ -54,19 +74,125 @@ All primitive types (except Map) have corresponding set types for storing unique | `Set(Bytes)` | `Set[bytes]` | Set of unique binary data | | `Set(Bool)` | `Set[bool]` | Set of unique booleans | | `Set(UnixTimestamp)` | `Set[datetime]` | Set of unique timestamps | +| `Set(Uuid)` | `Set[uuid.UUID]` | Set of unique UUIDs | +| `Set(TimeUuid)` | `Set[uuid.UUID]` | Set of unique time-based UUIDs | +| `Set(Decimal)` | `Set[decimal.Decimal]` | Set of unique arbitrary-precision decimals | **Note:** Set types automatically remove duplicate values. When converting from lists or other iterables to sets, duplicates are eliminated. +{% hint style="warning" %} +**Backend limitations for Set types:** + +- **No backend infers Set types from schema.** No offline store (BigQuery, Snowflake, Redshift, PostgreSQL, Spark, Athena, MSSQL) maps its native types to Feast Set types. You **must** explicitly declare Set types in your feature view schema. +- **No native PyArrow set type.** Feast converts Sets to `pyarrow.list_()` internally, but `feast_value_type_to_pa()` in `type_map.py` does not include Set mappings, which can cause errors in some code paths. +- **Online stores** that serialize proto bytes (e.g., SQLite, Redis, DynamoDB) handle Sets correctly. +- **Offline stores** may not handle Set types correctly during retrieval. For example, the Ray offline store only special-cases `_LIST` types, not `_SET`. +- Set types are best suited for **online serving** use cases where feature values are written as Python sets and retrieved via `get_online_features`. +{% endhint %} + +### Nested Collection Types + +Feast supports arbitrarily nested collections using a recursive `VALUE_LIST` / `VALUE_SET` design. The outer container determines the proto enum (`VALUE_LIST` for `Array(…)`, `VALUE_SET` for `Set(…)`), while the full inner type structure is persisted via a mandatory `feast:nested_inner_type` Field tag. + +| Feast Type | Python Type | ValueType | Description | +|------------|-------------|-----------|-------------| +| `Array(Array(T))` | `List[List[T]]` | `VALUE_LIST` | List of lists | +| `Array(Set(T))` | `List[List[T]]` | `VALUE_LIST` | List of sets | +| `Set(Array(T))` | `List[List[T]]` | `VALUE_SET` | Set of lists | +| `Set(Set(T))` | `List[List[T]]` | `VALUE_SET` | Set of sets | +| `Array(Array(Array(T)))` | `List[List[List[T]]]` | `VALUE_LIST` | 3-level nesting | + +Where `T` is any supported primitive type (Int32, Int64, Float32, Float64, String, Bytes, Bool, UnixTimestamp) or another nested collection type. + +**Notes:** +- Nesting depth is **unlimited**. `Array(Array(Array(T)))`, `Set(Array(Set(T)))`, etc. are all supported. +- Inner type information is preserved via Field tags (`feast:nested_inner_type`) and restored during deserialization. This tag is mandatory for nested collection types. +- Empty inner collections (`[]`) are stored as empty proto values and round-trip as `None`. For example, `[[1, 2], [], [3]]` becomes `[[1, 2], None, [3]]` after a write-read cycle. + ### Map Types Map types allow storing dictionary-like data structures: | Feast Type | Python Type | Description | |------------|-------------|-------------| -| `Map` | `Dict[str, Any]` | Dictionary with string keys and any supported Feast type as values (including nested maps) | +| `Map` | `Dict[str, Any]` | Dictionary with string keys and values of any supported Feast type (including nested maps) | | `Array(Map)` | `List[Dict[str, Any]]` | List of dictionaries | +| `ScalarMap` | `Dict[Any, Any]` | Dictionary with non-string scalar keys (int, float, bool, UUID, Decimal, bytes, datetime) and values of any supported Feast type | + +**Note:** `Map` keys must always be strings. `ScalarMap` supports non-string scalar keys — Feast infers `ScalarMap` automatically when the first key of a dict is not a string. Map values can be any supported Feast type, including primitives, arrays, or nested maps at the proto level. However, the PyArrow representation is `map`, which means backends that rely on PyArrow schemas (e.g., during materialization) treat Map as string-to-string. -**Note:** Map keys must always be strings. Map values can be any supported Feast type, including primitives, arrays, or nested maps. +{% hint style="warning" %} +`ScalarMap` is **not** inferred from any backend schema. You must declare it explicitly in your feature view schema. It is best suited for online serving use cases where the online store serializes proto bytes directly (e.g., Redis, DynamoDB, SQLite). +{% endhint %} + +**Backend support for Map:** + +| Backend | Native Type | Notes | +|---------|-------------|-------| +| PostgreSQL | `jsonb`, `jsonb[]` | `jsonb` → `Map`, `jsonb[]` → `Array(Map)` | +| Snowflake | `VARIANT`, `OBJECT` | Inferred as `Map` | +| Redshift | `SUPER` | Inferred as `Map` | +| Spark | `map` | `map<>` → `Map`, `array>` → `Array(Map)` | +| Athena | `map` | Inferred as `Map` | +| MSSQL | `nvarchar(max)` | Serialized as string | +| DynamoDB / Redis | Proto bytes | Full proto Map and ScalarMap support | + +### JSON Type + +The `Json` type represents opaque JSON data. Unlike `Map`, which is schema-free key-value storage, `Json` is stored as a string at the proto level but backends use native JSON types where available. + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `Json` | `str` (JSON-encoded) | JSON data stored as a string at the proto level | +| `Array(Json)` | `List[str]` | List of JSON strings | + +**Backend support for Json:** + +| Backend | Native Type | +|---------|-------------| +| PostgreSQL | `jsonb` | +| Snowflake | `JSON` / `VARIANT` | +| Redshift | `json` | +| BigQuery | `JSON` | +| Spark | Not natively distinguished from `String` | +| MSSQL | `nvarchar(max)` | + +{% hint style="info" %} +When a backend's native type is ambiguous (e.g., PostgreSQL `jsonb` could be `Map` or `Json`), **the schema-declared Feast type takes precedence**. The backend-to-Feast mappings are only used during schema inference when no explicit type is provided. +{% endhint %} + +### Struct Type + +The `Struct` type represents a schema-aware structured type with named, typed fields. Unlike `Map` (which is schema-free), a `Struct` declares its field names and their types, enabling schema validation. + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `Struct({"field": Type, ...})` | `Dict[str, Any]` | Named fields with typed values | +| `Array(Struct({"field": Type, ...}))` | `List[Dict[str, Any]]` | List of structs | + +**Example:** +```python +from feast.types import Struct, String, Int32, Array + +# Struct with named, typed fields +address_type = Struct({"street": String, "city": String, "zip": Int32}) +Field(name="address", dtype=address_type) + +# Array of structs +items_type = Array(Struct({"name": String, "quantity": Int32})) +Field(name="order_items", dtype=items_type) +``` + +**Backend support for Struct:** + +| Backend | Native Type | +|---------|-------------| +| BigQuery | `STRUCT` / `RECORD` | +| Spark | `struct<...>` / `array>` | +| PostgreSQL | `jsonb` (serialized) | +| Snowflake | `VARIANT` (serialized) | +| MSSQL | `nvarchar(max)` (serialized) | +| DynamoDB / Redis | Proto bytes | ## Complete Feature View Example @@ -77,7 +203,8 @@ from datetime import timedelta from feast import Entity, FeatureView, Field, FileSource from feast.types import ( Int32, Int64, Float32, Float64, String, Bytes, Bool, UnixTimestamp, - Array, Set, Map + Uuid, TimeUuid, Decimal, Array, Set, Map, ScalarMap, Json, Struct, + ZonedTimestamp ) # Define a data source @@ -107,7 +234,11 @@ user_features = FeatureView( Field(name="profile_picture", dtype=Bytes), Field(name="is_active", dtype=Bool), Field(name="last_login", dtype=UnixTimestamp), - + Field(name="event_time", dtype=ZonedTimestamp), + Field(name="session_id", dtype=Uuid), + Field(name="event_id", dtype=TimeUuid), + Field(name="price", dtype=Decimal), + # Array types Field(name="daily_steps", dtype=Array(Int32)), Field(name="transaction_history", dtype=Array(Int64)), @@ -117,17 +248,35 @@ user_features = FeatureView( Field(name="document_hashes", dtype=Array(Bytes)), Field(name="notification_settings", dtype=Array(Bool)), Field(name="login_timestamps", dtype=Array(UnixTimestamp)), - - # Set types (unique values only) + Field(name="related_session_ids", dtype=Array(Uuid)), + Field(name="event_chain", dtype=Array(TimeUuid)), + Field(name="historical_prices", dtype=Array(Decimal)), + + # Set types (unique values only — see backend caveats above) Field(name="visited_pages", dtype=Set(String)), Field(name="unique_categories", dtype=Set(Int32)), Field(name="tag_ids", dtype=Set(Int64)), Field(name="preferred_languages", dtype=Set(String)), - + Field(name="unique_device_ids", dtype=Set(Uuid)), + Field(name="unique_event_ids", dtype=Set(TimeUuid)), + Field(name="unique_prices", dtype=Set(Decimal)), + # Map types Field(name="user_preferences", dtype=Map), Field(name="metadata", dtype=Map), Field(name="activity_log", dtype=Array(Map)), + Field(name="event_counts", dtype=ScalarMap), # non-string keys, e.g. {1001: 5, 1002: 12} + + # Nested collection types + Field(name="weekly_scores", dtype=Array(Array(Float64))), + Field(name="unique_tags_per_category", dtype=Array(Set(String))), + + # JSON type + Field(name="raw_event", dtype=Json), + + # Struct type + Field(name="address", dtype=Struct({"street": String, "city": String, "zip": Int32})), + Field(name="order_items", dtype=Array(Struct({"name": String, "qty": Int32}))), ], source=user_features_source, ) @@ -151,12 +300,136 @@ tag_list = [100, 200, 300, 100, 200] tag_ids = set(tag_list) # {100, 200, 300} ``` +### UUID Type Usage Examples + +UUID types store universally unique identifiers natively, with support for both random UUIDs and time-based UUIDs: + +```python +import uuid + +# Random UUID (version 4) — use Uuid type +session_id = uuid.uuid4() # e.g., UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') + +# Time-based UUID (version 1) — use TimeUuid type +event_id = uuid.uuid1() # e.g., UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') + +# UUID values are returned as uuid.UUID objects from get_online_features() +response = store.get_online_features( + features=["user_features:session_id"], + entity_rows=[{"user_id": 1}], +) +result = response.to_dict() +# result["session_id"][0] is a uuid.UUID object + +# UUID lists +related_sessions = [uuid.uuid4(), uuid.uuid4(), uuid.uuid4()] + +# UUID sets (unique values) +unique_devices = {uuid.uuid4(), uuid.uuid4()} +``` + +### Decimal Type Usage Examples + +The `Decimal` type stores arbitrary-precision decimal numbers using Python's `decimal.Decimal`. +Values are stored as strings in the proto to preserve full precision — no floating-point rounding occurs. + +```python +import decimal + +# Scalar decimal — e.g., a financial price +price = decimal.Decimal("19.99") + +# High-precision value — all digits preserved +tax_rate = decimal.Decimal("0.08750000000000000000") + +# Decimal values are returned as decimal.Decimal objects from get_online_features() +response = store.get_online_features( + features=["product_features:price"], + entity_rows=[{"product_id": 42}], +) +result = response.to_dict() +# result["price"][0] is a decimal.Decimal object + +# Decimal lists — e.g., a history of prices +historical_prices = [ + decimal.Decimal("18.50"), + decimal.Decimal("19.00"), + decimal.Decimal("19.99"), +] + +# Decimal sets — unique price points seen +unique_prices = {decimal.Decimal("9.99"), decimal.Decimal("19.99"), decimal.Decimal("29.99")} +``` + +{% hint style="warning" %} +`Decimal` is **not** inferred from any backend schema. You must declare it explicitly in your feature view schema. The pandas dtype for `Decimal` columns is `object` (holding `decimal.Decimal` instances), not a numeric dtype. +{% endhint %} + +### ZonedTimestamp Type Usage Examples + +The `ZonedTimestamp` type stores a timezone-aware `datetime` as both the UTC instant +and its originating zone, so the original wall-clock zone round-trips losslessly. +By contrast, `UnixTimestamp` always decodes to UTC and discards the source zone. + +```python +from datetime import datetime, timezone +from zoneinfo import ZoneInfo + +# A datetime in a specific zone — both the instant and "America/Los_Angeles" are kept +event_time = datetime(2026, 6, 17, 9, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) + +# ZonedTimestamp values are returned as tz-aware datetime objects, in their own zone +response = store.get_online_features( + features=["event_features:event_time"], + entity_rows=[{"user_id": 1001}], +) +result = response.to_dict() +# result["event_time"][0] == event_time (same instant AND same zone, e.g. 09:00-07:00) + +# Two values at the same instant but different zones stay distinct +la = datetime(2026, 6, 17, 9, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) +utc = datetime(2026, 6, 17, 16, 0, 0, tzinfo=timezone.utc) # same instant as `la` + +# A naive (tz-less) datetime is interpreted as UTC +naive = datetime(2026, 6, 17, 12, 0, 0) # stored zone is empty, decoded as UTC +``` + +{% hint style="warning" %} +`ZonedTimestamp` is **not** inferred from any backend schema — you must declare it +explicitly in your feature view schema. It is not supported as an entity key. The +zone is stored as an IANA name (e.g. `America/Los_Angeles`) when available, falling +back to a fixed-offset string; offline stores that cannot natively carry a zone may +normalize to UTC on that backend. +{% endhint %} + +### Nested Collection Type Usage Examples + +```python +# List of lists — e.g., weekly score history per user +weekly_scores = [[85.0, 90.5, 78.0], [92.0, 88.5], [95.0, 91.0, 87.5]] + +# List of sets — e.g., unique tags assigned per category +unique_tags_per_category = [["python", "ml"], ["rust", "systems"], ["python", "web"]] + +# 3-level nesting — e.g., multi-dimensional matrices +Field(name="tensor", dtype=Array(Array(Array(Float64)))) + +# Mixed nesting +Field(name="grouped_tags", dtype=Array(Set(Array(String)))) +``` + +**Limitation:** Empty inner collections round-trip as `None`: +```python +# Input: [[1, 2], [], [3]] +# Output: [[1, 2], None, [3]] (empty [] becomes None after write-read cycle) +``` + ### Map Type Usage Examples Maps can store complex nested data structures: ```python -# Simple map +# Simple map (string keys) user_preferences = { "theme": "dark", "language": "en", @@ -184,6 +457,80 @@ activity_log = [ ] ``` +### ScalarMap Type Usage Examples + +`ScalarMap` supports non-string keys. Feast infers it automatically when the first dict key is not a string: + +```python +import uuid +import decimal + +# Integer keys — e.g., category ID → item count +event_counts = {1001: 5, 1002: 12, 1003: 0} + +# UUID keys — e.g., session ID → score +import uuid +session_scores = { + uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"): 0.95, + uuid.UUID("a8098c1a-f86e-11da-bd1a-00112444be1e"): 0.87, +} + +# Decimal keys — e.g., price bucket → product name +price_tier = { + decimal.Decimal("9.99"): "budget", + decimal.Decimal("49.99"): "standard", + decimal.Decimal("99.99"): "premium", +} + +# Type inference: Feast automatically picks SCALAR_MAP when the key is non-string +from feast.type_map import python_type_to_feast_value_type +from feast.value_type import ValueType + +python_type_to_feast_value_type({1: "a"}) # → ValueType.SCALAR_MAP +python_type_to_feast_value_type({"a": 1}) # → ValueType.MAP +python_type_to_feast_value_type({}) # → ValueType.MAP (empty dict defaults to MAP) +``` + +{% hint style="warning" %} +`ScalarMap` must be **explicitly declared** in your feature view schema — it is never inferred from backend type schemas. It is best suited for online serving via stores that use proto byte serialization (e.g., Redis, DynamoDB, SQLite). Materialization paths that use PyArrow (e.g., BigQuery, Snowflake, Redshift, Spark) do not have native `ScalarMap` support. +{% endhint %} + +### JSON Type Usage Examples + +Feast's `Json` type stores values as JSON strings at the proto level. You can pass either a +pre-serialized JSON string or a Python dict/list — Feast will call `json.dumps()` automatically +when the value is not already a string: + +```python +import json + +# Option 1: pass a Python dict — Feast calls json.dumps() internally during proto conversion +raw_event = {"type": "click", "target": "button_1", "metadata": {"page": "home"}} + +# Option 2: pass an already-serialized JSON string — Feast validates it via json.loads() +raw_event = '{"type": "click", "target": "button_1", "metadata": {"page": "home"}}' + +# When building a DataFrame for store.push(), values must be strings since +# Pandas/PyArrow columns expect uniform types: +import pandas as pd +event_df = pd.DataFrame({ + "user_id": ["user_1"], + "event_timestamp": [datetime.now()], + "raw_event": [json.dumps({"type": "click", "target": "button_1"})], +}) +store.push("event_push_source", event_df) +``` + +### Struct Type Usage Examples + +```python +# Struct — schema-aware, fields and types are declared +from feast.types import Struct, String, Int32 + +address = Struct({"street": String, "city": String, "zip": Int32}) +# Value: {"street": "123 Main St", "city": "Springfield", "zip": 62704} +``` + ## Type System in Practice The sections below explain how Feast uses its type system in different contexts. @@ -195,7 +542,11 @@ For example, if the `schema` parameter is not specified for a feature view, Feas Each of these columns must be associated with a Feast type, which requires conversion from the data source type system to the Feast type system. * The feature inference logic calls `_infer_features_and_entities`. * `_infer_features_and_entities` calls `source_datatype_to_feast_value_type`. -* `source_datatype_to_feast_value_type` cals the appropriate method in `type_map.py`. For example, if a `SnowflakeSource` is being examined, `snowflake_python_type_to_feast_value_type` from `type_map.py` will be called. +* `source_datatype_to_feast_value_type` calls the appropriate method in `type_map.py`. For example, if a `SnowflakeSource` is being examined, `snowflake_python_type_to_feast_value_type` from `type_map.py` will be called. + +{% hint style="info" %} +**Types that cannot be inferred:** `Set`, `Json`, `Struct`, `Decimal`, `ScalarMap`, `PdfBytes`, and `ImageBytes` types are never inferred from backend schemas. If you use these types, you must declare them explicitly in your feature view schema. +{% endhint %} ### Materialization diff --git a/docs/roadmap.md b/docs/roadmap.md index b7bab598cca..d92ffa38f24 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -17,33 +17,57 @@ The list below contains the functionality that contributors are planning to deve * [x] [Postgres (contrib plugin)](https://docs.feast.dev/reference/data-sources/postgres) * [x] [Spark (contrib plugin)](https://docs.feast.dev/reference/data-sources/spark) * [x] [Couchbase (contrib plugin)](https://docs.feast.dev/reference/data-sources/couchbase) + * [x] [Athena (contrib plugin)](https://docs.feast.dev/reference/data-sources/athena) + * [x] [Clickhouse (contrib plugin)](https://docs.feast.dev/reference/data-sources/clickhouse) + * [x] [Oracle (contrib plugin)](https://docs.feast.dev/reference/data-sources/oracle) + * [x] [MongoDB (contrib plugin)](https://docs.feast.dev/reference/data-sources/mongodb) + * [x] [Ray source (contrib plugin)](https://docs.feast.dev/reference/data-sources/ray) * [x] Kafka / Kinesis sources (via [push support into the online store](https://docs.feast.dev/reference/data-sources/push)) * **Offline Stores** * [x] [Snowflake](https://docs.feast.dev/reference/offline-stores/snowflake) * [x] [Redshift](https://docs.feast.dev/reference/offline-stores/redshift) * [x] [BigQuery](https://docs.feast.dev/reference/offline-stores/bigquery) - * [x] [Azure Synapse + Azure SQL (contrib plugin)](https://docs.feast.dev/reference/offline-stores/mssql.md) + * [x] [DuckDB](https://docs.feast.dev/reference/offline-stores/duckdb) + * [x] [Dask](https://docs.feast.dev/reference/offline-stores/dask) + * [x] [Remote](https://docs.feast.dev/reference/offline-stores/remote-offline-store) + * [x] [Azure Synapse + Azure SQL (contrib plugin)](https://docs.feast.dev/reference/offline-stores/mssql) * [x] [Hive (community plugin)](https://github.com/baineng/feast-hive) * [x] [Postgres (contrib plugin)](https://docs.feast.dev/reference/offline-stores/postgres) - * [x] [Trino (contrib plugin)](https://github.com/Shopify/feast-trino) + * [x] [Trino (contrib plugin)](https://docs.feast.dev/reference/offline-stores/trino) * [x] [Spark (contrib plugin)](https://docs.feast.dev/reference/offline-stores/spark) * [x] [Couchbase (contrib plugin)](https://docs.feast.dev/reference/offline-stores/couchbase) - * [x] [In-memory / Pandas](https://docs.feast.dev/reference/offline-stores/file) + * [x] [Athena (contrib plugin)](https://docs.feast.dev/reference/offline-stores/athena) + * [x] [Clickhouse (contrib plugin)](https://docs.feast.dev/reference/offline-stores/clickhouse) + * [x] [Ray (contrib plugin)](https://docs.feast.dev/reference/offline-stores/ray) + * [x] [Oracle (contrib plugin)](https://docs.feast.dev/reference/offline-stores/oracle) + * [x] [MongoDB (contrib plugin)](https://docs.feast.dev/reference/offline-stores/mongodb) + * [x] [Hybrid](https://docs.feast.dev/reference/offline-stores/hybrid) * [x] [Custom offline store support](https://docs.feast.dev/how-to-guides/customizing-feast/adding-a-new-offline-store) * **Online Stores** * [x] [Snowflake](https://docs.feast.dev/reference/online-stores/snowflake) * [x] [DynamoDB](https://docs.feast.dev/reference/online-stores/dynamodb) * [x] [Redis](https://docs.feast.dev/reference/online-stores/redis) + * [x] [Dragonfly](https://docs.feast.dev/reference/online-stores/dragonfly) * [x] [Datastore](https://docs.feast.dev/reference/online-stores/datastore) * [x] [Bigtable](https://docs.feast.dev/reference/online-stores/bigtable) * [x] [SQLite](https://docs.feast.dev/reference/online-stores/sqlite) - * [x] [Dragonfly](https://docs.feast.dev/reference/online-stores/dragonfly) - * [x] [IKV - Inlined Key Value Store](https://docs.feast.dev/reference/online-stores/ikv) + * [x] [Remote](https://docs.feast.dev/reference/online-stores/remote) + * [x] [Postgres](https://docs.feast.dev/reference/online-stores/postgres) + * [x] [HBase](https://docs.feast.dev/reference/online-stores/hbase) + * [x] [Cassandra / AstraDB](https://docs.feast.dev/reference/online-stores/cassandra) + * [x] [ScyllaDB](https://docs.feast.dev/reference/online-stores/scylladb) + * [x] [MySQL](https://docs.feast.dev/reference/online-stores/mysql) + * [x] [Hazelcast](https://docs.feast.dev/reference/online-stores/hazelcast) + * [x] [Elasticsearch](https://docs.feast.dev/reference/online-stores/elasticsearch) + * [x] [SingleStore](https://docs.feast.dev/reference/online-stores/singlestore) + * [x] [Couchbase](https://docs.feast.dev/reference/online-stores/couchbase) + * [x] [MongoDB](https://docs.feast.dev/reference/online-stores/mongodb) + * [x] [Aerospike](https://docs.feast.dev/reference/online-stores/aerospike) + * [x] [Qdrant (vector store)](https://docs.feast.dev/reference/online-stores/qdrant) + * [x] [Milvus (vector store)](https://docs.feast.dev/reference/online-stores/milvus) + * [x] [Faiss (vector store)](https://docs.feast.dev/reference/online-stores/faiss) + * [x] [Hybrid](https://docs.feast.dev/reference/online-stores/hybrid) * [x] [Azure Cache for Redis (community plugin)](https://github.com/Azure/feast-azure) - * [x] [Postgres (contrib plugin)](https://docs.feast.dev/reference/online-stores/postgres) - * [x] [Cassandra / AstraDB (contrib plugin)](https://docs.feast.dev/reference/online-stores/cassandra) - * [x] [ScyllaDB (contrib plugin)](https://docs.feast.dev/reference/online-stores/scylladb) - * [x] [Couchbase (contrib plugin)](https://docs.feast.dev/reference/online-stores/couchbase) * [x] [Custom online store support](https://docs.feast.dev/how-to-guides/customizing-feast/adding-support-for-a-new-online-store) * **Feature Engineering** * [x] On-demand Transformations (On Read) (Beta release. See [RFC](https://docs.google.com/document/d/1lgfIw0Drc65LpaxbUu49RCeJgMew547meSJttnUqz7c/edit#)) @@ -66,7 +90,7 @@ The list below contains the functionality that contributors are planning to deve * [x] [Offline Feature Server (alpha)](https://docs.feast.dev/reference/feature-servers/offline-feature-server) * [x] [Registry server (alpha)](https://github.com/feast-dev/feast/blob/master/docs/reference/feature-servers/registry-server.md) * **Data Quality Management (See [RFC](https://docs.google.com/document/d/110F72d4NTv80p35wDSONxhhPBqWRwbZXG4f9mNEMd98/edit))** - * [x] Data profiling and validation (Great Expectations) + * [x] [Feature Quality Monitoring](https://docs.feast.dev/how-to-guides/feature-monitoring) — built-in metrics, drift detection, serving log monitoring, and UI dashboard * **Feature Discovery and Governance** * [x] Python SDK for browsing feature registry * [x] CLI for browsing feature registry diff --git a/docs/tutorials/demo-notebooks.md b/docs/tutorials/demo-notebooks.md new file mode 100644 index 00000000000..8c0ba059f81 --- /dev/null +++ b/docs/tutorials/demo-notebooks.md @@ -0,0 +1,114 @@ +# Demo Notebooks + +Feast can generate tailored Jupyter notebooks for any Feast project. The notebooks adapt to your `feature_store.yaml` configuration and provide a hands-on walkthrough of core Feast functionality. + +## What you get + +For each project discovered, Feast creates a directory with notebooks covering: + +| Notebook | Description | +|----------|-------------| +| **01 — Feature Store Overview** | Explore registered entities, feature views, feature services, and data sources. | +| **02 — Historical Feature Retrieval** | Build a training dataset with point-in-time correct joins using `get_historical_features`. | +| **03 — Online Feature Serving** | Materialize features to the online store and retrieve them at low latency with `get_online_features`. | + +The content adapts automatically based on: + +* **Online / offline store types** — descriptions reflect the actual backends configured. +* **Registry type** — local registries include `feast apply`; remote registries use `refresh_registry()`. +* **Authentication** — auth details from `feature_store.yaml` are surfaced when configured. +* **Vector search** — a vector/RAG retrieval section is included when embeddings are detected. + +## Prerequisites + +* Python 3.9+ +* Feast installed (`pip install feast`) +* A feature repository with a valid `feature_store.yaml` + +## Using the CLI + +Run the command from (or pointing to) a directory containing `feature_store.yaml`: + +```bash +feast demo-notebooks +``` + +This searches for `feature_store.yaml` in the current directory and every file inside the `feast-config/` directory. Each file in `feast-config/` is treated as a separate project config. For each project found, notebooks are written to `./feast-demo-notebooks//`. + +### Options + +| Option | Default | Description | +|--------|---------|-------------| +| `-o, --output-dir` | `./feast-demo-notebooks` | Root directory for generated notebooks | +| `--overwrite` | `false` | Overwrite if the output directory already exists | + +```bash +# Write to a custom directory +feast demo-notebooks -o ./my-notebooks + +# Overwrite existing notebooks +feast demo-notebooks --overwrite + +# Use --chdir to point at a different feature repo +feast -c /path/to/feature_repo demo-notebooks +``` + +## Using the Python SDK + +```python +from feast import copy_demo_notebooks + +copy_demo_notebooks() +``` + +### Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `output_dir` | `str` | `"./feast-demo-notebooks"` | Root directory for generated notebooks | +| `repo_path` | `str` | `"."` | Directory to search for `feature_store.yaml` files | +| `overwrite` | `bool` | `False` | Overwrite existing output directories | + +### Examples + +```python +from feast import copy_demo_notebooks + +# Default — searches current directory, writes to ./feast-demo-notebooks/ +copy_demo_notebooks() + +# Custom paths +copy_demo_notebooks( + output_dir="/home/user/notebooks", + repo_path="/home/user/feast-projects/my-repo/feature_repo", + overwrite=True, +) +``` + +## Multi-project repositories + +If your `feast-config/` directory contains multiple files, each is treated as a separate project and a dedicated notebook directory is created: + +``` +feast-demo-notebooks/ +├── project_alpha/ +│ ├── 01_feature_store_overview.ipynb +│ ├── 02_historical_features_training.ipynb +│ └── 03_online_features_serving.ipynb +└── project_beta/ + ├── 01_feature_store_overview.ipynb + ├── 02_historical_features_training.ipynb + └── 03_online_features_serving.ipynb +``` + +## Running the notebooks + +Open any generated notebook in Jupyter, JupyterLab, or VS Code and run cells from top to bottom. Each notebook: + +1. Configures the path to your `feature_store.yaml` automatically (no manual editing needed). +2. Connects to the feature store using the Feast Python SDK. +3. Walks through relevant operations with real data from your project. + +{% hint style="info" %} +The first notebook (**01 — Overview**) includes a prerequisites check and `feast apply` / registry sync step. Subsequent notebooks assume these have already been completed. +{% endhint %} diff --git a/docs/tutorials/rag-with-docling.md b/docs/tutorials/rag-with-docling.md index 88f2bd2aad7..6b85db4177c 100644 --- a/docs/tutorials/rag-with-docling.md +++ b/docs/tutorials/rag-with-docling.md @@ -409,6 +409,234 @@ response = client.chat.completions.create( print('\n'.join([c.message.content for c in response.choices])) ``` +## Alternative: Using DocEmbedder for Simplified Ingestion + +Instead of manually chunking, embedding, and writing documents as shown above, you can use Feast's `DocEmbedder` class to handle the entire pipeline in a single step. `DocEmbedder` automates chunking, embedding generation, FeatureView creation, and writing to the online store. + +### Install Dependencies + +```bash +pip install feast[milvus,rag] +``` + +### Set Up and Ingest with DocEmbedder + +```python +from feast import DocEmbedder +import pandas as pd + +# Prepare your documents as a DataFrame +df = pd.DataFrame({ + "id": ["doc1", "doc2", "doc3"], + "text": [ + "Aaron is a prophet, high priest, and the brother of Moses...", + "God at Sinai granted Aaron the priesthood for himself...", + "His rod turned into a snake. Then he stretched out...", + ], +}) + +# DocEmbedder handles everything: generates FeatureView, applies repo, +# chunks text, generates embeddings, and writes to the online store +embedder = DocEmbedder( + repo_path="feature_repo/", + feature_view_name="text_feature_view", +) + +result = embedder.embed_documents( + documents=df, + id_column="id", + source_column="text", + column_mapping=("text", "text_embedding"), +) +``` + +### Retrieve and Query + +Once documents are ingested, you can retrieve them the same way as shown in Step 5 above: + +```python +from feast import FeatureStore + +store = FeatureStore("feature_repo/") + +query_embedding = embed_text("Who are the authors of the paper?") +context_data = store.retrieve_online_documents_v2( + features=[ + "text_feature_view:embedding", + "text_feature_view:text", + "text_feature_view:source_id", + ], + query=query_embedding, + top_k=3, + distance_metric="COSINE", +).to_df() +``` + +### Customizing the Pipeline + +`DocEmbedder` is extensible at every stage. Below are examples of how to create custom components and wire them together. + +#### Custom Chunker + +Subclass `BaseChunker` to implement your own chunking strategy. The `load_parse_and_chunk` method receives each document and must return a list of chunk dictionaries. + +```python +from feast.chunker import BaseChunker, ChunkingConfig +from typing import Any, Optional + +class SentenceChunker(BaseChunker): + """Chunks text by sentences instead of word count.""" + + def load_parse_and_chunk( + self, + source: Any, + source_id: str, + source_column: str, + source_type: Optional[str] = None, + ) -> list[dict]: + import re + + text = str(source) + # Split on sentence boundaries + sentences = re.split(r'(?<=[.!?])\s+', text) + + chunks = [] + current_chunk = [] + chunk_index = 0 + + for sentence in sentences: + current_chunk.append(sentence) + combined = " ".join(current_chunk) + + if len(combined.split()) >= self.config.chunk_size: + chunks.append({ + "chunk_id": f"{source_id}_{chunk_index}", + "original_id": source_id, + source_column: combined, + "chunk_index": chunk_index, + }) + # Keep overlap by retaining the last sentence + current_chunk = [sentence] + chunk_index += 1 + + # Don't forget the last chunk + if current_chunk and len(" ".join(current_chunk).split()) >= self.config.min_chunk_size: + chunks.append({ + "chunk_id": f"{source_id}_{chunk_index}", + "original_id": source_id, + source_column: " ".join(current_chunk), + "chunk_index": chunk_index, + }) + + return chunks +``` + +Or simply configure the built-in `TextChunker`: + +```python +from feast import TextChunker, ChunkingConfig + +chunker = TextChunker(config=ChunkingConfig( + chunk_size=200, + chunk_overlap=50, + min_chunk_size=30, + max_chunk_chars=1000, +)) +``` + +#### Custom Embedder + +Subclass `BaseEmbedder` to use a different embedding model. Register modality handlers in `_register_default_modalities` and implement the `embed` method. + +```python +from feast.embedder import BaseEmbedder, EmbeddingConfig +from typing import Any, List, Optional +import numpy as np + +class OpenAIEmbedder(BaseEmbedder): + """Embedder that uses the OpenAI API for text embeddings.""" + + def __init__(self, model: str = "text-embedding-3-small", config: Optional[EmbeddingConfig] = None): + self.model = model + self._client = None + super().__init__(config) + + def _register_default_modalities(self) -> None: + self.register_modality("text", self._embed_text) + + @property + def client(self): + if self._client is None: + from openai import OpenAI + self._client = OpenAI() + return self._client + + def get_embedding_dim(self, modality: str) -> Optional[int]: + # text-embedding-3-small produces 1536-dim vectors + if modality == "text": + return 1536 + return None + + def embed(self, inputs: List[Any], modality: str) -> np.ndarray: + if modality not in self._modality_handlers: + raise ValueError(f"Unsupported modality: '{modality}'") + return self._modality_handlers[modality](inputs) + + def _embed_text(self, inputs: List[str]) -> np.ndarray: + response = self.client.embeddings.create(input=inputs, model=self.model) + return np.array([item.embedding for item in response.data]) +``` + +#### Custom Logical Layer Function + +The schema transform function transforms the chunked + embedded DataFrame into the exact schema your FeatureView expects. It must accept a `pd.DataFrame` and return a `pd.DataFrame`. + +```python +import pandas as pd +from datetime import datetime, timezone + +def my_schema_transform_fn(df: pd.DataFrame) -> pd.DataFrame: + """Map chunked + embedded columns to the FeatureView schema.""" + return pd.DataFrame({ + "passage_id": df["chunk_id"], + "text": df["text"], + "embedding": df["text_embedding"], + "event_timestamp": [datetime.now(timezone.utc)] * len(df), + "source_id": df["original_id"], + # Add any extra columns your FeatureView expects + "chunk_index": df["chunk_index"], + }) +``` + +#### Putting It All Together + +Pass your custom components to `DocEmbedder`: + +```python +from feast import DocEmbedder + +embedder = DocEmbedder( + repo_path="feature_repo/", + feature_view_name="text_feature_view", + chunker=SentenceChunker(config=ChunkingConfig(chunk_size=150, min_chunk_size=20)), + embedder=OpenAIEmbedder(model="text-embedding-3-small"), + schema_transform_fn=my_schema_transform_fn, + vector_length=1536, # Match the OpenAI embedding dimension +) + +# Embed and ingest +result = embedder.embed_documents( + documents=df, + id_column="id", + source_column="text", + column_mapping=("text", "text_embedding"), +) +``` + +> **Note:** When using a custom `schema_transform_fn`, ensure the returned DataFrame columns match your FeatureView schema. When using a custom embedder with a different output dimension, set `vector_length` accordingly (or let it auto-detect via `get_embedding_dim`). + +For a complete end-to-end example, see the [DocEmbedder notebook](https://github.com/feast-dev/feast/tree/master/examples/rag-retriever/rag_feast_docembedder.ipynb). + ## Why Feast for RAG? Feast makes it remarkably easy to set up and manage a RAG system by: diff --git a/docs/tutorials/validating-historical-features.md b/docs/tutorials/validating-historical-features.md deleted file mode 100644 index 1984adcdcf9..00000000000 --- a/docs/tutorials/validating-historical-features.md +++ /dev/null @@ -1,916 +0,0 @@ -# Validating historical features with Great Expectations - -In this tutorial, we will use the public dataset of Chicago taxi trips to present data validation capabilities of Feast. -- The original dataset is stored in BigQuery and consists of raw data for each taxi trip (one row per trip) since 2013. -- We will generate several training datasets (aka historical features in Feast) for different periods and evaluate expectations made on one dataset against another. - -Types of features we're ingesting and generating: -- Features that aggregate raw data with daily intervals (eg, trips per day, average fare or speed for a specific day, etc.). -- Features using SQL while pulling data from BigQuery (like total trips time or total miles travelled). -- Features calculated on the fly when requested using Feast's on-demand transformations - -Our plan: - -0. Prepare environment -1. Pull data from BigQuery (optional) -2. Declare & apply features and feature views in Feast -3. Generate reference dataset -4. Develop & test profiler function -5. Run validation on different dataset using reference dataset & profiler - - -> The original notebook and datasets for this tutorial can be found on [GitHub](https://github.com/feast-dev/dqm-tutorial). - -### 0. Setup - -Install Feast Python SDK and great expectations: - - -```python -!pip install 'feast[ge]' -``` - - -### 1. Dataset preparation (Optional) - -**You can skip this step if you don't have GCP account. Please use parquet files that are coming with this tutorial instead** - - -```python -!pip install google-cloud-bigquery -``` - - -```python -import pyarrow.parquet - -from google.cloud.bigquery import Client -``` - - -```python -bq_client = Client(project='kf-feast') -``` - -Running some basic aggregations while pulling data from BigQuery. Grouping by taxi_id and day: - - -```python -data_query = """SELECT - taxi_id, - TIMESTAMP_TRUNC(trip_start_timestamp, DAY) as day, - SUM(trip_miles) as total_miles_travelled, - SUM(trip_seconds) as total_trip_seconds, - SUM(fare) as total_earned, - COUNT(*) as trip_count -FROM `bigquery-public-data.chicago_taxi_trips.taxi_trips` -WHERE - trip_miles > 0 AND trip_seconds > 60 AND - trip_start_timestamp BETWEEN '2019-01-01' and '2020-12-31' AND - trip_total < 1000 -GROUP BY taxi_id, TIMESTAMP_TRUNC(trip_start_timestamp, DAY)""" -``` - - -```python -driver_stats_table = bq_client.query(data_query).to_arrow() - -# Storing resulting dataset into parquet file -pyarrow.parquet.write_table(driver_stats_table, "trips_stats.parquet") -``` - - -```python -def entities_query(year): - return f"""SELECT - distinct taxi_id -FROM `bigquery-public-data.chicago_taxi_trips.taxi_trips` -WHERE - trip_miles > 0 AND trip_seconds > 0 AND - trip_start_timestamp BETWEEN '{year}-01-01' and '{year}-12-31' -""" -``` - - -```python -entities_2019_table = bq_client.query(entities_query(2019)).to_arrow() - -# Storing entities (taxi ids) into parquet file -pyarrow.parquet.write_table(entities_2019_table, "entities.parquet") -``` - - -## 2. Declaring features - - -```python -import pyarrow.parquet -import pandas as pd - -from feast import FeatureView, Entity, FeatureStore, Field, BatchFeatureView -from feast.types import Float64, Int64 -from feast.value_type import ValueType -from feast.data_format import ParquetFormat -from feast.on_demand_feature_view import on_demand_feature_view -from feast.infra.offline_stores.file_source import FileSource -from feast.infra.offline_stores.file import SavedDatasetFileStorage -from datetime import timedelta - -``` - - -```python -batch_source = FileSource( - timestamp_field="day", - path="trips_stats.parquet", # using parquet file that we created on previous step - file_format=ParquetFormat() -) -``` - - -```python -taxi_entity = Entity(name='taxi', join_keys=['taxi_id']) -``` - - -```python -trips_stats_fv = BatchFeatureView( - name='trip_stats', - entities=[taxi_entity], - schema=[ - Field(name="total_miles_travelled", dtype=Float64), - Field(name="total_trip_seconds", dtype=Float64), - Field(name="total_earned", dtype=Float64), - Field(name="trip_count", dtype=Int64), - - ], - ttl=timedelta(seconds=86400), - source=batch_source, -) -``` - -*Read more about feature views in [Feast docs](https://docs.feast.dev/getting-started/concepts/feature-view)* - - -```python -@on_demand_feature_view( - sources=[ - trips_stats_fv, - ], - schema=[ - Field(name="avg_fare", dtype=Float64), - Field(name="avg_speed", dtype=Float64), - Field(name="avg_trip_seconds", dtype=Float64), - Field(name="earned_per_hour", dtype=Float64), - ] -) -def on_demand_stats(inp: pd.DataFrame) -> pd.DataFrame: - out = pd.DataFrame() - out["avg_fare"] = inp["total_earned"] / inp["trip_count"] - out["avg_speed"] = 3600 * inp["total_miles_travelled"] / inp["total_trip_seconds"] - out["avg_trip_seconds"] = inp["total_trip_seconds"] / inp["trip_count"] - out["earned_per_hour"] = 3600 * inp["total_earned"] / inp["total_trip_seconds"] - return out -``` - -*Read more about on demand feature views [here](../reference/beta-on-demand-feature-view.md)* - - -```python -store = FeatureStore(".") # using feature_store.yaml that stored in the same directory -``` - - -```python -store.apply([taxi_entity, trips_stats_fv, on_demand_stats]) # writing to the registry -``` - - -## 3. Generating training (reference) dataset - - -```python -taxi_ids = pyarrow.parquet.read_table("entities.parquet").to_pandas() -``` - -Generating range of timestamps with daily frequency: - - -```python -timestamps = pd.DataFrame() -timestamps["event_timestamp"] = pd.date_range("2019-06-01", "2019-07-01", freq='D') -``` - -Cross merge (aka relation multiplication) produces entity dataframe with each taxi_id repeated for each timestamp: - - -```python -entity_df = pd.merge(taxi_ids, timestamps, how='cross') -entity_df -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
taxi_idevent_timestamp
091d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-01
191d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-02
291d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-03
391d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-04
491d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-05
.........
1569797ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-06-27
1569807ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-06-28
1569817ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-06-29
1569827ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-06-30
1569837ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-07-01
-

156984 rows × 2 columns

-
- - - -Retrieving historical features for resulting entity dataframe and persisting output as a saved dataset: - - -```python -job = store.get_historical_features( - entity_df=entity_df, - features=[ - "trip_stats:total_miles_travelled", - "trip_stats:total_trip_seconds", - "trip_stats:total_earned", - "trip_stats:trip_count", - "on_demand_stats:avg_fare", - "on_demand_stats:avg_trip_seconds", - "on_demand_stats:avg_speed", - "on_demand_stats:earned_per_hour", - ] -) - -store.create_saved_dataset( - from_=job, - name='my_training_ds', - storage=SavedDatasetFileStorage(path='my_training_ds.parquet') -) -``` - -```python -, full_feature_names = False, tags = {}, _retrieval_job = , min_event_timestamp = 2019-06-01 00:00:00, max_event_timestamp = 2019-07-01 00:00:00)> -``` - - -## 4. Developing dataset profiler - -Dataset profiler is a function that accepts dataset and generates set of its characteristics. This charasteristics will be then used to evaluate (validate) next datasets. - -**Important: datasets are not compared to each other! -Feast use a reference dataset and a profiler function to generate a reference profile. -This profile will be then used during validation of the tested dataset.** - - -```python -import numpy as np - -from feast.dqm.profilers.ge_profiler import ge_profiler - -from great_expectations.core.expectation_suite import ExpectationSuite -from great_expectations.dataset import PandasDataset -``` - - -Loading saved dataset first and exploring the data: - - -```python -ds = store.get_saved_dataset('my_training_ds') -ds.to_df() -``` - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
total_earnedavg_trip_secondstaxi_idtotal_miles_travelledtrip_countearned_per_hourevent_timestamptotal_trip_secondsavg_fareavg_speed
068.252270.00000091d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...24.702.054.1189432019-06-01 00:00:00+00:004540.034.12500019.585903
1221.00560.5000007a4a6162eaf27805aef407d25d5cb21fe779cd962922cb...54.1824.059.1436222019-06-01 00:00:00+00:0013452.09.20833314.499554
2160.501010.769231f4c9d05b215d7cbd08eca76252dae51cdb7aca9651d4ef...41.3013.043.9726032019-06-01 00:00:00+00:0013140.012.34615411.315068
3183.75697.550000c1f533318f8480a59173a9728ea0248c0d3eb187f4b897...37.3020.047.4159562019-06-01 00:00:00+00:0013951.09.1875009.625116
4217.751054.076923455b6b5cae6ca5a17cddd251485f2266d13d6a2c92f07c...69.6913.057.2064512019-06-01 00:00:00+00:0013703.016.75000018.308692
.................................
15697938.001980.0000000cccf0ec1f46d1e0beefcfdeaf5188d67e170cdff92618...14.901.069.0909092019-07-01 00:00:00+00:001980.038.00000027.090909
156980135.00551.250000beefd3462e3f5a8e854942a2796876f6db73ebbd25b435...28.4016.055.1020412019-07-01 00:00:00+00:008820.08.43750011.591837
156981NaNNaN9a3c52aa112f46cf0d129fafbd42051b0fb9b0ff8dcb0e...NaNNaNNaN2019-07-01 00:00:00+00:00NaNNaNNaN
15698263.00815.00000008308c31cd99f495dea73ca276d19a6258d7b4c9c88e43...19.964.069.5705522019-07-01 00:00:00+00:003260.015.75000022.041718
156983NaNNaN7ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...NaNNaNNaN2019-07-01 00:00:00+00:00NaNNaNNaN
-

156984 rows × 10 columns

-
- - - -Feast uses [Great Expectations](https://docs.greatexpectations.io/docs/) as a validation engine and [ExpectationSuite](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/core/expectation_suite/index.html#great_expectations.core.expectation_suite.ExpectationSuite) as a dataset's profile. Hence, we need to develop a function that will generate ExpectationSuite. This function will receive instance of [PandasDataset](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/dataset/index.html?highlight=pandasdataset#great_expectations.dataset.PandasDataset) (wrapper around pandas.DataFrame) so we can utilize both Pandas DataFrame API and some helper functions from PandasDataset during profiling. - - -```python -DELTA = 0.1 # controlling allowed window in fraction of the value on scale [0, 1] - -@ge_profiler -def stats_profiler(ds: PandasDataset) -> ExpectationSuite: - # simple checks on data consistency - ds.expect_column_values_to_be_between( - "avg_speed", - min_value=0, - max_value=60, - mostly=0.99 # allow some outliers - ) - - ds.expect_column_values_to_be_between( - "total_miles_travelled", - min_value=0, - max_value=500, - mostly=0.99 # allow some outliers - ) - - # expectation of means based on observed values - observed_mean = ds.trip_count.mean() - ds.expect_column_mean_to_be_between("trip_count", - min_value=observed_mean * (1 - DELTA), - max_value=observed_mean * (1 + DELTA)) - - observed_mean = ds.earned_per_hour.mean() - ds.expect_column_mean_to_be_between("earned_per_hour", - min_value=observed_mean * (1 - DELTA), - max_value=observed_mean * (1 + DELTA)) - - - # expectation of quantiles - qs = [0.5, 0.75, 0.9, 0.95] - observed_quantiles = ds.avg_fare.quantile(qs) - - ds.expect_column_quantile_values_to_be_between( - "avg_fare", - quantile_ranges={ - "quantiles": qs, - "value_ranges": [[None, max_value] for max_value in observed_quantiles] - }) - - return ds.get_expectation_suite() -``` - -Testing our profiler function: - - -```python -ds.get_profile(profiler=stats_profiler) -``` - 02/02/2022 02:43:47 PM INFO: 5 expectation(s) included in expectation_suite. result_format settings filtered. - - - - -**Verify that all expectations that we coded in our profiler are present here. Otherwise (if you can't find some expectations) it means that it failed to pass on the reference dataset (do it silently is default behavior of Great Expectations).** - -Now we can create validation reference from dataset and profiler function: - - -```python -validation_reference = ds.as_reference(name="validation_reference_dataset", profiler=stats_profiler) -``` - -and test it against our existing retrieval job - - -```python -_ = job.to_df(validation_reference=validation_reference) -``` - - 02/02/2022 02:43:52 PM INFO: 5 expectation(s) included in expectation_suite. result_format settings filtered. - 02/02/2022 02:43:53 PM INFO: Validating data_asset_name None with expectation_suite_name default - - -Validation successfully passed as no exception were raised. - - -### 5. Validating new historical retrieval - -Creating new timestamps for Dec 2020: - - -```python -from feast.dqm.errors import ValidationFailed -``` - - -```python -timestamps = pd.DataFrame() -timestamps["event_timestamp"] = pd.date_range("2020-12-01", "2020-12-07", freq='D') -``` - - -```python -entity_df = pd.merge(taxi_ids, timestamps, how='cross') -entity_df -``` - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
taxi_idevent_timestamp
091d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-01
191d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-02
291d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-03
391d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-04
491d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-05
.........
354437ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-03
354447ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-04
354457ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-05
354467ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-06
354477ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-07
-

35448 rows × 2 columns

-
- - -```python -job = store.get_historical_features( - entity_df=entity_df, - features=[ - "trip_stats:total_miles_travelled", - "trip_stats:total_trip_seconds", - "trip_stats:total_earned", - "trip_stats:trip_count", - "on_demand_stats:avg_fare", - "on_demand_stats:avg_trip_seconds", - "on_demand_stats:avg_speed", - "on_demand_stats:earned_per_hour", - ] -) -``` - -Execute retrieval job with validation reference: - - -```python -try: - df = job.to_df(validation_reference=validation_reference) -except ValidationFailed as exc: - print(exc.validation_report) -``` - - 02/02/2022 02:43:58 PM INFO: 5 expectation(s) included in expectation_suite. result_format settings filtered. - 02/02/2022 02:43:59 PM INFO: Validating data_asset_name None with expectation_suite_name default - - [ - { - "expectation_config": { - "expectation_type": "expect_column_mean_to_be_between", - "kwargs": { - "column": "trip_count", - "min_value": 10.387244591346153, - "max_value": 12.695521167200855, - "result_format": "COMPLETE" - }, - "meta": {} - }, - "meta": {}, - "result": { - "observed_value": 6.692920555429092, - "element_count": 35448, - "missing_count": 31055, - "missing_percent": 87.6071992778154 - }, - "exception_info": { - "raised_exception": false, - "exception_message": null, - "exception_traceback": null - }, - "success": false - }, - { - "expectation_config": { - "expectation_type": "expect_column_mean_to_be_between", - "kwargs": { - "column": "earned_per_hour", - "min_value": 52.320624975640214, - "max_value": 63.94743052578249, - "result_format": "COMPLETE" - }, - "meta": {} - }, - "meta": {}, - "result": { - "observed_value": 68.99268345164135, - "element_count": 35448, - "missing_count": 31055, - "missing_percent": 87.6071992778154 - }, - "exception_info": { - "raised_exception": false, - "exception_message": null, - "exception_traceback": null - }, - "success": false - }, - { - "expectation_config": { - "expectation_type": "expect_column_quantile_values_to_be_between", - "kwargs": { - "column": "avg_fare", - "quantile_ranges": { - "quantiles": [ - 0.5, - 0.75, - 0.9, - 0.95 - ], - "value_ranges": [ - [ - null, - 16.4 - ], - [ - null, - 26.229166666666668 - ], - [ - null, - 36.4375 - ], - [ - null, - 42.0 - ] - ] - }, - "result_format": "COMPLETE" - }, - "meta": {} - }, - "meta": {}, - "result": { - "observed_value": { - "quantiles": [ - 0.5, - 0.75, - 0.9, - 0.95 - ], - "values": [ - 19.5, - 28.1, - 38.0, - 44.125 - ] - }, - "element_count": 35448, - "missing_count": 31055, - "missing_percent": 87.6071992778154, - "details": { - "success_details": [ - false, - false, - false, - false - ] - } - }, - "exception_info": { - "raised_exception": false, - "exception_message": null, - "exception_traceback": null - }, - "success": false - } - ] - - -Validation failed since several expectations didn't pass: -* Trip count (mean) decreased more than 10% (which is expected when comparing Dec 2020 vs June 2019) -* Average Fare increased - all quantiles are higher than expected -* Earn per hour (mean) increased more than 10% (most probably due to increased fare) - diff --git a/examples/agent_feature_store/README.md b/examples/agent_feature_store/README.md new file mode 100644 index 00000000000..039b580fd59 --- /dev/null +++ b/examples/agent_feature_store/README.md @@ -0,0 +1,420 @@ +# Feast-Powered AI Agent Example + +This example demonstrates an **AI agent with persistent memory** that uses **Feast as both a feature store and a context memory layer** through the **Model Context Protocol (MCP)**. This demo uses **Milvus** as the vector-capable online store, but Feast supports multiple vector backends -- including **Milvus, Elasticsearch, Qdrant, PGVector, and FAISS** -- swappable via configuration. + +## Why Feast for Agents? + +Agents need more than just access to data -- they need to **remember** what happened in prior interactions. Feast's online store is entity-keyed, low-latency, governed, and supports both reads and writes, making it a natural fit for agent context and memory. + +| Capability | How Feast Provides It | +|---|---| +| **Structured context** | Entity-keyed feature retrieval (customer profiles, account data) | +| **Document search** | Vector similarity search via pluggable backends (Milvus, Elasticsearch, Qdrant, PGVector, FAISS) | +| **Persistent memory** | Auto-checkpointed after each turn via `write_to_online_store` | +| **Governance** | RBAC, audit trails, and feature-level permissions | +| **TTL management** | Declarative expiration on feature views (memory auto-expires) | +| **Offline analysis** | Memory is queryable offline like any other feature | + +## Architecture + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E3F2FD', 'primaryBorderColor': '#1565C0', 'primaryTextColor': '#0D47A1', 'lineColor': '#546E7A', 'secondaryColor': '#F3E5F5', 'tertiaryColor': '#E8F5E9'}}}%% +flowchart LR + User((("🧑 User"))):::userClass + + subgraph AgentLoop ["🤖 Agent Loop"] + LLM["LLM Engine\ntool-calling · reasoning"]:::agentClass + end + + subgraph Feast ["🏗️ Feast MCP Server"] + direction TB + ReadAPI["get_online_features\nretrieve_online_documents"]:::feastClass + WriteAPI["write_to_online_store"]:::feastClass + end + + subgraph VectorStore ["🗄️ Online Store"] + direction TB + Profiles[("👤 customer_profile\nplan · spend · tickets")]:::storageClass + Articles[("📚 knowledge_base\nvector embeddings")]:::storageClass + Memory[("🧠 agent_memory\ntopic · resolution · prefs")]:::memoryClass + end + + User -->|"query"| LLM + LLM -->|"MCP: recall_memory\nlookup_customer\nsearch_kb"| ReadAPI + LLM -->|"MCP: auto-checkpoint"| WriteAPI + ReadAPI --> Profiles + ReadAPI --> Articles + ReadAPI --> Memory + WriteAPI --> Memory + ReadAPI -.->|"results"| LLM + LLM -->|"answer"| User + + classDef userClass fill:#E8EAF6,stroke:#283593,color:#1A237E + classDef agentClass fill:#E3F2FD,stroke:#1565C0,color:#0D47A1 + classDef feastClass fill:#FFF3E0,stroke:#E65100,color:#BF360C + classDef storageClass fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20 + classDef memoryClass fill:#F3E5F5,stroke:#6A1B9A,color:#4A148C +``` + +## Tools (backed by Feast) + +The agent has four tools. Feast is both the **read path** (context) and the **write path** (memory): + +| Tool | Direction | What it does | When the LLM calls it | +|---|---|---|---| +| `lookup_customer` | READ | Fetches customer profile features (plan, spend, tickets) | Questions about the customer's account | +| `search_knowledge_base` | READ | Retrieves support articles from the vector store | Questions needing product docs | +| `recall_memory` | READ | Reads past interaction context (last topic, open issues, preferences) | Start of every conversation | + +Memory is **auto-saved after each agent turn** (not as an LLM tool call). This follows the same pattern used by production frameworks -- see [Memory as Infrastructure](#memory-as-infrastructure) below. + +### Feast as Context Memory + +The `agent_memory` feature view stores per-customer interaction state: + +```python +agent_memory = FeatureView( + name="agent_memory", + entities=[customer], + schema=[ + Field(name="last_topic", dtype=String), + Field(name="last_resolution", dtype=String), + Field(name="interaction_count", dtype=Int64), + Field(name="preferences", dtype=String), + Field(name="open_issue", dtype=String), + ], + ttl=timedelta(days=30), +) +``` + +This gives agents **persistent, governed, entity-keyed memory** that survives across sessions, is versioned, and lives under the same RBAC as every other feature -- unlike an ad-hoc Redis cache or an in-process dict. + +### Memory as Infrastructure + +Production agent frameworks treat memory as **infrastructure, not an LLM decision**. The framework auto-saves state after each step - the LLM never needs to "decide" to persist: + +| Framework | Memory mechanism | How it works | +|---|---|---| +| **LangGraph** | Checkpointers (`MemorySaver`, `PostgresSaver`) | Every graph step is checkpointed automatically by `thread_id` | +| **CrewAI** | Built-in memory (`memory=True`) | Short-term, long-term, and entity memory auto-persist after each task | +| **AutoGen** | Teachable agents | Post-conversation hooks extract and store learnings in a vector DB | +| **OpenAI Agents SDK** | Application-level | Serialize `RunResult` between turns; framework manages state | + +This demo follows the same pattern: the agent's three read tools (`recall_memory`, `lookup_customer`, `search_knowledge_base`) are exposed to the LLM for reasoning, while **memory persistence is handled by the framework after each turn** via `_auto_save_memory`. This ensures consistent, reliable memory regardless of LLM behaviour - no risk of the LLM forgetting to save, double-saving, or writing inconsistent state. + +Feast is a natural fit for this checkpoint layer because it already provides: +- **Entity-keyed storage**: memory is keyed by customer ID (or any entity) +- **TTL management**: memory auto-expires via declarative feature view TTL +- **Schema enforcement**: typed fields prevent corrupt memory writes +- **RBAC and audit trails**: memory reads/writes are governed like any other feature +- **Offline queryability**: agent memory can be analysed in batch pipelines + +## Prerequisites + +- Python 3.10+ +- Feast with MCP and Milvus support +- OpenAI API key (for live tool-calling; demo mode works without it) + +## Quickstart + +### One command + +```bash +cd examples/agent_feature_store +./run_demo.sh + +# Or with live LLM tool-calling: +OPENAI_API_KEY=sk-... ./run_demo.sh +``` + +The script installs dependencies, generates sample data, starts the Feast server, runs the agent, and cleans up on exit. + +### Step by step + +### 1. Install dependencies + +```bash +pip install "feast[mcp,milvus]" +``` + +### 2. Generate sample data and apply the registry + +```bash +cd examples/agent_feature_store +python setup_data.py +``` + +This creates: +- **3 customer profiles** with attributes like plan tier, spend, and satisfaction score +- **6 knowledge-base articles** with 384-dimensional vector embeddings +- **Empty agent memory scaffold** (populated as the agent runs) + +### 3. Start the Feast MCP Feature Server + +```bash +cd feature_repo +feast serve --host 0.0.0.0 --port 6566 --workers 1 +``` + +### 4. Run the agent + +In a new terminal: + +```bash +# Without API key: runs in demo mode (simulated tool selection) +python agent.py +``` + +To run with a real LLM, set the API key and (optionally) the base URL: + +```bash +# OpenAI +export OPENAI_API_KEY="sk-..." #pragma: allowlist secret +python agent.py + +# Ollama (free, local -- no API key needed) +ollama pull llama3.1:8b +export OPENAI_API_KEY="ollama" #pragma: allowlist secret +export OPENAI_BASE_URL="http://localhost:11434/v1" +export LLM_MODEL="llama3.1:8b" +python agent.py + +# Any OpenAI-compatible provider (Azure, vLLM, LiteLLM, etc.) +export OPENAI_API_KEY="your-key" #pragma: allowlist secret +export OPENAI_BASE_URL="https://your-endpoint/v1" +export LLM_MODEL="your-model" +python agent.py +``` + +### Demo mode output + +Without an API key, the agent simulates the decision-making process with memory: + +``` +================================================================= + Scene 1: Enterprise customer (C1001) asks about SSO + Customer: C1001 | Query: "How do I set up SSO for my team?" +================================================================= + [Demo mode] Simulating agent reasoning + + Round 1 | recall_memory(customer_id=C1001) + -> No prior interactions found + + Round 1 | lookup_customer(customer_id=C1001) + -> Alice Johnson | enterprise plan | $24,500 spend | 1 open tickets + + Round 1 | search_knowledge_base(query="How do I set up SSO for my team?...") + -> Best match: "Configuring single sign-on (SSO)" + + Round 2 | Generating personalised response... + + ───────────────────────────────────────────────────────────── + Agent Response: + ───────────────────────────────────────────────────────────── + Hi Alice! + Since you're on our Enterprise plan, SSO is available for your + team. Go to Settings > Security > SSO and enter your Identity + Provider metadata URL. We support SAML 2.0 and OIDC... + + [Checkpoint] Memory saved: topic="SSO setup" + +================================================================= + Scene 4: C1001 returns -- does the agent remember Scene 1? + Customer: C1001 | Query: "I'm back about my SSO question from earlier." +================================================================= + [Demo mode] Simulating agent reasoning + + Round 1 | recall_memory(customer_id=C1001) + -> Previous topic: SSO setup + -> Open issue: none + -> Interaction count: 1 + + Round 1 | lookup_customer(customer_id=C1001) + -> Alice Johnson | enterprise plan | $24,500 spend | 1 open tickets + + Round 2 | Generating personalised response... + + ───────────────────────────────────────────────────────────── + Agent Response: + ───────────────────────────────────────────────────────────── + Welcome back, Alice! I can see from our records that we last + discussed "SSO setup". How can I help you today? + + [Checkpoint] Memory saved: topic="SSO setup" +``` + +Scene 4 demonstrates memory continuity -- the agent recalls the SSO conversation from Scene 1 without the customer re-explaining. + +### Live mode output (with API key) + +With an API key, the LLM autonomously decides which tools to use: + +``` +================================================================= + Scene 1: Enterprise customer (C1001) asks about SSO + Customer: C1001 | Query: "How do I set up SSO for my team?" +================================================================= + [Round 1] Tool call: recall_memory({'customer_id': 'C1001'}) + [Round 1] Tool call: lookup_customer({'customer_id': 'C1001'}) + [Round 1] Tool call: search_knowledge_base({'query': 'SSO setup'}) + Agent finished after 2 round(s) + + ───────────────────────────────────────────────────────────── + Agent Response: + ───────────────────────────────────────────────────────────── + Hi Alice! Since you're on our Enterprise plan, SSO is available + for your team. Go to Settings > Security > SSO and enter your + Identity Provider metadata URL. We support SAML 2.0 and OIDC... + + [Checkpoint] Memory saved: topic="SSO setup" +``` + +## How It Works + +> **Why a raw loop?** This example builds the agent from scratch using the OpenAI tool-calling API and the MCP Python SDK to keep dependencies minimal and make every Feast call visible. All Feast interactions go through the MCP protocol -- the agent connects to Feast's MCP endpoint, discovers tools dynamically, and invokes them via `session.call_tool()`. In production, you would use a framework like LangChain/LangGraph, LlamaIndex, CrewAI, or AutoGen -- Feast's MCP endpoint lets any of them auto-discover the tools with zero custom wiring (see [MCP Integration](#mcp-integration) below). + +### The Agent Loop (`agent.py`) + +```python +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +async with streamablehttp_client("http://localhost:6566/mcp") as (r, w, _): + async with ClientSession(r, w) as session: + await session.initialize() + tools = await session.list_tools() # discover Feast tools + + for round in range(MAX_ROUNDS): + # 1. Send messages + read tools to LLM + response = call_llm(messages, tools=[...]) + + # 2. If LLM says "stop" -> return the answer + if response.finish_reason == "stop": + break + + # 3. Execute tool calls via MCP + for tool_call in response.tool_calls: + result = await session.call_tool(name, args) + messages.append(tool_result(result)) + + # 4. Framework-style checkpoint: auto-save via MCP + await session.call_tool("write_to_online_store", {...}) +``` + +The LLM sees the tool definitions (JSON Schema) and decides: +- **Which tools to call** (can call zero, one, or multiple per round) +- **What arguments to pass** (e.g., which customer ID to look up) +- **When to stop** (once it has enough information to answer) + +All Feast calls go through **MCP** (`session.call_tool()`), not direct REST. Memory is saved **automatically after each turn** by the framework, not by the LLM. This mirrors how production frameworks handle persistence (see [Memory as Infrastructure](#memory-as-infrastructure)). + +### Feature Definitions (`feature_repo/features.py`) + +- **`customer_profile`**: Structured data (name, plan, spend, tickets, satisfaction) +- **`knowledge_base`**: Support articles with 384-dim vector embeddings (Milvus in this demo; swappable to Elasticsearch, Qdrant, PGVector, or FAISS) +- **`agent_memory`**: Per-customer interaction history (last topic, resolution, preferences, open issues) + +### MCP Integration + +The Feast Feature Server exposes all endpoints as MCP tools at `http://localhost:6566/mcp`. +Any MCP-compatible framework can connect: + +```python +# LangChain / LangGraph +from langchain_mcp_adapters.client import MultiServerMCPClient +from langgraph.prebuilt import create_react_agent + +async with MultiServerMCPClient( + {"feast": {"url": "http://localhost:6566/mcp", "transport": "streamable_http"}} +) as client: + tools = client.get_tools() + agent = create_react_agent(llm, tools) + result = await agent.ainvoke({"messages": "How do I set up SSO?"}) +``` + +```python +# LlamaIndex +from llama_index.tools.mcp import aget_tools_from_mcp_url +from llama_index.core.agent.function_calling import FunctionCallingAgent +from llama_index.llms.openai import OpenAI + +tools = await aget_tools_from_mcp_url("http://localhost:6566/mcp") +agent = FunctionCallingAgent.from_tools(tools, llm=OpenAI(model="gpt-4o-mini")) +response = await agent.achat("How do I set up SSO?") +``` + +```json +// Claude Desktop / Cursor +{ + "mcpServers": { + "feast": { + "url": "http://localhost:6566/mcp", + "transport": "streamable_http" + } + } +} +``` + +> **Building the same agent with a framework:** The examples above show the Feast-specific part -- connecting to the MCP endpoint and getting the tools. Once you have the tools, building the agent follows each framework's standard patterns. The key difference from this demo's raw loop: frameworks handle the tool-calling loop, message threading, and (with LangGraph checkpointers or CrewAI `memory=True`) automatic state persistence natively. Feast's MCP endpoint means zero custom integration code -- the tools are discovered and callable immediately. + +**Adapting to your use case:** The demo's system prompt, tool wrappers (`lookup_customer`, `recall_memory`), and feature views are all specific to customer support. For your own agent, you define your feature views in Feast (e.g., `product_catalog`, `order_history`, `fraud_signals`), run `feast apply`, and start the server. The same three generic MCP tools -- `get_online_features`, `retrieve_online_documents`, and `write_to_online_store` -- serve any domain. With a framework like LangChain or LlamaIndex, you don't even need custom tool wrappers -- the LLM calls the generic Feast tools directly with your feature view names and entities. + +## Production Deployment + +For production, Feast fits into a layered platform architecture: + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E3F2FD', 'primaryBorderColor': '#1565C0', 'lineColor': '#546E7A'}}}%% +flowchart TB + Agent(("🤖 AI Agent")):::agentClass + + subgraph Platform ["🛡️ Production Platform"] + direction TB + Gateway["🔐 MCP Gateway\nJWT auth · tool filtering"]:::platformClass + Sandbox["📦 Sandboxed Container\nkernel-level isolation"]:::platformClass + Guardrails["🛑 Guardrails Orchestrator\ninput/output screening"]:::platformClass + end + + subgraph Observability ["📊 Observability + Lifecycle"] + direction LR + OTel["OpenTelemetry + MLflow\ntraces · metrics · audit"]:::obsClass + Kagenti["Kagenti Operator\nAgentCard CRDs · discovery"]:::lifecycleClass + end + + subgraph FeastSvc ["🏗️ Feast MCP Server"] + FS["/mcp · /get-online-features · /write-to-online-store"]:::feastClass + end + + subgraph Store ["🗄️ Online Store"] + direction LR + P[("👤 Profiles")]:::storageClass + K[("📚 Knowledge Base")]:::storageClass + M[("🧠 Agent Memory")]:::memoryClass + end + + Agent --> Gateway + Gateway --> Sandbox + Sandbox --> Guardrails + Guardrails --> FS + FS --> P + FS --> K + FS <--> M + OTel -.->|"traces"| FS + Kagenti -.->|"discover"| FS + + classDef agentClass fill:#E3F2FD,stroke:#1565C0,color:#0D47A1 + classDef platformClass fill:#FFEBEE,stroke:#C62828,color:#B71C1C + classDef obsClass fill:#FFF8E1,stroke:#F57F17,color:#E65100 + classDef lifecycleClass fill:#E0F2F1,stroke:#00695C,color:#004D40 + classDef feastClass fill:#FFF3E0,stroke:#E65100,color:#BF360C + classDef storageClass fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20 + classDef memoryClass fill:#F3E5F5,stroke:#6A1B9A,color:#4A148C +``` + +This demo uses Milvus Lite (embedded). For production, swap to any supported vector-capable backend by updating `feature_store.yaml`: + +- **Milvus cluster**: Deploy via the [Milvus Operator](https://milvus.io/docs/install_cluster-milvusoperator.md) and set `host`/`port` instead of `path`. +- **Elasticsearch**: Set `online_store: type: elasticsearch` with your cluster URL. +- **Qdrant**: Set `online_store: type: qdrant` with your Qdrant endpoint. +- **PGVector**: Set `online_store: type: postgres` with `pgvector_enabled: true`. +- **FAISS**: Set `online_store: type: faiss` for in-process vector search. diff --git a/examples/agent_feature_store/agent.py b/examples/agent_feature_store/agent.py new file mode 100644 index 00000000000..3297cbe393e --- /dev/null +++ b/examples/agent_feature_store/agent.py @@ -0,0 +1,816 @@ +""" +Customer-support AI agent powered by Feast features and memory via MCP. + +The LLM decides which tools to call, when to call them, and what to do +with the results. Feast acts as both the **context provider** (read) and +the **memory store** (write). + +All Feast interactions use the **Model Context Protocol (MCP)**: the agent +connects to the Feast MCP server, discovers available tools dynamically, +and invokes them through the standard protocol -- exactly how production +frameworks like LangChain, LlamaIndex, and CrewAI integrate with MCP +tool servers. + +The agent has three Feast-backed read tools: + + - lookup_customer: Retrieve customer profile features. + - search_knowledge_base: Retrieve support articles. + - recall_memory: Read past interaction context for this customer. + +Memory is automatically saved after every agent turn (framework-style +checkpointing), not as an explicit LLM tool call. This mirrors how +production frameworks like LangGraph, CrewAI, and AutoGen handle +persistence -- as infrastructure, not an LLM decision. + +Memory is entity-keyed (per customer), TTL-managed, versioned, and governed +by the same RBAC as every other feature -- unlike an ad-hoc Redis cache or +an in-process dict. + +Prerequisites: + 1. Run `python setup_data.py` to populate sample data. + 2. Start the Feast server: `cd feature_repo && feast serve --host 0.0.0.0 --port 6566 --workers 1` + 3. Set OPENAI_API_KEY (required for tool-calling). + +Usage: + python agent.py +""" + +import asyncio +import json +import os +import sys +from typing import Any + +import requests + +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +FEAST_SERVER = os.getenv("FEAST_SERVER_URL", "http://localhost:6566") +FEAST_MCP_URL = os.getenv("FEAST_MCP_URL", f"{FEAST_SERVER}/mcp") +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") +OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") +LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini") +MAX_TOOL_ROUNDS = 5 + +# Module-level MCP session (initialised in main) +_mcp_session: ClientSession | None = None +# Maps Feast MCP tool names discovered at startup +_feast_tools: dict[str, str] = {} + + +async def _call_feast_tool(tool_name: str, arguments: dict) -> dict: + """Call a Feast MCP tool and parse the JSON response.""" + assert _mcp_session is not None, "MCP session not initialised" + mcp_tool = _feast_tools.get(tool_name) + if not mcp_tool: + raise ValueError( + f"Feast MCP tool '{tool_name}' not found. " + f"Available: {list(_feast_tools.keys())}" + ) + result = await _mcp_session.call_tool(mcp_tool, arguments) + text = result.content[0].text if result.content else "{}" + return json.loads(text) + + +async def _discover_feast_tools() -> dict[str, str]: + """List MCP tools and build a lookup mapping logical names to MCP names.""" + assert _mcp_session is not None + tools_result = await _mcp_session.list_tools() + tool_map: dict[str, str] = {} + for tool in tools_result.tools: + name = tool.name + if "get_online_features" in name: + tool_map["get_online_features"] = name + elif "retrieve_online_documents" in name: + tool_map["retrieve_online_documents"] = name + elif "write_to_online_store" in name: + tool_map["write_to_online_store"] = name + return tool_map + + +# --------------------------------------------------------------------------- +# Tools: each wraps a Feast MCP call +# --------------------------------------------------------------------------- +# These tool specs are domain-specific wrappers for the customer-support demo. +# With a framework (LangChain, LlamaIndex), you can skip these entirely -- +# the LLM calls Feast's generic MCP tools (get_online_features, etc.) directly. + +TOOLS_SPEC = [ + { + "type": "function", + "function": { + "name": "lookup_customer", + "description": ( + "Look up a customer's profile from the feature store. Returns " + "name, email, plan tier, account age, total spend, open support " + "tickets, and satisfaction score." + ), + "parameters": { + "type": "object", + "properties": { + "customer_id": { + "type": "string", + "description": "The customer ID, e.g. 'C1001'", + } + }, + "required": ["customer_id"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "search_knowledge_base", + "description": ( + "Search the support knowledge base for articles relevant to " + "the user's question. Returns article titles, content, and " + "categories. Use this when you need product documentation or " + "how-to information." + ), + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query describing what the user needs help with", + } + }, + "required": ["query"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "recall_memory", + "description": ( + "Recall past interaction context for a customer from the memory " + "store. Returns the last topic discussed, how it was resolved, " + "interaction count, stated preferences, and any open issue. " + "Call this at the start of a conversation to personalise your " + "response based on history." + ), + "parameters": { + "type": "object", + "properties": { + "customer_id": { + "type": "string", + "description": "The customer ID to recall memory for", + } + }, + "required": ["customer_id"], + }, + }, + }, +] + + +def _parse_online_features(data: dict) -> dict[str, Any]: + """Parse the response from get-online-features into a flat dict.""" + results = data.get("results", []) + feature_names = data.get("metadata", {}).get("feature_names", []) + features: dict[str, Any] = {} + for i, name in enumerate(feature_names): + values = results[i].get("values", []) + val = values[0] if values else None + if val is not None: + features[name] = val + return features + + +async def tool_lookup_customer(customer_id: str) -> dict[str, Any]: + """Fetch customer profile features from Feast via MCP.""" + data = await _call_feast_tool( + "get_online_features", + { + "features": [ + "customer_profile:name", + "customer_profile:email", + "customer_profile:plan_tier", + "customer_profile:account_age_days", + "customer_profile:total_spend", + "customer_profile:open_tickets", + "customer_profile:satisfaction_score", + ], + "entities": {"customer_id": [customer_id]}, + }, + ) + return _parse_online_features(data) + + +async def tool_search_knowledge_base(query: str) -> list[dict[str, Any]]: + """Search knowledge-base articles from Feast via MCP.""" + data = await _call_feast_tool( + "retrieve_online_documents", + { + "features": [ + "knowledge_base:title", + "knowledge_base:content", + "knowledge_base:category", + ], + "query_string": query, + "top_k": 3, + "api_version": 2, + }, + ) + results = data.get("results", []) + feature_names = data.get("metadata", {}).get("feature_names", []) + + num_docs = len(results[0]["values"]) if results else 0 + docs = [] + for doc_idx in range(num_docs): + doc = {} + for feat_idx, name in enumerate(feature_names): + doc[name] = results[feat_idx]["values"][doc_idx] + if doc.get("title"): + docs.append(doc) + return docs + + +async def tool_recall_memory(customer_id: str) -> dict[str, Any]: + """Read the agent's memory for a customer from Feast via MCP.""" + data = await _call_feast_tool( + "get_online_features", + { + "features": [ + "agent_memory:last_topic", + "agent_memory:last_resolution", + "agent_memory:interaction_count", + "agent_memory:preferences", + "agent_memory:open_issue", + ], + "entities": {"customer_id": [customer_id]}, + }, + ) + memory = _parse_online_features(data) + has_memory = any(v is not None for k, v in memory.items() if k != "customer_id") + if not has_memory: + return {"status": "no_previous_interactions", "customer_id": customer_id} + return memory + + +async def tool_save_memory( + customer_id: str, + topic: str, + resolution: str, + open_issue: str = "", + preferences: str = "", +) -> dict[str, str]: + """Write interaction memory back to Feast via MCP.""" + from datetime import datetime, timezone + + existing = await tool_recall_memory(customer_id) + prev_count = existing.get("interaction_count", 0) + prev_preferences = existing.get("preferences", "") + prev_open_issue = existing.get("open_issue", "") + + now = datetime.now(timezone.utc).isoformat() + await _call_feast_tool( + "write_to_online_store", + { + "feature_view_name": "agent_memory", + "df": { + "customer_id": [customer_id], + "last_topic": [topic], + "last_resolution": [resolution], + "interaction_count": [prev_count + 1], + "preferences": [preferences or prev_preferences], + "open_issue": [open_issue or prev_open_issue], + "event_timestamp": [now], + }, + "allow_registry_cache": True, + }, + ) + return {"status": "saved", "customer_id": customer_id, "topic": topic} + + +TOOL_REGISTRY: dict[str, Any] = { + "lookup_customer": lambda args: tool_lookup_customer( + args.get("customer_id") or next(iter(args.values())) + ), + "search_knowledge_base": lambda args: tool_search_knowledge_base( + args.get("query") or args.get("search_query") or next(iter(args.values())) + ), + "recall_memory": lambda args: tool_recall_memory( + args.get("customer_id") or next(iter(args.values())) + ), +} + + +# --------------------------------------------------------------------------- +# Agent loop +# --------------------------------------------------------------------------- + +# Demo-specific prompt: replace with your own domain instructions and tool +# names when adapting this example to a different use case. +SYSTEM_PROMPT = """\ +You are a customer-support agent. You MUST follow these steps in order: + +1. ALWAYS call BOTH recall_memory AND lookup_customer in your first round. + Call them together in the same round. You MUST call lookup_customer even if + recall_memory returns no history -- you need the customer's name and plan. +2. If the question is about a product feature (SSO, API, invoices, passwords, + upgrades, etc.), also call search_knowledge_base with a short keyword. +3. Once you have the tool results, write a helpful, personalised answer. + Use the customer's name and plan tier. Enterprise customers get full access; + starter/pro customers may need to upgrade for certain features. + +Memory is saved automatically -- do NOT try to save it yourself. + +Rules: +- Never call the same tool twice with the same arguments. +- After you have tool results, WRITE your answer immediately. Do not call more tools. +""" + + +def _call_llm(messages: list, use_tools: bool = True) -> dict: + """Make a single LLM API call, returning the parsed choice dict.""" + url = f"{OPENAI_BASE_URL}/chat/completions" + payload: dict[str, Any] = { + "model": LLM_MODEL, + "messages": messages, + "temperature": 0.3, + } + if use_tools: + payload["tools"] = TOOLS_SPEC + resp = requests.post( + url, + headers={ + "Authorization": f"Bearer {OPENAI_API_KEY}", + "Content-Type": "application/json", + }, + json=payload, + ) + if not resp.ok: + print(f"\n ERROR {resp.status_code} from {url}") + print(f" Response: {resp.text[:300]}") + print( + "\n Hint: set OPENAI_BASE_URL to an OpenAI-compatible endpoint.\n" + " Examples:\n" + " OpenAI: export OPENAI_BASE_URL=https://api.openai.com/v1\n" + " Ollama: export OPENAI_BASE_URL=http://localhost:11434/v1\n" + ) + resp.raise_for_status() + return resp.json()["choices"][0] + + +async def run_agent(customer_id: str, user_message: str) -> str: + """ + Agentic loop: the LLM decides which tools to call (if any), executes + them, feeds results back, and repeats until it produces a final answer. + Memory is auto-saved after every turn -- framework-style checkpointing, + not an LLM decision. + """ + if not OPENAI_API_KEY: + response = await _run_agent_demo_mode(customer_id, user_message) + else: + response = await _run_agent_llm(customer_id, user_message) + + await _auto_save_memory(customer_id, user_message, response) + return response + + +async def _auto_save_memory(customer_id: str, user_message: str, response: str) -> None: + """ + Framework-style memory checkpoint: automatically persist interaction + context after every agent turn. + + Production agent frameworks (LangGraph checkpointers, CrewAI memory, + AutoGen teachable agents) all treat memory as infrastructure -- the + framework saves state after each step, rather than relying on the LLM + to decide when to persist. This ensures consistent, reliable memory + regardless of LLM behaviour. + """ + topic = _extract_topic(user_message) + resolution = response[:120] if response else "Answered query" + try: + await tool_save_memory( + customer_id=customer_id, + topic=topic, + resolution=resolution, + ) + print(f' [Checkpoint] Memory saved: topic="{topic}"') + except Exception as e: + print(f" [Checkpoint] Failed to save memory: {e}") + + +async def _run_agent_llm(customer_id: str, user_message: str) -> str: + """Core LLM agent loop. Returns the response text.""" + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + { + "role": "user", + "content": f"[Customer ID: {customer_id}]\n\n{user_message}", + }, + ] + + seen_calls: set[str] = set() + collected_context: dict[str, Any] = {} + + for round_num in range(1, MAX_TOOL_ROUNDS + 1): + choice = _call_llm(messages) + assistant_msg = choice["message"] + messages.append(assistant_msg) + + content = assistant_msg.get("content") or "" + if choice["finish_reason"] == "stop": + if content: + print(f" ✓ Agent finished after {round_num} round(s)") + return content + break # empty stop -- fall through to forced response + + tool_calls = assistant_msg.get("tool_calls", []) + if not tool_calls: + if content: + return content + break + + tool_names = [tc["function"]["name"] for tc in tool_calls] + print(f" 🔧 Round {round_num}: LLM chose tool(s): {', '.join(tool_names)}") + + for tc in tool_calls: + fn_name = tc["function"]["name"] + fn_args = json.loads(tc["function"]["arguments"]) + call_key = f"{fn_name}:{json.dumps(fn_args, sort_keys=True)}" + + if call_key in seen_calls: + print(f" [Round {round_num}] Skipping duplicate: {fn_name}({fn_args})") + messages.append( + { + "role": "tool", + "tool_call_id": tc["id"], + "content": json.dumps( + { + "note": "Already called. Use the results you have and respond." + } + ), + } + ) + continue + + seen_calls.add(call_key) + print(f" [Round {round_num}] ➜ {fn_name}({fn_args})") + + handler = TOOL_REGISTRY.get(fn_name) + if handler: + result = await handler(fn_args) + result_str = json.dumps(result, default=str) + collected_context[fn_name] = result + else: + result_str = json.dumps({"error": f"Unknown tool: {fn_name}"}) + + messages.append( + { + "role": "tool", + "tool_call_id": tc["id"], + "content": result_str, + } + ) + + # If the LLM never produced a response, force one last call without tools + print(" ⏳ Forcing final response...") + messages.append( + { + "role": "user", + "content": "You have all the information. Write your answer to the customer now.", + } + ) + choice = _call_llm(messages, use_tools=False) + content = choice["message"].get("content") or "" + if content: + return content + + # Last resort: build a response from collected tool results + return _fallback_response(collected_context, customer_id, user_message) + + +def _fallback_response(context: dict, customer_id: str, user_message: str) -> str: + """Build a basic response from collected tool results when the LLM fails.""" + profile = context.get("lookup_customer", {}) + name = profile.get("name", customer_id) + plan = profile.get("plan_tier", "your") + memory = context.get("recall_memory", {}) + + parts = [] + if memory and memory.get("last_topic"): + parts.append( + f'Welcome back, {name}! I see we last discussed "{memory["last_topic"]}".' + ) + else: + parts.append(f"Hi {name}!") + + parts.append( + f"You're on the {plan} plan. I've noted your question about " + f'"{user_message[:50]}" and will follow up.' + ) + return " ".join(parts) + + +async def _run_agent_demo_mode(customer_id: str, user_message: str) -> str: + """ + Demo mode: simulates the agentic tool-calling flow and generates a + personalised response that shows how Feast context shapes the answer. + """ + print(" [Demo mode] Simulating agent reasoning\n") + + # ── Round 1: recall memory ────────────────────────────────────────── + print(f" Round 1 | recall_memory(customer_id={customer_id})") + memory = await tool_recall_memory(customer_id) + has_memory = memory.get("status") != "no_previous_interactions" + if has_memory: + print(f" -> Previous topic: {memory.get('last_topic')}") + print(f" -> Open issue: {memory.get('open_issue') or 'none'}") + print(f" -> Interaction count: {memory.get('interaction_count')}") + else: + print(" -> No prior interactions found") + + # ── Round 1: lookup customer ──────────────────────────────────────── + print(f"\n Round 1 | lookup_customer(customer_id={customer_id})") + profile = await tool_lookup_customer(customer_id) + name = profile.get("name", "Customer") + plan = profile.get("plan_tier", "unknown") + spend = profile.get("total_spend", 0) + tickets = profile.get("open_tickets", 0) + print( + f" -> {name} | {plan} plan | ${spend:,.0f} spend | {tickets} open tickets" + ) + + # ── Round 1: search knowledge base (if question needs docs) ───────── + needs_kb = any( + kw in user_message.lower() + for kw in [ + "how", + "what", + "set up", + "configure", + "reset", + "help", + "sso", + "api", + "invoice", + "upgrade", + "password", + ] + ) + kb_article = None + if needs_kb: + print(f'\n Round 1 | search_knowledge_base(query="{user_message[:50]}...")') + docs = await tool_search_knowledge_base(user_message) + if docs: + kb_article = _pick_best_article(user_message, docs) + print(f' -> Best match: "{kb_article.get("title")}"') + + # ── Round 2: generate response (simulated LLM reasoning) ──────────── + print("\n Round 2 | Generating personalised response...") + response = _build_demo_response( + name=name, + plan=plan, + profile=profile, + memory=memory if has_memory else None, + kb_article=kb_article, + user_message=user_message, + ) + + return response + + +def _pick_best_article(query: str, docs: list) -> dict: + """Simple keyword matching to select the most relevant article.""" + query_lower = query.lower() + keywords_to_category = { + "sso": "Configuring single sign-on", + "single sign": "Configuring single sign-on", + "password": "How to reset your password", # pragma: allowlist secret + "reset": "How to reset your password", + "invoice": "Understanding your invoice", + "billing": "Understanding your invoice", + "upgrade": "Upgrading your subscription", + "plan": "Upgrading your subscription", + "api": "Setting up API access", + "rate limit": "Setting up API access", + "support": "Contacting support", + "contact": "Contacting support", + } + for keyword, title_prefix in keywords_to_category.items(): + if keyword in query_lower: + for doc in docs: + if doc.get("title", "").startswith(title_prefix): + return doc + return docs[0] + + +def _extract_topic(message: str) -> str: + """Extract a short topic label from the user message.""" + topic_map = { + "sso": "SSO setup", + "invoice": "Invoice help", + "upgrade": "Plan upgrade", + "api": "API access", + "password": "Password reset", # pragma: allowlist secret + "reset": "Password reset", + } + lower = message.lower() + for keyword, topic in topic_map.items(): + if keyword in lower: + return topic + return message[:40] + + +def _build_demo_response( + name: str, + plan: str, + profile: dict, + memory: dict | None, + kb_article: dict | None, + user_message: str, +) -> str: + """Build a realistic personalised response based on Feast context.""" + parts = [] + + # Acknowledge returning customer if we have memory + if memory and memory.get("last_topic"): + parts.append( + f"Welcome back, {name}! I can see from our records that we last " + f'discussed "{memory["last_topic"]}".' + ) + if memory.get("open_issue"): + parts.append( + f"I also notice you have an open issue: {memory['open_issue']}. " + "Let me know if you'd like to follow up on that." + ) + else: + parts.append(f"Hi {name}!") + + # Role-based response logic + lower = user_message.lower() + + if "sso" in lower: + if plan == "enterprise": + parts.append( + "Since you're on our Enterprise plan, SSO is available for your " + "team. Go to Settings > Security > SSO and enter your Identity " + "Provider metadata URL. We support SAML 2.0 and OIDC. Once " + "configured, all team members will authenticate through your IdP." + ) + parts.append( + "As an Enterprise customer, you also have a dedicated Slack " + "channel and account manager if you need hands-on help." + ) + elif plan == "pro": + parts.append( + "SSO is only available on our Enterprise plan. You're currently " + "on the Pro plan. Would you like to learn about upgrading? The " + "Enterprise plan includes SSO, priority support, and a dedicated " + "account manager." + ) + else: + parts.append( + "SSO is an Enterprise-only feature. You're currently on the " + f"Starter plan (${profile.get('total_spend', 0):,.0f} total spend). " + "You'd need to upgrade to Enterprise to access SSO. I can walk " + "you through the upgrade options if you're interested." + ) + + elif "invoice" in lower: + parts.append( + "Invoices are generated on the first of each month and sent to " + f"{profile.get('email', 'your billing email')}." + ) + if plan == "enterprise": + parts.append( + "As an Enterprise customer, your invoice includes base plan " + "charges, any overage fees, and applied credits. You can also " + "reach your dedicated account manager for a detailed breakdown." + ) + else: + parts.append( + "You can download past invoices from Billing > Invoices. Each " + "invoice shows base charges and any overages." + ) + if profile.get("open_tickets", 0) > 0: + parts.append( + f"I also see you have {profile['open_tickets']} open support " + "ticket(s) -- let me know if any are billing-related." + ) + + elif "upgrade" in lower or "api" in lower: + if plan == "starter": + parts.append( + "Great question! You're on the Starter plan. Upgrading to Pro " + "gives you API access with 1,000 requests/minute. Enterprise " + "gets you 5,000 req/min plus priority support. The price " + "difference is prorated for your current billing cycle." + ) + elif plan == "pro": + parts.append( + "You're on the Pro plan with 1,000 API requests/minute. " + "Upgrading to Enterprise would give you 5,000 req/min, SSO, " + f"and a dedicated account manager. Given your ${profile.get('total_spend', 0):,.0f} " + "total spend, I can check if there are any loyalty discounts available." + ) + else: + parts.append( + "You're already on our Enterprise plan with the highest rate " + "limits (5,000 req/min). If you need even higher throughput, " + "I can connect you with your account manager to discuss custom limits." + ) + + elif memory and memory.get("last_topic"): + parts.append( + f"Yes, I have the full context from our previous conversation about " + f'"{memory["last_topic"]}". ' + f"We've now had {memory.get('interaction_count', 1)} interaction(s). " + "How can I help you today?" + ) + + else: + parts.append("How can I help you today?") + + return "\n".join(parts) + + +# --------------------------------------------------------------------------- +# Demo queries +# --------------------------------------------------------------------------- + +DEMO_QUERIES = [ + # Scene 1: Enterprise customer asks about SSO -- should get full access instructions + ("C1001", "How do I set up SSO for my team?"), + # Scene 2: Starter customer asks the SAME question -- should be told it's Enterprise-only + ("C1003", "How do I set up SSO for my team?"), + # Scene 3: Pro customer asks about invoices -- response uses their email/ticket context + ("C1002", "I need help understanding my last invoice."), + # Scene 4: C1001 returns -- agent should recall the SSO conversation from Scene 1 + ("C1001", "I'm back about my SSO question from earlier."), +] + + +async def main(): + global _mcp_session, _feast_tools + + print("=" * 65) + print(" Feast-Powered AI Agent Demo: Context + Memory via MCP") + print("=" * 65) + print() + print(" This demo shows two key capabilities:") + print(" 1. ROLE-BASED RESPONSES: Same question, different answer per plan tier") + print(" 2. PERSISTENT MEMORY: Agent recalls prior conversations via Feast") + print() + print(" Tools: recall_memory | lookup_customer | search_knowledge_base") + print(" Memory: auto-saved after each turn (framework-style checkpoint)") + print(f" Protocol: MCP ({FEAST_MCP_URL})") + print() + + try: + resp = requests.get(f"{FEAST_SERVER}/health") + resp.raise_for_status() + print(f"Feast server: healthy at {FEAST_SERVER}") + except (requests.ConnectionError, requests.HTTPError) as exc: + print(f"ERROR: Cannot reach Feast server at {FEAST_SERVER} ({exc})") + print( + "Start it with: cd feature_repo && feast serve --host 0.0.0.0 --port 6566 --workers 1" + ) + sys.exit(1) + + async with streamablehttp_client(FEAST_MCP_URL) as (read_stream, write_stream, _): + async with ClientSession(read_stream, write_stream) as session: + _mcp_session = session + await session.initialize() + + _feast_tools = await _discover_feast_tools() + print(f"MCP tools discovered: {', '.join(_feast_tools.values())}") + + if not OPENAI_API_KEY: + print( + "OPENAI_API_KEY not set -- running in demo mode (simulated reasoning)\n" + ) + else: + print(f"Using LLM: {LLM_MODEL} via {OPENAI_BASE_URL}\n") + + scene_labels = [ + "Scene 1: Enterprise customer (C1001) asks about SSO", + "Scene 2: Starter customer (C1003) asks the SAME SSO question", + "Scene 3: Pro customer (C1002) asks about invoices", + "Scene 4: C1001 returns -- does the agent remember Scene 1?", + ] + + for i, (customer_id, query) in enumerate(DEMO_QUERIES): + label = scene_labels[i] if i < len(scene_labels) else "" + print(f"\n{'=' * 65}") + print(f" {label}") + print(f' Customer: {customer_id} | Query: "{query}"') + print(f"{'=' * 65}") + + response = await run_agent(customer_id, query) + + print(f"\n {'─' * 61}") + print(" Agent Response:") + print(f" {'─' * 61}") + for line in response.split("\n"): + print(f" {line}") + print() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/agent_feature_store/feature_repo/feature_store.yaml b/examples/agent_feature_store/feature_repo/feature_store.yaml new file mode 100644 index 00000000000..7cc89d536c8 --- /dev/null +++ b/examples/agent_feature_store/feature_repo/feature_store.yaml @@ -0,0 +1,30 @@ +project: feast_agent +provider: local +registry: data/registry.db + +online_store: + type: milvus + path: data/online_store.db + vector_enabled: true + embedding_dim: 384 + index_type: "IVF_FLAT" + metric_type: "COSINE" + nlist: 128 + +offline_store: + type: file + +entity_key_serialization_version: 3 + +feature_server: + type: mcp + enabled: true + mcp_enabled: true + mcp_transport: http + mcp_server_name: "feast-agent-demo" + mcp_server_version: "1.0.0" + feature_logging: + enabled: false + +auth: + type: no_auth diff --git a/examples/agent_feature_store/feature_repo/features.py b/examples/agent_feature_store/feature_repo/features.py new file mode 100644 index 00000000000..935b9cb03c0 --- /dev/null +++ b/examples/agent_feature_store/feature_repo/features.py @@ -0,0 +1,83 @@ +from datetime import timedelta + +from feast import Entity, FeatureView, Field, FileSource +from feast.data_format import ParquetFormat +from feast.types import Array, Float32, Float64, Int64, String, ValueType + +customer = Entity( + name="customer_id", + description="Unique customer identifier", + value_type=ValueType.STRING, +) + +document = Entity( + name="doc_id", + description="Knowledge-base document chunk identifier", + value_type=ValueType.INT64, +) + +customer_profile_source = FileSource( + file_format=ParquetFormat(), + path="data/customer_profiles.parquet", + timestamp_field="event_timestamp", +) + +knowledge_base_source = FileSource( + file_format=ParquetFormat(), + path="data/knowledge_base.parquet", + timestamp_field="event_timestamp", +) + +agent_memory_source = FileSource( + file_format=ParquetFormat(), + path="data/agent_memory.parquet", + timestamp_field="event_timestamp", +) + +customer_profile = FeatureView( + name="customer_profile", + entities=[customer], + schema=[ + Field(name="name", dtype=String), + Field(name="email", dtype=String), + Field(name="plan_tier", dtype=String), + Field(name="account_age_days", dtype=Int64), + Field(name="total_spend", dtype=Float64), + Field(name="open_tickets", dtype=Int64), + Field(name="satisfaction_score", dtype=Float64), + ], + source=customer_profile_source, + ttl=timedelta(days=1), +) + +knowledge_base = FeatureView( + name="knowledge_base", + entities=[document], + schema=[ + Field( + name="vector", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="title", dtype=String), + Field(name="content", dtype=String), + Field(name="category", dtype=String), + ], + source=knowledge_base_source, + ttl=timedelta(days=7), +) + +agent_memory = FeatureView( + name="agent_memory", + entities=[customer], + schema=[ + Field(name="last_topic", dtype=String), + Field(name="last_resolution", dtype=String), + Field(name="interaction_count", dtype=Int64), + Field(name="preferences", dtype=String), + Field(name="open_issue", dtype=String), + ], + source=agent_memory_source, + ttl=timedelta(days=30), +) diff --git a/examples/agent_feature_store/run_demo.sh b/examples/agent_feature_store/run_demo.sh new file mode 100755 index 00000000000..487ed4a7c6e --- /dev/null +++ b/examples/agent_feature_store/run_demo.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# One-command setup and demo for the Feast-powered AI agent example. +# +# Usage: +# cd examples/agent_feature_store +# ./run_demo.sh # demo mode (no API key needed) +# OPENAI_API_KEY=sk-... ./run_demo.sh # live LLM tool-calling +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +PYTHON="${PYTHON:-$(command -v python3 || command -v python || true)}" +if [[ -z "$PYTHON" ]]; then + echo "ERROR: python3 or python not found on PATH." + exit 1 +fi +PIP="${PIP:-$(command -v pip3 || command -v pip || true)}" +if [[ -z "$PIP" ]]; then + echo "ERROR: pip3 or pip not found on PATH." + exit 1 +fi + +SERVER_PORT=6566 +SERVER_PID="" + +cleanup() { + if [[ -n "$SERVER_PID" ]]; then + echo "" + echo "Stopping Feast server (pid $SERVER_PID)..." + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# ── 1. Install dependencies ───────────────────────────────────────────────── +echo "==> Step 1/4: Installing dependencies..." +$PIP install -q "feast[mcp,milvus]" + +# ── 2. Generate data and apply registry ────────────────────────────────────── +echo "" +echo "==> Step 2/4: Generating sample data and applying Feast registry..." +$PYTHON setup_data.py + +# ── 3. Start the Feast MCP server in the background ───────────────────────── +echo "" +echo "==> Step 3/4: Starting Feast MCP feature server on port $SERVER_PORT..." +cd feature_repo +feast serve --host 0.0.0.0 --port "$SERVER_PORT" --workers 1 & +SERVER_PID=$! +cd "$SCRIPT_DIR" + +echo " Waiting for server to become healthy..." +for i in $(seq 1 30); do + if curl -sf "http://localhost:${SERVER_PORT}/health" > /dev/null 2>&1; then + echo " Server is ready." + break + fi + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + echo "ERROR: Server process exited unexpectedly." + exit 1 + fi + sleep 1 +done + +if ! curl -sf "http://localhost:${SERVER_PORT}/health" > /dev/null 2>&1; then + echo "ERROR: Server did not become healthy within 30 seconds." + exit 1 +fi + +# ── 4. Run the agent ──────────────────────────────────────────────────────── +echo "" +echo "==> Step 4/4: Running the agent..." +echo "" +$PYTHON agent.py + +echo "" +echo "Demo complete." diff --git a/examples/agent_feature_store/setup_data.py b/examples/agent_feature_store/setup_data.py new file mode 100644 index 00000000000..cd88e008af4 --- /dev/null +++ b/examples/agent_feature_store/setup_data.py @@ -0,0 +1,201 @@ +""" +Generates sample data, applies the Feast registry, and materializes features +into the online store so the agent demo is ready to run. + +Usage: + cd examples/agent_feature_store + python setup_data.py +""" + +import os +import sys + +import numpy as np +import pandas as pd + +REPO_DIR = os.path.join(os.path.dirname(__file__), "feature_repo") +DATA_DIR = os.path.join(REPO_DIR, "data") +os.makedirs(DATA_DIR, exist_ok=True) + +EMBEDDING_DIM = 384 +NOW = pd.Timestamp.now() + + +def generate_customer_profiles() -> pd.DataFrame: + customers = [ + { + "customer_id": "C1001", + "name": "Alice Johnson", + "email": "alice@example.com", + "plan_tier": "enterprise", + "account_age_days": 730, + "total_spend": 24500.00, + "open_tickets": 1, + "satisfaction_score": 4.5, + }, + { + "customer_id": "C1002", + "name": "Bob Smith", + "email": "bob@example.com", + "plan_tier": "pro", + "account_age_days": 365, + "total_spend": 8400.00, + "open_tickets": 3, + "satisfaction_score": 3.2, + }, + { + "customer_id": "C1003", + "name": "Carol Lee", + "email": "carol@example.com", + "plan_tier": "starter", + "account_age_days": 90, + "total_spend": 990.00, + "open_tickets": 0, + "satisfaction_score": 4.8, + }, + ] + df = pd.DataFrame(customers) + df["event_timestamp"] = NOW + return df + + +def generate_knowledge_base() -> pd.DataFrame: + articles = [ + { + "doc_id": 1, + "title": "How to reset your password", + "content": ( + "To reset your password, go to Settings > Security > Reset Password. " + "Enter your current password, then choose a new one that is at least " + "12 characters long. Click Save. If you forgot your current password, " + "click 'Forgot Password' on the login page to receive a reset link " + "via email." + ), + "category": "account", + }, + { + "doc_id": 2, + "title": "Upgrading your subscription plan", + "content": ( + "You can upgrade your plan from Starter to Pro or Enterprise at any " + "time. Navigate to Billing > Plans and select the plan you want. " + "The price difference is prorated for the current billing cycle. " + "Enterprise plans include priority support, custom integrations, " + "and a dedicated account manager." + ), + "category": "billing", + }, + { + "doc_id": 3, + "title": "Setting up API access", + "content": ( + "To generate an API key, go to Settings > Developer > API Keys and " + "click 'Create New Key'. Choose the appropriate scopes for your use " + "case. API keys are tied to your account and inherit your permissions. " + "Rate limits are 1000 requests/minute for Pro and 5000 for Enterprise." + ), + "category": "developer", + }, + { + "doc_id": 4, + "title": "Understanding your invoice", + "content": ( + "Invoices are generated on the first of each month and sent to the " + "billing email on file. Each invoice includes a breakdown of base plan " + "charges, overage fees, and any credits applied. You can download past " + "invoices from Billing > Invoices." + ), + "category": "billing", + }, + { + "doc_id": 5, + "title": "Configuring single sign-on (SSO)", + "content": ( + "SSO is available on Enterprise plans. To configure SSO, go to " + "Settings > Security > SSO and provide your Identity Provider (IdP) " + "metadata URL. We support SAML 2.0 and OIDC. Once configured, all " + "team members will authenticate through your IdP." + ), + "category": "account", + }, + { + "doc_id": 6, + "title": "Contacting support", + "content": ( + "You can reach our support team via the in-app chat widget, by " + "emailing support@example.com, or by opening a ticket at " + "https://support.example.com. Enterprise customers have access to " + "a dedicated Slack channel and a named account manager with a " + "guaranteed 1-hour response time." + ), + "category": "support", + }, + ] + + np.random.seed(42) + df = pd.DataFrame(articles) + df["vector"] = [ + np.random.randn(EMBEDDING_DIM).astype(np.float32).tolist() + for _ in range(len(df)) + ] + df["event_timestamp"] = NOW + return df + + +def main(): + print("Generating customer profile data...") + customers_df = generate_customer_profiles() + customers_path = os.path.join(DATA_DIR, "customer_profiles.parquet") + customers_df.to_parquet(customers_path, index=False) + print(f" Saved {len(customers_df)} customer profiles to {customers_path}") + + print("Generating knowledge-base data...") + kb_df = generate_knowledge_base() + kb_path = os.path.join(DATA_DIR, "knowledge_base.parquet") + kb_df.to_parquet(kb_path, index=False) + print(f" Saved {len(kb_df)} knowledge-base articles to {kb_path}") + + print("Generating empty agent memory scaffold...") + memory_df = pd.DataFrame( + { + "customer_id": pd.Series(dtype="str"), + "last_topic": pd.Series(dtype="str"), + "last_resolution": pd.Series(dtype="str"), + "interaction_count": pd.Series(dtype="int64"), + "preferences": pd.Series(dtype="str"), + "open_issue": pd.Series(dtype="str"), + "event_timestamp": pd.Series(dtype="datetime64[ns]"), + } + ) + memory_path = os.path.join(DATA_DIR, "agent_memory.parquet") + memory_df.to_parquet(memory_path, index=False) + print(f" Saved empty memory scaffold to {memory_path}") + + print("Applying Feast registry...") + sys.path.insert(0, REPO_DIR) + from feast import FeatureStore + from features import ( + agent_memory, + customer, + customer_profile, + document, + knowledge_base, + ) + + store = FeatureStore(repo_path=REPO_DIR) + store.apply([customer, document, customer_profile, knowledge_base, agent_memory]) + + print("Materializing customer profiles to the online store...") + store.write_to_online_store(feature_view_name="customer_profile", df=customers_df) + print(" Done.") + + print("Materializing knowledge-base to the online store...") + store.write_to_online_store(feature_view_name="knowledge_base", df=kb_df) + print(" Done.") + + print("\nSetup complete! Start the feature server with:") + print(" cd feature_repo && feast serve --host 0.0.0.0 --port 6566") + + +if __name__ == "__main__": + main() diff --git a/examples/mcp_feature_store/feature_store.yaml b/examples/mcp_feature_store/feature_store.yaml index 305be159956..82029eb111f 100644 --- a/examples/mcp_feature_store/feature_store.yaml +++ b/examples/mcp_feature_store/feature_store.yaml @@ -14,9 +14,10 @@ feature_server: type: mcp enabled: true mcp_enabled: true # Enable MCP support - defaults to false + mcp_transport: http mcp_server_name: "feast-feature-store" mcp_server_version: "1.0.0" feature_logging: enabled: false -entity_key_serialization_version: 3 \ No newline at end of file +entity_key_serialization_version: 3 diff --git a/examples/monitoring/monitoring-quickstart.ipynb b/examples/monitoring/monitoring-quickstart.ipynb new file mode 100644 index 00000000000..77101ffff51 --- /dev/null +++ b/examples/monitoring/monitoring-quickstart.ipynb @@ -0,0 +1,1256 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Feature Quality Monitoring — Quickstart\n", + "\n", + "This notebook walks you through Feast's data quality monitoring end-to-end:\n", + "\n", + "1. Set up a feature store with a PostgreSQL offline store\n", + "2. Register features and trigger baseline computation\n", + "3. Compute metrics across multiple granularities\n", + "4. Read metrics via the Python SDK and REST API\n", + "5. Set up serving log monitoring\n", + "6. Use on-demand exploration for custom date ranges\n", + "\n", + "**Prerequisites:** A running PostgreSQL instance and `feast[postgres]` installed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Install Feast" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "!uv pip install -q 'feast[postgres]'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Configure the Feature Store\n", + "\n", + "Create a minimal `feature_store.yaml` with a PostgreSQL offline store." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Working directory: /var/folders/cn/z7vz24yj25d8fjqdrs9jbsh00000gn/T/feast_monitoring_demo_kze7m3sk\n" + ] + } + ], + "source": [ + "import os\n", + "import tempfile\n", + "\n", + "REPO_DIR = tempfile.mkdtemp(prefix=\"feast_monitoring_demo_\")\n", + "os.makedirs(REPO_DIR, exist_ok=True)\n", + "print(f\"Working directory: {REPO_DIR}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "feature_store.yaml written.\n" + ] + } + ], + "source": [ + "# Adjust these to match your PostgreSQL instance\n", + "PG_HOST = os.environ.get(\"FEAST_PG_HOST\", \"localhost\")\n", + "PG_PORT = os.environ.get(\"FEAST_PG_PORT\", \"5432\")\n", + "PG_DB = os.environ.get(\"FEAST_PG_DB\", \"feast\")\n", + "PG_USER = os.environ.get(\"FEAST_PG_USER\", \"feast\")\n", + "PG_PASS = os.environ.get(\"FEAST_PG_PASS\", \"feast\")\n", + "\n", + "PG_SSLMODE = os.environ.get(\"FEAST_PG_SSLMODE\", \"disable\")\n", + "\n", + "feature_store_yaml = f\"\"\"\n", + "project: monitoring_demo\n", + "registry:\n", + " registry_type: sql\n", + " path: postgresql://{PG_USER}:{PG_PASS}@{PG_HOST}:{PG_PORT}/{PG_DB}?sslmode={PG_SSLMODE}\n", + "provider: local\n", + "offline_store:\n", + " type: postgres\n", + " host: {PG_HOST}\n", + " port: {PG_PORT}\n", + " database: {PG_DB}\n", + " user: {PG_USER}\n", + " password: {PG_PASS}\n", + " sslmode: {PG_SSLMODE}\n", + "online_store:\n", + " type: sqlite\n", + " path: {REPO_DIR}/online_store.db\n", + "entity_key_serialization_version: 3\n", + "\"\"\"\n", + "\n", + "with open(os.path.join(REPO_DIR, \"feature_store.yaml\"), \"w\") as f:\n", + " f.write(feature_store_yaml)\n", + "\n", + "print(\"feature_store.yaml written.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Create Sample Data and Feature Definitions" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample data: 5000 rows, 60 days\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
driver_idevent_timestampconv_rateacc_rateavg_daily_tripsvehicle_typecreated
011482025-02-080.3483070.79439014compact2025-02-08
115392025-02-210.3059450.74904625van2025-02-21
214872025-01-290.7916410.78449217sedan2025-01-29
318212025-01-150.2673080.72622617sedan2025-01-15
414372025-02-120.5446180.72956811suv2025-02-12
\n", + "
" + ], + "text/plain": [ + " driver_id event_timestamp conv_rate acc_rate avg_daily_trips \\\n", + "0 1148 2025-02-08 0.348307 0.794390 14 \n", + "1 1539 2025-02-21 0.305945 0.749046 25 \n", + "2 1487 2025-01-29 0.791641 0.784492 17 \n", + "3 1821 2025-01-15 0.267308 0.726226 17 \n", + "4 1437 2025-02-12 0.544618 0.729568 11 \n", + "\n", + " vehicle_type created \n", + "0 compact 2025-02-08 \n", + "1 van 2025-02-21 \n", + "2 sedan 2025-01-29 \n", + "3 sedan 2025-01-15 \n", + "4 suv 2025-02-12 " + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "\n", + "np.random.seed(42)\n", + "\n", + "N_ROWS = 5000\n", + "N_DAYS = 60\n", + "\n", + "base_date = datetime(2025, 1, 1)\n", + "timestamps = [base_date + timedelta(days=int(d)) for d in np.random.randint(0, N_DAYS, N_ROWS)]\n", + "\n", + "df = pd.DataFrame({\n", + " \"driver_id\": np.random.randint(1000, 2000, N_ROWS),\n", + " \"event_timestamp\": timestamps,\n", + " \"conv_rate\": np.clip(np.random.normal(0.5, 0.2, N_ROWS), 0, 1),\n", + " \"acc_rate\": np.clip(np.random.normal(0.7, 0.15, N_ROWS), 0, 1),\n", + " \"avg_daily_trips\": np.random.poisson(20, N_ROWS).astype(\"int32\"),\n", + " \"vehicle_type\": np.random.choice([\"sedan\", \"suv\", \"truck\", \"van\", \"compact\"], N_ROWS),\n", + " \"created\": timestamps,\n", + "})\n", + "\n", + "print(f\"Sample data: {len(df)} rows, {N_DAYS} days\")\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "!uv pip install -q 'psycopg2'" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded sample data into PostgreSQL table 'driver_stats_source'.\n" + ] + } + ], + "source": [ + "# Load sample data into PostgreSQL'\n", + "from sqlalchemy import create_engine\n", + "\n", + "engine = create_engine(f\"postgresql://{PG_USER}:{PG_PASS}@{PG_HOST}:{PG_PORT}/{PG_DB}\")\n", + "df.to_sql(\"driver_stats_source\", engine, if_exists=\"replace\", index=False)\n", + "print(\"Loaded sample data into PostgreSQL table 'driver_stats_source'.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Feature definitions written.\n" + ] + } + ], + "source": [ + "# Write feature definitions\n", + "definitions = '''\n", + "from datetime import timedelta\n", + "from feast import Entity, FeatureView, FeatureService, Field\n", + "from feast.types import Float32, Int32, String\n", + "from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import (\n", + " PostgreSQLSource,\n", + ")\n", + "\n", + "driver = Entity(name=\"driver\", join_keys=[\"driver_id\"])\n", + "\n", + "driver_stats_source = PostgreSQLSource(\n", + " name=\"driver_stats_source\",\n", + " query=\"SELECT * FROM driver_stats_source\",\n", + " timestamp_field=\"event_timestamp\",\n", + " created_timestamp_column=\"created\",\n", + ")\n", + "\n", + "driver_stats_fv = FeatureView(\n", + " name=\"driver_stats\",\n", + " entities=[driver],\n", + " ttl=timedelta(days=365),\n", + " schema=[\n", + " Field(name=\"conv_rate\", dtype=Float32),\n", + " Field(name=\"acc_rate\", dtype=Float32),\n", + " Field(name=\"avg_daily_trips\", dtype=Int32),\n", + " Field(name=\"vehicle_type\", dtype=String),\n", + " ],\n", + " source=driver_stats_source,\n", + ")\n", + "\n", + "driver_service = FeatureService(\n", + " name=\"driver_service\",\n", + " features=[driver_stats_fv],\n", + ")\n", + "'''\n", + "\n", + "with open(os.path.join(REPO_DIR, \"definitions.py\"), \"w\") as f:\n", + " f.write(definitions)\n", + "\n", + "print(\"Feature definitions written.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Apply — Registers Features & Triggers Baseline\n", + "\n", + "Running `feast apply` registers the feature definitions and automatically queues baseline metric computation." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/var/folders/cn/z7vz24yj25d8fjqdrs9jbsh00000gn/T/feast_monitoring_demo_kze7m3sk/definitions.py:9: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'driver'.\n", + " driver = Entity(name=\"driver\", join_keys=[\"driver_id\"])\n", + "The `path` of the `RegistryConfig` starts with a plain `postgresql` string. We are updating this to `postgresql+psycopg` to ensure that the `psycopg3` driver is used by `sqlalchemy`. If you want to use `psycopg2` pass `postgresql+psycopg2` explicitely to `path`. To silence this warning, pass `postgresql+psycopg` explicitely to `path`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Features registered. Baseline computation queued.\n" + ] + } + ], + "source": [ + "import sys\n", + "from feast import FeatureStore\n", + "\n", + "sys.path.insert(0, REPO_DIR)\n", + "from definitions import driver, driver_stats_source, driver_stats_fv, driver_service\n", + "\n", + "store = FeatureStore(repo_path=REPO_DIR)\n", + "store.apply([driver, driver_stats_source, driver_stats_fv, driver_service])\n", + "print(\"Features registered. Baseline computation queued.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5: Compute Batch Metrics\n", + "\n", + "### 5a. Auto-compute (recommended for production)\n", + "\n", + "Auto-compute detects the latest event timestamp and generates metrics for all 5 granularities: `daily`, `weekly`, `biweekly`, `monthly`, and `quarterly`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computed metrics for 20 features\n", + "Granularities: ['biweekly', 'daily', 'monthly', 'quarterly', 'weekly']\n" + ] + } + ], + "source": [ + "from feast.monitoring.monitoring_service import MonitoringService\n", + "\n", + "monitoring = MonitoringService(store)\n", + "\n", + "result = monitoring.auto_compute(\n", + " project=\"monitoring_demo\",\n", + ")\n", + "\n", + "print(f\"Computed metrics for {result.get('computed_features', 'N/A')} features\")\n", + "print(f\"Granularities: {result.get('granularities', [])}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5b. Targeted compute (specific date range)\n", + "\n", + "Compute `weekly` metrics for a specific window." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'status': 'completed', 'granularity': 'weekly', 'computed_features': 4, 'computed_feature_views': 1, 'computed_feature_services': 1, 'metric_dates': ['2025-01-01'], 'duration_ms': 43}\n" + ] + } + ], + "source": [ + "from datetime import date\n", + "\n", + "result = monitoring.compute_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " start_date=date(2025, 1, 1),\n", + " end_date=date(2025, 1, 7),\n", + " granularity=\"weekly\",\n", + ")\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5c. Set a manual baseline\n", + "\n", + "Use `set_baseline=True` to mark the computed metrics as the reference distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline set.\n" + ] + } + ], + "source": [ + "result = monitoring.compute_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " start_date=date(2025, 1, 1),\n", + " end_date=date(2025, 2, 28),\n", + " granularity=\"daily\",\n", + " set_baseline=True,\n", + ")\n", + "\n", + "print(\"Baseline set.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 6: Read Metrics\n", + "\n", + "### Per-feature metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Date: 2025-01-01 Mean: 0.4989 Null rate: 0.0000 Rows: 4922\n", + "Date: 2025-02-28 Mean: 0.5201 Null rate: 0.0000 Rows: 104\n" + ] + } + ], + "source": [ + "metrics = monitoring.get_feature_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_name=\"conv_rate\",\n", + " data_source_type=\"batch\",\n", + " granularity=\"daily\",\n", + ")\n", + "\n", + "for m in metrics[:3]:\n", + " print(f\"Date: {m['metric_date']} Mean: {m['mean']:.4f} Null rate: {m['null_rate']:.4f} Rows: {m['row_count']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Categorical feature metrics\n", + "\n", + "Categorical features (like `vehicle_type`) produce value-count histograms instead of numeric statistics." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Date: 2025-01-01 Type: categorical Rows: 4922 Null rate: 0.0000\n", + " Unique values: 5 Other count: 0\n", + " van: 1051\n", + " suv: 1028\n", + " sedan: 970\n", + " truck: 954\n", + " compact: 919\n", + "Date: 2025-02-28 Type: categorical Rows: 104 Null rate: 0.0000\n", + " Unique values: 5 Other count: 0\n", + " compact: 26\n", + " truck: 24\n", + " sedan: 19\n", + " van: 18\n", + " suv: 17\n" + ] + } + ], + "source": [ + "cat_metrics = monitoring.get_feature_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_name=\"vehicle_type\",\n", + " data_source_type=\"batch\",\n", + " granularity=\"daily\",\n", + ")\n", + "\n", + "for m in cat_metrics[:3]:\n", + " print(f\"Date: {m['metric_date']} Type: {m['feature_type']} \"\n", + " f\"Rows: {m['row_count']} Null rate: {m['null_rate']:.4f}\")\n", + " if m.get(\"histogram\"):\n", + " hist = m[\"histogram\"]\n", + " print(f\" Unique values: {hist['unique_count']} Other count: {hist['other_count']}\")\n", + " for entry in hist[\"values\"]:\n", + " print(f\" {entry['value']}: {entry['count']}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAGGCAYAAADmRxfNAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAANlNJREFUeJzt3QmcjeX///HPjDFjmca+G7ufnYTKUrLvhUhSiEqRkqTUD1HiS5SQoujrG/mWolWSXWTJvmdrpiKyjK0sM/f/8bl+//t0zmxmXMOZmfN6Ph7HmPvc576v+77PzFzv+1pOkOM4jgAAAACAhWCbFwMAAACAIlgAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gA8IsPPvhAgoKCZOPGjVdd96677jKP1NLtv/zyy9dYwsxNz4ueH2+lSpWSnj17Xvd9Hz582Oxb3wMu3W94eLjcKLw3ACDtESwA4Bq8/fbbPhXjQPXNN9+k2wp6ei5bWvvss8+kS5cuUqZMGcmRI4dUqFBBnn32WTl9+nSi63/xxRdyyy23SLZs2aREiRIyfPhwuXLlis86S5YskV69esn//M//mG3qth955BE5cuRIgu1p8NewFv/RsmXLFB+DlvWxxx6TAgUKSM6cOaVRo0ayadOmBOv997//lQcffFDKly9v9nEtNx1u5L6AQBLi7wIAwNV89913kh6DRf78+W/IHf4bZe/evRIcHJzqyvuUKVNSVYEvWbKk/PXXX5I1a9ZrKGXalE33HxKSef4EaiW5aNGiphKsQWH79u0yefJkcw60wpw9e3bPugsXLpT27dubSvKkSZPMuq+++qocO3ZMpk6d6lnv+eefl5MnT0rnzp1NxfrgwYNmm1999ZVs2bJFChcu7FOG4sWLy+jRo32WaZlSIi4uTtq0aSNbt26V5557zvxs6c+YlvGnn34y+3dpGXVZnTp15MSJE6k+VzdyX0CgyTy/VQFkWqGhof4uQkAICwu7rtvXO+JaqdPrqXfK/cnf+09r8+bNS3A3vVatWtKjRw+ZPXu2aWlwDRo0SKpXr24CuxuuIiIi5LXXXpOnn35aKlasaJZNmDBBGjRo4BM2tQWiYcOGJmBoGPGWK1cuE2yutfxr1qyRTz75RDp16mSW3Xfffaa1RFtT5syZ41n3P//5jxQrVsyUq2rVqul6X0CgoSsUgBT9IdZuACtWrEjw3Lvvvmue27Fjh2fZnj17zB/svHnzmgpc7dq1TdeLxFy8eFEGDhzo6ZLQoUMHOX78+FXHWPz999/mTrRWBnQfRYoUkY4dO8qBAweSPZbffvvNdO8oVKiQqUhXqVJFZsyYkarzoWMRdu7cac6H2+VDy6d3dPX/b7zxRoLXaEVGn/voo498xjjoudJKjVbs8uXLZyp2emzxffjhh6aiqHee9bzef//9Eh0dnaLyrl692txx1fNUtmxZc82SOi7vFpjLly/LiBEjzB1cfa2WTyuaixcvNs/rutoioLy7v3iPo3j99dflzTffNPvV871r165Ex1i49By2aNHCvBf0bvfIkSPFcRzP88uXLzev1a/e4m8zubK5y+K3ZGzevFlatWplroWO92jSpIn8+OOPiY4N+uGHH676vr2REuuio2VSu3fv9izT868PbeHwbrHp27evOc/6s+668847E7Rg6TJ9/3lvM354PHfuXKrLr/vVn0n9GXbpudWfjc8//9z8nnBFRkamumXNX/sCAg0tFgCuSrsNaEXr448/Nncr4/dB1sq5ezdPK9z169c3d/leeOEFU+nS12nXi08//dRT2XH1799f8uTJY+4UauVQK6FPPvmk2W5SYmNjpW3btqYPuFawtTJ+9uxZU+HVgKOV2MT88ccfcvvtt5uKoe5DKxPaLaR3795y5swZGTBgQIrOh5ZRy63n5KWXXjLLtKKifdD12PUO8TPPPOPzGl120003yT333OOzXCszWqHXLiRaiX3rrbfk1KlTMmvWLM86o0aNkqFDh5p19c6zVmC1C4tW8rQynDt37iTLqt1cmjdvbo5VK9Ja8dNzreW9Gl1fy6X7vPXWW8050sH22rWmWbNm0qdPH/n999/Nedc7u4mZOXOmCUpakdVgoZVSbbVI6rrqHXG9RmPHjpVvv/3W0/dfA0ZqpKRs3vR9e8cdd5hQMXjwYNNNSwOYVtg1QN52223W79sb7ejRo+ardvVx6ftFadj3piFOuzK5zydFQ4M+vLfp2rdvn/l5v3Tpknl/PfroozJs2LAUdXnT/eqYj/iVeH3fTZs2zWy7WrVqkhZu5L6AgOMAQAp07drVKViwoHPlyhXPsiNHjjjBwcHOyJEjPcuaNGniVKtWzfn77789y+Li4px69eo55cuX9yybOXOm3oZ2mjZtap53PfPMM06WLFmc06dPe5Y1bNjQPFwzZswwr50wYUKCcnpvS9cZPny45/vevXs7RYoUcf7880+f19x///1Orly5nAsXLqT4fFSpUsWnTK53333X7Hf37t2eZZcuXXLy58/v9OjRw7NMy6Xr3X333T6v79u3r1m+detW8/3hw4fN+Rg1apTPetu3b3dCQkISLI+vffv2TrZs2ZxffvnFs2zXrl1mm/H/BJQsWdKnjDVq1HDatGmT7Pb79euXYDvq0KFDZnlERIRz7NixRJ/T94BL96vL+vfv73Mtdf+hoaHO8ePHzbJly5aZ9fTr1baZVNkSe2/oedL9HDhwwLPs999/d2666SbnzjvvvKb3rb/p+13LtG/fPs+ycePGmfJHRUUlWL9OnTrO7bffnuw2X3nlFfP6JUuW+Czv1auX8/LLLzuffvqpM2vWLPO+1vXuu+++FJU1Z86cZhvxff3112Y73377bap+DtPLvoBAQ/segBTRGWd0cKd3FxTtUqB3n/U5pQM9ly5dau6sawvCn3/+aR466FG7t/z888+mK5I3vZPt3UVF7xrrnetffvklybJoy4feMdW7xvHFn0LVpXVJfV27du3M/92y6UPLFhMTk+isMKmlx67dhrSFwrVo0SKzn8T6n/fr18/ne/eYdNCtO9uPnmPdrneZdeCsdlFatmxZkmXR86j71tYiHdDrqlSpkjnmq9GWEL2Tr9ftWt17772mtSSl9K6/y21Z0jvg33//vVwvep50vIGeJ211cmn3ugceeMB0JdPWGtv37Y2k4wTef/99MzOU92BkHbSe1Hgafd+6zydm5cqVpmucvhcbN27s85zuS1tvtHvRQw89ZLoUaYuFtlbG706WGN1vUmXyLndauJH7AgINXaEApIh2UdHBmdrVQ/ueK/3/zTffbMY5qP3795tKu3bb0UdiNJxoNymXd4VXafcSpd2BkqLjKHQ6zdTM6qPdh3SKSe3qoI+kymZLK+MaXrRi98orr5hlGjL0mONXxpR3pU9pNy7toqHda5RW6vWcxl/PlVw3Ez1mrSQl9lo9f254SYp2P9KuW3p9taubvge00qgDf1OqdOnSKV5Xj9u7Yq/c95Z7Pq4HPU8XLlww5yQ+DWEa7HQ8i3b5s3nf6rXQAHstdGyN/vylxKpVq0z3Pg2P2o0u/naU9zgCl3ZZ8549ypuOBdJujPo+eO+991JUDg0106dPN6FQu7dpQNSbD940dGbJksXsN6kyeZc7pW7kvgD8g2ABIEX0Dp/e0Z0/f76ZmlHHK+gAVp1JxuX2nddZZ5K6I16uXDmf7/UPfWK8B+ymBbds2mqgM+UkJjUV5uR0797dzDijA7a1r7YOXNfBsSkZBBq/xUXLrct0LEhi5+p6fqicjuHQEKd3n/WOvlYodWD6O++84zPLUHLSupKWVIuUthbcSNfyvtUg/vDDD1/T/vQ9m5LPTdEpVO+++24TALRFMX741lYYpZ9FoQOTvekyHWcQn4YqHaejwUbDqI4VSgl3+24FX38e9PMivB06dMiMMdJyJfb5GO6ylE5b67qR+wLwD4IFgBTTLk///ve/zaBpnRVGK1FuNyjl3m3Wu+hNmza9buXQu/rr1q0zsxal9LMQ9G6lVoi0ApoWZUuqgqv0zr7uT1sqdNCv3g3XO/2J0RYJ77v62uqjYUIrQO6x6nnWddy79ymlZdCKfWJdmfQzK1JCB1trZVgfOmhXw4YO6naDRXLnIbX0uHVWKO/j1IG0yj0fbstA/A9+S6wLUkrLpudJPwAusXOid+o1EMavhF8LDdvujFqplZLKroZAfe8VLFjQBIDEQqe2MCodhO8dInSg+6+//mq6eHnTbowaKvQOv/7cu8EkJfRaKrcrXI0aNRIcv/tZGFoubWnR94B3ANefc702qX3v38h9AfgHwQJAimmFXCuaeudVg4VWTLwrxVqh0Vl0dDYdHSsQvxKiXU5S098+uX77X3/9tZlLP/7sS1oJT6xCqXeY9XXaRUlnjoo/J31qy6az3yT1qcZ6l7hr165mX3qetNUiqdYQnRJVK24une1J6bSnSvusDxkyxPRt1ylnvY9Nj1XvBus0sInRY9bK7IIFCyQqKsrTfUfLpGMvrkYrld7b1oqqtjh5T3Or50HpuUhudqqU0muqM2O5x6ffa3h0u9/ph+vpcWl/f21Bc2krWnwpLZtuT6+Btsxolys3xGirnF5DnWJXZ4uypT8PqamYp3YGKD0GrSjrtU3qvazdufRzKrQ7oM6c5ba86AfB6XvL/VwHdf78eWndurUZF6VjeZLqjqfjT7RF03vcgl4793Mu3NZLDYVJhXrdr7aw6Jgitww6lkhb/rRrYWo/Y+VG7gvAPwgWAFJMK3ha0Z07d66pdOhnFCRWUdaKmFamdfCmtmJoBW3t2rXmjqh21UiLrkY6Hat+jsD69evNwFktj/bl1i5H8ad0dY0ZM8ZUkLQVQctWuXJlUzHXQdv62vh9spOjnymhlTGtPGllW0OV9xgKLaNWkHV///rXv5LcjnbP0K4reqdZz5GGBx0wrHdc3RYL3YeGC630amVaW170ddotTe8wa9ezpGgg0Wlb9RzpudGpWzW8aAVz27ZtyR6jnh8NinqsGij1LrdWyLwHWOtz6qmnnjIVSK2o6hTA10IHz2pZtduPXiPt/qUB8sUXX/RUlLU7jn4StB6DVoT1/OgnQSc2PiY1ZdNzrHe49b2r50nDoQZkvVOvU9+md/r+0RYCnSpXB5vrw6VTv+r0wK5x48aZ95wGET0fGrQ1wGkrlI4pcXXr1s38fOnnvmgY9f7sCg2ZbrDTnx8N0vrQnwUdS6LvTe0qqe9Pndr1arSCr+MwtGVMP2fD/TRsbWHU97A3DZX6cG8I6M++G2K0RU0f6WVfQMDx97RUADKWxYsXmykZg4KCnOjo6ETX0Sk7u3fv7hQuXNjJmjWrU6xYMadt27bOvHnzEkzbuWHDBp/XJjadaPzpZpVODfvSSy85pUuXNvvQfXXq1MlnutD4U4qqP/74w0xDGhkZ6XmdTpE7bdq0VJ2Ho0ePmqlQdTpS3U9i01Dq9JQ6He+vv/6a4Dl3ulmd+lXLrdvJkyeP8+STTzp//fVXgvV1Gs8GDRqYqTL1UbFiRXMce/fuvWpZV6xY4dSqVctMp1qmTBnnnXfe8ew/uelmX331VefWW291cufO7WTPnt3sU6e31elzXTr9sE4RW6BAAfOecLfpTv+q05vGl9R0s3pcev2aN2/u5MiRwylUqJApZ2xsrM/rderZe++916yj56xPnz7Ojh07EmwzqbIl9d7YtGmT06JFCyc8PNxsu1GjRs6aNWt81knN+/ZG0n0n9UjsvTl//nzn5ptvdsLCwpzixYs7//u//+tzXd33Q1Lb1OdcBw8edDp37uyUKlXKTG2s507fb/o+856S92pOnjxppsjNly+f2YaWO/55Vu57N7FH/GuaHvYFBJIg/cff4QYAMqOaNWuaO/3aNz0+Haegd0f1LmhiHzYGAEBGw+dYAMB1oN2GtmzZYrpEAQAQCBhjAQBetAUhualLQ0NDTStEUrS/+k8//STjx483A3W9Z80CACAzI1gAgJc6deok++nJDRs29Pn08fh0cLN+sJx+2NpHH33k+TRfAAAyO8ZYAIAXnclGZ7VJbhpLd7YhAADwD4IFAAAAAGsM3gYAAABgjTEWFuLi4uT33383H1aV2Cf9AgAAABmZdm46e/asFC1aVIKDk2+TIFhY0FARGRnp72IAAAAA11V0dLQUL1482XUIFha0pcI90REREf4uDgAAAJCmzpw5Y26ku/Xe5BAsLLjdnzRUECwAAACQWaWk2z+DtwEAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAIA0Exvn+LsI8JMQf+04M5nw5VY5fsHfpQAAAPCvyPzh8kKHmv4uBvyEYJEGfjtxXqJiYv1dDAAAAMBv6AoFAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWDx/wUFBcmCBQv8XQwAAAAgQyJYAAAAALjxwSIuLk7Gjh0r5cqVk7CwMClRooSMGjXKPLd9+3Zp3LixZM+eXfLlyyePPfaYnDt3zvPanj17Svv27eW1116TQoUKSe7cuWXkyJFy5coVee655yRv3rxSvHhxmTlzpuc1hw8fNq0Jc+fOlXr16km2bNmkatWqsmLFCs86sbGx0rt3byldurTZd4UKFWTixIkJyj5jxgypUqWKKXeRIkXkySefNMtLlSplvnbo0MHsy/0eAAAAwHUKFkOGDJExY8bI0KFDZdeuXTJnzhwTEs6fPy8tWrSQPHnyyIYNG+STTz6R77//3lN5dy1dulR+//13WblypUyYMEGGDx8ubdu2Na9bt26dPP7449KnTx/59ddffV6nwePZZ5+VzZs3S926daVdu3Zy4sQJT9jRQKL71DINGzZMXnzxRfn44489r586dar069fPhB0NQF988YUJR0rLqzTQHDlyxPN9fBcvXpQzZ874PAAAAACIBDmO46R05bNnz0qBAgVk8uTJ8sgjj/g8N336dHn++eclOjpacubMaZZ98803JgBokNDwoS0Wy5cvl4MHD0pw8P9lmooVK0rBggVN0HBbH3LlyiXvvfee3H///abFQlsiNMzo9pW2cOiy/v37y+DBgxMtqwaao0ePyrx588z3xYoVk4cfflheffXVxE9EUJDMnz/ftKgk5eWXX5YRI0YkWP7IxIUSFRObwrMIAACQOZUrHCFTHr3D38VAGtIb6Vo3j4mJkYiIiLRrsdi9e7e5a9+kSZNEn6tRo4YnVKj69eub1oS9e/d6lmlXJDdUKA0c1apV83yfJUsW043q2LFjPtvXVgpXSEiI1K5d2+zTNWXKFKlVq5YJPuHh4TJt2jSJiooyz+m2NNwkVu7UttboSXUfGqIAAAAAiISkZmUdv2Ara9asCVoKElumgSSldPzFoEGDZPz48SaA3HTTTTJu3DjTtSqtyq10bIY+AAAAAFi0WJQvX95U0pcsWZLguUqVKsnWrVvNWAvXDz/8YFondDC1rR9//NHzf+0K9dNPP5l9uvvRgd19+/aVmjVrmrETBw4c8KyvQUMHZCdWbpeGG+2GBQAAAOA6BwudkUnHOei4hlmzZpnKu1b433//fenWrZt5vkePHrJjxw5ZtmyZGQPx0EMPme5OtrSrk46B2LNnjxmEferUKenVq5cn8GzcuFEWLVok+/btMwPL4w/A1vER2qLx1ltvyc8//yybNm2SSZMmeZ53g4eOy9BtAwAAALiOs0JppV1nZ9KZl7TFoEuXLmYMQ44cOUzF/uTJk1KnTh3p1KmTGdOgA73Tgg7e1oeO41i9erWZ1Sl//vzmOZ1FqmPHjqYst912m5ktSlsvvGngefPNN+Xtt9824zx0JioNGC4NHYsXL5bIyEjT6gEAAADgOs0K5Q/urFA6zezNN98s6XGUPLNCAQAAMCtUZnTdZoUCAAAAgMQQLAAAAADc2Olm/UEHVafz3loAAABAwKPFAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACshdhvAsXy5ZTQ7P4uBQAAgH9F5g/3dxHgRwSLNDCwXQ2JiIjwdzEAAAD8LjbOkSzBQf4uBvyArlAAAABIM4SKwEWwAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAA6V5snOPvIuAqQq62Aq5uwpdb5fgFf5cCAAAgc4rMHy4vdKjp72LgKggWaeC3E+clKibW38UAAAAA/IauUAAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIC1gA0Whw8flqCgINmyZYu/iwIAAABkeOkuWNx1110yYMAAfxcDAAAAQEYOFlfjOI5cuXLF38UAAAAAkF6DRc+ePWXFihUyceJE001JHx988IH5unDhQqlVq5aEhYXJ6tWrzbrt27f3eb22dGiLhysuLk7Gjh0r5cqVM68rUaKEjBo1KtF9x8bGSq9evaRixYoSFRV13Y8VAAAAyExCJB3RQLFv3z6pWrWqjBw50izbuXOn+frCCy/I66+/LmXKlJE8efKkaHtDhgyR6dOnyxtvvCENGjSQI0eOyJ49exKsd/HiRenatasZd7Fq1SopUKBAGh8ZAAAAkLmlq2CRK1cuCQ0NlRw5ckjhwoXNMjcIaNBo1qxZird19uxZE1QmT54sPXr0MMvKli1rAoa3c+fOSZs2bUy4WLZsmSlDUnQdfbjOnDmT6mMEAAAAMqN01RUqObVr107V+rt37zYhoEmTJsmupy0V58+fl++++y7ZUKFGjx5t1nEfkZGRqSoTAAAAkFllmGCRM2dOn++Dg4PNQG5vly9f9vw/e/bsKdpu69atZdu2bbJ27doUda2KiYnxPKKjo1NcfgAAACAzS3fBQrtC6UDqq9FxEDpmwpv3Z1KUL1/ehIslS5Yku50nnnhCxowZI3fffbcZOJ4cHQAeERHh8wAAAACQzsZYqFKlSsm6devMQOrw8HAzs1NiGjduLOPGjZNZs2ZJ3bp15cMPP5QdO3ZIzZo1zfPZsmWT559/XgYPHmzCSv369eX48eNmMHjv3r19ttW/f38TZtq2bWtmn4o/DgMAAABABmuxGDRokGTJkkUqV65sWiWSmvq1RYsWMnToUBMc6tSpYwZrd+/e3Wcdff7ZZ5+VYcOGSaVKlaRLly5y7NixRLenU9WOGDHCdI1as2bNdTk2AAAAILMKcuIPVECK6axQOoj7kYkLJSrm6t23AAAAkHrlCkfIlEfv8HcxArq+GxMTc9VhAOmuxQIAAABAxkOwAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYC7HfBIrlyymh2f1dCgAAgMwpMn+4v4uAFCBYpIGB7WpIRESEv4sBAACQacXGOZIlOMjfxUAy6AoFAACAdI9Qkf4RLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAg04iNc/xdhIAV4u8CZAYTvtwqxy/4uxQAAACBLTJ/uLzQoaa/ixGwCBZp4LcT5yUqJtbfxQAAAAD8hq5QAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrmSJYHD58WIKCgmTLli3+LgoAAAAQkDJFsAAAAADgXwQLAAAAABk3WMybN0+qVasm2bNnl3z58knTpk3l/Pnz5rn33ntPKlWqJNmyZZOKFSvK22+/7fPa9evXS82aNc3ztWvXls2bN/s8HxsbK71795bSpUub7VeoUEEmTpzos07Pnj2lffv28vrrr0uRIkVMGfr16yeXL1++AUcPAAAAZC4h/tjpkSNHpGvXrjJ27Fjp0KGDnD17VlatWiWO48js2bNl2LBhMnnyZBMeNDQ8+uijkjNnTunRo4ecO3dO2rZtK82aNZMPP/xQDh06JE8//bTP9uPi4qR48eLyySefmMCwZs0aeeyxx0yAuO+++zzrLVu2zCzTr/v375cuXbrIzTffbPYHAAAAIAMEiytXrkjHjh2lZMmSZpm2Xqjhw4fL+PHjzXNKWx127dol7777rgkWc+bMMcHh/fffNy0WVapUkV9//VWeeOIJz/azZs0qI0aM8Hyv21i7dq18/PHHPsEiT548JsBkyZLFtIy0adNGlixZkmSwuHjxonm4zpw5cx3ODgAAAJDx+CVY1KhRQ5o0aWLCRIsWLaR58+bSqVMnCQ0NlQMHDphuTN6Vew0huXLlMv/fvXu3VK9e3YQKV926dRPsY8qUKTJjxgyJioqSv/76Sy5dumRaI7xpKNFQ4dLWi+3btydZ7tGjR/sEFgAAAAB+HGOhlfnFixfLwoULpXLlyjJp0iQzDmLHjh3m+enTp5upY92HLv/xxx9TvP25c+fKoEGDTED57rvvzDYefvhhEy68acuGN52yVltDkjJkyBCJiYnxPKKjo1N97AAAAEBm5JcWC7cSX79+ffPQMRXaJeqHH36QokWLysGDB6Vbt26Jvk4Hdf/nP/+Rv//+29NqET906Hbq1asnffv29SzTlhBbYWFh5gEAAAAgHQSLdevWmbEM2gWqYMGC5vvjx4+b0KBdjZ566inT9ally5ZmTMPGjRvl1KlTMnDgQHnggQfkpZdeMl2ltAVBPxxPZ3byVr58eZk1a5YsWrTIjK/QILJhwwbzfwAAAACZJFhERETIypUr5c033zQDoLW1Qgdst2rVyjyfI0cOGTdunDz33HNmNigdizFgwADzXHh4uHz55Zfy+OOPm1mjtCvVv/71L7n33ns92+/Tp4+ZTUpnedKWEZ2BSlsvtOsVAAAAgLQX5Ogcr7gmGoq0ZeWRiQslKibW38UBAAAIaOUKR8iUR+/wdzEyZX1Xxxdr40By+ORtAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAayH2m0CxfDklNLu/SwEAABDYIvOH+7sIAY1gkQYGtqshERER/i4GAABAwIuNcyRLcJC/ixGQ6AoFAACATINQ4T8ECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYI1gAQAAAMAawQIAAACANYIFAAAAAGsECwAAAADWCBYAAADAdRQb50ggCPF3ATKDCV9uleMX/F0KAAAApDeR+cPlhQ41JRAQLNLAbyfOS1RMrL+LAQAAAPgNXaEAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALCW4YPFvHnzpFq1apI9e3bJly+fNG3aVM6fPy933XWXDBgwwGfd9u3bS8+ePc3/X3zxRbntttsSbK9GjRoycuTIG1Z+AAAAIDPI0MHiyJEj0rVrV+nVq5fs3r1bli9fLh07dhTHca762m7dusn69evlwIEDnmU7d+6Ubdu2yQMPPJDoay5evChnzpzxeQAAAADIBMHiypUrJkyUKlXKtFz07dtXwsPDr/raKlWqmNaJOXPmeJbNnj3btGKUK1cu0deMHj1acuXK5XlERkam6fEAAAAAGVWGDhYaDJo0aWICRefOnWX69Oly6tSpFL9eWy3cYKGtHB999JFZlpQhQ4ZITEyM5xEdHZ0mxwEAAABkdBk6WGTJkkUWL14sCxculMqVK8ukSZOkQoUKcujQIQkODk7QJery5cs+32s3qr1798qmTZtkzZo1Jih06dIlyf2FhYVJRESEzwMAAABABg8WKigoSOrXry8jRoyQzZs3S2hoqMyfP18KFChgukq5YmNjZceOHT6vLV68uDRs2NB0gdJHs2bNpGDBgn44CgAAACBjC5EMbN26dbJkyRJp3ry5CQT6/fHjx6VSpUqSM2dOGThwoHz99ddStmxZmTBhgpw+fTrBNrTr0/Dhw+XSpUvyxhtv+OU4AAAAgIwuQwcL7Yq0cuVKefPNN80MTSVLlpTx48dLq1atTLenrVu3Svfu3SUkJESeeeYZadSoUYJtdOrUSZ588knTrUqnowUAAACQekFOSuZmRaI0zOjsUI9MXChRMbH+Lg4AAADSmXKFI2TKo3dIRq/v6sRFVxtfnOHHWAAAAADwP4IFAAAAAGsECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYI1gAQAAAMAawQIAAACANYIFAAAAAGsECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYC3EfhMoli+nhGb3dykAAACQ3kTmD5dAQbBIAwPb1ZCIiAh/FwMAAADpUGycI1mCgySzoysUAAAAcB1lCYBQoQgWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAA6VRsnCMZRYi/C5AZTPhyqxy/4O9SAAAAIDOJzB8uL3SoKRkFwSIN/HbivETFxPq7GAAAAIDf0BUKAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAQGAGi2nTpknRokUlLi7OZ/k999wjvXr1kgMHDpj/FypUSMLDw6VOnTry/fff+6xbqlQpee2118z6N910k5QoUcJsFwAAAECABIvOnTvLiRMnZNmyZZ5lJ0+elG+//Va6desm586dk9atW8uSJUtk8+bN0rJlS2nXrp1ERUX5bGf8+PFSu3Zts07fvn3liSeekL179/rhiAAAAICMLUMGizx58kirVq1kzpw5nmXz5s2T/PnzS6NGjaRGjRrSp08fqVq1qpQvX15eeeUVKVu2rHzxxRc+29HwoYGiXLly8vzzz5vXe4eV+C5evChnzpzxeQAAAADIoMFCacvEp59+air7avbs2XL//fdLcHCwabEYNGiQVKpUSXLnzm26Q+3evTtBi0X16tU9/w8KCpLChQvLsWPHktzn6NGjJVeuXJ5HZGTkdTxCAAAAIOPIsMFCuzY5jiNff/21REdHy6pVq0zYUBoq5s+fb8ZQ6PItW7ZItWrV5NKlSz7byJo1q8/3Gi7ij9vwNmTIEImJifE8dL8AAAAAREIkg8qWLZt07NjRtFTs379fKlSoILfccot57ocffpCePXtKhw4dzPfagnH48GHrfYaFhZkHAAAAgEwSLJS2ULRt21Z27twpDz74oGe5jqv47LPPTKuGtkIMHTo02ZYIAAAAAAHaFUo1btxY8ubNa2ZyeuCBBzzLJ0yYYAZ416tXz4SLFi1aeFozAAAAAKS9DN1ioQO1f//99wTL9TMqli5d6rOsX79+Pt8n1jVKx2IAAAAACLAWCwAAAADpA8ECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGAtxH4TKJYvp4Rm93cpAAAAkJlE5g+XjIRgkQYGtqshERER/i4GAAAAMpnYOEeyBAdJRkBXKAAAACCdypJBQoUiWAAAAACwRrAAAAAAYI1gAQAAAMAawQIAAACANYIFAAAAAGsECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYI1gAQAAAMBaiP0mApfjOObrmTNn/F0UAAAAIM259Vy33pscgoWFEydOmK+RkZH+LgoAAABw3Zw9e1Zy5cqV7DoECwt58+Y1X6Oioq56opH50rsGyujoaImIiPB3cXADce0DF9c+MHHdAxfXXjwtFRoqihYtKldDsLAQHPx/Q1Q0VATyGy6Q6XXn2gcmrn3g4toHJq574OLaS4pvoDN4GwAAAIA1ggUAAAAAawQLC2FhYTJ8+HDzFYGFax+4uPaBi2sfmLjugYtrn3pBTkrmjgIAAACAZNBiAQAAAMAawQIAAACANYIFAAAAAGsEi2s0ZcoUKVWqlGTLlk1uu+02Wb9+vb+LBEujR4+WOnXqyE033SQFCxaU9u3by969e33W+fvvv6Vfv36SL18+CQ8Pl3vvvVf++OMPn3X0AxPbtGkjOXLkMNt57rnn5MqVKzf4aHCtxowZI0FBQTJgwADPMq575vXbb7/Jgw8+aK5t9uzZpVq1arJx40bP8zoMcdiwYVKkSBHzfNOmTeXnn3/22cbJkyelW7duZp773LlzS+/eveXcuXN+OBqkVGxsrAwdOlRKly5trmvZsmXllVdeMdfbxbXPHFauXCnt2rUzH+6mv9sXLFjg83xaXedt27bJHXfcYeqF+qF6Y8eOlYCkg7eROnPnznVCQ0OdGTNmODt37nQeffRRJ3fu3M4ff/zh76LBQosWLZyZM2c6O3bscLZs2eK0bt3aKVGihHPu3DnPOo8//rgTGRnpLFmyxNm4caNz++23O/Xq1fM8f+XKFadq1apO06ZNnc2bNzvffPONkz9/fmfIkCF+Oiqkxvr1651SpUo51atXd55++mnPcq575nTy5EmnZMmSTs+ePZ1169Y5Bw8edBYtWuTs37/fs86YMWOcXLlyOQsWLHC2bt3q3H333U7p0qWdv/76y7NOy5YtnRo1ajg//vijs2rVKqdcuXJO165d/XRUSIlRo0Y5+fLlc7766ivn0KFDzieffOKEh4c7EydO9KzDtc8c9PfxSy+95Hz22WeaGp358+f7PJ8W1zkmJsYpVKiQ061bN1OH+Oijj5zs2bM77777rhNoCBbX4NZbb3X69evn+T42NtYpWrSoM3r0aL+WC2nr2LFj5pfQihUrzPenT592smbNav4AuXbv3m3WWbt2recXWHBwsHP06FHPOlOnTnUiIiKcixcv+uEokFJnz551ypcv7yxevNhp2LChJ1hw3TOv559/3mnQoEGSz8fFxTmFCxd2xo0b51mm74ewsDBTcVC7du0y74UNGzZ41lm4cKETFBTk/Pbbb9f5CHCt2rRp4/Tq1ctnWceOHU3FUHHtM6f4wSKtrvPbb7/t5MmTx+f3vf5+qVChghNo6AqVSpcuXZKffvrJNJW5goODzfdr1671a9mQtmJiYszXvHnzmq963S9fvuxz7StWrCglSpTwXHv9ql0pChUq5FmnRYsWcubMGdm5c+cNPwaknHZ10q5M3tdXcd0zry+++EJq164tnTt3Nt3XatasKdOnT/c8f+jQITl69KjPtc+VK5fp/up97bVrhG7Hpevr34V169bd4CNCStWrV0+WLFki+/btM99v3bpVVq9eLa1atTLfc+0DQ1pdZ13nzjvvlNDQUJ+/Adqd+tSpUxJIQvxdgIzmzz//NH0zvSsQSr/fs2eP38qFtBUXF2f62NevX1+qVq1qlukvH/2lob9g4l97fc5dJ7H3hvsc0qe5c+fKpk2bZMOGDQme47pnXgcPHpSpU6fKwIED5cUXXzTX/6mnnjLXu0ePHp5rl9i19b72Gkq8hYSEmBsSXPv064UXXjDBX28SZMmSxfxdHzVqlOlHr7j2gSGtrrN+1fE6Sf0NyJMnjwQKggWQxN3rHTt2mDtYyNyio6Pl6aeflsWLF5tBdwisGwh6F/K1114z32uLhf7cv/POOyZYIPP6+OOPZfbs2TJnzhypUqWKbNmyxdxM0gG+XHvg2tEVKpXy589v7m7EnxFGvy9cuLDfyoW08+STT8pXX30ly5Ytk+LFi3uW6/XVrnCnT59O8trr18TeG+5zSH+0q9OxY8fklltuMXeh9LFixQp56623zP/1rhPXPXPSWWAqV67ss6xSpUpmhi/va5fc73v9qu8fbzobmM4iw7VPv3TWNm21uP/++003xoceekieeeYZMzug4toHhrS6zvwN+AfBIpW0ibxWrVqmb6b3XS/9vm7dun4tG+zouC4NFfPnz5elS5cmaNbU6541a1afa6/9J7US4l57/bp9+3afX0J6J1ynqItfgUH60KRJE3PN9I6l+9C72Nolwv0/1z1z0q6O8aeU1j73JUuWNP/X3wFaKfC+9tp9RvtVe197DZ0aUF36+0P/Lmg/baRPFy5cMH3kvelNQ71uimsfGNLqOus6Oq3t5cuXff4GVKhQIaC6QRn+Hj2eUaeb1RkDPvjgAzNbwGOPPWamm/WeEQYZzxNPPGGmnFu+fLlz5MgRz+PChQs+047qFLRLly41047WrVvXPOJPO9q8eXMzZe23337rFChQgGlHMxjvWaEU1z3zTi8cEhJiph79+eefndmzZzs5cuRwPvzwQ5+pKPX3++eff+5s27bNueeeexKdirJmzZpmytrVq1eb2cWYcjR969Gjh1OsWDHPdLM6FalOET148GDPOlz7zDPjn04Drg+t9k6YMMH8/5dffkmz66wzSel0sw899JCZblbrifq7hOlmkWKTJk0yFQ39PAudflbnNkbGpr9wEnvoZ1u49BdN3759zbRy+kujQ4cOJnx4O3z4sNOqVSszh7X+oXr22Wedy5cv++GIkFbBguueeX355ZcmFOrNoooVKzrTpk3zeV6noxw6dKipNOg6TZo0cfbu3euzzokTJ0wlQz8HQacYfvjhh01lBunXmTNnzM+4/h3Pli2bU6ZMGfNZB97ThXLtM4dly5Yl+rddw2VaXmf9DIwGDRqYbWho1cASiIL0H3+3mgAAAADI2BhjAQAAAMAawQIAAACANYIFAAAAAGsECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYI1gAQAAAMAawQIAcEMcPXpU+vfvL2XKlJGwsDCJjIyUdu3ayZIlS25oOYKCgmTBggU3dJ8AEAhC/F0AAEDmd/jwYalfv77kzp1bxo0bJ9WqVZPLly/LokWLpF+/frJnzx5/FxEAYCnIcRzHdiMAACSndevWsm3bNtm7d6/kzJnT57nTp0+bwBEVFWVaNLQFIzg4WFq2bCmTJk2SQoUKmfV69uxp1vVubRgwYIBs2bJFli9fbr6/6667pHr16pItWzZ57733JDQ0VB5//HF5+eWXzfOlSpWSX375xfP6kiVLmtADALBHVygAwHV18uRJ+fbbb03LRPxQoTRUxMXFyT333GPWXbFihSxevFgOHjwoXbp0SfX+/v3vf5v9rFu3TsaOHSsjR44021MbNmwwX2fOnClHjhzxfA8AsEdXKADAdbV//37RxvGKFSsmuY62Umzfvl0OHTpkxl6oWbNmSZUqVUzlv06dOinen7ZYDB8+3Py/fPnyMnnyZLP9Zs2aSYECBTxhpnDhwtbHBgD4By0WAIDrKiU9bnfv3m0ChRsqVOXKlU0A0OdSQ4OFtyJFisixY8dStQ0AQOoRLAAA15W2GuhMTLYDtHXcRfyQogPA48uaNavP97pv7WoFALi+CBYAgOsqb9680qJFC5kyZYqcP38+wfM6ILtSpUoSHR1tHq5du3aZ57TlQmk3Jh0X4U0HbqeWBo/Y2NhrOhYAQNIIFgCA605DhVbmb731Vvn000/l559/Nl2c3nrrLalbt640bdrUTEHbrVs32bRpk6xfv166d+8uDRs2lNq1a5ttNG7cWDZu3GjGXujrdRzFjh07Ul0WnRlKx1zo52qcOnXqOhwtAAQmggUA4LrTD8XTwNCoUSN59tlnpWrVqmYwtVbwp06darorff7555InTx658847TdDQ1/z3v//1bENbPYYOHSqDBw82g7nPnj1rwkdqjR8/3swSpeM5atasmcZHCgCBi8+xAAAAAGCNFgsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAEFv/D/X0w0X7J7aHAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " import matplotlib.pyplot as plt\n", + "\n", + " latest_cat = cat_metrics[0] if cat_metrics else None\n", + " if latest_cat and latest_cat.get(\"histogram\"):\n", + " hist = latest_cat[\"histogram\"]\n", + " labels = [e[\"value\"] for e in hist[\"values\"]]\n", + " counts = [e[\"count\"] for e in hist[\"values\"]]\n", + " if hist[\"other_count\"] > 0:\n", + " labels.append(\"(other)\")\n", + " counts.append(hist[\"other_count\"])\n", + "\n", + " fig, ax = plt.subplots(figsize=(8, 4))\n", + " ax.barh(labels, counts, color=\"steelblue\", edgecolor=\"white\")\n", + " ax.set_title(f\"vehicle_type distribution — {latest_cat['metric_date']}\")\n", + " ax.set_xlabel(\"Count\")\n", + " plt.tight_layout()\n", + " plt.show() # pragma: allowlist secret\n", + " else:\n", + " print(\"No categorical histogram data available.\")\n", + "except ImportError:\n", + " print(\"Install matplotlib to visualize: pip install matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Feature view aggregates" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Date: 2024-12-01 Total rows: 5000 Features w/ nulls: 0 Max null rate: 0.0\n", + "Date: 2025-01-01 Total rows: 4922 Features w/ nulls: 0 Max null rate: 0.0\n", + "Date: 2025-01-01 Total rows: 576 Features w/ nulls: 0 Max null rate: 0.0\n" + ] + } + ], + "source": [ + "view_metrics = monitoring.get_feature_view_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " data_source_type=\"batch\",\n", + ")\n", + "\n", + "for m in view_metrics[:3]:\n", + " print(f\"Date: {m['metric_date']} Total rows: {m['total_row_count']} \"\n", + " f\"Features w/ nulls: {m['features_with_nulls']} Max null rate: {m.get('max_null_rate', 'N/A')}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Feature service aggregates" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Date: 2025-01-01 Total features: 4 Avg null rate: 0.0\n", + "Date: 2025-01-01 Total features: 4 Avg null rate: 0.0\n", + "Date: 2025-02-28 Total features: 4 Avg null rate: 0.0\n" + ] + } + ], + "source": [ + "svc_metrics = monitoring.get_feature_service_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_service_name=\"driver_service\",\n", + " data_source_type=\"batch\",\n", + ")\n", + "\n", + "for m in svc_metrics[:3]:\n", + " print(f\"Date: {m['metric_date']} Total features: {m['total_features']} \"\n", + " f\"Avg null rate: {m.get('avg_null_rate', 'N/A')}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Baseline metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline mean: 0.4989\n", + "Baseline stddev: 0.1975\n", + "Baseline null_rate: 0.0000\n" + ] + } + ], + "source": [ + "baseline = monitoring.get_baseline(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_name=\"conv_rate\",\n", + " data_source_type=\"batch\",\n", + ")\n", + "\n", + "if baseline:\n", + " print(f\"Baseline mean: {baseline[0]['mean']:.4f}\")\n", + " print(f\"Baseline stddev: {baseline[0]['stddev']:.4f}\")\n", + " print(f\"Baseline null_rate: {baseline[0]['null_rate']:.4f}\")\n", + "else:\n", + " print(\"No baseline found.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 7: Visualize a Feature Distribution\n", + "\n", + "Use the histogram stored in the metrics to plot a distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/opt/homebrew/Cellar/python@3.12/3.12.11/Frameworks/Python.framework/Versions/3.12/lib/python3.12/pty.py:95: DeprecationWarning: This process (pid=12140) is multi-threaded, use of forkpty() may lead to deadlocks in the child.\n", + " pid, fd = os.forkpty()\n" + ] + } + ], + "source": [ + "!uv pip install -q 'matplotlib'" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARL1JREFUeJzt3Qd4FOXa//E7Cb2F3kNRkd4EBDwoUgQRUYr1KKIiKgIqqJQjCIIIBxVQATk2sCHKK6hU6eARUGlKURSlRCEUEQIoLZn3up/3P/vfDQmEMJOZ3f1+rmuNmV12752dze5vnhZjWZYlAAAAAADAcbHO3yUAAAAAAFCEbgAAAAAAXELoBgAAAADAJYRuAAAAAABcQugGAAAAAMAlhG4AAAAAAFxC6AYAAAAAwCWEbgAAAAAAXELoBgAAAADAJYRuAACyYNiwYRITExOyrVKlSnLvvfe6/tg7d+40jz116tTANn3cAgUKSHbRx9d9AAAAzo3QDQCICKtWrTIh8PDhwxJO5s2b59vw6ufanDZz5ky5/fbb5ZJLLpF8+fJJ1apV5YknnsjwePr888/liiuukDx58kiFChVk6NChcubMmZDbLFmyRO6//365/PLLzX3qfT/wwAOyd+/es+7v2muvNScy0l6uv/76TD8HrfXBBx+UEiVKSP78+aVFixayfv36s2730Ucfyd133y1VqlQxj6GPfaGy87EAINzl8LoAAACcCt3PPvusafEtXLiwJzVs27ZNYmNjLzjYTpw48YLCbcWKFeXvv/+WnDlzZqFKZ2rTx8+RI3K+RmiALFu2rAmIGqI3bdokEyZMMPtAw2TevHkDt50/f7507NjRBMhXX33V3Pa5556T/fv3y2uvvRa43YABA+TQoUNy6623mtD566+/mvucM2eObNy4UUqXLh1SQ/ny5WXUqFEh27SmzEhNTZX27dvLd999J0899ZQUL15cJk2aZGpct26deXyb1qjbGjVqJH/88ccF76vsfCwAiASR82kJAIgox48fNy1o4SR37tyu3r+2pGrgyZUrl2lh9ZLXj++0//mf/zmrFbZBgwbSrVs3+eCDD0wLte3JJ5+UOnXqyMKFCwMnHgoVKiTPP/+8PPbYY1KtWjWzbezYsdKsWbOQEzHact28eXMTvjWoB4uPjzehP6v164mnGTNmyC233GK23XbbbaaVXVvhp02bFrjte++9J+XKlTN11apVy9ePBQCRgO7lABAhfv/9d+nevbtpGdPwV7lyZenZs6ecOnUqcBttadNWt6JFi5rurk2aNJG5c+eG3M/y5ctNN9CPP/5YRo4caVrfNGC1atVKtm/fHrhd7969zRjiv/7666xa7rzzTtOKl5KSckHjo7du3Sr//Oc/pUiRIiasqO+//960XmvXXK1D71e77Aa3mum/1xY3pc/b7pqrY59t77//vglR2mKpz/+OO+6QxMTETNX33//+17TU6eNfeuml8p///Cfd26Ud03369GnT+q4tf/pvixUrZp7XokWLzPV6W21JVsFdioPHbb/44osyfvx487j6uuo+Sm9Md/Br3LZtW3PCQo+F4cOHi2VZZ72++jNY2vs8V232trQt4Bs2bJB27dqZAKrHhh4za9asCbmN3r/+26+++kr69esX6J7cqVMnOXDggHglvW7PWpP64YcfAtt0/+tFW8aDW/ofeeQRs581kNquueaas3o+6DY9/oLvM+2JlWPHjl1w/fq4pUqVks6dOwe26b7VMPzZZ5/JyZMnA9sTEhIuuEeGV48FAJGAlm4AiAB79uyRK6+8MjDOUlvaNITrl2MNxdoyum/fPrnqqqvM748++qgJgO+8847cdNNN5nZ2wLCNHj3afFnWVr0jR47ImDFj5K677pKvv/7aXK/jXzWUaWjXIG/T+589e7YJbXFxcRf0POxuuNpiaAdFDagaJO+77z4TuLds2SKvv/66+amBTgOcfvn/6aef5MMPP5Rx48aZ7q52EFB68mDIkCEmFGiLpYY77RasAUiD4rm6o2vX4TZt2pj70pCpoUhb8zR0nI/eXrsL62Pq65OcnCxr16413ZWvu+46eeihh8xrp89RWwTTM2XKFDlx4oR5XTV0a2DT1u706EkObUnVkyn6ei1YsCAw1ljD94XITG3B9PW4+uqrTeDu37+/6fquJyc0zK5YsUIaN24ccvs+ffqYkytanwZ+PbGgJ3J0DLBfJCUlmZ/28aT0eFENGzYMua2e4NATVPb1GdFArZfg+7TpMawnIPREmR5fPXr0kGeeeSZTwwj0cXWMedqAq8edvl/0vmvXri1OyM7HAoCIYAEAwt4999xjxcbGWt9+++1Z16Wmppqfjz/+uKZY68svvwxcd/ToUaty5cpWpUqVrJSUFLNt2bJl5nbVq1e3Tp48Gbjtyy+/bLZv2rQpcL/lypWzunTpEvJ4H3/8sbndypUrM13/0KFDzb+58847z7rur7/+Omvbhx9+eNZjvPDCC2bbjh07Qm67c+dOKy4uzho5cmTIdn0eOXLkOGt7Wh07drTy5Mlj7dq1K7Bt69at5j7TfoxWrFjR6tatW+D3unXrWu3btz/n/ffq1eus+1H6PHR7oUKFrP3796d73ZQpUwLb9HF1W58+fQLb9DXSx8+VK5d14MCBkNdXf57vPjOqTel2fd2C95M+zi+//BLYtmfPHqtgwYLWNddcE9im96//tnXr1oFjU/Xt29fs08OHD1t+0b17d1PTTz/9dNZxtnv37rNu36hRI6tJkybnvM8RI0aYf79kyZKQ7ffff781bNgw65NPPrHeffdd66abbjK3u+222zJVa/78+c19pDV37lxzPwsWLEj339WsWdNq3rx5ph7Di8cCgEhAfx8ACHPa6vnpp59Khw4dzmp9U3aXYJ0QSlui7G7bSrsAawuqtjRql9lg2rKsLeQ2bcVU2ups36+2TOv9BneH1ZZKHcMZ/DiZ9fDDD5+1LXgCK23xPXjwoGnJVenNlpzerNS6j7SVW/+tfdFWc21VX7ZsWYb/VluOv/jiCzNplk6uZatevbrpwn0+2oKuLcA///yzZFWXLl0CLfaZoa3FNn2N9HdtOV28eLG4RfeTjm/W/aTDAGxlypQxwwW0e7628gfT4y64u7oeX3o/u3btEj/QcclvvfWWmcE8eGIwnUAuo/H7OoTAvj49K1euNMMN9Fhs2bJlyHX6WNrqr702unbtarppa0u3DvNI20U/Pfq4GdUUXLcTsvOxACASELoBIMxpV2kNNOebpEjDjC6DlJYGSPv6YMEhU2lXYPXnn38GtmkXc/2CrcsnKQ3fGsI1jKddwzozdDx2Wjr7s05Opd1tNYBrALVvp93ez0cDrzbManDSfxt80XG1OuP0ufatPr/g0GVLb1+mpV26tcu/TjCl3W113LmOUb/YfZIR7e4bHHqVPrYKHt/uNN1POqwgo+NLT3qkHT+fmeMrLX0ttMt3Vi6ZOVZsX375pZkfQU+s6NCE9E4CBY9bDj4pFHySKNiPP/5ohnDo+/TNN9/MVB0a+JV9wkRPnqR9Xva8Cfq4GdUUXHdmZedjAUCkY0w3ACBdGY3HDp6US1ucdfIwbY3TFk0dy63BSMN4VqT3ZV1bBXWmZA2s9erVM63zGuJ07HJGY5uD6W30BIAu85Tec9L7c4uOGf/ll19Mq6W2BGvY0jHnkydPDpkN+1ycDjAZnQzJ7KR32Xl8paW9KLQHRlboLOTpTTyXli6DpfMcaDjWuQ7SLoumrfdK19rWScKC6TbtTZKWnnDQeQF0dnI9KVWwYMFM1Wzfv554Uvo+0PWwg+3YscO8B7Wu9Nb/trdldukxW3Y+FgBEOkI3AIQ5bbHVyas2b9583rWddR3p9Frg7OuzQkPxyy+/bFrbNRTpl3K7+/fF0lbPJUuWmC65OqGULb3u2hmFSZ31W4Octhjbrb4Xsm819Kb3eOnty/ToxGcaFPWiPQE0iOsEa3bozkqPgHOdYNDu/8HPUye1Uvq6BLcoawt8sPS6dWe2Nt1POht+RseXtsCnDahZoS3P9szvFyozQVBPkOjJnJIlS5pwnN4JGT3xo3RCvOCArZPO/fbbb6bbfDCdZV8Dt7YM67Fsh/bMsIdy2MML6tate9bzt9f61rq0hV6PgeAJznTiQ31tLvTYz87HAoBIR+gGgDCnX3p1LK0uiaVBIO24bg2cGp5uuOEGM0P06tWrpWnTpoG1sHW2YQ1kNWrUyNLja6u2zpStM6HrbNnaFdzp1tC0rZ/6PNKy1/ROGyZ1jOygQYNMcNd9FBwk9X61FVFncs/o8TXo6Zj53bt3B7pEa7d0Het9Phq4gu9bQ9xll10W0tU6uO5zzaKeWbr+8yuvvBJ4fvq7zn6ty3fZJ1f0een4Yj1ubJMmTTrrvjJbm96fBktt0ddu7HbA1xnzdWy0ju/XE0MXSwPrhYTWC6Hdp/U56PtJX9uMxtHXrFnTrA6g7xud4d0+Rl977TVzbNnrVtvvL33f6UoCOndAesMUlJ6w0jHSweOk9bWz1/G25w/QEyatW7dO9z70cbVlXucwsGvQuQt0LW2d7+FC15DPzscCgEhH6AaACKBLbGn35ebNm5uWNh1Hq1099UuwTmKlgWngwIFmSS1dR1mXDNMWWA3K2mX0k08+yfJaurp0kAbJp59+2rTmZbVreXo0qGnLsIZ6XfNaJ2jT56k1p6VrcCutQ9fg1qCpAUBbujW8aPDWQKhBU7v36n3MmjXL7C9dFi0jGtb1ZIJO9KVrMevyW7rcmIav843P1hMZumSW1qb7W0+KaFgJnuzMrltfEw1XGuK0/qzQiay0Vu1KrUt0aZd6XdLtX//6VyBEahdnHXOvz0FDou6fOXPmpDu2/UJq032sLaMasHU/abdsXTJMjwl9/fxOW7i1ZVmXO9P3jF5sOp+ALvFme+GFF0wXdA3puj+0l4me3NDeC/YcCUqX2Pvmm2/MuvJ6oiZ4bW49AWOf9NAJAXVte73oe0mHaOixqWuZ6/Gp77Hz0fCrPUy0R4VOiqhLkumJFB02oMdwMD3hohd7PL6eHLADvr7f9OKXxwKAiOD19OkAAGfokla6dFiJEiWs3LlzW5dccolZ8il42S9dzumWW26xChcubJbBuvLKK605c+aE3I+9pNSMGTPOu6SU7emnnzbXXXbZZVmq3V4yzF7WKthvv/1mderUydQcHx9v3XrrrWYpqrRLVtnLMekyZrp8Wtrlw3QppmbNmpnljvRSrVo1s3+2bdt23vpWrFhhNWjQwCyJpft18uTJgZrPtWTYc889Z/ax1p43b17zmLpE2alTpwK3OXPmjFnmS1+3mJiYwH3a+1uXqEoroyXD9Hnpa9ymTRsrX758VqlSpUyd9nJwNt3PutSb3qZIkSLWQw89ZG3evPms+8yoNpXe/l+/fr3Vtm1bq0CBAua+W7RoYa1atSrkNvaSYWmXt8toKbPsoo+d0SW9Za5mzZpl1atXz7zXypcvbw0ePDjkdbWPh4zuU6+z/frrr+a41qX79H2p+06PNz3OgpdVO59Dhw6ZZc6KFStm7kPrTm8ZQfvYTe+S9jX1w2MBQLiL0f94HfwBAAAAAIhELBkGAAAAAIBLGNMNAHCNztatl3PRscYZLR8FAAAQ7gjdAADXvPjii2dNrJSWvfYvAABAJGJMNwDANTobtL3WcEZ0tmuddRsAACASEboBAAAAAHAJE6kBAAAAAOASxnSLSGpqquzZs0cKFiwoMTExXpcDAAAAAPA57TR+9OhRKVu2rMTGZtyeTegWMYE7ISHB6zIAAAAAAGEmMTFRypcvn+H1hG4R08Jt76xChQp5XQ4AAAAAwOeSk5NN462dJzNC6NbZ5P5fl3IN3IRuAAAAAEBmnW+IMhOpAQAAAADgEkI3AAAAAAAuIXQDAAAAAOASQjcAAAAAAC4hdAMAAAAA4BJCNwAAAAAALiF0AwAAAADgEkI3AAAAAAAuIXQDAAAAABCJoXvYsGESExMTcqlWrVrg+hMnTkivXr2kWLFiUqBAAenSpYvs27cv5D52794t7du3l3z58knJkiXlqaeekjNnznjwbAAAAAAACJVDPFazZk1ZvHhx4PccOf5/SX379pW5c+fKjBkzJD4+Xnr37i2dO3eWr776ylyfkpJiAnfp0qVl1apVsnfvXrnnnnskZ86c8vzzz3vyfAAAAAAA8E3o1pCtoTmtI0eOyFtvvSXTpk2Tli1bmm1TpkyR6tWry5o1a6RJkyaycOFC2bp1qwntpUqVknr16smIESNkwIABphU9V65cHjwjAAAiW0qqJXGxMV6X4Zs6AADwdej++eefpWzZspInTx5p2rSpjBo1SipUqCDr1q2T06dPS+vWrQO31a7net3q1atN6NaftWvXNoHb1rZtW+nZs6ds2bJF6tev79GzAgAgcmnQHT1rgyQePOZZDQnFC8jATnzOAwD8z9PQ3bhxY5k6dapUrVrVdA1/9tln5eqrr5bNmzdLUlKSaakuXLhwyL/RgK3XKf0ZHLjt6+3rMnLy5ElzsSUnJzv8zAAAiGwauLcn8fkJAICvQ3e7du0C/1+nTh0TwitWrCgff/yx5M2b17XH1dZ0DfgAAAAAAETNkmHaqn355ZfL9u3bzTjvU6dOyeHDh0Nuo7OX22PA9Wfa2czt39MbJ24bNGiQGTNuXxITE115PgAAAACA6Oar0H3s2DH55ZdfpEyZMtKgQQMzC/mSJUsC12/bts0sEaZjv5X+3LRpk+zfvz9wm0WLFkmhQoWkRo0aGT5O7ty5zW2CLwAAAAAARFT38ieffFI6dOhgupTv2bNHhg4dKnFxcXLnnXeaJcK6d+8u/fr1k6JFi5pg3KdPHxO0dRI11aZNGxOuu3btKmPGjDHjuAcPHmzW9tZgDQBAOPHLbNx+qQMAgEjgaej+7bffTMD+448/pESJEtKsWTOzHJj+vxo3bpzExsZKly5dzMRnOjP5pEmTAv9eA/qcOXPMbOUaxvPnzy/dunWT4cOHe/isAADIGmYFBwAg8ngauqdPn37O63UZsYkTJ5pLRrSVfN68eS5UBwBA9mNWcAAAIouvxnQDAAAAABBJCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAIh4KamW+IFf6gAAANknRzY+FgAAnoiLjZHRszZI4sFjntWQULyADOxU37PHBwAA3iB0AwCiggbu7UnJXpcBAACiDN3LAQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAQETyyxJtfqkDAOANZi8HAAARiaXiAAB+QOgGAAARi6XiAABeo3s5AAAAAAAuIXQDAAAAAOASQjcAAICH/DLRml/qAIBI45sx3aNHj5ZBgwbJY489JuPHjzfbTpw4IU888YRMnz5dTp48KW3btpVJkyZJqVKlAv9u9+7d0rNnT1m2bJkUKFBAunXrJqNGjZIcOXzz1AAAADLEhG8AENl8kUy//fZb+c9//iN16tQJ2d63b1+ZO3euzJgxQ+Lj46V3797SuXNn+eqrr8z1KSkp0r59eyldurSsWrVK9u7dK/fcc4/kzJlTnn/+eY+eDQAAwIVhwjcAiFyedy8/duyY3HXXXfLGG29IkSJFAtuPHDkib731lowdO1ZatmwpDRo0kClTpphwvWbNGnObhQsXytatW+X999+XevXqSbt27WTEiBEyceJEOXXqlIfPCgAAAAAAH4TuXr16mdbq1q1bh2xft26dnD59OmR7tWrVpEKFCrJ69Wrzu/6sXbt2SHdz7YKenJwsW7ZsycZnAQAAAACAz7qX61jt9evXm+7laSUlJUmuXLmkcOHCIds1YOt19m2CA7d9vX1dRnR8uF5sGtIBAAAAAIiYlu7ExEQzadoHH3wgefLkydbH1onWdIy4fUlISMjWxweASOGX2Y79UgcAAIBvWrq1+/j+/fvliiuuCGzTidFWrlwpEyZMkC+++MKMyz58+HBIa/e+ffvMxGlKf37zzTch96vX29dlRGdJ79evX0hLN8EbAC4csy4DAAD4NHS3atVKNm3aFLLtvvvuM+O2BwwYYEKwzkK+ZMkS6dKli7l+27ZtZomwpk2bmt/158iRI014L1mypNm2aNEiKVSokNSoUSPDx86dO7e5AAAuHrMuAwAA+DB0FyxYUGrVqhWyLX/+/FKsWLHA9u7du5sW6aJFi5og3adPHxO0mzRpYq5v06aNCdddu3aVMWPGmHHcgwcPNpOzEaoBAAAAAF7zxTrdGRk3bpzExsaalm6d+ExnJp80aVLg+ri4OJkzZ4707NnThHEN7d26dZPhw4d7WjcAAAAAAL4L3cuXLw/5XSdY0zW39ZKRihUryrx587KhOgAAAAAAwmydbgAAAAAAIhWhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugHAh1JSLfEDv9QBAAAQrnJ4XQAA4GxxsTEyetYGSTx4zLMaEooXkIGd6nv2+AAAAJGA0A0APqWBe3tSstdlAAAA4CLQvRwAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAABwXimplviBX+oAgMzKkelbAgAAIGrFxcbI6FkbJPHgMc9qSCheQAZ2qu/Z4wNAVhC6AQAAkCkauLcnJXtdBgCEFbqXAwAAAADgEkI3AAAAAAAuIXQDAAAAAOASQjcAAAAAAC4hdAMAAAAA4BJCNwAAAAAALiF0AwAAAADgEkI3AAAAAACRGLpfe+01qVOnjhQqVMhcmjZtKvPnzw9cf+LECenVq5cUK1ZMChQoIF26dJF9+/aF3Mfu3bulffv2ki9fPilZsqQ89dRTcubMGQ+eDQAAAAAAPgrd5cuXl9GjR8u6detk7dq10rJlS7n55ptly5Yt5vq+ffvK7NmzZcaMGbJixQrZs2ePdO7cOfDvU1JSTOA+deqUrFq1St555x2ZOnWqPPPMMx4+KwAAAAAA/k8O8VCHDh1Cfh85cqRp/V6zZo0J5G+99ZZMmzbNhHE1ZcoUqV69urm+SZMmsnDhQtm6dassXrxYSpUqJfXq1ZMRI0bIgAEDZNiwYZIrVy6PnhkAAAAAAD4a062t1tOnT5fjx4+bbuba+n369Glp3bp14DbVqlWTChUqyOrVq83v+rN27domcNvatm0rycnJgdZyAAAAAACisqVbbdq0yYRsHb+t47ZnzZolNWrUkI0bN5qW6sKFC4fcXgN2UlKS+X/9GRy47evt6zJy8uRJc7FpSAcAAAAAwBct3Zdccon88ccfZ20/fPiwue5CVK1a1QTsr7/+Wnr27CndunUzXcbdNGrUKImPjw9cEhISXH08AAAAAEB0ylLo3rlzp+kOnpa2Hv/+++8XdF/amn3ZZZdJgwYNTBiuW7euvPzyy1K6dGkzQZoG+WA6e7lep/Rn2tnM7d/t26Rn0KBBcuTIkcAlMTHxgmoGAAAAAMDx7uWff/554P+/+OIL00ps0xC+ZMkSqVSpklyM1NRUE941hOfMmdPcpy4VprZt22aWCNPu6Ep/6uRr+/fvN8uFqUWLFpnlx7SLekZy585tLgAAAAAA+CZ0d+zY0fyMiYkx3cCDaUDWwP3SSy9l+v60xbldu3ZmcrSjR4+amcqXL18eCPTdu3eXfv36SdGiRU2Q7tOnjwnaOnO5atOmjQnXXbt2lTFjxphx3IMHDzZrexOqAQAAAABhFbq1FVpVrlxZvv32WylevPhFPbi2UN9zzz2yd+9eE7Lr1KljAvd1111nrh83bpzExsaalm5t/daZySdNmhT493FxcTJnzhwzFlzDeP78+c3JgOHDh19UXQAAAAAAeDZ7+Y4dOxx5cF2H+1zy5MkjEydONJeMVKxYUebNm+dIPQAAAAAA+GLJMB1rrRdtrbZbwG1vv/22E7UBAAAAABB9ofvZZ581XbgbNmwoZcqUMWO8AQAAAACAA6F78uTJMnXqVDOBGQAAAAAAcHCdbl0/+6qrrsrKPwUAAABck5JqiR/4pQ4AYdrS/cADD5jlvYYMGeJ8RQAAAEAWxcXGyOhZGyTx4DHPakgoXkAGdqrv2eMDiIDQfeLECXn99ddl8eLFZpkvXaM72NixY52qDwAAALggGri3JyV7XQYAZD10f//991KvXj3z/5s3bw65jknVAAAAAAC4iNC9bNmyrPwzAAAAAACiSpYmUgMAAAAAAC61dLdo0eKc3ciXLl2albsFAAAAACCiZCl02+O5badPn5aNGzea8d3dunVzqjYAAAAAAKIvdI8bNy7d7cOGDZNjx7xbngEAAAAAgIgd03333XfL22+/7eRdAoCjUlIt8QO/1AEAAAAftnRnZPXq1ZInTx4n7xIAHBUXGyOjZ20wa7h6JaF4ARnYqb5njw8AAACfh+7OnTuH/G5Zluzdu1fWrl0rQ4YMcao2AHCFBu7tSclelwEAAIAokKXQHR8fH/J7bGysVK1aVYYPHy5t2rRxqjYAAAAAAKIvdE+ZMsX5SgAAAAAAiDAXNaZ73bp18sMPP5j/r1mzptSvzxhFAAAAAAAuKnTv379f7rjjDlm+fLkULlzYbDt8+LC0aNFCpk+fLiVKlMjK3QIAAAAAEFGytGRYnz595OjRo7JlyxY5dOiQuWzevFmSk5Pl0Ucfdb5KAAAAAACipaV7wYIFsnjxYqlevXpgW40aNWTixIlMpAYAAAAAwMW0dKempkrOnDnP2q7b9DoAAAAAAJDF0N2yZUt57LHHZM+ePYFtv//+u/Tt21datWrlZH0AAAAAAERX6J4wYYIZv12pUiW59NJLzaVy5cpm26uvvup8lQAAAAAARMuY7oSEBFm/fr0Z1/3jjz+abTq+u3Xr1k7XBwAAAABAdLR0L1261EyYpi3aMTExct1115mZzPXSqFEjs1b3l19+6V61AAAAAABEaugeP3689OjRQwoVKnTWdfHx8fLQQw/J2LFjnawPAAAAAIDoCN3fffedXH/99Rler8uFrVu3zom6AAAAAACIrtC9b9++dJcKs+XIkUMOHDjgRF0AAAAAAERX6C5Xrpxs3rw5w+u///57KVOmjBN1AQAAAAAQXaH7hhtukCFDhsiJEyfOuu7vv/+WoUOHyo033uhkfQAAAAAARMeSYYMHD5aZM2fK5ZdfLr1795aqVaua7bps2MSJEyUlJUWefvppt2oFAAAAACByQ3epUqVk1apV0rNnTxk0aJBYlmW26/Jhbdu2NcFbbwMAAAAAAC4wdKuKFSvKvHnz5M8//5Tt27eb4F2lShUpUqSIOxUCAAAAABAtodumIbtRo0bOVgMAAAAAQLROpAYAAAAAADKP0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAQCSG7lGjRkmjRo2kYMGCUrJkSenYsaNs27Yt5DYnTpyQXr16SbFixaRAgQLSpUsX2bdvX8htdu/eLe3bt5d8+fKZ+3nqqafkzJkz2fxsAAAAAADwUehesWKFCdRr1qyRRYsWyenTp6VNmzZy/PjxwG369u0rs2fPlhkzZpjb79mzRzp37hy4PiUlxQTuU6dOyapVq+Sdd96RqVOnyjPPPOPRswIAAAAA4P/kEA8tWLAg5HcNy9pSvW7dOrnmmmvkyJEj8tZbb8m0adOkZcuW5jZTpkyR6tWrm6DepEkTWbhwoWzdulUWL14spUqVknr16smIESNkwIABMmzYMMmVK5dHzw4AAAAAEO18NaZbQ7YqWrSo+anhW1u/W7duHbhNtWrVpEKFCrJ69Wrzu/6sXbu2Cdy2tm3bSnJysmzZsiXbnwMAAAAAAL5o6Q6Wmpoqjz/+uPzjH/+QWrVqmW1JSUmmpbpw4cIht9WArdfZtwkO3Pb19nXpOXnypLnYNKADAAAAABCxLd06tnvz5s0yffr0bJnALT4+PnBJSEhw/TEBAAAAANHHF6G7d+/eMmfOHFm2bJmUL18+sL106dJmgrTDhw+H3F5nL9fr7Nuknc3c/t2+TVqDBg0yXdntS2JiogvPCgAAAAAQ7TwN3ZZlmcA9a9YsWbp0qVSuXDnk+gYNGkjOnDllyZIlgW26pJguEda0aVPzu/7ctGmT7N+/P3AbnQm9UKFCUqNGjXQfN3fu3Ob64AsAAAAAABE1plu7lOvM5J999plZq9seg61dvvPmzWt+du/eXfr162cmV9Nw3KdPHxO0deZypUuMabju2rWrjBkzxtzH4MGDzX1ruAYAAAAAICpD92uvvWZ+XnvttSHbdVmwe++91/z/uHHjJDY2Vrp06WImP9OZySdNmhS4bVxcnOma3rNnTxPG8+fPL926dZPhw4dn87MBAAAAAMBHoVu7l59Pnjx5ZOLEieaSkYoVK8q8efMcrg4AAAAAgAiYSA0AAAAAgEhE6AYAAACyWUrq+Xt8RlMdQCTztHs5AAAAEI3iYmNk9KwNknjwmGc1JBQvIAM71ffs8YFoQegGAAAAPKCBe3tSstdlAHAZ3csBAAAAAHAJoRtARI0J80sdAAAAgKJ7OQBHMDYNAAAAOBuhG4BjGJsGAAAAhKJ7OQAAAAAALiF0AwAAAADgEkI3AAAAAAAuIXQDAAAAAOASQjcAAAAAAC4hdAMAAAAA4BJCNwAAAAAALiF0AwAAAADgEkI3AAAAAAAuIXQDAAAAAOASQjcAAAAAAC4hdAMAAAAA4BJCNwAAAAAALiF0AwAAAADgEkI3AAAAAAAuIXQDPpeSaokf+KUOAAAAIJzk8LoAAOcWFxsjo2dtkMSDxzyrIaF4ARnYqb5njw8AAACEK0I3EAY0cG9PSva6DAAAAAAXiO7lAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAgXSmpltcl+KIG4GLkuKh/DQAAACBixcXGyOhZGyTx4DFPHj+heAEZ2Km+J48NOIXQDQAAACBDGri3JyV7XQYQtuheDgAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAARGLoXrlypXTo0EHKli0rMTEx8umnn4Zcb1mWPPPMM1KmTBnJmzevtG7dWn7++eeQ2xw6dEjuuusuKVSokBQuXFi6d+8ux455s44gAAAAAAC+Cd3Hjx+XunXrysSJE9O9fsyYMfLKK6/I5MmT5euvv5b8+fNL27Zt5cSJE4HbaODesmWLLFq0SObMmWOC/IMPPpiNzwIAAAAAgPTlEA+1a9fOXNKjrdzjx4+XwYMHy80332y2vfvuu1KqVCnTIn7HHXfIDz/8IAsWLJBvv/1WGjZsaG7z6quvyg033CAvvviiaUEHAAAAAMArvh3TvWPHDklKSjJdym3x8fHSuHFjWb16tfldf2qXcjtwK719bGysaRnPyMmTJyU5OTnkAgAAAABA1IRuDdxKW7aD6e/2dfqzZMmSIdfnyJFDihYtGrhNekaNGmUCvH1JSEhw5TkAAAAAAKKbb0O3mwYNGiRHjhwJXBITE70uCQAAAAAQgXwbukuXLm1+7tu3L2S7/m5fpz/3798fcv2ZM2fMjOb2bdKTO3duM9t58AUAAAAAgKgJ3ZUrVzbBecmSJYFtOvZax2o3bdrU/K4/Dx8+LOvWrQvcZunSpZKammrGfgMAAAAAELWzl+t62tu3bw+ZPG3jxo1mTHaFChXk8ccfl+eee06qVKliQviQIUPMjOQdO3Y0t69evbpcf/310qNHD7Os2OnTp6V3795mZnNmLgcAAACiQ0qqJXGxMVFfA/zJ09C9du1aadGiReD3fv36mZ/dunWTqVOnSv/+/c1a3rrutrZoN2vWzCwRlidPnsC/+eCDD0zQbtWqlZm1vEuXLmZtbwAAAADRQcPu6FkbJPHgMU8eP6F4ARnYqb4njw3/8zR0X3vttWY97ozExMTI8OHDzSUj2io+bdo0lyoEAAAAEA40cG9PYilg+I9vx3QDAAAAABDuCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN2IWimpGa8RH411AAAAAHBeDhfuEwgLcbExMnrWBkk8eMyzGhKKF5CBnep79vgAAAAA3EXoRlTTwL09KdnrMgAAAABEKLqXAwAAAADgEkI3InaMsl/qAAAAABC96F4OxzFWGgAAAAD+D6EbrmCsNAAAAADQvRwAAAAAANcQugEAAAAAcAmhGwAAAADgqwmJU3xQg1MY0w0AAAAA8M3EyAkRNikyoTuM6NkefQN4zS91AAAAAHAHEyM7h9AdRrw+4xSJZ50AAAAAwE2E7jDDGScAAAAACB9MpAYAAAAAgEsI3QAAAAAQJTNy+6GGaEP3cgAAAACIgjmamJ/JG4RuAAAAAMgmzNEUfeheDgAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuCRiQvfEiROlUqVKkidPHmncuLF88803XpcEAAAAAIhyERG6P/roI+nXr58MHTpU1q9fL3Xr1pW2bdvK/v37vS4NAAAAABDFIiJ0jx07Vnr06CH33Xef1KhRQyZPniz58uWTt99+2+vSAAAAAABRLIeEuVOnTsm6detk0KBBgW2xsbHSunVrWb16dbr/5uTJk+ZiO3LkiPmZnJwsflcin8ip+DhPHz8z+4k6M4c6nUWdzqJOZ1Gns6jTWdTpLOrM/hrt21Jn9NXpJbtGy7LOebsY63y38Lk9e/ZIuXLlZNWqVdK0adPA9v79+8uKFSvk66+/PuvfDBs2TJ599tlsrhQAAAAAEGkSExOlfPnykdvSnRXaKq5jwG2pqaly6NAhKVasmMTExEik0jMxCQkJ5qAoVKiQ+BV1Oos6nUWdzqJOZ1Gns6jTWdTpLOp0FnVGZ50XS9uvjx49KmXLlj3n7cI+dBcvXlzi4uJk3759Idv199KlS6f7b3Lnzm0uwQoXLizRQg/8cDj4qdNZ1Oks6nQWdTqLOp1Fnc6iTmdRp7OoMzrrvBjx8fGRP5Farly5pEGDBrJkyZKQlmv9Pbi7OQAAAAAA2S3sW7qVdhXv1q2bNGzYUK688koZP368HD9+3MxmDgAAAACAVyIidN9+++1y4MABeeaZZyQpKUnq1asnCxYskFKlSnldmq9ol3pdyzxt13q/oU5nUaezqNNZ1Oks6nQWdTqLOp1Fnc6izuisM7uE/ezlAAAAAAD4VdiP6QYAAAAAwK8I3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNCNsKJrsMM57E9nsT+dxf50FvvTWexPZ7E/ncX+dBb70zmpUbovCd0IC7t27ZLff/9dYmM5ZJ3w888/y6+//sr+dAjHp7PYn85ifzqL/eksPo+cxfHpLI5P5/wc5fsyOp81jO3bt8u4ceOkf//+Mn/+fNm3b5/40caNG6VBgwby5Zdfip/99NNPZq34e++9V959913ZtGmT+NF3330ntWrVki+++EL8jOPTWexPZ7E/ncX+dBafR87i+HQWx6dz2JdhRNfpRvTZtGmTVaRIEatZs2ZW48aNrdy5c1t33nmnNW/ePMtPNm7caOXNm9d64oknzrouNTXV8ostW7ZYhQsXtq6//npzKVWqlNWyZUtrypQplp9s2LDB7M8nn3zS8jOOT2exP53F/nQW+9NZfB45i+PTWRyfzmFfhhdCdxT666+/rBtvvNHq06ePdebMGbNt/vz5Vps2baxrr73WmjlzpuUHP/74o/lwGzZsmPlda/3vf/9r6vv+++8DtXvt1KlTVteuXa0HHngg8MH2zTffmN9r1Khhvfbaa5Yf/PTTT1aOHDms4cOHm99Pnz5tLViwwHr99det5cuXW/v27bP8gOPTWexPZ7E/ncX+dBafR87i+HQWx6dz2Jfhh9AdhfSPb/369a3nnnsuZPvq1autm266yZwtW7NmjeWlEydOWP/85z+tokWLWt9++63Z1qFDB6tmzZpW8eLFrbi4OOupp56yfv31V8tr+sfu6quvtnr16nXWh+AjjzxiNWjQwPrss88sr/849+/f38qTJ481d+5cs61du3Zmf5YtW9Zs1z/eegx4jePTWexPZ7E/ncX+dBafR9F3fP79998cn1F4fLIvww9juqNwxsCTJ09KmTJl5ODBg2ZbSkqK+dmkSRN58sknZffu3fLpp5+abXpixgu5c+eWBx98UFq1amVqqlKliql9ypQpZvyK/nzjjTfkvffe87RO+3Hr1KkjBw4ckD///DNwXdWqVeXhhx+WYsWKycyZMz2tM2fOnNK1a1d56KGHpG/fvlKxYkWz7cMPP5TExERT34YNG2Tq1Kme1snx6Syt6cSJE2GxP3v06OH7/an7juPTOWfOnGF/OkwfOxw+j+6++27ffx6pv/76y/fHZ548eaR79+6+Pz61pnA5Pu+66y5fH5989wxTXqd+eGPChAlWrly5rC+++ML8npKSErhu0qRJVsGCBa39+/dbXtOuJ/ZYlV9++SXkutGjR5uxLH/88YcntQWPkfr444/NeBXtLpN27NSMGTNM1xqvzjIHv7Zbt261HnzwQXOmUf8/2Ntvv23lzJnT2r17t+W1iRMnhsXxuWLFCt8en8F4v198S2cw7bbnx/2Zts4vv/zSl/vz0KFDYbE/09bp1/2p3TO3b9/u+88jrVO7mvr98+jnn3+2XnjhBd8fn1rnv//9b98fn2mPwWnTpvny+Exbi3bL9+PxGezDDz/05b5Mb16EB32+L7MDoTsKJCYmmvET+kEc/Obr1q2b+bDQcT/BFi5caNWuXTvb/zhnVKd2O5k9e7YZBxL8gacfhHXq1DHdV7LT0aNHz6pFDRkyxIypev/990O+/OrkJtqVJrv/8GVUp37pWbp0aWC/2dd98sknZhzQ4cOHs7XOpKQka+3atea405pt3bt399XxmVGd2p3PT8fnrl27zJcaPXGh47v8uj8zqtNv+1Mnqmnfvr21ePHikO06bs5P+zOjOvWY9dP+XL9+vRUbG2t+Bv9d8tv+zKhOv+3P7777zrr88svN+0j/RtkGDx7sq8+jjOrctm2brz6PtE7tpl2xYsWQIO234zO4zgMHDvj2+NTvGzp5Vs+ePa1Ro0b59vjMqE7tqu2X41NDqR5v+rn5ww8/BLYPHDjQV/syozq3bNnim33pFUJ3hNMzdTqbYaNGjcy4noYNG1q9e/cOjFW67bbbrHz58lnvvPOOtWPHDrNNZ76sW7eu9eeff3paZ/A4lbQtOOrRRx+1OnfubCY6ya6ZOfUMXdu2bc0fE/sPh/3hpnTclH5RGzFihAkSR44cMduqVKkS8sHoRZ3BXxzT21/6uuvkMMGBMjte9+rVq5vjLSYmxrrhhhvMNqVfeO666y7fHJ9p69QvPbbgfevl8al1JiQkWC1atLDi4+PNTw0NSo8/Hffnl/2ZUZ0qvUl/vNif+jh6clJr1MmUggOtfXxqK4PX+/Ncdfrp+NQvgRpc+vXrd9Z1Bw8eNDNC++H4TK/O4H2UXnDxYn9qUChWrJj12GOPpft3+/HHH/fF59H56jx58qQvPo/s2b/1vaStwq+88krI8emXz6O0db766qu+fL/r33kdU37rrbeaGbV1bLz2urLpvvPD8ZlendqDwRb8Hc+r41O/b+h3ZP1epydbmjRpYl5/v73X06vz3nvvPed3+Sc8eK97hdAdwfSskX4Y6JtR//+3334zb0g986VfzIIPeH1zVKhQwYRd/XAM/gLsVZ21atUy4Sa9s2h6llS/YG7evDnb6tQP2WrVqpluMFdddZU5O5deoB03bpw5a6f7VJ9X6dKls3V/ZrZOm3ZL/Ne//mU+vLULUHZ+EStTpox5LfVMrJ5RLl++vPliZtMvCHoW18vjM6M69XhNj1fHp9alx9rTTz9t9pvWoftNu5/Z9MuW1+/3jOqcPn26r/anTSek0WWCOnXqZLVu3TrQxdT+AjFgwABP92dGdWpLg5/2p/5t0aCgPYKCuxvrl7Tgkyza2uTl/syoTv1inl7Y9vL41Peynqiw39v6XtegqKHQNmbMGE8/jzJbp9efR/aSRvp5Y7+f9PNTv4/46fMoozp///33dG/v1fGpQU9b1nUSLaXf7bRr8dixY0Nup93jvTw+M1unfaLCi+NTe4boSf9BgwaZv0F6wnfo0KGmAUB7N/nlvZ7ZOr1+r3uJ0B3BtPumdudatWpVYJueSdLu27pdz+rZvvrqKzP+44MPPjChzS91Vq1aNaRO/QOiy3RUrlzZfPhkFz3TqWO8dLZSreG6664zM0MGB9rgL44a1JYtW2a+oAd/aPuhzuCz3NrdR2+j+zk796d+eXnooYdMl2dt5bD33eTJk81JobRn43WIgRfH5/nq1NAVXKe2QHhxfB4/ftx0f9QxU3oM2DXdcsst1siRI61nn302JNRqF0kv9uf56tQlRYLrXLdunSf7M5j2FtHxkF9//bU5e69n5LUu/bJoj0Hz6vg8X526z7RO/Rvr5fGpf8+bN29uvlzZtNVNW5T0C5nWFNyq6NXn0fnq1B4ZL7/8cuA63YdeHp/6vrHr0RYlncn40ksvNRftNWafZNWeT158HmWmTj1RZNepJza8+DzSk6kaSu0gq/Qzs1ChQqY7bNoT1l693y+0Tq/e70r/RurJ/+C5Bu677z7zftIeV/q5avPy+Dxfndrd3KYnCL04PleuXGnVq1fP2rNnT8g+095iun68niQIHq7h1b48V516IuDGoMY+PQHkxb70GqE7gukkMPrH9sUXXwzZrkFBzzDrOCQdX+X3OvUspIYcm477TjtJiNs0HOgfZz0RoDTABgdau3uc12thZrbO4KCofyjtL+XZ+eVWP9imTJkSsv3TTz81rcrJycmmxvRa5v1YZ7B58+Zl+/GpS8Z8/vnn5kuWTQOsBgX94qAtIfp+D+5F4IXM1hncrdeL93swHR+pdSnttq0tyeXKlTM1Z9S65Lc6g8fQerE/9eSVjje87LLLrI4dO5qTAvoFTIOLngDSbrsaEqdOnZqtdWWlziuvvNJ67733fHF8ao3690nH6+pJFu0CrRddwkpbnNLrKebHOnV5K5uefM/uzyMNzum1umtd11xzTaBLrNefR5mtM5hXx6f2aNLeALpuuJ5g1b/zOqmX9gzSXmIatuy/V17KTJ3NmjXz9PhctGiRGbuv4/VtGlT1b9H48eNNo5WedPVaZur86KOPPN2XXiN0RzD9A6xjPnQmS3ucbHCLk7aG3nHHHZbXwqXOtIFav6DZgXbmzJmBcT9er4uY2TpnzZpleSn4bKhds34J02EFwUE27UyX2S1c6gweF6ln5HXsoX0s6pdF/RKh3SG1q2w41BkcFL2kLQfaGmfTrttas7bY6WzBfuH3OvWEi4ZXPcHatGlTa+/evYHrdBKqf/zjHybUei0c6rTDnwYwfZ317/szzzwTchvtMaJdTb2cufhC6gxuafSixmD233U92aqt8fba116G7nCp06ZjirXLtp7409ddg6ye+Ldpy7x2f9ZW2XCo0+5J4AUNppUqVTLfk/X9og0l2uNBh2gpDbX6uem1cKnTS4TuCKfjJHRSA50wLe2H2ksvvWRdccUVJth6LVzqTBu+tCb9Q60hQVuXH374Yats2bIhQc3PdfqhpS74C4Ke+dSzzseOHTO/63gfbRnxw6yW4VKnzT4G7bp1SRE/zhDq9zq1Lm1F0q7kXbt2Ne8bnWRHW++0dVaXjfODcKhTTwDOmTPHmj9/fuBvk/1TJ87U+v0QGMKlTv2Sq93htTeDvubB9PXWVrqdO3daXguXOtNrELjkkkus+++/3/Izv9apPcH0pI++xnqSOu0s69qjRHvmec3PddonVnRyNO31qa+zzikTHF5vv/1201PMS+FSp9dyeL1OONyTmpoqtWrVks8++0xatWplfn/kkUekRYsW5voff/xRypcvLzlyeHsYhEudweLi4uTMmTOSL18++fzzz6Vjx45y9913S86cOWXlypVSpkwZCYc6y5Yt63WJEhsbG/j/U6dOydGjR81rPXToUBkzZoysXr1a4uPjxWvhUqetdOnSIXVv2rTJvM9y584tfuLnOvXEtL5/9GfTpk1NjXPnzpV69epJxYoV5d1335VKlSp5XWbY1Jk3b1657rrrTH36t0nZPw8ePGjqDX6feSUc6tTXukKFCvL666/LHXfcYV7vUaNGyaBBg+TkyZOyZMkSKVasmBQqVIg6syAlJcX8Derfv7+MHTtW1q1bJw0aNBC/8XOdBQsWNBf9Tqc1/vDDD3L11Veb6/T7XoECBaRcuXJel+nrOmNiYkxdjRo1kkWLFpn3zPHjx6VatWrmev27n5ycLM2aNfOkvnCr03Nep35cPD3jnrZLsX0W3t6uZ+t0ggNtMdZZDW+++WYz+Ubw2ErqPH+dadm305ZjnSgiO2cHjcQ6dYIabZXTGYx13cngsUFui8Q67V4O2hJfokQJ377ufq9Tx/lq1+20r7Pd0yE7RFKdaVuV9XXXORJ0fGV2iYQ67Z86tEAnK9MJi7Q+bYnXv/PZOUFRJNSZHh02lCtXrpAJ9LJDJNWpQ5q0l532ttPejNoirxOA+e1197rOc9WY3jJv2lNRu27rcmc6eW92CZc6/ShG/+N18EfWbd26VZ5//nlJSkqSKlWqyI033ijt27cPnAHVs/P2z927d5uzoEuXLpWEhAS56aabAmehqDPzdaY1YcIEefTRR03N9evXp86LqHPVqlXmTGiRIkXM2dIrrriCOi+iTu3dMHPmTPNe0jP2fn3d/V7n6dOnzVn7woULm9/1Y1PP7GeXSKvTNmvWLJkxY4YsX77ctID67XUPhzq1dUlb3f/44w/57bffZP78+aZluXHjxnLppZdS5wXWmZ5///vf5rY1a9akzgus0/4bpK3Hr7zyiuzcudP0vnnsscekevXq1HkBNQbbsWOHvPnmmzJlyhTf/U3yQ51+RegOY9u2bTMfWO3atTNdB/VDTLsNaxgYN25coBtsrly5sv3LV6TXGezAgQOm20x2fXGI5Dr1g+62226TqVOnSo0aNajzIuvctWuXCbN64srPr7tf69QucsHd3O3gkJ0iqc703kfvv/++3H777eYLHHVeXJ1eiOQ6zxVwqTPzddp/j/7++28zdENPDuptqfPCa7TpyVX9dyVKlDCNU9khXOr0Na+b2pE12oVDu7ppF5jgySCee+450z27R48eZy1x5MWsxZFap862vH//fup0sE57luD0lj2hzqzXmZ0TPkVyneHyPgqXOu3XPTuXWYzkOsPldafO6Ksz7fe69LogR2udWXnNw+U7stcrpfiR9zOWIEu0NXjPnj2mi4dNJ4LQ7sM6UdaGDRtk9OjRZrt26ejdu7fpMqNn8qjz4uvs1auXvPzyy9TpYJ2vvvqqOWOfnS0ikV6nvu7Z2XMkkusMl/dRuNRpv+7Z2SIfyXWGy+tOndFXZ9rvddn1tz4c6szKax4u35G9qNPvCN1hyB4RoONI9cu/dt0IfhPcf//9ZtzE7NmzTVcPHW+h2/SSnV8cqJM6L6TO7t27my5y2fWBHA116utOndH1PgqXOsPldQ+XOsPldadO6qTO8KkxnOoMC143tSPrdD1rnQ1QZ1g8evRoSHcYXatV18ScPXu2x1VSp9Oo01nU6SzqdBZ1Oos6nUWdzqLO6KszHGoMpzr9jNAd5pYuXWqWLOrVq5d14MCBwHYdh6ZLbq1atcryA+p0FnU6izqdRZ3Ook5nUaezqNNZ1Bl9dYZDjeFUp18RuiPA559/bt4EnTt3tqZPn27Waxw4cKBZBzMxMdHyC+p0FnU6izqdRZ3Ook5nUaezqNNZ1Bl9dYZDjeFUpx8RuiPEunXrrObNm1sVK1a0Lr30Uuvyyy+31q9fb/kNdTqLOp1Fnc6iTmdRp7Oo01nU6SzqjL46w6HGcKrTb1inO4LoGsyHDh2So0ePSpkyZaR48eLiR9TpLOp0FnU6izqdRZ3Ook5nUaezqDP66gyHGsOpTj8hdAMAAAAA4BLmcgcAAAAAwCWEbgAAAAAAXELoBgAAAADAJYRuAAAAAABcQugGAAAAAMAlhG4AAAAAAFxC6AYAAAAAwCWEbgAAAAAAXELoBgAA6dq5c6fExMTIxo0bvS4FAICwRegGACBK3XvvvSZU25dixYrJ9ddfL99//725PiEhQfbu3Su1atXyulQAAMIWoRsAgCimIVuDtV6WLFkiOXLkkBtvvNFcFxcXJ6VLlzbbAABA1hC6AQCIYrlz5zbBWi/16tWTgQMHSmJiohw4cOCs7uXLly83v2s4b9iwoeTLl0+uuuoq2bZtm9dPAwAA3yJ0AwAA49ixY/L+++/LZZddZrqaZ+Tpp5+Wl156SdauXWtawe+///5srRMAgHBCfzEAAKLYnDlzpECBAub/jx8/LmXKlDHbYmMzPi8/cuRIad68ufl/bRlv3769nDhxQvLkyZNtdQMAEC5o6QYAIIq1aNHCdB/XyzfffCNt27aVdu3aya5duzL8N3Xq1An8v4Z0tX///mypFwCAcEPoBgAgiuXPn990J9dLo0aN5M033zQt3m+88UaG/yZnzpyB/9cx3io1NTVb6gUAINwQugEAQEiI1q7lf//9t9elAAAQERjTDQBAFDt58qQkJSWZ///zzz9lwoQJZkK1Dh06eF0aAAARgdANAEAUW7BgQWBcdsGCBaVatWoyY8YMufbaa82SYQAA4OLEWJZlXeR9AAAAAACAdDCmGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAEHf8L6qfN8v/SQz9AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " import matplotlib.pyplot as plt\n", + "\n", + " # Get the latest daily metric for conv_rate\n", + " latest = metrics[0] if metrics else None\n", + " if latest and latest.get(\"histogram\"):\n", + " hist = latest[\"histogram\"]\n", + " bins = hist[\"bins\"]\n", + " counts = hist[\"counts\"]\n", + "\n", + " fig, ax = plt.subplots(figsize=(10, 4))\n", + " ax.bar(\n", + " [f\"{bins[i]:.2f}\" for i in range(len(counts))],\n", + " counts,\n", + " color=\"steelblue\",\n", + " edgecolor=\"white\",\n", + " )\n", + " ax.set_title(f\"conv_rate distribution — {latest['metric_date']}\")\n", + " ax.set_xlabel(\"Bin\")\n", + " ax.set_ylabel(\"Count\")\n", + " plt.xticks(rotation=45)\n", + " plt.tight_layout()\n", + " plt.show() # pragma: allowlist secret\n", + " else:\n", + " print(\"No histogram data available.\")\n", + "except ImportError:\n", + " print(\"Install matplotlib to visualize: pip install matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 8: Time-Series Trend\n", + "\n", + "Plot how a metric (e.g., `mean`) evolves over time." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 data points from 2025-01-01 to 2025-02-28\n", + " 2025-01-01: mean=0.4989, null_rate=0.0000\n", + " 2025-02-28: mean=0.5201, null_rate=0.0000\n", + " ...\n" + ] + } + ], + "source": [ + "timeseries = monitoring.get_timeseries(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_name=\"conv_rate\",\n", + " data_source_type=\"batch\",\n", + " granularity=\"daily\",\n", + " start_date=date(2025, 1, 1),\n", + " end_date=date(2025, 3, 1),\n", + ")\n", + "\n", + "if timeseries:\n", + " dates = [t[\"metric_date\"] for t in timeseries]\n", + " means = [t[\"mean\"] for t in timeseries]\n", + " null_rates = [t[\"null_rate\"] for t in timeseries]\n", + "\n", + " print(f\"{len(timeseries)} data points from {dates[0]} to {dates[-1]}\")\n", + " for t in timeseries[:5]:\n", + " print(f\" {t['metric_date']}: mean={t['mean']:.4f}, null_rate={t['null_rate']:.4f}\")\n", + " print(\" ...\")\n", + "else:\n", + " print(\"No time-series data.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAJOCAYAAABm7rQwAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAepVJREFUeJzt3Ql4lNX59/F7Jvs2gewsYacKiqyCoK0bLRSrolgBrSAiWlusihuogOBC3RFFsfXvVhdwodatvLVoqxZURK2IQmVfsxGSyb7NvNd9JjMkmQEDSSZ5Jt/Pdc01mWfOTM4kD8jz8z73sbndbrcAAAAAAAAAQWQP5jcDAAAAAAAAFKEUAAAAAAAAgo5QCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAJrNnXfeKTabrd6xHj16yOWXXy7txb/+9S/zM9B7AABweIRSAAAATbRmzRoTxhQUFIiVgiPvLTY2Vrp16ybnnnuuPPvss1JRUSFt0RlnnFFv3oe76ecDAABtX3hrTwAAACAUQqkFCxaYaqAOHTqIVTz55JMSHx9vQqi9e/fK//t//0+uuOIKWbx4sbzzzjuSmZl51O95xx13yOzZs1tkvrfffrtceeWVvsfr1q2TJUuWyG233Sb9+vXzHT/ppJNa5PsDAIDmRSgFAADQQElJicTFxUmou+iiiyQlJcX3eN68efLSSy/JlClT5Ne//rV8+umnR/2e4eHh5tYSfv7zn9d7HB0dbUIpPa5VVO399wkAgNWwfA8AADSZVtlMnz5dOnfuLFFRUdKzZ0+55pprpLKy0jdm27ZtJuhISkoyy8VOOeUUeffddwP24nn11Vflnnvuka5du5rg4eyzz5YtW7b4xs2cOdNU+JSWlvrNZfLkyZKRkSE1NTVHtZTtu+++k0suuUQ6duwop512mnnum2++MdVPvXr1MvPQ99VKogMHDtR7/c0332y+1s/tXUK2Y8cO35gXX3xRhg4dKjExMebzT5o0SXbv3i1t0aWXXmqqkT777DN5//33fcc//vhj8/vTZX76O9YqqhtuuEHKysp+tKdUXXoe6POPPPJIwIozfe6VV1455vkf6ffZ2N+FBlwnnniieY8zzzzTnK9dunSR+++/3+/77dmzR8aPH29Cr7S0NPMzaavLHwEAaGuolAIAAE2yb98+GT58uOmndNVVV8nxxx9vQqrXX3/dhEaRkZGSnZ0to0aNMo//8Ic/SHJysjz//PNy3nnnmXEXXHBBvff84x//KHa7XW666SYpLCw0YYCGJRqUqIkTJ8rSpUtNqKVBiZe+/9tvv22CpLCwsKP6HPo+ffv2lXvvvVfcbrc5pqGMhijTpk0zgdTGjRvlT3/6k7nXKiINPy688EL53//+Z4IUDVq8lUepqanmXsO1uXPnysUXX2zCntzcXHnsscfkZz/7mXz11VdtcrnfZZddZj7nP/7xD1910muvvWZ+vho26u/v888/N59DQxl9rrE04Dv11FNNRZYGOHXpsYSEBDn//POb/BkC/T6P5ndx8OBBGTt2rPn96ng9T2+99VYZMGCA/PKXvzRjNJDTwHTXrl3mvNZQ9i9/+Yt88MEHTZ4/AADtghsAAKAJpkyZ4rbb7e5169b5Pedyucz99ddfr6mA++OPP/Y9V1RU5O7Zs6e7R48e7pqaGnPsww8/NOP69evnrqio8I199NFHzfENGzb43rdLly7uCRMm1Pt+r776qhn30UcfNXr+8+fPN6+ZPHmy33OlpaV+x1555RW/7/HAAw+YY9u3b683dseOHe6wsDD3PffcU++4fo7w8HC/48Hi/cy5ubkBnz948KB5/oILLjjiz2LRokVum83m3rlzp99719W9e3f31KlTfY+feuopM+b777/3HausrHSnpKTUG/djXnvtNfM+et782O/zaH4Xp59+unmPF154wXdMz8eMjIx659zixYvNOD3vvEpKStx9+vTxmxcAAPDH8j0AAHDMXC6XvPnmm2bXtmHDhvk9713G9d5775lqqrrLqHT5nVZW6TI3XSZVl1YmaYWV109/+lNzr1VL3vfVShh93+LiYt+4FStWmGVWdb9PY/32t7/1O6ZLvLzKy8slLy/PLDtUX3755Y++58qVK83PSCtt9LXem1ZdaRXPhx9+KG2R/m5UUVFRwJ+F9mjSz6HVb1qFpFVGR0N/HrocUiujvLTJur7nb37zm2b5DA1/n0f7u9CfQd256Pmo57D3HFR6/nXq1Mn05vLSpX56XgMAgB/H8j0AAHDMdPmT0+k0/XeOZOfOnTJixAi/494d0/T5uu+hfYvq0r5A3iVVXrqET3eJe+utt0zvIA2nNCS4+uqrj9jT6HC0H1RD+fn5Zle95cuXS05OTr3ndFnhj/nhhx9MaKOhRyARERGHfa3249Lvfyw0QNF+ScfKG/TpUjovXaKmjdD1513399DYn0VdukxOg8yXX35Z7rrrLnNMAyoNFM866yxpDg1/n0f7u9B+Zg3PIz0Ptc+Yl563ffr08Rt33HHHNcMnAAAg9BFKAQCANudw/aC8vYGUViz16NHDNEXXUEp7SWmPHw2rjkXdSiAvrarR5tvayHzQoEGmekarbbTXkN7/GB2jgcXf//73gJ/JW5EUiH5fbbJ9LE4//XTTNP5Yffvtt+ZeAxelTeO1t5SGZNpXSfuGaWNv7R2m/bsa87NoSHf4015U+jm1T5OGXb/73e9ML7Hm0PD3ebS/i8acgwAAoGkIpQAAwDHTZt4Oh8MXYhxO9+7dZfPmzX7HN23a5Hv+WGho9Oijj5pqLV26pyGVd3ldU2k10OrVq02llFYI1a24aehwlVm9e/c2IYZW7fzkJz85qu8/cODAervfHQ1vZdmx0mbdasyYMeZ+w4YNppm7NqfXMMnrWOenNNjT80crpLSKTpuoa4P1ltKU38Xh6Hmr576+b91zINC5DgAA/BFKAQCAY6ZVLePHj5cXX3xRvvjiC7++Ut6L9XHjxpmldmvXrpWRI0f6+hLpDm8aJPXv3/+Yvr9WRenOfBqWrFq1Sq677jppLt5KmYaVMfo5GtKqIaU7ENalO7fNmTPHBFv6M6obXOj7auWR7mR3uGBp9OjREmy6pO7pp582vyfdWe5wPwv9WgPBYxUeHi6TJ0823+/777831VInnXSStJSm/C4OR89r3aFQd+bz7gKp4Zqe1wAA4McRSgEAgCa59957zYW5LhnTBs/aJ2r//v1madYnn3xi+gfNnj1bXnnlFfnlL38pf/jDH0y/Iw2Stm/fLm+88cYxL9kaMmSIWWJ2++23S0VFxTEv3QtEK8B+9rOfmdCrqqrK9DvSz6lzbmjo0KHmXucxadIk059IeyZpdc7dd99twhBt6K4BnvZp0vf461//an5eN910k7QWDVN02Zr2r9KleNps/D//+Y+p0tLfn5cu19PPonPVcfqz0d9bw95SR0urrpYsWWKajN93333SklridzFjxgx5/PHHzedYv369aXquVWba7BwAAPw4QikAANAkGtZ89tlnMnfuXLMUS5fS6TENoLwX5+np6aZ3kPYjeuyxx8xOdloVo32gzjnnnCZ9fw2i7rnnHhNOaUjVnLSK59prr5WlS5eaappf/OIXpidR586d6407+eSTTcPuZcuWmYot7V+kYYdWUGkgp8vFHnnkEVOlozIzM817nXfeedKarrnmGnOvO+GlpKSYvlnPPPOM6dEVFRXlG6chm/6uNFBctGiRGX/BBRfIzJkzTYB1rDTMO+GEE0yl1KWXXiotrbl/F3p+6xJPPUf0vNbH+jn03NfliQAA4Mhsbro1AgAAoJUMHjzYVM5puAMAANqX5tneBAAAADhK2ofs66+/rtc8HQAAtB9USgEAgJBUXFxsbkeiu795m3gjeHTHOu3B9NBDD0leXp5s27bNLAkEAADtC5VSAAAgJD344IOm8fSRbrt3727tabZL2mB92rRppoG8NsAnkAIAoH2iUgoAAIQkrb7R25GcdtppBCIAAACthFAKAAAAAAAAQcfyPQAAAAAAAARdePC/ZehxuVyyb98+SUhIEJvN1trTAQAAAAAAaDW6KK+oqEg6d+4sdvvh66EIpZqBBlKZmZmtPQ0AAAAAAIA2QzeV6dq162GfJ5RqBloh5f1hOxwOsWq1V25urtka+0gpJgAAAAAAaBmuELk2dzqdpnjHm5ccDqFUM/Au2dNAysqhVHl5uZm/lU98AAAAAACsyhVi1+Y/1uLI+p8QAAAAAAAAlkMoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAACgfYdSS5culR49ekh0dLSMGDFCPv/888OOfe6550zDrLo3fZ1XVVWV3HrrrTJgwACJi4uTzp07y5QpU2Tfvn313ic/P18uvfRS00SsQ4cOMn36dCkuLm7RzwkAAAAAANBQrrNMvt9XZO7bgzYTSq1YsUJmzZol8+fPly+//FIGDhwoY8aMkZycnMO+RoOk/fv3+247d+70PVdaWmreZ+7cueZ+5cqVsnnzZjnvvPPqvYcGUhs3bpT3339f3nnnHfnoo4/kqquuatHPCgAAAAAAUNeqr3bJ1Mf/JX9893/mXh+HOpvb7XZLG6CVUSeffLI8/vjjvm0QMzMz5dprr5XZs2cHrJS6/vrrpaCgoNHfY926dTJ8+HATXnXr1k2+//576d+/vzk+bNgwM2bVqlUybtw42bNnj6muagyn0ymJiYlSWFhogjIr0p+3BoBpaWkhse0kAAAAAABtXWV1jWzLLpKvt+fJsx9urvec3WaTF/5wpqQ6YsRqGpuThEsbUFlZKevXr5c5c+b4jmkwMnr0aFm7du1hX6fL7Lp3724ClSFDhsi9994rJ5xwwmHH6w9Dl/npMj2l761fewMppd9Tv/dnn30mF1xwQcD3qaioMLe6P2yl89CbFem8NZ+06vwBAAAAAGjLyiurZVtOkWzZ75QtWYWyJcspO3OLxXWYWiE9vvdAsSTHR4nVNDZbaBOhVF5entTU1Eh6enq94/p406ZNAV9z3HHHyTPPPCMnnXSSCZsefPBBGTVqlFmK17VrV7/x5eXlpsfU5MmTfSldVlaWqQyqKzw8XJKSksxzh7No0SJZsGCB3/Hc3FzzfaxITxj9OWowRaUUAAAAAADHrqyyRnYeKJUdeaWyM89zv7+wXALlTwnR4dKlQ7Rsyqrf39puE4msKTtiW6O2qqioyDqh1LEYOXKkuXlpINWvXz956qmn5K677qo3VpueX3zxxSZwefLJJ5v8vbWiS/tf1a2U0qWGqampll6+p1Vk+hkIpQAAAAAAaJyisipT+bQ1SyugPFVQe/NLA45Nio+SPhkO6ZOR6Lnv5JCUhGhzPf7/vt4tS977VlxuTyD1h3EnyvG9/IturKDuRnRtPpRKSUmRsLAwyc7OrndcH2dkZDTqPSIiImTw4MGyZcuWgIGU9pH64IMP6oVG+t4NE8fq6mqzI9+Rvm9UVJS5NaRhjpUDHf1DYPXPAAAAAABASykoqfAET/sL5Yf9ugSvULIKAu+Ul5YY4wug+nZKlN4ZDklOOHxY88sh3WVIrxT5bts+6d+rs6R3iBOramyu0CZCqcjISBk6dKisXr1axo8f76vc0cczZ85s1Hvo8r8NGzaYJuUNA6kffvhBPvzwQ0lOTq73Gq200kbp2s9Kv7/S4Eq/tzZeBwAAAAAA7dOBonJP76f9Tl8AlesM3LKnU8dYE0Bp+KQhlAZQHeKOvhdUqiNG+nVOsGRz82PRJkIppcvhpk6dapqO6w55ixcvlpKSEpk2bZp5fsqUKdKlSxfTz0ktXLhQTjnlFOnTp48Jlh544AFTDXXllVf6AqmLLrpIvvzyS3nnnXdMaOXtE6U9ozQI0+V+Y8eOlRkzZsiyZcvMazQEmzRpUqN33gMAAAAAANalrX40bNLQyRM+eSqh8osPbXBWV9ekOOmj4VMnh/Q1AVSiJMREBH3eoaDNhFITJ040jcLnzZtnwqNBgwbJqlWrfM3Pd+3aVa/86+DBgyZM0rEdO3Y0lU5r1qyR/v37m+f37t0rb731lvla36surZo644wzzNcvvfSSCaLOPvts8/4TJkyQJUuWBPGTAwAAAACAYAVQ2QVlJnz6IetQAFVYWuk3Vvs6dU2O91Q/dfIsweuVniBxUQRQzcXm1t8ImkQbnScmJprd66zc6Fz7a+luhPSUAgAAAABYncvtlv35pb6ld9774vJqv7Fhdpt0T02obT5eG0ClJUh0ZHBreVwhcm3e2JykzVRKAQAAAAAAHIsal1v2HCg2VU9a/aQBlO6GV1rpH0BFhNmlR5ongPJWQfVMS5DI8LBWmXt7RigFAAAAAAAso7rGJbvyig9VP+13ytZsp1RU1fiNjQy3S690bwNyz0543dMSTDCF1kcoBQAAAAAA2qTK6hrZmVvsW3qnAdS2bKdU1bj8xkZHhJld77w74Ol9ZkqchFl4GVyoI5QCAAAAAACtTiudtufo0jtnbQBVKDtyiqTa5d8KOzYq/FD/pwzPErwuSRpA2Vpl7jg2hFIAAAAAACCoyiqrTcWTBk/eEEororQ5eUMJMRGm8qluD6hOHWPFbiOAsjpCKQAAAAAA0GJKyqtMzydP/ydPI/LdecXiHz+JJMZGmuDJ1wOqU6KkJ8aIjQAqJBFKAQAAAACAZuEsqzS73nkDqB+yCmVffmnAsckJUb6ld94eUHqMAKr9IJQCAAAAAABHraCkorYBee0yvKxCyS4oCzhWq518PaA6JZqG5Enx0UGfM9oWQikAAAAAAHBEB4rKfQGUdye8PGd5wLHa78lb+dSnk8N8rcvygIYIpQAAAAAAgOF2uyXXWV7bgNwTPmkQlV9c4TdWF9l1SY7zC6DioyNaZe6wHkIpAAAAAADaaQCVVVBWpwG5J4AqLK30G2u3iWSmxNc2IPf0geqd7pDYKGIFHDvOHgAAAAAAQpzL7ZZ9+SX1ekBpCFVcXu03Nsxukx6pCb7KJw2ieqY7JDoirFXmjtBFKAUAAAAAQAipcblld16xr/JJg6itWYVSVlnjNzYizC490zSAql2Cl+GQHmkJEhlOAIWWRygFAAAAAIBFVde4ZGeuJ4Dy9oDaluWUimqX39iocLv0SnfUC6C6pSaYYApoDYRSAAAAAABYQGV1jQmgNHzyBlDbs4ukqsY/gNKldr0zHL4eUHqfmRInYXYCKLQdhFIAAAAAALQxFVU1si3b6VmCt9+zBG9HbpFZmtdQXFS4qX7SyidvAKW74tltuj8e0HYRSgEAAAAA0IrKKqtlqzYf9y7B2++UXXnFpjl5QwkxEfWqnzSIyugYSwAFSyKUAgAAAAAgSErKqzy73/kCqELZc6BE/OMnkQ5xkSZ46puR6KuESkuMERsBFEIEoRQAAAAAAC3AWVrp2/3OsxNeoezLLw04NiUh2tOAPONQI/Kk+CgCKIQ0QikAAAAAAJqooKSiTgNyp6mAyi4sCzg2PTHGV/nkXYrXMT4q6HMGWhuhFAAAAAAAjeR2uyW/2BNAbakTQuUVlQcc3zkp1oROdXtAOWIjgz5voC0ilAIAAAAA4DABVE5hma/y6YfanfAOllT4jdVFdl2T43xL7zSE6p3hkPjoiFaZO2AFhFIAAAAAgHZPA6j9B0vr94DaXyjOsiq/sXabSLeUBE/41MnhC6BiIrnEBo4Gf2IAAAAAAO2Ky+2WvQdK6jQg91RClVRU+40Ns9ukR2r9AKpnukOiI8JaZe5AKCGUAgAAAACErBqXS3bnHQqg9H5btlPKKmv8xkaE2aVneoKv/5PeuqfGS2Q4ARTQEgilAAAAAAAhoarGJbtyiw4twasNoCqqXX5jo8Lt0ivDU/nk7QGlAVR4mL1V5g60R4RSAAAAAADLqayukR05Rb7d7zSA2p5TZIKphmIiw6R3nd3vNIDKTIk3S/MAtB5CKQAAAABAm1ZeVSPbs+s2IHfKjtwiqXG5/cbGR4eb0KlPbQClQVTnpDix2wiggLaGUAoAAAAA0GaUVlTL1mxP5ZO3B9TuvGIJkD+JIybCt/TO04g8UTI6xIiNAAqwBEIpAAAAAECrKC6v8lU+eaugdFe8APmTdIyLkr61u995A6hURzQBFGBhhFIAAAAAgBbnLK2UH0wApdVPThNA7T9YGnBsiiO6TgNyzxK85ITooM8ZQMsilAIAAAAANKuDxRW+pXcmhMpySk5hWcCx6R1ipG+DHlAd4qKCPmcAwUcoBQAAAAA4Jm63Ww4UVdRpQK4BVKE5FkiXpDjP7nedPFVQvTMc4oiJDPq8AbQNhFIAAAAAgEYFUNmFZbUNyA/1gCooqfQbq12eMlPifZVPGkL1TndIXHREq8wdQNtEKAUAAAAAqMfldpt+T57+T54QSgOoorIqv7F2m026p2oApdVPniqoXukOiYnkchPAkbWpvyWWLl0qDzzwgGRlZcnAgQPlsccek+HDhwcc+9xzz8m0adPqHYuKipLy8nLf45UrV8qyZctk/fr1kp+fL1999ZUMGjSo3mvOOOMM+fe//13v2NVXX21eBwAAAAChrsbllr35Jb6ld3q/NcspJRXVfmPD7TbpkZZQ2//JE0L1THNIVERYq8wdgLW1mVBqxYoVMmvWLBMGjRgxQhYvXixjxoyRzZs3S1paWsDXOBwO87xXw61AS0pK5LTTTpOLL75YZsyYcdjvrc8tXLjQ9zg2NrZZPhMAAAAAtCU1Lpfsyi32VT79UBtAlVfV+I2NCLObiqc+Wv1UuxOeVkRFhhNAAQixUOrhhx824ZC3+knDqXfffVeeeeYZmT17dsDXaAiVkZFx2Pe87LLLzP2OHTuO+L01hDrS+wAAAACA1VTVuGRnTtGhXfCynLIt2ymV1S6/sVrp1Ls2gDI9oDISpVtKvISH2Vtl7gDahzYRSlVWVpoldnPmzPEds9vtMnr0aFm7du1hX1dcXCzdu3cXl8slQ4YMkXvvvVdOOOGEo/7+L730krz44osmmDr33HNl7ty5VEsBAAAAsIzK6hrZrgFUnR5QO3KKTDDVUGxkuNn1zhM+eXpAdU2OlzB7/ZUnANAuQqm8vDypqamR9PT0esf18aZNmwK+5rjjjjNVVCeddJIUFhbKgw8+KKNGjZKNGzdK165dG/29L7nkEhNsde7cWb755hu59dZbzZJA7Ud1OBUVFebm5XQ6zb2GY3qzIp237qZh1fkDAAAA7YUutduerbvfeZbg6fK7nXnFpjdUQ/HR4abqScMnbxDVqWOsaU5en14L+L8eQHC5QuTavLHzbxOh1LEYOXKkuXlpINWvXz956qmn5K677mr0+1x11VW+rwcMGCCdOnWSs88+W7Zu3Sq9e/cO+JpFixbJggUL/I7n5ubWa7RutRNGwz09+bVKDQAAAEDrK6uskZ0HSmVnXqnsqL3tLywXd4D8KCE6XHqkxEr3lFjpkRxrvk5JiKzfe7e6RPJyS4L6GQC0v2vzoqIi64RSKSkpEhYWJtnZ2fWO6+PG9nqKiIiQwYMHy5YtW5o0F22yrvR9DhdK6TJDbcpet1IqMzNTUlNTTfN1q574+h8r/QxWPvEBAAAAqyour/I1IN9iqqCcsi+/RALVLyXFRx2qftJKqE4OSUmI9tv8CYC1uELk2jw6Oto6oVRkZKQMHTpUVq9eLePHj/f9IvTxzJkzG/Ueuvxvw4YNMm7cuCbN5euvvzb3WjF1OFFRUebWkJ4wVj5p9MS3+mcAAAAArKCwtNL0f6rbhHz/wdKAY1Md0b7d77w74SUnNO6CD4D12ELg2ryxc28ToZTSyqOpU6fKsGHDZPjw4bJ48WIpKSnx7cY3ZcoU6dKli1k6pxYuXCinnHKK9OnTRwoKCuSBBx6QnTt3ypVXXul7z/z8fNm1a5fs27fPPNZeUUqrr/SmS/RefvllE2QlJyebnlI33HCD/OxnPzO9qgAAAACgqfKLy03lkyd88gRQOYVlAcdmdIjx7X6nDci1GqpDnP//EAeAUNBmQqmJEyeankzz5s2TrKwsGTRokKxatcrX/FzDpbpJ28GDB2XGjBlmbMeOHU2l1Zo1a6R///6+MW+99ZYv1FKTJk0y9/Pnz5c777zTVGj985//9AVgugRvwoQJcscddwT1swMAAACwPu0Bk1dU7gmfapuQ69f5xYc2SaqrS1JcvR3wNIhKiIkI+rwBoLXY3Po3J5pEe0olJiaaZmRW7imVk5MjaWlpli4RBAAAAIJBL6OyC8rkB9P/qVB+0F5Q+wvNsryG7DaRrsnxvgBK73tlOCQuigAKQGhemzc2J2kzlVIAAAAA0Ba53G7Zn19aJ4DyVEJpY/KG7DabdE+NN5VP3hCqd7pDoiO59AKAhvibEQAAAABq1bjcsvdAsa/5uLcHVGlFtd/YcLtNeqQl1DYg9yy/65WeIJHhYa0ydwCwGkIpAAAAAO1Sjcslu3KLfZVPGkBtzXJKeVWN39jIcLv0Snf4+j/1zUiU7mkJEhFm3eU1ANDaCKUAAAAAhLyqGpfsyCnyVD7pErz9Ttme45TKapff2KiIME/4lHFoCV5mSryEE0ABQLMilAIAAAAQUiqqamR7bQDl2Qmv0ARS1S7/PZ5iI8OlT6f6AVSX5HgJ0+7kAIAWRSgFAAAAwLLKK6tla7az3g54O3OLTXPyhuKjI0wApUvvvEvwOiXFmubkAIDgI5QCAAAAYAklFVWyLcvpa0Ku93sOaADlPzYxNtJX+eTdCS89MUZsBFAA0GYQSgEAAABoc4rKqnz9n7wB1N78koBjkxOizPI73xK8Tg5JSYgmgAKANo5QCgAAAECrKiipMMGTpwG5hlCFklVQFnBsWmKMqX7yVEF5Aqik+OigzxkA0HSEUgAAAACC5kBReW0Dck8IpV/nOssDju3UMbZeANU7wyEd4qKCPmcAQMsglAIAAADQ7NxutwmbPA3IPUvw9Ov84oqA47smxZneT95G5L0zEiUhJiLo8wYABA+hFAAAAIAmB1C63M4XQNX2gSosrfQba7eJZKbE1y698/SA6pWeIHFRBFAA0N4QSgEAAABoNJfbLfvyS2SLLr8zy/A8S/CKy6v9xobZbdI9NeHQEjwNoNISJDqSyxAAAKEUAAAAgMOocbllz4Hi2gooz/K7rVlOKa30D6AiwuzSIy2htv+TwwRQPdMSJDI8rFXmDgBo+wilAAAAAEh1jUt25RX7Kp+0EmprtlMqqmr8xkaG26V3uid48lZBdUtNMMEUAACNRSgFAAAAtDOV1TWyM/dQAKX327OLpKrG5Tc2OiLM7Hrn3QFP7zNT4iTMTgAFAGgaQikAAAAghGml0/Ycp/ygPaBqQ6gdOUVS7XL7jY2NCvctvdMd8PS+S5IGULZWmTsAILQRSgEAAAAhoqyyWrZle8InE0JlFZqKKG1O3lBCTISv8skbRHXqGCt2GwEUACA4CKUAAAAACyoprzI9n8wSPFMB5ZTdecXiHz+JdIiL9C2/8/aASkuMERsBFACgFRFKAQAAAG2cs6zSNB73NCDXnfAKZV9+acCxyQlRvqV33kooPUYABQBoawilAAAAgDakoKSitgF5bRVUVqFkF5QFHJueGHOoB1SnRNOQPCk+OuhzBgDgWBBKAQAAAK3kQFG5b/ndD1meSqg8Z3nAsdrvydcDqpPDfJ0YGxn0OQMA0FwIpQAAAIAW5na7JddZXtuA3FP9pJVQ+cUVfmN1kV2X5LhDPaBqA6j46IhWmTsAAC2FUAoAAABo5gAqq6CsTgNyTwBVWFrpN9ZuE+mWkuALnjSI6pXukNgo/pkOAAh9/NcOAAAAOEYut1v2HijxBU8aRG3NKpTi8mq/sWF2m/RI9QRQ3iqonukOiY4Ia5W5AwDQ2gilAAAAgEaocblkd54ngPI2ItcAqqyyxm9sRJhdeqZpAFXbAyrDIT3SEiQynAAKAAAvQikAAACggeoal+zMLa4TQBXKtiynVFS7/MZGhdvNkru6AVT31AQJD7O3ytwBALAKQikAAAC0a5XVNSaA0vDJ2wdqe06RVNX4B1AxkWHSW5uPZxzqAZWZEidhdgIoAACOFqEUAAAA2o2KqhrZlu309IDa7+kBtSO3SGpcbr+xcVHhpvpJAyhvDyjdFc9u0/3xAABAq4dSNTU18txzz8nq1aslJydHXK76/0fpgw8+aOq3AAAAAI5aWWW1bM3yBFCeCiin7MorNs3JG0qIiTDBU1+tgqoNojp1jBUbARQAAG03lLruuutMKHXOOefIiSeeyH+4AQAAEHQl5VW+3e88VVCFsudAifjHTyId4iL9Aqi0xBj+HQsAgNVCqeXLl8urr74q48aNa54ZAQAAAEfgLK2UH2qX33mroPYfLA04NiUh2tOAPONQI/Kk+CgCKAAAQiGUioyMlD59+jTPbAAAAIA6CkoqDjUg16V4+wslu7As4Nj0DjGm71PdHlAd46OCPmcAABCkUOrGG2+URx99VB5//HH+jxMAAACOidvtlgNFFb6ld94QKq+oPOD4zkmxvt3vvEGUIzYy6PMGAACtGEp98skn8uGHH8rf//53OeGEEyQiIqLe8ytXrmzqtwAAAECIBVA5hWUNekA55WBJhd9Y/V+eXZPjPOFTnQAqLrr+vzkBAEA7DKU6dOggF1xwQbNMZunSpfLAAw9IVlaWDBw4UB577DEZPnx4wLHaXH3atGn1jkVFRUl5eXm9QGzZsmWyfv16yc/Pl6+++koGDRpU7zU6Xqu9tDdWRUWFjBkzRp544glJT09vls8EAADQ3gMo7ffkW35XWwnlLKvyG2u32aR7arwneOrkWYLXK90hMZFN/icrAABog5r8X/hnn322WSayYsUKmTVrlgmRRowYIYsXLzYB0ebNmyUtLS3gaxwOh3neq+HywZKSEjnttNPk4osvlhkzZgR8jxtuuEHeffddee211yQxMVFmzpwpF154ofznP/9pls8FAADQXrjcbrPjnYZO3gbkW7OcUlJR7Tc23G6THmkJ9QKoHmkOiY4Ia5W5AwCA4Gsz/9vp4YcfNsGRt/pJwykNi5555hmZPXt2wNdoCJWRkXHY97zsssvM/Y4dOwI+X1hYKP/3f/8nL7/8spx11lm+kK1fv37y6aefyimnnNIMnwwAACD01LhcsjuvxLf8Tu+3ZTulrLLGb2xEmF16pnsCKA2f9KYVUZHhBFAAALRnzRJKvf766/Lqq6/Krl27pLKyst5zX3755Y++Xl+jS+zmzJnjO2a322X06NGydu3aw76uuLhYunfvLi6XS4YMGSL33nuv6WvVWPo9q6qqzPfxOv7446Vbt27m+xJKAQAAiFTVuGRXbtGhHlC1AVRFtctvbFS4XXrV2f1ObxpAhYfZW2XuAAAghEOpJUuWyO233y6XX365/O1vfzOVTlu3bpV169bJ73//+0a9R15entTU1Pj1cdLHmzZtCvia4447zlRRnXTSSabi6cEHH5RRo0bJxo0bpWvXro36vtq7KjIy0vTFavh99bnD0d5TevNyOp3mXsMxvVmRzlt7Plh1/gAAoHlUVtfIjpxiT+8n0wPKKdtznFJd4/YbGxMZJr3THbUNyB3m1jU5XsLs/jsy828MAADaz7W5q5Hzb3IopU3B//SnP8nkyZNN8/FbbrlFevXqJfPmzTPNxVvKyJEjzc1LAylddvfUU0/JXXfdJS1p0aJFsmDBAr/jubm59RqtW+2E0XBPT36tUgMAAKFPK512HyiVHXmlsjPPc7/3YJkEyJ8kNjJMeqTESne9JcdKz5RYSUuMMs3JfdxlciCvLKifAQCAUOIKkWvzoqKi4IRSumRPAyEVExPj+8baz0mXvz3++OM/+h4pKSkSFhYm2dnZ9Y7r4yP1jKorIiJCBg8eLFu2bGn03PW9delgQUFBvWqpH/u+usxQm7LXrZTKzMyU1NRU03zdqie+9ujSz2DlEx8AAARWWlFtltz5dsDLcsruvGJxBQigHDER9aqfdAleRocYv01lAABA83KFyLV5dHR0cEIpDW+0Ikp7O2kvJm0QPnDgQNm+fbtJ9hpDl9ANHTpUVq9eLePHj/f9IvSx7obXGLr8b8OGDTJu3LhGz12/p4ZZ+n0mTJhgjulufhq01a3CaigqKsrcGtITxsonjZ74Vv8MAABApLi8yhM87a/tAZVVKHsPlEigf5l1jIuSvp08S/D6mp3wEiXVEU0ABQBAK7GFwLV5Y+fe5FBKd6176623TJWS9pO64YYbTOPzL774Qi688MJGv49WHk2dOlWGDRsmw4cPl8WLF0tJSYlvN74pU6ZIly5dzNI5tXDhQlOJ1adPH1Pp9MADD8jOnTvlyiuv9L2nhmUaMO3bt88XOHmDNL0lJibK9OnTzfdOSkoyVU7XXnutCaRocg4AAKygsLSyNoDSHfA8VVD7D5YGHJviiPYFT31qm5EnJzTu/2QCAAA0tyaHUtpPytvAShubJycny5o1a+S8886Tq6++utHvM3HiRNOTSXtRaZPxQYMGyapVq3zNzzVcqpu0HTx4UGbMmGHGduzY0VQ96fft37+/b4yGZd5QS02aNMncz58/X+68807z9SOPPGLeVyultHn5mDFjTJ8sAACAtuZgcYWv8smEUFlOySkM3MNJl9uZ3e+0Aqo2hOoQ51/pDQAA0Fps7sauscNhaU8prbrSZmRW7imVk5MjaWlpli4RBAAgFOg/z/KKys3yOw2gvEHUgaJDu//W1SUpzlf5pCFU7wyHOGIigz5vAADQNK4QuTZvbE7S5Eop9fHHH5td77Zu3WqW7ukyu7/85S/Ss2dPOe2005rjWwAAAIRsAJVdWFa7/M7TgFwDqIKSSr+x2uUpMyXeL4CKi4polbkDAAA0RZNDqTfeeMPstHfppZfKV199ZZbAKU3D7r33Xnnvvfea+i0AAABCgsvtNv2eGgZQRWVVfmPtNpt0T42vbUDuaUTeK90hMZHN8v8UAQAAWl2T/1Vz9913y7Jly0wj8uXLl/uOn3rqqeY5AACA9qjG5Za9+SW1vZ88PaA0hCqtqPYbG263SY+0hNoG5J4eUD3TEiQqIqxV5g4AAGCJUEp3tPvZz37md1zXDuqueAAAAKGuxuWSXbnFvsonrYLamuWU8qoav7ERYXZT8dSnU+0SvIxEUxEVGU4ABQAA2pcmh1IZGRmyZcsW6dGjR73jn3zyifTq1aupbw8AANCmVNW4ZGdOUZ0G5E7Zlu2UymrPbsR1aaVT73Rv/yeHCaC6pcRLeJh1G5cCAAC0mVBqxowZct1118kzzzwjNptN9u3bJ2vXrpWbbrpJ5s6d2zyzBAAAaAWV1TWyPafIEz7VBlA7copMMNVQbGS4aTruqX7y9IDqmhwvYXZtTw4AAIBmD6Vmz55ttiw8++yzpbS01Czli4qKMqHUtdde29S3BwAACIryymrZ1iCA2plbZHpDNRQfHeFZfpdxqAdUp6RY05wcAAAAjWNz6z7EzaCystIs4ysuLpb+/ftLfHy8tBdOp9P00NIdBx0Oh1iRBos5OTmSlpYmdjtLCgAAoU2bjW/V5XfaA6p2J7w9B4olQP4kibGRtQ3IPVVQGkSld4gxFeIAAADNyRUi1+aNzUmOuVLqiiuuaNQ4XdYHAADQWorKqmoDKK2A8oRQe/JLAo5Nio8yAZSpgKrtAZXqiCaAAgAAaAHHHEo999xz0r17dxk8eLA0U7EVAABAkxSWVvoqn7QRuS7B23+wNOBYDZu8u995A6jkhOigzxkAAKC9OuZQ6pprrpFXXnlFtm/fLtOmTZPf/OY3kpSU1LyzAwAAOIz84vLa/k9O3054uc7ygGM7dYz1NB+v7f+kDck7xEUFfc4AAABopp5SFRUVsnLlSrNEb82aNXLOOefI9OnT5Re/+EW7KnOnpxQAAC1H/6miYZOpfNrvrF2GVyj5xRUBx3dNiqvXA6p3RqIkxEQEfd4AAADt9drc2dI9pZTusjd58mRz27lzp1nS97vf/U6qq6tl48aN7arZOQAAaJ4AKrugzARPniV4nh5QuiyvIbtNpGtyvGcJnukD5ZBeGQ6JiyKAAgAAsIImhVJ1aYKn1VH6j8mamprmelsAABCiXG637M8v9VU+eRuRF5dX+Y2122zSPfVQAKVVUL3THRId2Wz/lAEAAECQNelfcnWX733yySfyq1/9Sh5//HEZO3aspcvMAABA86pxuWXvgeJD1U+1TchLK6r9xobbbdIjLeFQBVSnROmZliCR4WGtMncAAAC0sVBKl+ktX75cMjMz5YorrjBNz1NSUpp3dgAAwHJqXC7ZmVt8qAfU/kLZmu2Uiir/SurIcLv0StcG5I7aJXiJ0j0tQSLC+J9bAAAAoe6YQ6lly5ZJt27dpFevXvLvf//b3ALRSioAABCaqmpcsiOnqDaA0j5QTtme45TKapff2OiIMLPrnXcHPA2iuqXGSxjV1QAAAO3SMYdSU6ZMaVc77AEA0N5ppdP22gDKLMPbX2gCqWqX/0a+sVHh9aqf9OsuyRpA8W8HAAAANDGU0p32AABAaCqvrDZL7jwNyD33uiRPm5M3FB8d4at88vaA6tQx1jQnBwAAAA6HLWsAAGjnSiqqZGtt8KTNx7UKandesfjHTyKJsZG+AMrbiDw9MYbqaQAAABw1QikAANoRZ1mlL4Dy7oS3N78k4NjkhKg6/Z80gHJISkI0ARQAAACaBaEUAAAhqqCkwlf55KmCKpSsgrKAY9MSYw5VP9UGUEnx0UGfMwAAANoPQikAAELAgaLy2gbk3j5QhZLnLA84Vvs9eSqgPDvh6RI8XZYHAAAABBOhFAAAFuJ2uyXXWe4LnrQSSr/OL67wG6uL7Lokx/kqn7QKqnd6oiTERLTK3AEAAIC6CKUAAGjDAZQut/MFULU9oApLK/3G2m0imSnxh3pAmQDKIbFR/KceAAAAbRP/UgUAoA1wud2yL79Etux31gmgCqW4vNpvbJjdJt1TE+otv+uV7pDoiLBWmTsAAABwLAilAAAIshqXW/YcKPbtfqcBlO6IV1rpH0BFhNmlR1pCbQNyhwmgeqYlSGQ4ARQAAACsjVAKAIAWVF3jkl153gBKK6CcsjXbKRVVNX5jI8PtZsmdBk/enfC6pSaYYAoAAAAINYRSAAA0k8rqGtmZeyiA0vvt2UVSVePyG6tL7XrXBk/ePlCZKXESZieAAgAAQPtAKAUAwDHQSqftOU75Yb9n+Z2GUDtyiqTa5fYbq83GvZVP3h5QXZI0gNL98QAAAID2iVAKAIAfUVZZLduyNYDyLL/TAEororQ5eUMJMRGHwqfaICqjY6zYbQRQAAAAQF2EUgAA1FFSXuVpPl67A54GUXsOlIh//CTSIS6y3vI7DaHSEmPERgAFAAAA/ChCKQBAu+Usq/RVPnn7QO3LLw04NiUh2rf7nTeISk6IIoACAAAAjhGhFACgXSgoqagNnmqX4WUVSnZBWcCx6YkxfgFUx/iooM8ZAAAACGWEUgCAkOJ2uyW/uDaA0uV3tUvx8pzlAcd36hhbpwG5Q/pmJIojNjLo8wYAAADaG0IpAIClA6hcZ7kvgPIsw3PKwZIKv7G6yK5Lcly9HlC9MxwSHx3RKnMHAAAA2rs2FUotXbpUHnjgAcnKypKBAwfKY489JsOHDw849rnnnpNp06bVOxYVFSXl5eX1Llbmz58vf/7zn6WgoEBOPfVUefLJJ6Vv376+MT169JCdO3fWe59FixbJ7Nmzm/3zAQCOnf6dvv9gqacJuamA8gRRzrIqv7F2m0i3lART+eQNoHqlOyQ2qk39Zw8AAABo19rMv85XrFghs2bNkmXLlsmIESNk8eLFMmbMGNm8ebOkpaUFfI3D4TDPezVsNnv//ffLkiVL5Pnnn5eePXvK3LlzzXt+9913Eh0d7Ru3cOFCmTFjhu9xQkJCi3xGAEDjuNxu2XugxLMDnrcH1P5CKamo9hsbZrdJj1RPAOWtguqZ7pDoiLBWmTsAAAAAi4VSDz/8sAmGvNVPGk69++678swzzxy2aklDqIyMjMP+H3UNtu644w45//zzzbEXXnhB0tPT5c0335RJkybVC6EO9z4AgJZV43LJ7jxPAOVtRL41q1DKKmv8xkaE2aVnWkKdBuQO6ZGWIJHhBFAAAACA1bSJUKqyslLWr18vc+bM8R2z2+0yevRoWbt27WFfV1xcLN27dxeXyyVDhgyRe++9V0444QTz3Pbt280yQH0Pr8TERFOFpe9ZN5T64x//KHfddZd069ZNLrnkErnhhhskPPzwP5qKigpz83I6neZe56E3K9J5a5Bn1fkDsIbqGpfsyis24dNW04DcKduynVJR7f93T1S43Sy50+BJez9pCNUtJV7Cw+x+Y/m7CwAAAKHAFSLX5o2df5sIpfLy8qSmpsZUMdWljzdt2hTwNccdd5ypojrppJOksLBQHnzwQRk1apRs3LhRunbtagIp73s0fE/vc+oPf/iDCbSSkpJkzZo1Jhjbv3+/qdw6HO05tWDBAr/jubm59XpaWe2E0Z+jnvwaCAJAU1XVuGRPfpnsyCuVnXml5n53fplUu9x+Y6Mj7NItOVZ6pHhu3ZNjpVOHaLM075ByyT9gzb9jAQAAgPZ0bV5UVGSdUOpYjBw50ty8NJDq16+fPPXUU6bqqbG0j5WXBlyRkZFy9dVXm+BJG6cHosFV3ddppVRmZqakpqaaPldWPfF1OaR+Biuf+ABaR3lVjWzPccrW/U75wVRAFcrO3GKpCRBAxUWFm+onXYJn7jMc0jkpTuwN+gICAAAA7Y0rRK7N6/bxbvOhVEpKioSFhUl2dna94/q4sb2eIiIiZPDgwbJlyxbz2Ps6fY9OnTrVe89BgwYd9n10eV91dbXs2LHDVGMFomFVoMBKTxgrnzR64lv9MwBoeWWV1Wbpnaf/kzYgd5oledqcvCFHTISn/1NGoi+E6tQx1m9jCgAAAAChc23e2Lm3iVBKq5OGDh0qq1evlvHjx/vSQX08c+bMRr2HLv/bsGGDjBs3zjzW3fY0mNL38IZQWtH02WefyTXXXHPY9/n666/ND+9wO/4BQHtSUl51aPe72kbkuiuef/wk0jEuSvp20sonTwClPaBSHdEEUAAAAADabiildDnc1KlTZdiwYTJ8+HCzc15JSYlvN74pU6ZIly5dzLI6tXDhQjnllFOkT58+UlBQIA888IDs3LlTrrzySvO8XgRdf/31cvfdd0vfvn1NSDV37lzp3LmzL/jShucaUp155plmBz59rE3Of/Ob30jHjh1b8acBAMHnLK2UH2orn7wB1P6DpQHHpjiiTfjUt3YZngZQSfFRBFAAAAAArBdKTZw40TQKnzdvnmlErtVNq1at8jUq37VrV73yr4MHD8qMGTPMWA2QtNJKG5X379/fN+aWW24xwdZVV11lgqvTTjvNvKd3baMuwVu+fLnceeedZjc9Da40lKrbLwoAQtHB4gpf8LTFVEE5JbuwLODY9A4xngDK1wMqUTrGB+65BwAAAACNZXNrS3c0iS4LTExMNB3yrdzoPCcnxyxbtPK6VQD16V/xB4r8A6i8osC72HVOiq0TQOkyPIc4YiKDPm8AAACgPXKFyLV5Y3OSNlMpBQBoegCVU1hW2//p0BK8gpJKv7G6yK5rcpwnfPIGUBkOiYuOaJW5AwAAAGh/CKUAwKIBlPZ7qhtAaRWUs6zKb6zdZpPuqfG+yicNonqlOyQmkv8EAAAAAGg9XJEAQBvncrtlz4GS2qV3nuqnrVlOKamo9hsbbrdJj7SEOjvgOaRnmkOiIsJaZe4AAAAAcDiEUgDQhtS4XLI7r6S2AsoTQG3LdkpZZY3f2Igwu/RMT/D1f9J7rYiKDCeAAgAAAND2EUoBQCupqnHJrtyiQ0vwagOoimqX31itdOqd7jDL77wBVLeUeAkPs27zQwAAAADtG6EUAARBZXWNbM8p8u1+p0HUjpwiE0w1FBsZLr0zNIDyNB/XAKprcryE2bU9OQAAAACEBkIpAGhm5VU1puJJAyhvFdTO3CKpcbn9xsZHhx/q/1TbiLxzUpxpTg4AAAAAoYxQCgCaoLSiWrbWC6AKZXdesQTIn8QRE+Hp/+QLoBIlo0OM2AigAAAAALRDhFIA0EjF5VW+5uNb9jvN13sPlEiA/EmS4qMOLb+rDaBSHdEEUAAAAABQi1AKAAIoLK2s7f+kIZQngNp/sDTg2BRHtC946lvbiDw5ITrocwYAAAAAKyGUAtDu5ReX+yqfvD2gcgrLAo7V5Xa+HlC1lVAd4qKCPmcAAAAAsDpCKQDthtvtlryihgFUoRwoqgg4vktSnG/3Ow2hdEc8R0xk0OcNAAAAAKGIUApAyAZQ2YVl9XbA0wCqoKTSb6x2ecpMifdVPnkDqLioiFaZOwAAAAC0B4RSACzP5Xabfk+eBuSHAqiisiq/sXabTbqnxtfugOcJoHqlOyQmkr8OAQAAACCYuAoDYCk1LrfsPVBsgqcfsg6FUKUV1X5jw+026ZGWUKf/U6L0TEuQqIiwVpk7AAAAAOAQQikAbVaNyyW7cmsDqNr+T1uznFJeVeM3NiLMbiqezO53tQGUBlJ6HAAAAADQ9hBKAWgTqmpcsjOnqF7107Zsp1RWu/zGaqVT73RvA3KHCaC6pcRLOAEUAAAAAFgGoRSAoKusrpHtGkDV9oDS+x05RVLtcvuNjY0M9wVP3p3wuiTHS5hd25MDAAAAAKyKUApAiyqvrJat2dp43OkLoHbmFpvm5A3FR0eYAKqvBlCmEXmidEqKNc3JAQAAAAChhVAKQLMpqaiSbaYB+aEAas8BDaD8xybGRtbbAU8DqPQOMWIjgAIAAACAdoFQCsAxKSqrkq1ZnuDJWwW1J78k4Nik+Chf8ORdipfqiCaAAgAAAIB2jFAKwI8qLK309X/SHfD066yCsoBjNWwyDcjrBFDJCdFBnzMAAAAAoG0jlAJQT35xeW0A5fQFULnO8oBjO3WMNc3HNXjSIKp3hkM6xEUFfc4AAAAAAOshlALaKbfbbcImDZ40gPrB3BdKfnFFwPFdk+LMEjxvI/LeGYmSEBMR9HkDAAAAAEIDoRTQTgKo7IIyEzzV7QGly/IasttEuibHe5bg1TYi75XhkLgoAigAAAAAQPMhlAJCjMvtlv35pbXhU2FtBZRTisur/MaG2W3SPTXBswRPA6hOidIrLUGiI/mrAQAAAADQsrjyBCysxuWWPQeKaxuQO00QtTXLKaWV1X5jI8Ls0iOtfgDVMy1BIsPDWmXuAAAAAID2jVAKsIjqGpfsyiv2NR/X6qet2U6pqKrxGxsZbpde6dqA3OHbCa97WoIJpgAAAAAAaAsIpYA2qLK6Rnbm1g+gtuc4pbLa5Tc2OiLM7HrnDZ80iOqWGi9hdgIoAAAAAEDbRSgFtDKtdNqeU+TrAaVL8XbkFEm1y+03NjYq/NDyOw2gOiVKl6Q40xsKAAAAAAArIZQCgqi8stosudPg6YfaHfC0IkqbkzcUHx1RW/1UWwXVKVE6dYwVu40ACgAAAABgfYRSQAspqagyTcdNAFXbiHx3XrH4x08iibGRfgFUemKM2AigAAAAAAAhilAKaAbOskq/AGpvfknAsckJUab3k68HVCeHpCREE0ABAAAAANoVQingKBWUVJjQydOA3NMHKqugLODYtMQY6VvbA8obQCXFRwd9zgAAAAAAtDWEUsARHCgq91U+efpAFUqeszzgWO335KmAOhRC6bI8AAAAAADQxkOppUuXygMPPCBZWVkycOBAeeyxx2T48OEBxz733HMybdq0eseioqKkvPxQYOB2u2X+/Pny5z//WQoKCuTUU0+VJ598Uvr27esbk5+fL9dee628/fbbYrfbZcKECfLoo49KfHx8C35StDV6ruQ6y33Bk6cCyin5xRV+Y3WRXZfkuENL8Do5pHd6oiTERLTK3AEAAAAAsKI2E0qtWLFCZs2aJcuWLZMRI0bI4sWLZcyYMbJ582ZJS0sL+BqHw2Ge92rYk+f++++XJUuWyPPPPy89e/aUuXPnmvf87rvvJDras4Tq0ksvlf3798v7778vVVVVJui66qqr5OWXX27hT4zWDKB0ud2h/k+eAKqwtNJvrN0mkpkSX6f/U6L0TndIbFSb+aMDAAAAAIAl2dx6hd4GaBB18skny+OPP24eu1wuyczMNFVMs2fPDlgpdf3115sKqED0Y3Xu3FluvPFGuemmm8yxwsJCSU9PN6+dNGmSfP/999K/f39Zt26dDBs2zIxZtWqVjBs3Tvbs2WNe3xhOp1MSExPN+2tQZkX6887JyTEBoFaMhQqX2y378kvqLcHTEKq4vNpvbJjdJt1TEzzL72oDqF7pDomOCGuVuQMAAAAA2hdXiFybNzYnaRPlHpWVlbJ+/XqZM2eO75j+8EePHi1r16497OuKi4ule/fu5pc2ZMgQuffee+WEE04wz23fvt0sA9T38NIfiIZf+p4aSul9hw4dfIGU0vH6vT/77DO54IILAn7fiooKc6v7w1Y6D71Zkc5bgzyrzl/VuNyy50CxJ3yqDaC2ZjulrLLGb2x4mE16pmn45L0lSo+0eIkM9w+grPwzAQAAAABYhysErs1VY+ffJkKpvLw8qampMVVMdenjTZs2BXzNcccdJ88884ycdNJJJnl78MEHZdSoUbJx40bp2rWrCaS879HwPb3P6X3DpYHh4eGSlJTkGxPIokWLZMGCBX7Hc3Nz6/W0stoJoz9HPfmtkMZWu9yy72CZ7MgrlZ15peZ+V36ZVFb7n/iRYTbJTI6VHimeW/eUWOnSIVrCw+p+zgopyPfvHwUAAAAAQLC4LHZtfjhFRUXWCaWOxciRI83NSwOpfv36yVNPPSV33XVXi35vrejS/ld1K6V0qWFqaqqll+9pTy79DG3txK+srpGdubUVULXL8LbnFElVjX8ApUvtetepftL7zJQ4CWtjnwkAAAAAACtdmx8Nbx9vS4RSKSkpEhYWJtnZ2fWO6+OMjIxGvUdERIQMHjxYtmzZYh57X6fv0alTp3rvOWjQIN8YXatZV3V1tdmR70jfV3f501tDesJY+aTRE7+1P0NFVY1sz3F6ekDt12V4hbIjp8hURjWkzcY1dKrbhLxLkgZQ9RveAwAAAABgFbY2cG3eVI2de5sIpSIjI2Xo0KGyevVqGT9+vC8d1MczZ85s1Hvo8r8NGzaYJuVKd9vTYEnfwxtCaUWT9oq65pprzGOttNJG6drPSr+/+uCDD8z31t5TaFllldWy1fR/8gRQGkTtyis2zckbSoiJOBQ+1QZRGR1jxd5gx0UAAAAAAGANbSKUUrocburUqabp+PDhw2Xx4sVSUlIi06ZNM89PmTJFunTpYvo5qYULF8opp5wiffr0McHSAw88IDt37pQrr7zSlyzq7nx333239O3b14RUc+fONTvqeYMvXe43duxYmTFjhixbtkyqqqpMCKZN0Bu78x4ap6S8qrYBuQZQhSaA2nOgRAJt/dghLtIXQHnuHZKWGGN+pwAAAAAAIDS0mVBq4sSJplH4vHnzTJNxrW5atWqVr1H5rl276pV/HTx40IRJOrZjx46m0mnNmjXSv39/35hbbrnFBFtXXXWVCa5OO+0085511za+9NJLJog6++yzzftPmDBBlixZEuRPH1qcpZW+AMosw8sqlH35pQHHpiREH1qCVxtEJSdEEUABAAAAABDibG5t6Y4m0WWBiYmJpkO+lRuda38t3Y3waNatFpRUmODJEz55gqjsgrKAY9MTY2qDp0N9oDrG+/fmAgAAAACgPXId47W5VXOSNlMphbZNs8v8Yk8AZZbf1e6El1dUHnB856TY2v5Ph5bgOWIjgz5vAAAAAADQNhFKwch1lsn3+4rEFp0gaYmxklNY5ql8MgGUpxH5wZIKv9fpIruuyXG+pXcaQPXOcEh8dESrfA4AAAAAAGANhFKQVV/tksXvbKhtOv4/iY4Ik/KqGr9xdptIt5QE6dPp0PK7XukOiY3iNAIAAAAAAEeHNKGd0wqpR9/1BlIeGkhpANUzzduA3GECqJ7pDhNYAQAAAAAANBWhVDu3N79EXAFa3d89+WQZ2jutNaYEAAAAAADaAeu2ckez6JIUZ6qi6rLbbNItNaG1pgQAAAAAANoBQql2LtURI9edM8AXTOn9deecaI4DAAAAAAC0FJbvQcYO7iaDeybLd9v2Sf9enSW9Q1xrTwkAAAAAAIQ4KqVgaGVUv84JVEgBAAAAAICgIJQCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0NDpvBm6329w7nU6xKpfLJUVFRRIdHS12O1klAAAAAADBFirX5t58xJuXHA6hVDPQE0ZlZma29lQAAAAAAADaTF6SmJh42Odt7h+LrdCoJHPfvn2SkJAgNptNrJpiaqi2e/ducTgcrT0dAAAAAADaHWeIXJtr1KSBVOfOnY9Y8UWlVDPQH3DXrl0lFOhJb+UTHwAAAAAAq3OEwLX5kSqkvKy7QBEAAAAAAACWRSgFAAAAAACAoCOUghEVFSXz58839wAAAAAAIPii2tm1OY3OAQAAAAAAEHRUSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAg6QikAAAAAAAAEXXjwv2Xocblcsm/fPklISBCbzdba0wEAAAAAAGg1brdbioqKpHPnzmK3H74eilCqGWgglZmZ2drTAAAAAAAAaDN2794tXbt2PezzhFLNQCukvD9sh8MhVq32ys3NldTU1COmmAAAAAAAoGW4QuTa3Ol0muIdb15yOIRSzcC7ZE8DKSuHUuXl5Wb+Vj7xAQAAAACwKleIXZv/WIsj639CAAAAAAAAWA6hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAg6QikAAAAAAAAEHaEUAAAAAAAAgo5QCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgsF0otXbpUevToIdHR0TJixAj5/PPPjzj+tddek+OPP96MHzBggLz33nuHHfvb3/5WbDabLF68uAVmDgAAAAAAAEuGUitWrJBZs2bJ/Pnz5csvv5SBAwfKmDFjJCcnJ+D4NWvWyOTJk2X69Ony1Vdfyfjx483t22+/9Rv717/+VT799FPp3LlzED4JAAAAAABA+2apUOrhhx+WGTNmyLRp06R///6ybNkyiY2NlWeeeSbg+EcffVTGjh0rN998s/Tr10/uuusuGTJkiDz++OP1xu3du1euvfZaeemllyQiIiJInwYAAAAAAKD9skwoVVlZKevXr5fRo0f7jtntdvN47dq1AV+jx+uOV1pZVXe8y+WSyy67zARXJ5xwQgt+AgAAAAAAAHiFi0Xk5eVJTU2NpKen1zuujzdt2hTwNVlZWQHH63Gv++67T8LDw+UPf/hDo+dSUVFhbl5Op9MXcOnNinTebrfbsvMHAAAAAMDqXCFybd7Y+VsmlGoJWnmlS/y0P5U2OG+sRYsWyYIFC/yO5+bmSnl5uVj1hCksLDQnv1agAQAAAACA4HKFyLV5UVFRaIVSKSkpEhYWJtnZ2fWO6+OMjIyAr9HjRxr/8ccfmybp3bp18z2v1Vg33nij2YFvx44dAd93zpw5puF63UqpzMxMSU1NFYfDIVY98TWY089g5RMfAAAAAACrcoXItXl0dHRohVKRkZEydOhQWb16tdlBz/vL0sczZ84M+JqRI0ea56+//nrfsffff98cV9pLKlDPKT2uzdQPJyoqytwa0hPGyieNnvhW/wwAAAAAAFiZLQSuzRs7d8uEUkqrk6ZOnSrDhg2T4cOHm2qmkpISX4A0ZcoU6dKli1lep6677jo5/fTT5aGHHpJzzjlHli9fLl988YX86U9/Ms8nJyebW126+55WUh133HGt8AkBAAAAAADaB0uFUhMnTjR9m+bNm2ealQ8aNEhWrVrla2a+a9euemncqFGj5OWXX5Y77rhDbrvtNunbt6+8+eabcuKJJ7bipwAAAAAAAIDNrd2z0CTaUyoxMdE0I7NyTyntr5WWlmbpEkEAAAAAAKzKFSLX5o3NSaz7CQEAAAAAAGBZhFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAg6QikAAAAAAAAEHaEUAAAAAAAAgo5QCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAILOcqHU0qVLpUePHhIdHS0jRoyQzz///IjjX3vtNTn++OPN+AEDBsh7773ne66qqkpuvfVWczwuLk46d+4sU6ZMkX379gXhkwAAAAAAALRflgqlVqxYIbNmzZL58+fLl19+KQMHDpQxY8ZITk5OwPFr1qyRyZMny/Tp0+Wrr76S8ePHm9u3335rni8tLTXvM3fuXHO/cuVK2bx5s5x33nlB/mQAAAAAAADti83tdrvFIrQy6uSTT5bHH3/cPHa5XJKZmSnXXnutzJ4922/8xIkTpaSkRN555x3fsVNOOUUGDRoky5YtC/g91q1bJ8OHD5edO3dKt27dGjUvp9MpiYmJUlhYKA6HQ6xIf5Ya7qWlpYndbqmsEgAAAACAkOAKkWvzxuYklvmElZWVsn79ehk9erTvmP6C9PHatWsDvkaP1x2vtLLqcOOV/sBsNpt06NChGWcPAAAAAACAusLFIvLy8qSmpkbS09PrHdfHmzZtCviarKysgOP1eCDl5eWmx5Qu+TtSkldRUWFudRNAb6KpNyvSeWvRnFXnDwAAAACA1blC5Nq8sfO3TCjV0rTp+cUXX2x++U8++eQRxy5atEgWLFjgdzw3N9cEW1Y9YbRKTD+/lUsEAQAAAACwKleIXJsXFRWFViiVkpIiYWFhkp2dXe+4Ps7IyAj4Gj3emPHeQEr7SH3wwQc/2hdqzpw5puF63Uop7W2Vmppq6Z5SumxRP4OVT3wAAAAAAKzKFSLX5tHR0aEVSkVGRsrQoUNl9erVZgc97y9LH8+cOTPga0aOHGmev/76633H3n//fXO8YSD1ww8/yIcffijJyck/OpeoqChza0hPGCufNHriW/0zAAAAAABgZbYQuDZv7NwtE0oprU6aOnWqDBs2zOyQt3jxYrO73rRp08zzU6ZMkS5dupjldeq6666T008/XR566CE555xzZPny5fLFF1/In/70J18gddFFF8mXX35pdujTnlXeflNJSUkmCAMAAAAAAEDzs1QoNXHiRNO3ad68eSY8GjRokKxatcrXzHzXrl310rhRo0bJyy+/LHfccYfcdttt0rdvX3nzzTflxBNPNM/v3btX3nrrLfO1vlddWjV1xhlnBPXzAQAAAAAAtBc2t3bPQpNoT6nExETTjMzKPaVycnIkLS3N0iWCAAAAAABYlStErs0bm5NY9xMCAAAAAADAsgilAAAAAAAAEHSEUgAAAAAAALBGKFVQUCBPP/20zJkzR/Lz880x3cFOG4cDAAAAAAAAzb773jfffCOjR482Dat27NghM2bMkKSkJFm5cqXZ/e6FF1442rcEAAAAAABAO3PUlVKzZs2Syy+/XH744QeJjo72HR83bpx89NFHzT0/AAAAAAAAhKCjDqXWrVsnV199td/xLl26SFZWVnPNCwAAAAAAACHsqEOpqKgocTqdfsf/97//SWpqanPNCwAAAAAAACHsqEOp8847TxYuXChVVVXmsc1mM72kbr31VpkwYUJLzBEAAAAAAADtPZR66KGHpLi4WNLS0qSsrExOP/106dOnjyQkJMg999zTMrMEAAAAAABA+959T3fde//99+U///mP/Pe//zUB1ZAhQ8yOfAAAAAAAAECLhFIvvPCCTJw4UU499VRz86qsrJTly5fLlClTjvYtAQAAAAAA0M4c9fK9adOmSWFhod/xoqIi8xwAAAAAAADQ7KGU2+02zc0b2rNnj1naBwAAAAAAADTb8r3BgwebMEpvZ599toSHH3ppTU2NbN++XcaOHdvYtwMAAAAAAEA71uhQavz48eb+66+/ljFjxkh8fLzvucjISOnRo4dMmDChZWYJAAAAAACA9hlKzZ8/39xr+KSNzqOjo1tyXgAAAAAAAAhhR7373tSpU1tmJgAAAAAAAGg3jjqU0v5RjzzyiLz66quya9cuqaysrPd8fn5+c84PAAAAAAAAIeiod99bsGCBPPzww2YJX2FhocyaNUsuvPBCsdvtcuedd7bMLAEAAAAAANC+Q6mXXnpJ/vznP8uNN95oduCbPHmyPP300zJv3jz59NNPW2aWAAAAAAAAaN+hVFZWlgwYMMB8rTvwabWU+tWvfiXvvvtu888QAAAAAAAAIeeoQ6muXbvK/v37zde9e/eWf/zjH+brdevWSVRUVPPPEAAAAAAAACHnqEOpCy64QFavXm2+vvbaa2Xu3LnSt29fmTJlilxxxRUtMUcAAAAAAAC09933/vjHP/q+1mbn3bt3lzVr1phg6txzz23u+QEAAAAAACAEHXUo1dApp5xibuqLL76QYcOGNce8AAAAAAAAEMKOevlecXGxlJWV1Tv29ddfmyqpESNGNOfcAAAAAAAA0N5Dqd27d8vIkSMlMTHR3GbNmiWlpaWml5SGUXFxcWYZHwAAAAAAANBsy/duvvlmKS8vl0cffVRWrlxp7j/++GMTSG3dutXsygcAAAAAAAA0a6XURx99JE8++aTMnDlTli9fLm63Wy699FJ5/PHHgxpILV26VHr06CHR0dEmEPv888+POP61116T448/3owfMGCAvPfee/We188xb9486dSpk8TExMjo0aPlhx9+aOFPAQAAAAAA0L41OpTKzs6Wnj17mq/T0tIkNjZWfvnLX0owrVixwiwbnD9/vnz55ZcycOBAGTNmjOTk5AQcr8sJJ0+eLNOnT5evvvpKxo8fb27ffvutb8z9998vS5YskWXLlslnn31mliHqe2pVWLviPCCRezebewAAAAAA0Aqc7eva3ObWUqFGCAsLk6ysLElNTTWPHQ6H/Pe///UFVcGglVEnn3yyqc5SLpdLMjMz5dprr5XZs2f7jZ84caKUlJTIO++84zumOwUOGjTIhFD60Tt37iw33nij3HTTTeb5wsJCSU9Pl+eee04mTZrUqHk5nU7TZ0tfqz8Xy1n/vrjfeVJPBnHbbGL75QyRQWe29qwAAAAAAGg/vv5Q3H//86Fr83N/JzJktFhRY3OSRveU0gDnJz/5idhsNt8ufIMHDxa7vX6xVX5+vrSEyspKWb9+vcyZM8d3TL+3Lrdbu3ZtwNfoca2sqkuroN58803z9fbt203Qpu/hpT80Db/0tYcLpSoqKsyt7g/bG5LpzVKcB8RWG0gpc//enzw3AAAAAAAQNDbvvQZTbz8p7l4DRRzJYjWNzUYaHUo9++yz0pry8vKkpqbGVDHVpY83bdoU8DUaOAUar8e9z3uPHW5MIIsWLZIFCxb4Hc/NzbXcsj8tC0xqXLEcAAAAAAAIEpvbJQe3fi+VXX4iVlNUVNS8odTUqVObMp+QotVadSuwtFJKlxHq0kbLLd+LDvOUBdYJptw2u7ivWWzJNBYAAAAAAMtxHhDbk9f5XZt36N3Pktfmutlcs4ZSrS0lJcX0tdKG63Xp44yMjICv0eNHGu+912O6+17dMdp36nCioqLMrSFdTthwOWOb1yFV5NzfmbJATWH1pLede43Y0jJbe2YAAAAAALQP0bGBr831mt2CGpuNWCZBiYyMlKFDh8rq1avrrVHUxyNHjgz4Gj1ed7x6//33feO1SbsGU3XHaNWT7sJ3uPcMSUNGi/u6ZZJ/7g3m3qqN1AAAAAAAsKwh7e/a3DKVUkqXzOkywmHDhsnw4cNl8eLFZne9adOmmeenTJkiXbp0MT2f1HXXXSenn366PPTQQ3LOOefI8uXL5YsvvpA//cnTxFubtl9//fVy9913S9++fU1INXfuXLMj3/jx46VdcSR71qlasCwQAAAAAICQ4Ghf1+aWCqUmTpxomonPmzfPNCLXJXarVq3yNSrftWtXvRKxUaNGycsvvyx33HGH3HbbbSZ40p33TjzxRN+YW265xQRbV111lRQUFMhpp51m3rOx6x8BAAAAAABw9GxuN1uvNZUu+UtMTJTCwkLrNTqvsxQyJydH0tLSrNcXCwAAAACAEOAKkWvzxuYkjaqUqrvT3I95+OGHGz0WAAAAAAAA7VOjQqmvvvqqUW+mPZoAAAAAAACAZgmlPvzww8YMAwAAAAAAABrFugsUAQAAAAAAENqVUhdeeGGj33DlypVNmQ8AAAAAAADagUaFUtoxHQAAAAAAAAhqKPXss8822zcEAAAAAAAA6CkFAAAAAACAtlkpVVfPnj3FZrMd9vlt27Y1dU4AAAAAAAAIcUcdSl1//fX1HldVVclXX30lq1atkptvvrk55wYAAAAAAIAQddSh1HXXXRfw+NKlS+WLL75ojjkBAAAAAAAgxDVbT6lf/vKX8sYbbzTX2wEAAAAAACCENVso9frrr0tSUlJzvR0AAAAAAABC2FEv3xs8eHC9Rudut1uysrIkNzdXnnjiieaeHwAAAAAAAELQUYdS48ePr/fYbrdLamqqnHHGGXL88cc359wAAAAAAAAQoo46lJo/f37LzAQAAAAAAADtRrP1lAIAAAAAAACavVJKl+nV7SUViD5fXV3d6G8OAAAAAACA9qnRodRf//rXwz63du1aWbJkibhcruaaFwAAAAAAAEJYo0Op888/3+/Y5s2bZfbs2fL222/LpZdeKgsXLmzu+QEAAAAAACAEHVNPqX379smMGTNkwIABZrne119/Lc8//7x07969+WcIAAAAAACA9h1KFRYWyq233ip9+vSRjRs3yurVq02V1IknnthyMwQAAAAAAED7Xb53//33y3333ScZGRnyyiuvBFzOBwAAAAAAADSGze12uxu7+15MTIyMHj1awsLCDjtu5cqV0t44nU5JTEw0lWQOh0OsSJvU5+TkSFpamvldAwAAAACA4HKFyLV5Y3OSRldKTZkyRWw2W3PNDwAAAAAAAO1Yo0Op5557rmVnAgAAAAAAgHbDurVgAAAAAAAAsCxCKQAAAAAAAASdZUKp/Px8ufTSS02DrA4dOsj06dOluLj4iK8pLy+X3//+95KcnCzx8fEyYcIEyc7O9j3/3//+VyZPniyZmZmmiXu/fv3k0UcfDcKnAQAAAAAAaN8sE0ppILVx40Z5//335Z133pGPPvpIrrrqqiO+5oYbbpC3335bXnvtNfn3v/8t+/btkwsvvND3/Pr1601H+xdffNG89+233y5z5syRxx9/PAifCAAAAAAAoP2yud1ut7Rx33//vfTv31/WrVsnw4YNM8dWrVol48aNkz179kjnzp39XqPbDqampsrLL78sF110kTm2adMmUw21du1aOeWUUwJ+L62s0u/3wQcfNPtWh21ZqGw7CQAAAACAVblC5Nq8sTmJJT6hhki6ZM8bSKnRo0ebX9Bnn30W8DVaBVVVVWXGeR1//PHSrVs3836Hoz+wpKSkZv4EAAAAAAAAqCtcLCArK8ukhHWFh4eb8EifO9xrIiMjTZhVV3p6+mFfs2bNGlmxYoW8++67R5xPRUWFudVNAL2Jpt6sSOetRXNWnT8AAAAAAFbnCpFr88bOv1VDqdmzZ8t99913xDG6lC4Yvv32Wzn//PNl/vz58otf/OKIYxctWiQLFizwO56bm2uaq1v1hNEqMT35rVwiCAAAAACAVblC5Nq8qKio7YdSN954o1x++eVHHNOrVy/JyMgwayrrqq6uNjvy6XOB6PHKykopKCioVy2lu+81fM13330nZ599tmmcfscdd/zovLUZ+qxZs+pVSukOftrDyso9pWw2m/kMVj7xAQAAAACwKleIXJtHR0e3/VBKf8h6+zEjR4404ZL2iRo6dKg5po3I9Zc1YsSIgK/RcREREbJ69WqZMGGCObZ582bZtWuXeT8v3XXvrLPOkqlTp8o999zTqHlHRUWZW0N6wlj5pNET3+qfAQAAAAAAK7OFwLV5Y+duiU+oO+aNHTtWZsyYIZ9//rn85z//kZkzZ8qkSZN8O+/t3bvXNDLX55V2eZ8+fbqpaPrwww9NoDVt2jQTSHl33tMle2eeeaZZrqfjtNeU3nQZHgAAAAAAANp5o3P10ksvmSBKl9lp4qbVT0uWLPE9rzvtaSVUaWmp79gjjzziG6uNyceMGSNPPPGE7/nXX3/dBFAvvviiuXl1795dduzYEcRPBwAAAAAA0L7Y3No9C02iPaW0MkubkVm5p5T27dJdDq1cIggAAAAAgFW5QuTavLE5iXU/IQAAAAAAACyLUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAg6QikAAAAAAAAEHaEUAAAAAAAAgo5QCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNBZJpTKz8+XSy+9VBwOh3To0EGmT58uxcXFR3xNeXm5/P73v5fk5GSJj4+XCRMmSHZ2dsCxBw4ckK5du4rNZpOCgoIW+hQAAAAAAACwVCilgdTGjRvl/fffl3feeUc++ugjueqqq474mhtuuEHefvttee211+Tf//637Nu3Ty688MKAYzXkOumkk1po9gAAAAAAALBcKPX999/LqlWr5Omnn5YRI0bIaaedJo899pgsX77cBE2BFBYWyv/93//Jww8/LGeddZYMHTpUnn32WVmzZo18+umn9cY++eSTpjrqpptuCtInAgAAAAAAaN8sEUqtXbvWLNkbNmyY79jo0aPFbrfLZ599FvA169evl6qqKjPO6/jjj5du3bqZ9/P67rvvZOHChfLCCy+Y9wMAAAAAAEDLCxcLyMrKkrS0tHrHwsPDJSkpyTx3uNdERkaaMKuu9PR032sqKipk8uTJ8sADD5iwatu2bY2aj75Ob15Op9Pcu1wuc7Minbfb7bbs/AEAAAAAsDpXiFybN3b+rRpKzZ49W+67774fXbrXUubMmSP9+vWT3/zmN0f1ukWLFsmCBQv8jufm5prm6lY9YXTJo578VIwBAAAAABB8rhC5Ni8qKmr7odSNN94ol19++RHH9OrVSzIyMiQnJ6fe8erqarMjnz4XiB6vrKw0vaLqVkvp7nve13zwwQeyYcMGef31181j/aWrlJQUuf322wMGT94wa9asWfUqpTIzMyU1NdXsDmjVE193HtTPYOUTHwAAAAAAq3KFyLV5dHR02w+l9Iestx8zcuRIEy5pnyhtWO4NlPSXpY3PA9FxERERsnr1apkwYYI5tnnzZtm1a5d5P/XGG29IWVmZ7zXr1q2TK664Qj7++GPp3bv3YecTFRVlbg3pCWPlk0ZPfKt/BgAAAAAArMwWAtfmjZ27JXpK6RK7sWPHyowZM2TZsmWmgfnMmTNl0qRJ0rlzZzNm7969cvbZZ5uG5cOHD5fExESZPn26qWjS3lNawXTttdeaQOqUU04xr2kYPOXl5fm+X8NeVAAAAAAAAGg+lgil1EsvvWSCKA2eNHHT6qclS5b4ntegSiuhSktLfcceeeQR31htTD5mzBh54oknWukTAAAAAAAAwMvm9jZSwjHTnlJamaXNyKzcU0r7dukuh1YuEQQAAAAAwKpcIXJt3ticxLqfEAAAAAAAAJZFKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAi68OB/y9DjdrvNvdPpFKtyuVxSVFQk0dHRYreTVQIAAAAAEGyuELk29+Yj3rzkcAilmoGeMCozM7O1pwIAAAAAANBm8pLExMTDPm9z/1hshUYlmfv27ZOEhASx2Wxi1RRTQ7Xdu3eLw+Fo7ekAAAAAANDuOEPk2lyjJg2kOnfufMSKLyqlmoH+gLt27SqhQE96K5/4AAAAAABYnSMErs2PVCHlZd0FigAAAAAAALAsQikAAAAAAAAEHaEUjKioKJk/f765BwAAAAAAwRfVzq7NaXQOAAAAAACAoKNSCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIICjZ5BAAAAAAAdYXXewQ0s+LiYomKipKIiAgTTNlsttaeEgAAAAAA7crWrVvllVdekZKSEjnxxBPl0ksvlbaASim0mO+//14uuOACWbFihVRWVppAioopAAAAAACCZ8OGDTJq1Cj54osv5O2335bHH39c3nvvPWkLqJRCi9i5c6dMmDDBpLFaLRUdHS3nnXeeREZGUjEFAAAAAEAQZGdny8SJE2X69Oly7733Sl5enpx11lmyb98+aQuolEKzq6mpkTfeeEP69Okjn3/+uXTo0MGc/G+99RYVUwAAAAAABMnmzZvNNfjvf/978zglJUUGDhwo//3vf+V3v/uduVZvTYRSaHZhYWEmeZ0yZYo52d99911JT0/3BVMVFRUEUwAAAAAAtLDw8HApLS31LdfT6/KXXnpJ7Ha7qZpavny5XHzxxdJabG6SAbSAqqoq09zcSyukzj//fFM6eNttt5mv9fm//e1v5msAAAAAANC89Br8D3/4g1nF1LdvX/nggw9k5cqVpr2Oev755+Xuu++Wv/71r6YBerDRUwrNQhPW3bt3S2xsrKSlpUnHjh3F5XKZ9LW6utr0knrzzTdl/PjxJpnVJX4ffvihqZw6+eSTpXPnzq39EQAAAAAAsLTS0lJzi4mJMdfhumppyZIlpoeU9n7Ozc2V008/3Te+e/fu5tq9blFJMBFKocm++eYb+fWvf22CJl2apye9dvM/5ZRTfOWCGkxFRUWZyijdke+yyy4zf0A++ugjAikAAAAAAJpo48aNcv3110tWVpZ5fOWVV8rUqVPNNbre9Jpdr8vz8/MlMTHRjPnHP/4hqampptdUayCUQpPoyX7uuefKpEmTTDf/7777TlasWCE/+9nP5IUXXjDHvcGU/gHQIEqT2ISEBBNInXDCCa39EQAAAAAAsLTvv/9ezjzzTHMNrk3NtYfUU089JaNGjTKrk5QGU1u3bpXrrrtOevToYdru6PW7rmJKTk5ulXnTUwpN8vXXX5uqp7ffftuc1KqsrEzmzZtnSgR1reo555zjW8r3xBNPyMyZM2X9+vUyePDg1p4+AAAAAACWdvDgQRNG9enTR5YuXeo7PnToUBk+fLg8+eSTvmty3XVv9uzZ5rpdVy3dfvvtrVosQqUUmqSwsNCUCHqzTT3Rde3q/fffb07ySy65RL744gvTUE1NnDhRxo4dK7169WrlmQMAAAAAYH179+4Vh8Nhrre9G43pKqWzzz5bDhw4YI7ZbDazemngwIHy2muvSXx8vGm/o8v5WpO9Vb87LO+0006Tn/70pzJnzhyzLlWTVw2m9ITXY4MGDZJXXnnFhFZ6XEsCCaQAAAAAAGgeWumklVLaRsfbPkclJSVJcXGx+Vqv0cPCwsxjDaRUawdSilAKTaIntaaxO3bsMMv1nE6nCaZUly5dzMm+adMm8wfAexwAAAAAADSdtyhkwoQJ5rEWhHivvUtKSsxue166ounOO+80FVNtBcv3cMz0ZNeT/5prrjHN0nRnPV2yp2tStXRQaWVUx44dzUmvfzB0PAAAAAAAaDpvAOW9PtdbdXW1qZbSDca8u+zNnTtX7rnnHtMXWotL2goaneOYadCkJ7O3Ydpdd90l7777rhQUFMh5550nu3fvlnfeeUc+/fRTdtkDAAAAAKAFr82L6yzNU48++qh888030r17d1m0aJF88sknpvl5W8J6KjSKBk+BTvqdO3fKgAED5F//+pdJXu+77z75xS9+IRs2bDDrU9euXUsgBQAAAABAM3O73aYqynttPn78eBM8eenyvWeffdYs22uLgZRi+R5+dHc9LffzNjD3lgZ6T/pTTz1VfvWrX5mG5+r00083N/3DUXctKwAAAAAAODb79u2TdevWSXl5udndfsiQIWapni7T27Ztm5xxxhnyy1/+0ndtrjIyMkyV1HvvvSf9+vWTtojlezis7777TkaNGiW33HKL3HbbbeZY3WDqiiuukIiICFm2bJmvV5R3HSsAAAAAAGi6DRs2yAUXXGD6Nefk5JhjTzzxhJxzzjnmGnzs2LGSkpIiL774Yr3rcX0uKytLOnXqJG0VoRQC2rNnj+kLpeV+eXl5cvPNN8vs2bPrLd2rqqoyoRQAAAAAAGh+W7duNauRfvOb35hrcu3drIGU7qr3/PPPS1xcnFRWVppr87qBVN2CkraM5XvwoyfvG2+8IT179pSZM2fK559/Lvfee695Tv8QEEgBAAAAANCyKisrZenSpWYFk24sptfgHTp0kJNPPtn0dPb2fo6MjPR7rRUCKUUohYAn77hx4yQtLU3OPPNMGTRokCn702793mBK/zBYJXkFAAAAAMBq7Ha79OnTxxSM6DW4t13OWWedJQsXLjQ9oBMSEuq9xmotdQilEJA2TtOTX+m61SuvvNKc2HUrpvRkf/vtt2XkyJFm/SoAAAAAAGge4eHhppdUw55Q3sooba3jDaE2bdokxx9/vKUCKUUoBV8n/71798qBAwdk9OjRJpHVm24vqX8QNHTSxuZKgyk98XXso48+Krt27Wrt6QMAAAAAEDLX5nl5eTJmzBhJT083x73X5rpiyel0SmlpqQmnNISaM2eO3HfffXLw4EFxOByWCqYIpSDffPON/OpXvzJlf//73/9kwIABctVVV5lGavHx8b7G5qmpqTJ9+nQTSOlufLqWde3atW26kz8AAAAAAFa9Np8xY4Zcdtll5trc20JHwygNqGJiYmTBggWm79Snn34qiYmJYjU0BGrnNH2dNGmSXHLJJfLuu++aVFZL/p577jnTOK2oqMgEUt4Galox9d1335k/JJ988okMGzastT8CAAAAAAAheW3+/PPP+67NvT2dNaDSohEtJtGVTB9++KEMHz5crIhQqp3LysqSsrIyc+L36NHDnNgaSGmZ4Jo1a0wJYHl5uTn5tULqxRdflH/84x/mpO/fv39rTx8AAAAAgHZzba5yc3Nlw4YN8s4778jnn38uQ4cOFasilGrnvGtQvX2hdJ2qHtMk9vTTTzcJ7bp168xzOu7UU0+Vzz77TIYMGdLKMwcAAAAAoP1dm3fp0kVuvPFGWb9+vQwcOFCszObW8he0WxUVFXLaaadJRkaGvPnmm2apnreBmp4aeoIPHjzYlAxabWtJAAAAAABC7drcOz4qKkqsjkqpdkz7ROlJ/Oyzz8pHH30k11xzjTnuPek1gDrvvPMkJyfHHCeQAgAAAACg9a7N3bV1RaEQSClCqXZM+0TpznonnniiSVtfeeUVmTJlimRnZ/vGbN++XTp27GjGAQAAAACA1rs2d9VuQhYqWL7XjjRcfuctBSwuLjalf19//bVpqta9e3dJSkqS5ORk+dvf/iZr1641W1ECAAAAAICm4dr8ECql2oGtW7fKwYMH6530msLqSb9jxw75yU9+YhqmnX322bJx40YZN26caZyWlpZmOvmH2kkPAAAAAECwcW3uj0qpEPff//7XNEN7+umn5Yorrqj33O7du80ueueff778+c9/NmWA2kzNm9rqYy0jBAAAAAAAx45r88AIpUL8pD/11FNl5syZ8sc//tHv+ccee0y2bdsmDz/8cL2k1nvis9seAAAAAABNw7X54RFKhahNmzaZ0r558+bJ3LlzTbL6r3/9S7Zs2WKap/Xt21dSU1NDOnEFAAAAAKA1cW1+ZOE/8jwsSE/mV1991axNveiii8yxn//853LgwAGzTlWbpPXs2dOksCeddFJrTxcAAAAAgJDDtfmPa38xXDug6erVV18tM2bMMGtWNZXt0KGD2VoyNzdXHnzwQbM+9e677zbd/QEAAAAAQPPi2vzHUSkVotLT082JrV38tUu/ft2vXz/z3AUXXCA7d+6U++67TwoLCyU+Pr61pwsAAAAAQMjh2vzICKVCxL59++TLL7+UyspK6datmwwbNsysS73jjjvMSd67d28zTssGNYnt06ePdOzYUSIjI1t76gAAAAAAhASuzY8OoVQI2LBhg4wfP15SUlJMx/4ePXrILbfcIr/+9a+lU6dOkpGR4evUrye9+uc//yldu3aV2NjYVp49AAAAAADWx7X50aOnlMVt3bpVxo0bZ5qm/eMf/5BVq1bJCSecYO41eW24deSuXbvk5ptvlr/85S/y0EMPSVxcXKvOHwAAAAAAq+Pa/NjY3PqTgSVpOeCcOXNkz5495kT2lvs988wzJo3dvHmz6ebvpetXn3rqKVmzZo288sorMmjQoFacPQAAAAAA1se1+bFj+Z7Ft5fUMj9tkqYnvTd5HTVqlGmQVlVVVW/88OHDpaioSBYuXChdunRptXkDAAAAABAquDY/doRSFhYdHW3Wq/bs2bPecd1iMiIiot6Jv379ehk6dKicffbZrTBTAAAAAABCE9fmx46eUhazf/9+U+qn61I1jfWe9LpG1bs+VbeSPHjwoO818+bNk5///Ody4MABk9gCAAAAAIBjx7V586BSykK++eYbOe+88yQqKkqys7NN9349qceMGSNJSUm+EkG92e12UyZ49913y4MPPigff/xxvTWsAAAAAADg6HFt3nxodG4Rubm58rOf/UwuvPBCmT59uikPnDVrlvnDcPHFF8vvf/97SU1NNWNzcnJk7Nix8pOf/ET++te/muZpWh4IAAAAAACOHdfmzYtKKQud+OXl5ebE79Wrlzm2fPlymT17tqxcudJsH6knf2xsrCkF/Prrr2XTpk3y2WeftetO/gAAAAAANBeuzZsXPaUsQhujVVdXS2lpqXlcVlZm7v/4xz/KmWeeKU8++aRs2bLFHOvYsaP87ne/ky+//JKTHgAAAACAZsK1efNi+Z6F6LaRuhb1gw8+MI8rKirMGlZ18sknS58+feSVV14xjzW51TJCAAAAAADQfLg2bz5USrVRJSUlUlRUJE6n03fsqaeeko0bN8oll1xiHutJrwmt0jWt+hovTnoAAAAAAJqGa/OWRSjVBn333Xdmferpp58u/fr1k5deeskc168fffRRef/99+XXv/61KRvUTv7eBmq6dlX/IFD8BgAAAABA03Bt3vJodN4GT3pNVqdMmSLDhg2T9evXy7Rp06R///4yePBgs+2knuC6LvWkk06S448/XiIjI+Xdd9+VTz/9VMLD+ZUCAAAAANAUXJsHBz2l2pD8/HyZPHmyOZk1dfXSZmkDBgyQJUuW+I5p+eDdd99tXqPlgNdcc435wwEAAAAAAI4d1+bBQ3TXhmjJX0FBgVx00UXmscvlMiWAPXv2NCe40gxRbwkJCXLffffVGwcAAAAAAJqGa/Pg4afVhqSnp8uLL74oP/3pT83jmpoac9+lSxffiW2z2czXdZus6TEAAAAAANB0XJsHD6FUG9O3b19fwhoREWG+1vRVm6V5LVq0SJ5++mlfd39OfAAAAAAAmg/X5sHB8r02ShNXPeG9J7U3jZ03b55Zr/rVV1/ROA0AAAAAgBbEtXnLolKqDfP2oNcTPDMzUx588EG5//775YsvvpCBAwe29vQAAAAAAAh5XJu3HOK8NsybwGqp4J///GdxOBzyySefyJAhQ1p7agAAAAAAtAtcm7ccKqUsYMyYMeZ+zZo1MmzYsNaeDgAAAAAA7Q7X5s3P5vbWoaFNKykpkbi4uNaeBgAAAAAA7RbX5s2LUAoAAAAAAABBx/I9AAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAbcjll18uNpvN3CIiIiQ9PV1+/vOfyzPPPCMul6vR7/Pcc89Jhw4dWnSuAAAATUEoBQAA0MaMHTtW9u/fLzt27JC///3vcuaZZ8p1110nv/rVr6S6urq1pwcAANAsCKUAAADamKioKMnIyJAuXbrIkCFD5LbbbpO//e1vJqDSCij18MMPy4ABAyQuLk4yMzPld7/7nRQXF5vn/vWvf8m0adOksLDQV3V15513mucqKirkpptuMu+trx0xYoQZDwAAEGyEUgAAABZw1llnycCBA2XlypXmsd1ulyVLlsjGjRvl+eeflw8++EBuueUW89yoUaNk8eLF4nA4TMWV3jSIUjNnzpS1a9fK8uXL5ZtvvpFf//rXpjLrhx9+aNXPBwAA2h+b2+12t/YkAAAAcKinVEFBgbz55pt+z02aNMkESd99953fc6+//rr89re/lby8PPNYK6quv/56815eu3btkl69epn7zp07+46PHj1ahg8fLvfee2+LfS4AAICGwv2OAAAAoE3S/5eoS/HUP//5T1m0aJFs2rRJnE6n6TVVXl4upaWlEhsbG/D1GzZskJqaGvnJT35S77gu6UtOTg7KZwAAAPAilAIAALCI77//Xnr27GkaoGvT82uuuUbuueceSUpKkk8++USmT58ulZWVhw2ltOdUWFiYrF+/3tzXFR8fH6RPAQAA4EEoBQAAYAHaM0ornW644QYTKrlcLnnooYdMbyn16quv1hsfGRlpqqLqGjx4sDmWk5MjP/3pT4M6fwAAgIYIpQAAANoYXU6XlZVlAqTs7GxZtWqVWaqn1VFTpkyRb7/9VqqqquSxxx6Tc889V/7zn//IsmXL6r1Hjx49TGXU6tWrTYN0rZ7SZXuXXnqpeQ8NtDSkys3NNWNOOukkOeecc1rtMwMAgPaH3fcAAADaGA2hOnXqZIIl3Rnvww8/NDvt/e1vfzPL7jRkevjhh+W+++6TE088UV566SUTWtWlO/Bp4/OJEydKamqq3H///eb4s88+a0KpG2+8UY477jgZP368rFu3Trp169ZKnxYAALRX7L4HAAAAAACAoKNSCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAAECC7f8D11SrppRFFAIAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " import matplotlib.pyplot as plt\n", + "\n", + " if timeseries:\n", + " fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6), sharex=True)\n", + "\n", + " ax1.plot(dates, means, marker=\".\", color=\"steelblue\")\n", + " ax1.set_ylabel(\"Mean\")\n", + " ax1.set_title(\"conv_rate — Daily Trend\")\n", + " ax1.grid(True, alpha=0.3)\n", + "\n", + " ax2.plot(dates, null_rates, marker=\".\", color=\"coral\")\n", + " ax2.set_ylabel(\"Null Rate\")\n", + " ax2.set_xlabel(\"Date\")\n", + " ax2.grid(True, alpha=0.3)\n", + "\n", + " plt.xticks(rotation=45)\n", + " plt.tight_layout()\n", + " plt.show() # pragma: allowlist secret\n", + "except ImportError:\n", + " print(\"Install matplotlib to visualize: pip install matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 9: On-Demand Exploration (Transient Compute)\n", + "\n", + "Compute metrics for an arbitrary date range without storing them. Useful for ad-hoc investigation." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "conv_rate (numeric):\n", + " rows=905 nulls=0 null_rate=0.0000\n", + " mean=0.5041 stddev=0.1929\n", + " p50=0.4964 p95=0.8221 p99=0.9757\n", + "\n", + "avg_daily_trips (numeric):\n", + " rows=905 nulls=0 null_rate=0.0000\n", + " mean=20.1525 stddev=4.4410\n", + " p50=20.0000 p95=27.0000 p99=31.9600\n", + "\n", + "vehicle_type (categorical):\n", + " rows=905 nulls=0 null_rate=0.0000\n", + " unique_values=5\n", + " van: 194\n", + " sedan: 193\n", + " suv: 186\n", + " truck: 171\n", + " compact: 161\n", + "\n" + ] + } + ], + "source": [ + "transient_result = monitoring.compute_transient(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_names=[\"conv_rate\", \"avg_daily_trips\", \"vehicle_type\"],\n", + " start_date=date(2025, 1, 10),\n", + " end_date=date(2025, 1, 20),\n", + ")\n", + "\n", + "for fm in transient_result.get(\"metrics\", []):\n", + " print(f\"{fm['feature_name']} ({fm['feature_type']}):\")\n", + " print(f\" rows={fm['row_count']} nulls={fm['null_count']} null_rate={fm['null_rate']:.4f}\")\n", + " if fm[\"feature_type\"] == \"numeric\":\n", + " print(f\" mean={fm['mean']:.4f} stddev={fm['stddev']:.4f}\")\n", + " print(f\" p50={fm['p50']:.4f} p95={fm['p95']:.4f} p99={fm['p99']:.4f}\")\n", + " elif fm[\"feature_type\"] == \"categorical\" and fm.get(\"histogram\"):\n", + " hist = fm[\"histogram\"]\n", + " print(f\" unique_values={hist['unique_count']}\")\n", + " for entry in hist[\"values\"]:\n", + " print(f\" {entry['value']}: {entry['count']}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 10: REST API Usage\n", + "\n", + "Once the Feast registry server is running, all monitoring endpoints are available via HTTP.\n", + "\n", + "```bash\n", + "# Start the server\n", + "feast serve_registry\n", + "```\n", + "\n", + "### Compute metrics via REST" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'job_id': '077f59c5-c341-4fbb-9adc-b0111fc9228b', 'status': 'completed', 'computed_feature_views': 1, 'computed_features': 20, 'granularities': ['biweekly', 'daily', 'monthly', 'quarterly', 'weekly'], 'duration_ms': 98}\n", + "[{'project_id': 'monitoring_demo', 'feature_view_name': 'driver_stats', 'feature_name': 'conv_rate', 'metric_date': '2025-01-01', 'granularity': 'daily', 'data_source_type': 'batch', 'computed_at': '2026-04-21T13:41:42.687597+05:30', 'is_baseline': True, 'feature_type': 'numeric', 'row_count': 4922, 'null_count': 0, 'null_rate': 0.0, 'mean': 0.4988999058272324, 'stddev': 0.1975387054069251, 'min_val': 0.0, 'max_val': 1.0, 'p50': 0.4998365219303598, 'p75': 0.633892663793526, 'p90': 0.7521919750314627, 'p95': 0.825733080299169, 'p99': 0.9640086762359101, 'histogram': {'bins': [0.0, 0.05, 0.1, 0.15000000000000002, 0.2, 0.25, 0.30000000000000004, 0.35000000000000003, 0.4, 0.45, 0.5, 0.55, 0.6000000000000001, 0.65, 0.7000000000000001, 0.75, 0.8, 0.8500000000000001, 0.9, 0.9500000000000001, 1.0], 'counts': [53, 67, 75, 146, 180, 267, 355, 399, 432, 493, 505, 420, 411, 330, 283, 186, 124, 93, 46, 57], 'bin_width': 0.05}}, {'project_id': 'monitoring_demo', 'feature_view_name': 'driver_stats', 'feature_name': 'conv_rate', 'metric_date': '2025-02-28', 'granularity': 'daily', 'data_source_type': 'batch', 'computed_at': '2026-04-21T19:02:39.068597+05:30', 'is_baseline': False, 'feature_type': 'numeric', 'row_count': 104, 'null_count': 0, 'null_rate': 0.0, 'mean': 0.5201334885346333, 'stddev': 0.21216576270117404, 'min_val': 0.09993354474902831, 'max_val': 1.0, 'p50': 0.5065079886167952, 'p75': 0.6963620898617928, 'p90': 0.7809868206291576, 'p95': 0.8538056054296318, 'p99': 0.9187701931117264, 'histogram': {'bins': [0.09993354474902831, 0.1449368675115769, 0.18994019027412548, 0.23494351303667405, 0.27994683579922264, 0.32495015856177123, 0.3699534813243198, 0.4149568040868684, 0.459960126849417, 0.5049634496119656, 0.5499667723745142, 0.5949700951370628, 0.6399734178996113, 0.6849767406621599, 0.7299800634247084, 0.774983386187257, 0.8199867089498056, 0.8649900317123542, 0.9099933544749028, 0.9549966772374514, 1.0], 'counts': [4, 1, 6, 7, 5, 5, 7, 6, 11, 5, 4, 10, 5, 8, 9, 3, 4, 2, 1, 1], 'bin_width': 0.045003322762548585}}]\n", + "[{'project_id': 'monitoring_demo', 'feature_view_name': 'driver_stats', 'feature_name': 'conv_rate', 'metric_date': '2025-01-01', 'granularity': 'daily', 'data_source_type': 'batch', 'computed_at': '2026-04-21T13:41:42.687597+05:30', 'is_baseline': True, 'feature_type': 'numeric', 'row_count': 4922, 'null_count': 0, 'null_rate': 0.0, 'mean': 0.4988999058272324, 'stddev': 0.1975387054069251, 'min_val': 0.0, 'max_val': 1.0, 'p50': 0.4998365219303598, 'p75': 0.633892663793526, 'p90': 0.7521919750314627, 'p95': 0.825733080299169, 'p99': 0.9640086762359101, 'histogram': {'bins': [0.0, 0.05, 0.1, 0.15000000000000002, 0.2, 0.25, 0.30000000000000004, 0.35000000000000003, 0.4, 0.45, 0.5, 0.55, 0.6000000000000001, 0.65, 0.7000000000000001, 0.75, 0.8, 0.8500000000000001, 0.9, 0.9500000000000001, 1.0], 'counts': [53, 67, 75, 146, 180, 267, 355, 399, 432, 493, 505, 420, 411, 330, 283, 186, 124, 93, 46, 57], 'bin_width': 0.05}}]\n" + ] + } + ], + "source": [ + "# This cell is for reference — run it when the registry server is up.\n", + "\n", + "import requests\n", + "\n", + "BASE_URL = \"http://localhost:6572/api/v1\"\n", + "\n", + "# Auto-compute all metrics\n", + "resp = requests.post(f\"{BASE_URL}/monitoring/auto_compute\", json={\n", + " \"project\": \"monitoring_demo\",\n", + "})\n", + "print(resp.json())\n", + "\n", + "# Read per-feature metrics\n", + "resp = requests.get(f\"{BASE_URL}/monitoring/metrics/features\", params={\n", + " \"project\": \"monitoring_demo\",\n", + " \"feature_view_name\": \"driver_stats\",\n", + " \"feature_name\": \"conv_rate\",\n", + " \"granularity\": \"daily\",\n", + " \"data_source_type\": \"batch\",\n", + "})\n", + "print(resp.json())\n", + "\n", + "# Read baseline\n", + "resp = requests.get(f\"{BASE_URL}/monitoring/metrics/baseline\", params={\n", + " \"project\": \"monitoring_demo\",\n", + " \"feature_view_name\": \"driver_stats\",\n", + " \"feature_name\": \"conv_rate\",\n", + " \"data_source_type\": \"batch\",\n", + "})\n", + "print(resp.json())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 11: Monitoring Feature Serving Logs\n", + "\n", + "If your feature service has logging enabled, you can compute metrics from actual production traffic.\n", + "\n", + "### Define a feature service with logging" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "See the code cell above for the logging config pattern.\n", + "Once applied, log metrics can be computed with:\n", + " CLI: feast monitor run --source-type log\n", + " API: POST /monitoring/compute/log\n", + " SDK: monitoring.compute_log_metrics(project, feature_service_name)\n" + ] + } + ], + "source": [ + "# Example feature service definition with logging\n", + "#\n", + "# from feast import FeatureService, LoggingConfig\n", + "# from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import (\n", + "# PostgreSQLLoggingDestination,\n", + "# )\n", + "#\n", + "# driver_service = FeatureService(\n", + "# name=\"driver_service\",\n", + "# features=[driver_stats_fv],\n", + "# logging_config=LoggingConfig(\n", + "# destination=PostgreSQLLoggingDestination(table_name=\"feast_driver_logs\"),\n", + "# sample_rate=1.0,\n", + "# ),\n", + "# )\n", + "print(\"See the code cell above for the logging config pattern.\")\n", + "print(\"Once applied, log metrics can be computed with:\")\n", + "print(\" CLI: feast monitor run --source-type log\")\n", + "print(\" API: POST /monitoring/compute/log\")\n", + "print(\" SDK: monitoring.compute_log_metrics(project, feature_service_name)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Compute log metrics (SDK)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment when you have a feature service with logging enabled\n", + "#\n", + "# result = monitoring.compute_log_metrics(\n", + "# project=\"monitoring_demo\",\n", + "# feature_service_name=\"driver_service\",\n", + "# granularity=\"daily\",\n", + "# )\n", + "# print(result)\n", + "\n", + "# Or auto-compute all log metrics\n", + "# result = monitoring.auto_compute_log_metrics(project=\"monitoring_demo\")\n", + "# print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Read log vs. batch metrics side-by-side" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Uncomment the cell above once log metrics have been computed.\n" + ] + } + ], + "source": [ + "# Compare batch vs. log metrics for the same feature\n", + "#\n", + "# batch = monitoring.get_feature_metrics(\n", + "# project=\"monitoring_demo\",\n", + "# feature_view_name=\"driver_stats\",\n", + "# feature_name=\"conv_rate\",\n", + "# data_source_type=\"batch\",\n", + "# granularity=\"daily\",\n", + "# )\n", + "#\n", + "# log = monitoring.get_feature_metrics(\n", + "# project=\"monitoring_demo\",\n", + "# feature_view_name=\"driver_stats\",\n", + "# feature_name=\"conv_rate\",\n", + "# data_source_type=\"log\",\n", + "# granularity=\"daily\",\n", + "# )\n", + "#\n", + "# print(\"Batch metrics:\")\n", + "# for m in batch[:3]:\n", + "# print(f\" {m['metric_date']}: mean={m['mean']:.4f}\")\n", + "#\n", + "# print(\"\\nLog metrics:\")\n", + "# for m in log[:3]:\n", + "# print(f\" {m['metric_date']}: mean={m['mean']:.4f}\")\n", + "\n", + "print(\"Uncomment the cell above once log metrics have been computed.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 12: Scheduling in Production\n", + "\n", + "### Cron (simplest)\n", + "\n", + "```bash\n", + "# Compute all batch + log metrics daily at 2 AM\n", + "0 2 * * * cd /path/to/feast/repo && feast monitor run --source-type all >> /var/log/feast-monitor.log 2>&1\n", + "```\n", + "\n", + "### Airflow\n", + "\n", + "```python\n", + "from airflow.operators.bash import BashOperator\n", + "\n", + "monitor_task = BashOperator(\n", + " task_id=\"feast_monitor\",\n", + " bash_command=\"feast monitor run --source-type all\",\n", + " cwd=\"/path/to/feast/repo\",\n", + ")\n", + "```\n", + "\n", + "### Kubernetes CronJob\n", + "\n", + "```yaml\n", + "apiVersion: batch/v1\n", + "kind: CronJob\n", + "metadata:\n", + " name: feast-monitor\n", + "spec:\n", + " schedule: \"0 2 * * *\"\n", + " jobTemplate:\n", + " spec:\n", + " template:\n", + " spec:\n", + " containers:\n", + " - name: feast-monitor\n", + " image: feast-image:latest\n", + " command: [\"feast\", \"monitor\", \"run\", \"--source-type\", \"all\"]\n", + " volumeMounts:\n", + " - name: feast-repo\n", + " mountPath: /feast/repo\n", + " restartPolicy: OnFailure\n", + " volumes:\n", + " - name: feast-repo\n", + " configMap:\n", + " name: feast-repo-config\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Capability | CLI | REST API | SDK |\n", + "|-----------|-----|----------|-----|\n", + "| Auto-compute (all granularities) | `feast monitor run` | `POST /monitoring/auto_compute` | `monitoring.auto_compute_metrics()` |\n", + "| Targeted compute | `feast monitor run --feature-view X --granularity daily` | `POST /monitoring/compute` | `monitoring.compute_metrics()` |\n", + "| Set baseline | `feast monitor run --set-baseline` | `POST /monitoring/compute` (with `set_baseline: true`) | `monitoring.compute_metrics(set_baseline=True)` |\n", + "| Log metrics | `feast monitor run --source-type log` | `POST /monitoring/compute/log` | `monitoring.compute_log_metrics()` |\n", + "| On-demand exploration | — | `POST /monitoring/compute/transient` | `monitoring.compute_transient()` |\n", + "| Read metrics | — | `GET /monitoring/metrics/*` | `monitoring.get_feature_metrics()` etc. |\n", + "| Read baseline | — | `GET /monitoring/metrics/baseline` | `monitoring.get_baseline()` |\n", + "| Time-series | — | `GET /monitoring/metrics/timeseries` | `monitoring.get_timeseries()` |" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv312", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/online_store/aerospike_overrides_and_hooks/README.md b/examples/online_store/aerospike_overrides_and_hooks/README.md new file mode 100644 index 00000000000..7143b6a3dff --- /dev/null +++ b/examples/online_store/aerospike_overrides_and_hooks/README.md @@ -0,0 +1,65 @@ +# Aerospike: per-feature-view overrides + prewriting hooks + +A short companion to [`docs/reference/online-stores/aerospike.md`](../../../docs/reference/online-stores/aerospike.md) +demonstrating three deployment patterns the Aerospike online store supports +without needing any Feast extension code: + +1. **Per-feature-view namespace overrides** — pin one view to a RAM-only + namespace and another to an SSD-backed one without splitting the project. +2. **Per-feature-view set overrides** — isolate one view in its own set so + `feast apply` deletions or admin truncates only touch that view. +3. **Prewriting hooks** — apply a project-wide write-side transformation + (PII masking in this example) without sprinkling it through every + materialization job. + +Nothing here is Aerospike-specific *infrastructure* — it's all configured +in `feature_store.yaml`. This directory only adds the hook-target Python +module the YAML references. + +## Files + +| file | purpose | +|---|---| +| [`hooks.py`](hooks.py) | A pure-Python prewriting-hook module containing `hash_pii_string_features`, the same example used in the docs. Drop into any module on the writer's `PYTHONPATH`. | +| [`feature_store.yaml`](feature_store.yaml) | Reference `online_store` block showing all three features wired together. Copy the `online_store` section into your own `feature_store.yaml` — the rest is project-specific scaffolding. | + +## Prerequisites + +* Feast installed with the Aerospike extra (`pip install 'feast[aerospike]'`). +* An Aerospike cluster reachable from your writer process. The + [Aerospike online-store reference](../../../docs/reference/online-stores/aerospike.md) + shows a minimal local CE config (`127.0.0.1:3000`); run Aerospike however + you normally would (Docker, Kubernetes, bare metal). +* On every process that calls `online_write_batch` through this store + (materialization workers, the registry CLI host, the feature server if + you run one), the `FEAST_PII_SALT` environment variable must be set + before the first write — `hash_pii_string_features` raises rather than + silently writing plaintext if the salt isn't configured. +* The two namespaces referenced by `namespace_overrides` (`feast_ram` and + `feast_ssd` in the sample YAML) must already exist on the Aerospike + cluster — Aerospike cannot create namespaces at runtime. + +## Trying it out + +1. Drop `hooks.py` into a module on your `PYTHONPATH` that the writer + process can import (e.g. inside your existing feature-repo package). + The example uses the qualified path + `examples.online_store.aerospike_overrides_and_hooks.hooks.hash_pii_string_features`. +2. Copy the `online_store:` block from `feature_store.yaml` into your + own feature repo, adjusting hosts / namespaces / the hook import path + for your project. +3. `export FEAST_PII_SALT=...` (anything random and stable across + processes — rotate by re-running materialization with a new salt). +4. `feast apply` — the new config is registered. +5. Materialize as usual — the hook runs once per `online_write_batch`, + and any feature named `email`, `phone_number` or `ssn` lands in + Aerospike as a salted SHA-256 hex digest instead of plaintext. + +## Read-side note + +Prewriting hooks are **only** invoked on the write path. If your hook is +a one-way transform (hashing, encryption-without-decryption-key) you +have to apply the same transform to the candidate value at read time +yourself. Two-way transforms (deterministic encryption, Base64) need a +matching post-read step in your serving code; the Aerospike store does +not currently expose a symmetric "postreading hook". diff --git a/examples/online_store/aerospike_overrides_and_hooks/feature_store.yaml b/examples/online_store/aerospike_overrides_and_hooks/feature_store.yaml new file mode 100644 index 00000000000..bd1061f30aa --- /dev/null +++ b/examples/online_store/aerospike_overrides_and_hooks/feature_store.yaml @@ -0,0 +1,59 @@ +# Reference feature_store.yaml demonstrating all three Aerospike +# extension points wired together. Copy the `online_store:` block into +# your own feature repo and adjust hosts / namespaces / hook import +# path for your project. +# +# Prerequisites: +# - The `feast_ram` and `feast_ssd` namespaces must already exist on +# the Aerospike cluster. Aerospike cannot create namespaces at +# runtime; a missing namespace surfaces as AEROSPIKE_ERR_PARAM on +# the first read or write touching that view. +# - `FEAST_PII_SALT` must be set in every process that calls +# online_write_batch through this store. + +project: my_feature_repo +registry: data/registry.db +provider: local + +online_store: + type: aerospike + + hosts: + - ["aerospike.internal", 3000] + + # Store-level defaults. Anything not listed in *_overrides below + # falls back to these. + namespace: feast + set_name_template: "{project}_{collection_suffix}" + + # Pin individual feature views to different namespaces -- typically + # one in-memory namespace for hot, latency-sensitive views and one + # device-backed namespace for cold, wide views. + namespace_overrides: + driver_realtime_stats: feast_ram + driver_history_lookup: feast_ssd + + # Isolate one feature view in its own set so that admin operations on + # it (truncate, scan-based deletion via `feast apply`) do not touch + # the records of other views. + set_overrides: + isolated_view: my_feature_repo_isolated + + # Project-wide write-side hook. The store dynamically imports the + # callable on first use and caches it. Adjust the import path to + # whatever module is on your writers' PYTHONPATH; the value below + # assumes you have the example folder on PYTHONPATH from the + # repository root. + prewriting_hook: examples.online_store.aerospike_overrides_and_hooks.hooks.hash_pii_string_features + + # Standard timing knobs (optional -- shown for completeness). + ttl_seconds: 86400 + read_timeout_ms: 150 + write_timeout_ms: 300 + batch_total_timeout_ms: 500 + socket_timeout_ms: 50 + max_retries: 2 + +# Offline store / entity_key_serialization_version / etc. are +# project-specific and intentionally omitted; this file is a snippet, +# not a runnable repo. diff --git a/examples/online_store/aerospike_overrides_and_hooks/hooks.py b/examples/online_store/aerospike_overrides_and_hooks/hooks.py new file mode 100644 index 00000000000..15e6f8a10c7 --- /dev/null +++ b/examples/online_store/aerospike_overrides_and_hooks/hooks.py @@ -0,0 +1,109 @@ +"""Sample prewriting hooks for the Feast Aerospike online store. + +Reference the callable from ``feature_store.yaml`` via its import string, +e.g.:: + + online_store: + type: aerospike + ... + prewriting_hook: examples.online_store.aerospike_overrides_and_hooks.hooks.hash_pii_string_features + +The Aerospike online store invokes the configured callable once per +``online_write_batch`` call, passing the rows about to be written. The +callable must return a row list with the same schema. Returning ``[]`` +short-circuits the write — same path as an empty input, no wire call is +issued. +""" + +from __future__ import annotations + +import hashlib +import os +from datetime import datetime +from typing import List, Optional, Tuple + +from feast import FeatureView +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RepoConfig + +# Names of features that must never reach the online store as plaintext. +# Match is by exact feature name; tweak to your project's conventions +# (regex, suffix-based, FV-tag-driven, etc.). +_SENSITIVE_FEATURES = frozenset({"email", "phone_number", "ssn"}) + +# Type alias for the per-row payload Feast hands to ``online_write_batch``. +WriteRow = Tuple[ + EntityKeyProto, + dict, + datetime, + Optional[datetime], +] + + +def hash_pii_string_features( + config: RepoConfig, + table: FeatureView, + data: List[WriteRow], +) -> List[WriteRow]: + """Replace any sensitive string feature with a salted SHA-256 hex digest. + + Determinism: same plaintext + same ``FEAST_PII_SALT`` → same digest. + Downstream lookups that hash the candidate value the same way still + hit; lookups against the raw plaintext silently miss. + + Safety: an unset salt raises rather than falling back to plaintext. + Set ``FEAST_PII_SALT`` on every process that materialises features + (workers, registry CLI host, feature server). + """ + salt = os.environ.get("FEAST_PII_SALT") + if salt is None: + raise RuntimeError( + "FEAST_PII_SALT is not set; refusing to write feature batches " + "without a configured PII salt." + ) + salt_bytes = salt.encode("utf-8") + + def _digest(plaintext: str) -> str: + h = hashlib.sha256() + h.update(salt_bytes) + h.update(plaintext.encode("utf-8")) + return h.hexdigest() + + transformed: List[WriteRow] = [] + for entity_key, values, event_ts, created_ts in data: + new_values = dict(values) + for feature_name in _SENSITIVE_FEATURES.intersection(new_values): + v: ValueProto = new_values[feature_name] + if v.HasField("string_val") and v.string_val: + new_values[feature_name] = ValueProto(string_val=_digest(v.string_val)) + transformed.append((entity_key, new_values, event_ts, created_ts)) + return transformed + + +def drop_rows_with_negative_amounts( + config: RepoConfig, + table: FeatureView, + data: List[WriteRow], +) -> List[WriteRow]: + """Defensive sample hook: filter rows whose ``amount`` feature is < 0. + + Demonstrates that hooks can also *remove* rows. Returning an empty + list short-circuits the wire call entirely — useful for emergency + feature-write quarantines without a code deploy. + """ + keep: List[WriteRow] = [] + for entity_key, values, event_ts, created_ts in data: + amount: Optional[ValueProto] = values.get("amount") + if ( + amount is not None + and amount.HasField("double_val") + and amount.double_val < 0 + ): + continue + if amount is not None and amount.HasField("float_val") and amount.float_val < 0: + continue + if amount is not None and amount.HasField("int64_val") and amount.int64_val < 0: + continue + keep.append((entity_key, values, event_ts, created_ts)) + return keep diff --git a/examples/rag-retriever/README.md b/examples/rag-retriever/README.md index 4c9eb9bf8c2..7df89957cfa 100644 --- a/examples/rag-retriever/README.md +++ b/examples/rag-retriever/README.md @@ -62,6 +62,59 @@ Navigate to the examples/rag-retriever directory. Here you will find the followi Open `rag_feast.ipynb` and follow the steps in the notebook to run the example. +## Using DocEmbedder for Simplified Ingestion + +As an alternative to the manual data preparation steps in the notebook above, Feast provides the `DocEmbedder` class that automates the entire document-to-embeddings pipeline: chunking, embedding generation, FeatureView creation, and writing to the online store. + +### Install Dependencies + +```bash +pip install feast[milvus,rag] +``` + +### Quick Start + +```python +from feast import DocEmbedder +from datasets import load_dataset + +# Load your dataset +dataset = load_dataset("facebook/wiki_dpr", "psgs_w100.nq.exact", split="train[:1%]", + with_index=False, trust_remote_code=True) +df = dataset.select(range(100)).to_pandas() + +# DocEmbedder handles everything in one step +embedder = DocEmbedder( + repo_path="feature_repo_docembedder/", + feature_view_name="text_feature_view", +) + +result = embedder.embed_documents( + documents=df, + id_column="id", + source_column="text", + column_mapping=("text", "text_embedding"), +) +``` + +### What DocEmbedder Does + +1. **Generates a FeatureView**: Automatically creates a Python file with Entity and FeatureView definitions compatible with `feast apply` +2. **Applies the repo**: Registers the FeatureView in the Feast registry and deploys infrastructure (e.g., Milvus collection) +3. **Chunks documents**: Splits text into smaller passages using `TextChunker` (configurable chunk size, overlap, etc.) +4. **Generates embeddings**: Produces vector embeddings using `MultiModalEmbedder` (defaults to `all-MiniLM-L6-v2`) +5. **Writes to online store**: Stores the processed data in your configured online store (e.g., Milvus) + +### Customization + +* **Custom Chunker**: Subclass `BaseChunker` for your own chunking strategy +* **Custom Embedder**: Subclass `BaseEmbedder` to use a different embedding model +* **Logical Layer Function**: Provide a `SchemaTransformFn` to control how the output maps to your FeatureView schema + +### Example Notebook + +See **`rag_feast_docembedder.ipynb`** for a complete end-to-end example that uses DocEmbedder with the Wiki DPR dataset and then queries the results using `FeastRAGRetriever`. + ## FeastRagRetriver Low Level Design Low level design for feast rag retriever diff --git a/examples/rag-retriever/rag_feast_docembedder.ipynb b/examples/rag-retriever/rag_feast_docembedder.ipynb new file mode 100644 index 00000000000..47728fef556 --- /dev/null +++ b/examples/rag-retriever/rag_feast_docembedder.ipynb @@ -0,0 +1,648 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "2855fd1a", + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install --quiet feast[milvus] sentence-transformers datasets\n", + "# %pip install bigtree==0.19.2\n", + "# %pip install marshmallow==3.10.0 " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3bb14cf1", + "metadata": {}, + "outputs": [], + "source": [ + "from datasets import load_dataset\n", + "# load wikipedia dataset - 1% of the training split\n", + "dataset = load_dataset(\n", + " \"facebook/wiki_dpr\",\n", + " \"psgs_w100.nq.exact\",\n", + " split=\"train[:1%]\",\n", + " with_index=False,\n", + " trust_remote_code=True,\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "92a5e18c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idtexttitleembeddings
01Aaron Aaron ( or ; \"Ahärôn\") is a prophet, hig...Aaron[0.013342111, 0.58217376, -0.31309745, -0.6991...
12God at Sinai granted Aaron the priesthood for ...Aaron[-0.19236332, 0.539003, -0.5652932, -0.5195250...
23his rod turn into a snake. Then he stretched o...Aaron[-0.23045847, 0.28877887, -0.3449004, -0.14077...
34however, Aaron and Hur remained below to look ...Aaron[0.107315615, 0.5992388, -0.37498242, -0.53419...
45Aaron and his sons to the priesthood, and arra...Aaron[0.32623303, 0.51600194, -0.5568064, -0.494033...
\n", + "
" + ], + "text/plain": [ + " id text title \\\n", + "0 1 Aaron Aaron ( or ; \"Ahärôn\") is a prophet, hig... Aaron \n", + "1 2 God at Sinai granted Aaron the priesthood for ... Aaron \n", + "2 3 his rod turn into a snake. Then he stretched o... Aaron \n", + "3 4 however, Aaron and Hur remained below to look ... Aaron \n", + "4 5 Aaron and his sons to the priesthood, and arra... Aaron \n", + "\n", + " embeddings \n", + "0 [0.013342111, 0.58217376, -0.31309745, -0.6991... \n", + "1 [-0.19236332, 0.539003, -0.5652932, -0.5195250... \n", + "2 [-0.23045847, 0.28877887, -0.3449004, -0.14077... \n", + "3 [0.107315615, 0.5992388, -0.37498242, -0.53419... \n", + "4 [0.32623303, 0.51600194, -0.5568064, -0.494033... " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset.column_names\n", + "df = dataset.select(range(100)).to_pandas()\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "088eaf84", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "import os\n", + "\n", + "\n", + "def write_feature_store_yaml(file_path: str, project_name: str) -> str:\n", + " \"\"\"\n", + " Write a feature_store.yaml file to the specified path.\n", + "\n", + " Args:\n", + " file_path: Full path where the YAML file should be written\n", + " (e.g. \"feature_repo/feature_store.yaml\").\n", + " project_name: The project name to use in the YAML.\n", + "\n", + " Returns:\n", + " The absolute path of the written file.\n", + " \"\"\"\n", + " config = {\n", + " \"project\": project_name,\n", + " \"provider\": \"local\",\n", + " \"registry\": \"data/registry.db\",\n", + " \"online_store\": {\n", + " \"type\": \"milvus\",\n", + " \"host\": \"http://localhost\",\n", + " \"port\": 19530,\n", + " \"vector_enabled\": True,\n", + " \"embedding_dim\": 384,\n", + " \"index_type\": \"FLAT\",\n", + " \"metric_type\": \"COSINE\",\n", + " },\n", + " \"offline_store\": {\n", + " \"type\": \"file\",\n", + " },\n", + " \"entity_key_serialization_version\": 3,\n", + " \"auth\": {\n", + " \"type\": \"no_auth\",\n", + " },\n", + " }\n", + "\n", + " os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)\n", + "\n", + " with open(file_path, \"w\") as f:\n", + " yaml.dump(config, f, default_flow_style=False, sort_keys=False)\n", + "\n", + " return os.path.abspath(file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f951c804", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mkdir: feature_repo_docebedder: File exists\n", + "/Users/chpatel/projects/feast/examples/rag-retriever\n" + ] + } + ], + "source": [ + "%mkdir feature_repo_docebedder\n", + "!pwd" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "27be0f7e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "YAML written to: /Users/chpatel/projects/feast/examples/rag-retriever/feature_repo_docebedder/feature_store.yaml\n" + ] + } + ], + "source": [ + "path = write_feature_store_yaml(\"feature_repo_docebedder/feature_store.yaml\", \"my_project\")\n", + "print(f\"YAML written to: {path}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a19428c3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "No project found in the repository. Using project name my_project defined in feature_store.yaml\n", + "Applying changes for project my_project\n", + "Connecting to Milvus remotely at http://localhost:19530\n", + "Deploying infrastructure for \u001b[1m\u001b[32mtext_feature_view\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chpatel/projects/feast/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py:86: UserWarning: Field name \"vector_enabled\" in \"MilvusOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\n", + " class MilvusOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig):\n" + ] + } + ], + "source": [ + "from feast import DocEmbedder\n", + "\n", + "de = DocEmbedder(repo_path=\"feature_repo_docebedder\", feature_view_name=\"text_feature_view\",yaml_file=\"feature_store.yaml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ed217e95", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b19b54476308402eaa251a88fc098300", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Batches: 0%| | 0/4 [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
passage_idtextembeddingevent_timestampsource_id
01_0Aaron Aaron ( or ; \"Ahärôn\") is a prophet, hig...[0.002557202707976103, 0.12003513425588608, -0...2026-02-11 12:26:29.098091+00:001
11_1Israelites, Aaron served as his brother's spok...[-0.01853535883128643, 0.13290095329284668, -0...2026-02-11 12:26:29.098091+00:001
22_0God at Sinai granted Aaron the priesthood for ...[0.014343681745231152, 0.10290483385324478, -0...2026-02-11 12:26:29.098091+00:002
32_1could not speak well, God appointed Aaron as M...[0.0504433810710907, 0.1175316572189331, -0.00...2026-02-11 12:26:29.098091+00:002
43_0his rod turn into a snake. Then he stretched o...[-0.06228446215391159, 0.10652626305818558, 0....2026-02-11 12:26:29.098091+00:003
..................
19598_1State College before entering Columbia Univers...[0.03597380220890045, 0.04296444356441498, 0.0...2026-02-11 12:26:29.098091+00:0098
19699_0joined the Merchant Marine to earn money to co...[0.05798682942986488, -0.007653537206351757, -...2026-02-11 12:26:29.098091+00:0099
19799_1spent several months in a mental institution a...[0.05905637890100479, 0.030195411294698715, -0...2026-02-11 12:26:29.098091+00:0099
198100_0harboring stolen goods in his dorm room. It wa...[-0.005938616115599871, 0.02653227001428604, -...2026-02-11 12:26:29.098091+00:00100
199100_1Eugene to party meetings. Ginsberg later said ...[0.007752032019197941, 0.06832979619503021, 0....2026-02-11 12:26:29.098091+00:00100
\n", + "

200 rows × 5 columns

\n", + "" + ], + "text/plain": [ + " passage_id text \\\n", + "0 1_0 Aaron Aaron ( or ; \"Ahärôn\") is a prophet, hig... \n", + "1 1_1 Israelites, Aaron served as his brother's spok... \n", + "2 2_0 God at Sinai granted Aaron the priesthood for ... \n", + "3 2_1 could not speak well, God appointed Aaron as M... \n", + "4 3_0 his rod turn into a snake. Then he stretched o... \n", + ".. ... ... \n", + "195 98_1 State College before entering Columbia Univers... \n", + "196 99_0 joined the Merchant Marine to earn money to co... \n", + "197 99_1 spent several months in a mental institution a... \n", + "198 100_0 harboring stolen goods in his dorm room. It wa... \n", + "199 100_1 Eugene to party meetings. Ginsberg later said ... \n", + "\n", + " embedding \\\n", + "0 [0.002557202707976103, 0.12003513425588608, -0... \n", + "1 [-0.01853535883128643, 0.13290095329284668, -0... \n", + "2 [0.014343681745231152, 0.10290483385324478, -0... \n", + "3 [0.0504433810710907, 0.1175316572189331, -0.00... \n", + "4 [-0.06228446215391159, 0.10652626305818558, 0.... \n", + ".. ... \n", + "195 [0.03597380220890045, 0.04296444356441498, 0.0... \n", + "196 [0.05798682942986488, -0.007653537206351757, -... \n", + "197 [0.05905637890100479, 0.030195411294698715, -0... \n", + "198 [-0.005938616115599871, 0.02653227001428604, -... \n", + "199 [0.007752032019197941, 0.06832979619503021, 0.... \n", + "\n", + " event_timestamp source_id \n", + "0 2026-02-11 12:26:29.098091+00:00 1 \n", + "1 2026-02-11 12:26:29.098091+00:00 1 \n", + "2 2026-02-11 12:26:29.098091+00:00 2 \n", + "3 2026-02-11 12:26:29.098091+00:00 2 \n", + "4 2026-02-11 12:26:29.098091+00:00 3 \n", + ".. ... ... \n", + "195 2026-02-11 12:26:29.098091+00:00 98 \n", + "196 2026-02-11 12:26:29.098091+00:00 99 \n", + "197 2026-02-11 12:26:29.098091+00:00 99 \n", + "198 2026-02-11 12:26:29.098091+00:00 100 \n", + "199 2026-02-11 12:26:29.098091+00:00 100 \n", + "\n", + "[200 rows x 5 columns]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "de.embed_documents(documents=df, id_column=\"id\", source_column=\"text\", column_mapping= (\"text\", \"text_embedding\"))" + + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b25b69df", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/chpatel/projects/feast/examples/rag-retriever/feature_repo_docebedder\n" + ] + } + ], + "source": [ + "%cd feature_repo_docebedder" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5bf57671", + "metadata": {}, + "outputs": [], + "source": [ + "from feast import FeatureStore\n", + "import pandas as pd\n", + "\n", + "store = FeatureStore(repo_path=\".\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2bd1f1da", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "92de6524b74c4a98ac1b56668926681a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading checkpoint shards: 0%| | 0/2 [00:00:241: DeprecationWarning: builtin type SwigPyPacked has no __module__ attribute\n", + ":241: DeprecationWarning: builtin type SwigPyObject has no __module__ attribute\n", + ":241: DeprecationWarning: builtin type swigvarlink has no __module__ attribute\n" + ] + } + ], + "source": [ + "import sys\n", + "sys.path.append(\"..\")\n", + "from text_feature_view import text_feature_view\n", + "from feast.vector_store import FeastVectorStore\n", + "from feast.rag_retriever import FeastIndex, FeastRAGRetriever\n", + "\n", + "generator_config=generator_model.config\n", + "question_encoder = AutoModel.from_pretrained(\"sentence-transformers/all-MiniLM-L6-v2\")\n", + "question_encoder_tokenizer = AutoTokenizer.from_pretrained(\"sentence-transformers/all-MiniLM-L6-v2\")\n", + "\n", + "\n", + "query_encoder_config = {\n", + " \"model_type\": \"bert\",\n", + " \"hidden_size\": 384\n", + "}\n", + "\n", + "vector_store = FeastVectorStore(\n", + " repo_path=\".\",\n", + " rag_view=text_feature_view,\n", + " features=[\"text_feature_view:text\", \"text_feature_view:embedding\", \"text_feature_view:passage_id\",\"text_feature_view:source_id\"]\n", + ")\n", + "\n", + "feast_index = FeastIndex()\n", + "\n", + "config = RagConfig(\n", + " question_encoder=query_encoder_config,\n", + " generator=generator_config.to_dict(),\n", + " index=feast_index\n", + ")\n", + "retriever = FeastRAGRetriever(\n", + " question_encoder=question_encoder,\n", + " question_encoder_tokenizer=question_encoder_tokenizer,\n", + " generator_tokenizer=generator_tokenizer,\n", + " feast_repo_path=\".\",\n", + " feature_view=vector_store.rag_view,\n", + " features=vector_store.features,\n", + " generator_model=generator_model, \n", + " search_type=\"vector\",\n", + " id_field=\"passage_id\",\n", + " text_field=\"text\",\n", + " config=config,\n", + " index=feast_index,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "09793dd4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connecting to Milvus remotely at http://localhost:19530\n", + "Generated Answer: Context: \n", + "\n", + "Question: What is the capital of Ireland?\n", + "\n", + "Answer: The capital of Ireland is Dublin.\n", + "\n", + "Context: \n", + "\n", + "Question: What is the capital of Ireland?\n", + "\n", + "Answer: The capital of Ireland is Dublin.\n", + "\n", + "Context: \n", + "\n", + "Question: What is the capital city of Australia?\n", + "\n", + "Answer: The capital city of Australia is Canberra.\n", + "\n", + "Context: \n", + "\n", + "Question: What is the capital of Ireland?\n", + "\n", + "Answer: The capital of I\n" + ] + } + ], + "source": [ + "query = \"What is the capital of Ireland?\"\n", + "answer = retriever.generate_answer(query, top_k=10)\n", + "print(\"Generated Answer:\", answer)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/ray-llm-posttrain/.gitignore b/examples/ray-llm-posttrain/.gitignore new file mode 100644 index 00000000000..4bf1a46bf0e --- /dev/null +++ b/examples/ray-llm-posttrain/.gitignore @@ -0,0 +1,13 @@ +# Feast / Ray local artifacts +data/ +.feast/ +ray_storage/ +/tmp/ray/ +ray_results/ +.ray/ + +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ +.env diff --git a/examples/ray-llm-posttrain/README.md b/examples/ray-llm-posttrain/README.md new file mode 100644 index 00000000000..64252a5a603 --- /dev/null +++ b/examples/ray-llm-posttrain/README.md @@ -0,0 +1,35 @@ +# How to Use Feast for SLM/LLM Post-Training (with Ray) + +| Name | Type | Fields | +|---|---|---| +| `web_documents` | FeatureView | `human`, `bot`, `human_repeat_ratio`, `bot_repeat_ratio` | +| `train_example` | OnDemandFeatureView | `cleaned_human`, `cleaned_bot`, `char_count`, `is_trainable`, `sft_text` | +| `llm_posttrain` | FeatureService | `web_documents` + `train_example` | + +Source data is **prepared parquet** (`document_id` + `event_timestamp` already present). No Feast core patches. + +## Paths + +| Flag | What happens | +|---|---| +| (default) | `to_ray_dataset()` + preprocess `sft_text` (ODFV does **not** run) | +| `--via-df` | `to_df()` so ODFV `train_example` runs | + +## Setup + +```bash +uv pip install -e "../../sdk/python[ray]" -r requirements.txt +PYTHONPATH=../../sdk/python python scripts/prepare_data.py +cd feature_repo && feast apply && cd .. +``` + +## Run (data load only) + +```bash +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df +``` + +## Blog + +[How to Use Feast for SLM/LLM Post-Training with Ray](/blog/feast-ray-llm-posttrain) diff --git a/examples/ray-llm-posttrain/feature_repo/feature_definitions.py b/examples/ray-llm-posttrain/feature_repo/feature_definitions.py new file mode 100644 index 00000000000..a85470c3729 --- /dev/null +++ b/examples/ray-llm-posttrain/feature_repo/feature_definitions.py @@ -0,0 +1,108 @@ +"""Feast feature definitions for Ray + ODFV LLM post-training. + +Pipeline (supported Feast APIs only): + scripts/prepare_data.py → parquet with document_id + event_timestamp + → RaySource (parquet) + → FeatureView web_documents + → OnDemandFeatureView train_example + → FeatureService llm_posttrain +""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +from feast import Entity, FeatureService, FeatureView, Field, ValueType +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Bool, Float64, Int64, String + +_REPO_DIR = Path(__file__).resolve().parent +_PARQUET = str(_REPO_DIR / "data" / "tiny_webtext.parquet") + +document = Entity( + name="document", + join_keys=["document_id"], + value_type=ValueType.STRING, + description="Document id (added by scripts/prepare_data.py)", +) + +# Parquet already has document_id + event_timestamp (see prepare_data.py). +# Do not rely on BatchFeatureView UDFs to invent timestamps during entity-less +# retrieval — that path is not supported without Feast core changes. +tiny_web = RaySource( + name="tiny_webtext", + reader_type="parquet", + path=_PARQUET, + timestamp_field="event_timestamp", +) + +web_documents = FeatureView( + name="web_documents", + entities=[document], + ttl=timedelta(days=365), + schema=[ + Field(name="human", dtype=String), + Field(name="bot", dtype=String), + Field(name="human_repeat_ratio", dtype=Float64), + Field(name="bot_repeat_ratio", dtype=Float64), + ], + source=tiny_web, + online=False, + description="Conversation columns from prepared parquet", + tags={"use_case": "llm_posttrain", "source": "parquet"}, +) + + +@on_demand_feature_view( + sources=[web_documents], + schema=[ + Field(name="cleaned_human", dtype=String), + Field(name="cleaned_bot", dtype=String), + Field(name="char_count", dtype=Int64), + Field(name="is_trainable", dtype=Bool), + Field(name="sft_text", dtype=String), + ], + mode="pandas", +) +def train_example(inputs): + """Quality gate + human→bot SFT formatting.""" + import pandas as pd + + min_chars = 64 + max_repeat_ratio = 0.65 + + cleaned_human = inputs["human"].fillna("").astype(str).str.strip() + cleaned_bot = inputs["bot"].fillna("").astype(str).str.strip() + char_count = cleaned_bot.str.len().astype("int64") + + human_ratio = inputs["human_repeat_ratio"].fillna(1.0).astype(float) + bot_ratio = inputs["bot_repeat_ratio"].fillna(1.0).astype(float) + is_trainable = ( + (char_count >= min_chars) + & (human_ratio <= max_repeat_ratio) + & (bot_ratio <= max_repeat_ratio) + ) + + sft_text = ( + "<|im_start|>user\n" + cleaned_human + "<|im_end|>\n" + "<|im_start|>assistant\n" + cleaned_bot + "<|im_end|>" + ) + + return pd.DataFrame( + { + "cleaned_human": cleaned_human, + "cleaned_bot": cleaned_bot, + "char_count": char_count, + "is_trainable": is_trainable, + "sft_text": sft_text, + } + ) + + +llm_posttrain = FeatureService( + name="llm_posttrain", + features=[web_documents, train_example], + tags={"use_case": "llm_posttrain", "model": "gpt2"}, +) diff --git a/examples/ray-llm-posttrain/feature_repo/feature_store.yaml b/examples/ray-llm-posttrain/feature_repo/feature_store.yaml new file mode 100644 index 00000000000..226dbdfb45c --- /dev/null +++ b/examples/ray-llm-posttrain/feature_repo/feature_store.yaml @@ -0,0 +1,25 @@ +project: ray_llm_posttrain +registry: data/registry.db +provider: local + +# Laptop-friendly Ray offline store (no KubeRay) +offline_store: + type: ray + storage_path: data/ray_storage + enable_ray_logging: false + ray_conf: + num_cpus: 2 + object_store_memory: 104857600 + _memory: 524288000 + +batch_engine: + type: ray.engine + max_workers: 2 + +online_store: + type: sqlite + path: data/online_store.db + +entity_key_serialization_version: 3 +auth: + type: no_auth diff --git a/examples/ray-llm-posttrain/requirements.txt b/examples/ray-llm-posttrain/requirements.txt new file mode 100644 index 00000000000..4a82b391c07 --- /dev/null +++ b/examples/ray-llm-posttrain/requirements.txt @@ -0,0 +1,8 @@ +# Feast + Ray offline store / compute engine +feast[ray]>=0.50.0 +datasets>=2.19.0 + +# Short GPT-2 SFT +transformers>=4.40.0 +torch>=2.1.0 +accelerate>=0.30.0 diff --git a/examples/ray-llm-posttrain/scripts/prepare_data.py b/examples/ray-llm-posttrain/scripts/prepare_data.py new file mode 100644 index 00000000000..efd1a5e032b --- /dev/null +++ b/examples/ray-llm-posttrain/scripts/prepare_data.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Prepare a small local parquet seed for the example. + +Hugging Face tiny-webtext has no document_id / event_timestamp. Feast entity-less +retrieval needs those columns on the *source* data. We synthesize them here +(outside Feast) and write parquet — no Feast core changes required. + + PYTHONPATH=../../sdk/python python scripts/prepare_data.py +""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +import pandas as pd + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUT_PATH = REPO_ROOT / "feature_repo" / "data" / "tiny_webtext.parquet" +SPLIT = "train[:2000]" +DATASET = "nampdn-ai/tiny-webtext" + + +def main() -> int: + from datasets import load_dataset + + print(f"Loading {DATASET} split={SPLIT!r}...") + ds = load_dataset(DATASET, split=SPLIT) + df = ds.to_pandas() + + demo_base_ts = pd.Timestamp("2024-06-01", tz="UTC") + demo_window_seconds = 30 * 24 * 3600 + + humans = df["human"].fillna("").astype(str) + bots = df["bot"].fillna("").astype(str) + doc_ids: list[str] = [] + timestamps: list[pd.Timestamp] = [] + for human, bot in zip(humans, bots, strict=True): + digest = hashlib.sha256(f"{human}\n{bot}".encode()).hexdigest() + doc_ids.append(digest[:16]) + offset = int(digest[:8], 16) % demo_window_seconds + timestamps.append(demo_base_ts + pd.Timedelta(seconds=offset)) + + df = df.copy() + df["document_id"] = doc_ids + df["event_timestamp"] = timestamps + + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + df.to_parquet(OUT_PATH, index=False) + print(f"Wrote {len(df)} rows → {OUT_PATH}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/ray-llm-posttrain/scripts/train_sft.py b/examples/ray-llm-posttrain/scripts/train_sft.py new file mode 100644 index 00000000000..a3f4939f855 --- /dev/null +++ b/examples/ray-llm-posttrain/scripts/train_sft.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Feast conversation features → training rows (paths match the blog). + +Paths: + A) Default: get_historical_features → to_ray_dataset() → preprocess sft_text + (ODFVs do NOT run on to_ray_dataset) + B) --via-df: get_historical_features → to_df() (ODFV train_example runs) + + PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run + PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df +""" + +from __future__ import annotations + +import argparse +import sys +from datetime import datetime, timezone +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +FEATURE_REPO = REPO_ROOT / "feature_repo" +DATA_DIR = REPO_ROOT / "data" + +# Matches the blog Option A snippet (length gate) +_MIN_CHARS = 64 + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--max-steps", type=int, default=20) + parser.add_argument("--batch-size", type=int, default=2) + parser.add_argument("--max-length", type=int, default=256) + parser.add_argument( + "--output-dir", + type=Path, + default=DATA_DIR / "gpt2-sft", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print a few training rows; skip the optional GPT-2 smoke", + ) + parser.add_argument( + "--via-df", + action="store_true", + help="Use to_df() so OnDemandFeatureView train_example runs", + ) + return parser.parse_args() + + +def _date_window() -> tuple[datetime, datetime]: + return ( + datetime(2024, 6, 1, tzinfo=timezone.utc), + datetime(2024, 7, 1, tzinfo=timezone.utc), + ) + + +def _preprocess_sft_batch(batch): + """Build sft_text from FeatureView columns (blog Option A).""" + import pandas as pd + + if not isinstance(batch, pd.DataFrame): + batch = pd.DataFrame(batch) + + human = batch["human"].fillna("").astype(str).str.strip() + bot = batch["bot"].fillna("").astype(str).str.strip() + ok = bot.str.len() >= _MIN_CHARS + sft_text = ( + "<|im_start|>user\n" + human + "<|im_end|>\n" + "<|im_start|>assistant\n" + bot + "<|im_end|>" + ) + return pd.DataFrame({"sft_text": sft_text}).loc[ok].reset_index(drop=True) + + +def retrieve_via_ray_stream(): + """Option A: to_ray_dataset() + preprocess (ODFV does not run).""" + from feast import FeatureStore + + store = FeatureStore(repo_path=str(FEATURE_REPO)) + start_date, end_date = _date_window() + + print("get_historical_features → to_ray_dataset() (preprocess sft_text on Ray)") + job = store.get_historical_features( + features=[ + "web_documents:human", + "web_documents:bot", + "web_documents:human_repeat_ratio", + "web_documents:bot_repeat_ratio", + ], + start_date=start_date, + end_date=end_date, + ) + ds = job.to_ray_dataset() + return ds.map_batches(_preprocess_sft_batch, batch_format="pandas") + + +def retrieve_via_df(): + """Option B: to_df() so ODFV train_example runs, then Ray from pandas.""" + import ray + from feast import FeatureStore + + store = FeatureStore(repo_path=str(FEATURE_REPO)) + start_date, end_date = _date_window() + + print("get_historical_features → to_df() (ODFV train_example runs)") + df = store.get_historical_features( + features=store.get_feature_service("llm_posttrain"), + start_date=start_date, + end_date=end_date, + ).to_df() + + if "is_trainable" not in df.columns or "sft_text" not in df.columns: + raise RuntimeError("Expected ODFV columns is_trainable / sft_text from to_df()") + + mask = df["is_trainable"].fillna(False).astype(bool) + mask &= df["sft_text"].fillna("").astype(str).str.len() > 0 + slim = df.loc[mask, ["sft_text"]].reset_index(drop=True) + return ray.data.from_pandas(slim) + + +def train_gpt2_optional( + ray_ds, *, max_steps: int, batch_size: int, max_length: int, output_dir: Path +) -> None: + import torch + from transformers import ( + AutoModelForCausalLM, + AutoTokenizer, + DataCollatorForLanguageModeling, + Trainer, + TrainingArguments, + ) + + rows = ray_ds.take(min(500, max(50, max_steps * batch_size * 4))) + texts = [r["sft_text"] for r in rows if r.get("sft_text")] + if not texts: + raise RuntimeError("No trainable SFT rows") + + print(f"[optional] GPT-2 smoke on {len(texts)} rows, {max_steps} steps...") + tokenizer = AutoTokenizer.from_pretrained("gpt2") + if tokenizer.pad_token is None: + tokenizer.pad_token = tokenizer.eos_token + model = AutoModelForCausalLM.from_pretrained("gpt2") + encodings = tokenizer( + texts, + truncation=True, + max_length=max_length, + padding="max_length", + return_tensors="pt", + ) + + class _TextDataset(torch.utils.data.Dataset): + def __len__(self) -> int: + return encodings["input_ids"].shape[0] + + def __getitem__(self, idx: int) -> dict: + return { + "input_ids": encodings["input_ids"][idx], + "attention_mask": encodings["attention_mask"][idx], + "labels": encodings["input_ids"][idx].clone(), + } + + output_dir.mkdir(parents=True, exist_ok=True) + args = TrainingArguments( + output_dir=str(output_dir), + per_device_train_batch_size=batch_size, + max_steps=max_steps, + logging_steps=max(1, max_steps // 5), + save_steps=max_steps, + learning_rate=5e-5, + report_to=[], + remove_unused_columns=False, + ) + trainer = Trainer( + model=model, + args=args, + train_dataset=_TextDataset(), + data_collator=DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False), + ) + trainer.train() + trainer.save_model(str(output_dir)) + tokenizer.save_pretrained(str(output_dir)) + print(f"Saved optional checkpoint to {output_dir}") + + +def main() -> int: + args = _parse_args() + if not (FEATURE_REPO / "feature_store.yaml").exists(): + print(f"Missing feature repo at {FEATURE_REPO}", file=sys.stderr) + return 1 + + if args.via_df: + ds = retrieve_via_df() + else: + ds = retrieve_via_ray_stream() + + sample = ds.take(3) + print(f"Sample training rows: {len(sample)}") + for i, row in enumerate(sample): + preview = str(row.get("sft_text", row))[:160].replace("\n", "\\n") + print(f" [{i}] {preview}...") + + if args.dry_run: + print("Done (trainer skipped).") + return 0 + + train_gpt2_optional( + ds, + max_steps=args.max_steps, + batch_size=args.batch_size, + max_length=args.max_length, + output_dir=args.output_dir, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/go.mod b/go.mod index 221403635a2..4de94b829d6 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/feast-dev/feast -go 1.24.0 - -toolchain go1.24.4 +go 1.25.0 require ( cloud.google.com/go/storage v1.58.0 @@ -17,27 +15,28 @@ require ( github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 + github.com/jackc/pgx/v5 v5.8.0 github.com/mattn/go-sqlite3 v1.14.23 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.20.0 github.com/roberson-io/mmh3 v0.0.0-20190729202758-fdfce3ba6225 github.com/rs/zerolog v1.33.0 github.com/spaolacci/murmur3 v1.1.0 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 - golang.org/x/sync v0.18.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba - google.golang.org/grpc v1.76.0 - google.golang.org/protobuf v1.36.10 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 + golang.org/x/sync v0.20.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa + google.golang.org/grpc v1.81.1 + google.golang.org/protobuf v1.36.11 ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -45,7 +44,7 @@ require ( cloud.google.com/go/iam v1.5.3 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect @@ -70,13 +69,12 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.3 // indirect @@ -86,14 +84,13 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.8.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect @@ -105,32 +102,32 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/zeebo/errs v1.4.0 // indirect - github.com/zeebo/xxh3 v1.0.2 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/tools v0.44.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.256.0 // indirect google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9c4f039f558..f936e04c8f5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= @@ -22,8 +22,8 @@ cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4 cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= @@ -94,29 +94,27 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -155,16 +153,24 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -216,8 +222,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0= +github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/roberson-io/mmh3 v0.0.0-20190729202758-fdfce3ba6225 h1:ZMsPCp7oYgjoIFt1c+sM2qojxZXotSYcMF8Ur9/LJlM= github.com/roberson-io/mmh3 v0.0.0-20190729202758-fdfce3ba6225/go.mod h1:XEESr+X1SY8ZSuc3jqsTlb3clCkqQJ4DcF3Qxv1N3PM= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -228,8 +234,8 @@ github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -237,92 +243,94 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= -golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/go/infra/docker/feature-server/Dockerfile b/go/infra/docker/feature-server/Dockerfile index b1fcda18c9b..3f7a2ab7b94 100644 --- a/go/infra/docker/feature-server/Dockerfile +++ b/go/infra/docker/feature-server/Dockerfile @@ -1,4 +1,5 @@ -FROM golang:1.24.12 +FROM golang:1.25 +ENV GOTOOLCHAIN=auto # Update the package list and install the ca-certificates package RUN apt-get update && apt-get install -y ca-certificates diff --git a/go/internal/feast/metrics/metrics.go b/go/internal/feast/metrics/metrics.go index 804eef6fa1b..d4783f257b7 100644 --- a/go/internal/feast/metrics/metrics.go +++ b/go/internal/feast/metrics/metrics.go @@ -30,7 +30,6 @@ var ( TimeHistogramType = reflect.TypeOf((*TimeHistogram)(nil)).Elem() ) - func RegisterTimeHistogram(name, help, namespace string, labelNames []string, tag reflect.StructTag) (func(prometheus.Labels) interface{}, prometheus.Collector, error) { f, collector, err := prometheusvanilla.BuildHistogram(name, help, namespace, labelNames, tag) if err != nil { diff --git a/go/internal/feast/onlineserving/serving.go b/go/internal/feast/onlineserving/serving.go index ff70443015a..1ce5f6c555c 100644 --- a/go/internal/feast/onlineserving/serving.go +++ b/go/internal/feast/onlineserving/serving.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "errors" "fmt" + "regexp" "sort" "strings" @@ -20,6 +21,8 @@ import ( "github.com/feast-dev/feast/go/types" ) +var versionTagRegex = regexp.MustCompile(`^[vV]\d+$`) + /* FeatureVector type represent result of retrieving single feature for multiple rows. It can be imagined as a column in output dataframe / table. @@ -492,6 +495,18 @@ func ParseFeatureReference(featureRef string) (featureViewName, featureName stri featureViewName = parsedFeatureName[0] featureName = parsedFeatureName[1] } + + // Handle @version qualifier on feature view name + if atIdx := strings.Index(featureViewName, "@"); atIdx >= 0 { + suffix := featureViewName[atIdx+1:] + if versionTagRegex.MatchString(suffix) { + e = fmt.Errorf("versioned feature refs (@%s) are not supported by the Go feature server", suffix) + return + } + if strings.EqualFold(suffix, "latest") { + featureViewName = featureViewName[:atIdx] + } + } return } diff --git a/go/internal/feast/onlinestore/dynamodbonlinestore.go b/go/internal/feast/onlinestore/dynamodbonlinestore.go index e6d620ee10c..235449bfdf2 100644 --- a/go/internal/feast/onlinestore/dynamodbonlinestore.go +++ b/go/internal/feast/onlinestore/dynamodbonlinestore.go @@ -53,7 +53,7 @@ func NewDynamodbOnlineStore(project string, config *registry.RepoConfig, onlineS ctx := context.Background() cfg, err := awsConfig.LoadDefaultConfig(ctx) if err != nil { - panic(err) + return nil, err } store.client = dynamodb.NewFromConfig(cfg) @@ -237,24 +237,64 @@ func (d *DynamodbOnlineStore) OnlineRead(ctx context.Context, entityKeys []*type // process response from dynamodb for j := 0; j < batchSize; j++ { - entityId := Responses[j]["entity_id"].(*dtypes.AttributeValueMemberS).Value - timestampString := Responses[j]["event_ts"].(*dtypes.AttributeValueMemberS).Value + entityIdAttr, ok := Responses[j]["entity_id"] + if !ok || entityIdAttr == nil { + continue + } + entityIdMember, ok := entityIdAttr.(*dtypes.AttributeValueMemberS) + if !ok { + return fmt.Errorf("unexpected DynamoDB attribute type for 'entity_id' in table %s", tableName) + } + entityId := entityIdMember.Value + + tsAttr, ok := Responses[j]["event_ts"] + if !ok || tsAttr == nil { + continue + } + tsMember, ok := tsAttr.(*dtypes.AttributeValueMemberS) + if !ok { + return fmt.Errorf("unexpected DynamoDB attribute type for 'event_ts' in table %s", tableName) + } + timestampString := tsMember.Value + t, err := time.Parse("2006-01-02 15:04:05-07:00", timestampString) if err != nil { return err } timeStamp := timestamppb.New(t) - featureValues := Responses[j]["values"].(*dtypes.AttributeValueMemberM).Value + rawValues, ok := Responses[j]["values"] + if !ok || rawValues == nil { + continue + } + valuesMap, ok := rawValues.(*dtypes.AttributeValueMemberM) + if !ok { + return fmt.Errorf("unexpected DynamoDB attribute type for 'values' in table %s", tableName) + } + featureValues := valuesMap.Value entityIndex := entityIndexMap[entityId] for _, featureName := range featureNames { - featureValue := featureValues[featureName].(*dtypes.AttributeValueMemberB).Value + featureIndex := featureNamesIndex[featureName] + rawVal, exists := featureValues[featureName] + if !exists || rawVal == nil { + mu.Lock() + results[entityIndex][featureIndex] = FeatureData{ + Reference: serving.FeatureReferenceV2{FeatureViewName: featureViewName, FeatureName: featureName}, + Timestamp: timestamppb.Timestamp{Seconds: timeStamp.Seconds, Nanos: timeStamp.Nanos}, + Value: types.Value{Val: &types.Value_NullVal{NullVal: types.Null_NULL}}, + } + mu.Unlock() + continue + } + memberB, ok := rawVal.(*dtypes.AttributeValueMemberB) + if !ok { + return fmt.Errorf("unexpected DynamoDB attribute type for feature %q in view %q", featureName, featureViewName) + } var value types.Value - if err := proto.Unmarshal(featureValue, &value); err != nil { + if err := proto.Unmarshal(memberB.Value, &value); err != nil { return err } - featureIndex := featureNamesIndex[featureName] mu.Lock() results[entityIndex][featureIndex] = FeatureData{Reference: serving.FeatureReferenceV2{FeatureViewName: featureViewName, FeatureName: featureName}, diff --git a/go/internal/feast/onlinestore/postgresonlinestore.go b/go/internal/feast/onlinestore/postgresonlinestore.go index a05e21df775..4077a9e06fa 100644 --- a/go/internal/feast/onlinestore/postgresonlinestore.go +++ b/go/internal/feast/onlinestore/postgresonlinestore.go @@ -166,7 +166,7 @@ func buildPostgresConnString(config map[string]interface{}) string { if sslMode, ok := config["sslmode"].(string); ok && sslMode != "" { query.Set("sslmode", sslMode) } else { - query.Set("sslmode", "disable") + query.Set("sslmode", "require") } if v, ok := config["sslcert_path"].(string); ok && v != "" { @@ -194,4 +194,4 @@ func buildPostgresConnString(config map[string]interface{}) string { } return connURL.String() -} \ No newline at end of file +} diff --git a/go/internal/feast/onlinestore/postgresonlinestore_test.go b/go/internal/feast/onlinestore/postgresonlinestore_test.go index 2d81ba2cddb..b241e857e18 100644 --- a/go/internal/feast/onlinestore/postgresonlinestore_test.go +++ b/go/internal/feast/onlinestore/postgresonlinestore_test.go @@ -34,7 +34,7 @@ func TestBuildPostgresConnStringDefaults(t *testing.T) { } connStr := buildPostgresConnString(config) assert.Contains(t, connStr, "localhost:5432") - assert.Contains(t, connStr, "sslmode=disable") + assert.Contains(t, connStr, "sslmode=require") } func TestBuildPostgresConnStringWithSSL(t *testing.T) { diff --git a/go/internal/feast/registry/registry.go b/go/internal/feast/registry/registry.go index 51aa031bbda..3ff94807049 100644 --- a/go/internal/feast/registry/registry.go +++ b/go/internal/feast/registry/registry.go @@ -81,6 +81,10 @@ func (r *Registry) InitializeRegistry() error { } func (r *Registry) RefreshRegistryOnInterval() { + if r.cachedRegistryProtoTtl <= 0 { + log.Info().Msg("Registry cache TTL is non-positive; skipping periodic refresh") + return + } ticker := time.NewTicker(r.cachedRegistryProtoTtl) for ; true; <-ticker.C { err := r.refresh() diff --git a/go/internal/feast/registry/registry_test.go b/go/internal/feast/registry/registry_test.go index 6f75dbbbeb2..0f5d1c20ea7 100644 --- a/go/internal/feast/registry/registry_test.go +++ b/go/internal/feast/registry/registry_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/assert" ) func TestCloudRegistryStores(t *testing.T) { @@ -99,6 +100,24 @@ func TestCloudRegistryStores(t *testing.T) { } } +func TestRefreshRegistryOnIntervalNonPositiveTTL(t *testing.T) { + tests := []struct { + name string + ttl time.Duration + }{ + {name: "zero ttl", ttl: 0}, + {name: "negative ttl", ttl: -1 * time.Second}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + r := &Registry{cachedRegistryProtoTtl: test.ttl} + assert.NotPanics(t, func() { + r.RefreshRegistryOnInterval() + }) + }) + } +} + // MockS3Client is mock client for testing S3 registry store type MockS3Client struct { GetObjectFn func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) diff --git a/go/internal/feast/server/http_server.go b/go/internal/feast/server/http_server.go index adfd40110e7..876f42f846b 100644 --- a/go/internal/feast/server/http_server.go +++ b/go/internal/feast/server/http_server.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/tls" "encoding/json" "fmt" "net/http" @@ -396,6 +397,34 @@ func (s *httpServer) Serve(host string, port int) error { return err } +func (s *httpServer) ServeTLS(host string, port int, certFile string, keyFile string) error { + mux := http.NewServeMux() + mux.Handle("/get-online-features", metricsMiddleware(recoverMiddleware(http.HandlerFunc(s.getOnlineFeatures)))) + mux.Handle("/health", metricsMiddleware(http.HandlerFunc(healthCheckHandler))) + s.server = &http.Server{ + Addr: fmt.Sprintf("%s:%d", host, port), + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 15 * time.Second, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + tls.X25519MLKEM768, + //tls.SecP256r1MLKEM768, // Only available in Go 1.26 + }, + }, + } + err := s.server.ListenAndServeTLS(certFile, keyFile) + // Don't return the error if it's caused by graceful shutdown using Stop() + if err == http.ErrServerClosed { + return nil + } + log.Fatal().Stack().Err(err).Msg("Failed to start HTTPS server") + return err +} + func healthCheckHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Healthy") diff --git a/go/main.go b/go/main.go index f49a27efa46..7f89fe66c3b 100644 --- a/go/main.go +++ b/go/main.go @@ -11,6 +11,7 @@ import ( "strings" "sync" "syscall" + "time" "github.com/feast-dev/feast/go/internal/feast" "github.com/feast-dev/feast/go/internal/feast/registry" @@ -36,15 +37,28 @@ import ( var tracer trace.Tracer +var newSignalStopChannel = func() (chan os.Signal, func()) { + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + return stop, func() { + signal.Stop(stop) + } +} + type ServerStarter interface { StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error StartGrpcServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error + StartHttpsServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions, certFile string, keyFile string) error } type RealServerStarter struct{} func (s *RealServerStarter) StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { - return StartHttpServer(fs, host, port, metricsPort, writeLoggedFeaturesCallback, loggingOpts) + return StartHttpServer(fs, host, port, metricsPort, writeLoggedFeaturesCallback, loggingOpts, false, "", "") +} + +func (s *RealServerStarter) StartHttpsServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions, certFile string, keyFile string) error { + return StartHttpServer(fs, host, port, metricsPort, writeLoggedFeaturesCallback, loggingOpts, true, certFile, keyFile) } func (s *RealServerStarter) StartGrpcServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { @@ -58,18 +72,22 @@ func main() { port := 8080 metricsPort := 9090 server := RealServerStarter{} + certFile := "" + keyFile := "" // Current Directory repoPath, err := os.Getwd() if err != nil { log.Error().Stack().Err(err).Msg("Failed to get current directory") } - flag.StringVar(&serverType, "type", serverType, "Specify the server type (http or grpc)") + flag.StringVar(&serverType, "type", serverType, "Specify the server type (http, https or grpc)") flag.StringVar(&repoPath, "chdir", repoPath, "Repository path where feature store yaml file is stored") flag.StringVar(&host, "host", host, "Specify a host for the server") flag.IntVar(&port, "port", port, "Specify a port for the server") flag.IntVar(&metricsPort, "metrics-port", metricsPort, "Specify a port for the metrics server") + flag.StringVar(&certFile, "tls-cert-file", "", "Path to the TLS certificate file") + flag.StringVar(&keyFile, "tls-key-file", "", "Path to the TLS key file") flag.Parse() // Initialize tracer @@ -119,8 +137,10 @@ func main() { err = server.StartHttpServer(fs, host, port, metricsPort, nil, loggingOptions) } else if serverType == "grpc" { err = server.StartGrpcServer(fs, host, port, metricsPort, nil, loggingOptions) + } else if serverType == "https" { + err = server.StartHttpsServer(fs, host, port, metricsPort, nil, loggingOptions, certFile, keyFile) } else { - fmt.Println("Unknown server type. Please specify 'http' or 'grpc'.") + fmt.Println("Unknown server type. Please specify 'http' or 'grpc' or 'https'.") } if err != nil { @@ -227,27 +247,34 @@ func StartGrpcServer(fs *feast.FeatureStore, host string, port int, metricsPort // StartHttpServerWithLogging starts HTTP server with enabled feature logging // Go does not allow direct assignment to package-level functions as a way to // mock them for tests -func StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { +func StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions, httpsEnable bool, certFile string, keyFile string) error { + if httpsEnable && (certFile == "" || keyFile == "") { + return fmt.Errorf("--tls-cert-file and --tls-key-file must be provided for HTTPS server.") + } + loggingService, err := constructLoggingService(fs, writeLoggedFeaturesCallback, loggingOpts) if err != nil { return err } ser := server.NewHttpServer(fs, loggingService) log.Info().Msgf("Starting a HTTP server on host %s, port %d", host, port) + // Start metrics server - metricsServer := &http.Server{Addr: fmt.Sprintf(":%d", metricsPort)} + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + metricsServer := &http.Server{ + Addr: fmt.Sprintf(":%d", metricsPort), + Handler: mux, + } go func() { log.Info().Msgf("Starting metrics server on port %d", metricsPort) - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) - metricsServer.Handler = mux if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Error().Err(err).Msg("Failed to start metrics server") } }() - stop := make(chan os.Signal, 1) - signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + stop, stopCleanup := newSignalStopChannel() + defer stopCleanup() var wg sync.WaitGroup wg.Add(1) @@ -263,7 +290,9 @@ func StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort log.Error().Err(err).Msg("Error when stopping the HTTP server") } log.Info().Msg("Stopping metrics server...") - if err := metricsServer.Shutdown(context.Background()); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := metricsServer.Shutdown(ctx); err != nil { log.Error().Err(err).Msg("Error stopping metrics server") } if loggingService != nil { @@ -279,7 +308,11 @@ func StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort } }() - err = ser.Serve(host, port) + if httpsEnable { + err = ser.ServeTLS(host, port, certFile, keyFile) + } else { + err = ser.Serve(host, port) + } close(serverExited) wg.Wait() return err diff --git a/go/main_test.go b/go/main_test.go index f1f2ae98698..7eb0e0a6676 100644 --- a/go/main_test.go +++ b/go/main_test.go @@ -1,12 +1,28 @@ package main import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "net/http" + "os" + "strings" + "syscall" "testing" + "time" "github.com/feast-dev/feast/go/internal/feast" "github.com/feast-dev/feast/go/internal/feast/server/logging" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) // MockServerStarter is a mock of ServerStarter interface for testing @@ -67,3 +83,130 @@ func TestConstructLoggingService(t *testing.T) { assert.NoError(t, err) // Further assertions can be added here based on the expected behavior of constructLoggingService } + +func TestStartHttpsServerHealthEndpoint(t *testing.T) { + certPath, keyPath := createSelfSignedTLSFiles(t) + host := "127.0.0.1" + port := getFreePort(t) + metricsPort := getFreePort(t) + + stop := make(chan os.Signal, 1) + prevNewSignalStopChannel := newSignalStopChannel + newSignalStopChannel = func() (chan os.Signal, func()) { + return stop, func() {} + } + t.Cleanup(func() { + newSignalStopChannel = prevNewSignalStopChannel + }) + + errCh := make(chan error, 1) + go func() { + errCh <- StartHttpServer(&feast.FeatureStore{}, host, port, metricsPort, nil, &logging.LoggingOptions{}, true, certPath, keyPath) + }() + + httpsClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + }, + } + t.Cleanup(httpsClient.CloseIdleConnections) + + url := fmt.Sprintf("https://%s:%d/health", host, port) + + var ( + resp *http.Response + err error + ) + require.Eventually(t, func() bool { + resp, err = httpsClient.Get(url) + if err != nil { + return false + } + return true + }, 5*time.Second, 100*time.Millisecond) + require.NoError(t, err) + t.Cleanup(func() { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + }) + + body, readErr := io.ReadAll(resp.Body) + require.NoError(t, readErr) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "Healthy", strings.TrimSpace(string(body))) + + stop <- syscall.SIGTERM + + select { + case startErr := <-errCh: + require.NoError(t, startErr) + case <-time.After(5 * time.Second): + t.Fatal("StartHttpsServer did not shutdown within timeout") + } +} + +func TestStartHttpsServerTLSFilesRequired(t *testing.T) { + err := StartHttpServer(&feast.FeatureStore{}, "127.0.0.1", 0, 0, nil, &logging.LoggingOptions{}, true, "", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "--tls-cert-file and --tls-key-file must be provided") +} + +func getFreePort(t *testing.T) int { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer func() { + _ = listener.Close() + }() + + addr, ok := listener.Addr().(*net.TCPAddr) + require.True(t, ok) + return addr.Port +} + +func createSelfSignedTLSFiles(t *testing.T) (string, string) { + t.Helper() + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + tmpl := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "localhost", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + + der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) + require.NoError(t, err) + + certFile, err := os.CreateTemp(t.TempDir(), "feast-test-cert-*.pem") + require.NoError(t, err) + defer func() { + _ = certFile.Close() + }() + + keyFile, err := os.CreateTemp(t.TempDir(), "feast-test-key-*.pem") + require.NoError(t, err) + defer func() { + _ = keyFile.Close() + }() + + err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: der}) + require.NoError(t, err) + + err = pem.Encode(keyFile, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + require.NoError(t, err) + + return certFile.Name(), keyFile.Name() +} diff --git a/infra/charts/feast-feature-server/Chart.yaml b/infra/charts/feast-feature-server/Chart.yaml index 1b4e503dffd..f77f882a9d2 100644 --- a/infra/charts/feast-feature-server/Chart.yaml +++ b/infra/charts/feast-feature-server/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: feast-feature-server description: Feast Feature Server in Go or Python type: application -version: 0.60.0 +version: 0.65.0 keywords: - machine learning - big data diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index 438b3de9105..cd8cc475031 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -1,6 +1,6 @@ # Feast Python / Go Feature Server Helm Charts -Current chart version is `0.60.0` +Current chart version is `0.65.0` ## Installation @@ -42,7 +42,7 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | fullnameOverride | string | `""` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"quay.io/feastdev/feature-server"` | Docker image for Feature Server repository | -| image.tag | string | `"0.60.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | +| image.tag | string | `"0.65.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | | imagePullSecrets | list | `[]` | | | livenessProbe.initialDelaySeconds | int | `30` | | | livenessProbe.periodSeconds | int | `30` | | diff --git a/infra/charts/feast-feature-server/templates/deployment.yaml b/infra/charts/feast-feature-server/templates/deployment.yaml index 6cd17d4a4d1..ef0cc9671de 100644 --- a/infra/charts/feast-feature-server/templates/deployment.yaml +++ b/infra/charts/feast-feature-server/templates/deployment.yaml @@ -11,10 +11,12 @@ spec: {{- include "feast-feature-server.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} + {{- if or .Values.podAnnotations .Values.metrics.enabled }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} - {{- if .Values.metrics.enabled }} + {{- end }} + {{- if $.Values.metrics.enabled }} instrumentation.opentelemetry.io/inject-python: "true" {{- end }} {{- end }} diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index 9bb76d0a724..f2bc97d7a2b 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -9,7 +9,7 @@ image: repository: quay.io/feastdev/feature-server pullPolicy: IfNotPresent # image.tag -- The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) - tag: 0.60.0 + tag: 0.65.0 logLevel: "WARNING" # Set log level DEBUG, INFO, WARNING, ERROR, and CRITICAL (case-insensitive) diff --git a/infra/charts/feast/Chart.yaml b/infra/charts/feast/Chart.yaml index 12e465ec052..dc49ff3fb2f 100644 --- a/infra/charts/feast/Chart.yaml +++ b/infra/charts/feast/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 description: Feature store for machine learning name: feast -version: 0.60.0 +version: 0.65.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index d577e3a14ec..2a288bb48aa 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -8,7 +8,7 @@ This repo contains Helm charts for Feast Java components that are being installe ## Chart: Feast -Feature store for machine learning Current chart version is `0.60.0` +Feature store for machine learning Current chart version is `0.65.0` ## Installation @@ -65,8 +65,8 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/java-demo) fo | Repository | Name | Version | |------------|------|---------| | https://charts.helm.sh/stable | redis | 10.5.6 | -| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.60.0 | -| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.60.0 | +| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.65.0 | +| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.65.0 | ## Values diff --git a/infra/charts/feast/charts/feature-server/Chart.yaml b/infra/charts/feast/charts/feature-server/Chart.yaml index f3ea165878f..b20c1778a18 100644 --- a/infra/charts/feast/charts/feature-server/Chart.yaml +++ b/infra/charts/feast/charts/feature-server/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Feast Feature Server: Online feature serving service for Feast" name: feature-server -version: 0.60.0 -appVersion: v0.60.0 +version: 0.65.0 +appVersion: v0.65.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/feature-server/README.md b/infra/charts/feast/charts/feature-server/README.md index 290965a972f..571449d9009 100644 --- a/infra/charts/feast/charts/feature-server/README.md +++ b/infra/charts/feast/charts/feature-server/README.md @@ -1,6 +1,6 @@ # feature-server -![Version: 0.60.0](https://img.shields.io/badge/Version-0.60.0-informational?style=flat-square) ![AppVersion: v0.60.0](https://img.shields.io/badge/AppVersion-v0.60.0-informational?style=flat-square) +![Version: 0.65.0](https://img.shields.io/badge/Version-0.65.0-informational?style=flat-square) ![AppVersion: v0.65.0](https://img.shields.io/badge/AppVersion-v0.65.0-informational?style=flat-square) Feast Feature Server: Online feature serving service for Feast @@ -17,7 +17,7 @@ Feast Feature Server: Online feature serving service for Feast | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"quay.io/feastdev/feature-server-java"` | Docker image for Feature Server repository | -| image.tag | string | `"0.60.0"` | Image tag | +| image.tag | string | `"0.65.0"` | Image tag | | ingress.grpc.annotations | object | `{}` | Extra annotations for the ingress | | ingress.grpc.auth.enabled | bool | `false` | Flag to enable auth | | ingress.grpc.class | string | `"nginx"` | Which ingress controller to use | diff --git a/infra/charts/feast/charts/feature-server/values.yaml b/infra/charts/feast/charts/feature-server/values.yaml index d5ab59d7ef7..3367dd665fa 100644 --- a/infra/charts/feast/charts/feature-server/values.yaml +++ b/infra/charts/feast/charts/feature-server/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Feature Server repository repository: quay.io/feastdev/feature-server-java # image.tag -- Image tag - tag: 0.60.0 + tag: 0.65.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/charts/transformation-service/Chart.yaml b/infra/charts/feast/charts/transformation-service/Chart.yaml index c18f681a85a..9dbc3f73cb4 100644 --- a/infra/charts/feast/charts/transformation-service/Chart.yaml +++ b/infra/charts/feast/charts/transformation-service/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Transformation service: to compute on-demand features" name: transformation-service -version: 0.60.0 -appVersion: v0.60.0 +version: 0.65.0 +appVersion: v0.65.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/transformation-service/README.md b/infra/charts/feast/charts/transformation-service/README.md index 76253c8bb0c..ad1dd75cd65 100644 --- a/infra/charts/feast/charts/transformation-service/README.md +++ b/infra/charts/feast/charts/transformation-service/README.md @@ -1,6 +1,6 @@ # transformation-service -![Version: 0.60.0](https://img.shields.io/badge/Version-0.60.0-informational?style=flat-square) ![AppVersion: v0.60.0](https://img.shields.io/badge/AppVersion-v0.60.0-informational?style=flat-square) +![Version: 0.65.0](https://img.shields.io/badge/Version-0.65.0-informational?style=flat-square) ![AppVersion: v0.65.0](https://img.shields.io/badge/AppVersion-v0.65.0-informational?style=flat-square) Transformation service: to compute on-demand features @@ -13,7 +13,7 @@ Transformation service: to compute on-demand features | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"quay.io/feastdev/feature-transformation-server"` | Docker image for Transformation Server repository | -| image.tag | string | `"0.60.0"` | Image tag | +| image.tag | string | `"0.65.0"` | Image tag | | nodeSelector | object | `{}` | Node labels for pod assignment | | podLabels | object | `{}` | Labels to be added to Feast Serving pods | | replicaCount | int | `1` | Number of pods that will be created | diff --git a/infra/charts/feast/charts/transformation-service/values.yaml b/infra/charts/feast/charts/transformation-service/values.yaml index e00e9c4f523..266cd4b48aa 100644 --- a/infra/charts/feast/charts/transformation-service/values.yaml +++ b/infra/charts/feast/charts/transformation-service/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Transformation Server repository repository: quay.io/feastdev/feature-transformation-server # image.tag -- Image tag - tag: 0.60.0 + tag: 0.65.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/requirements.yaml b/infra/charts/feast/requirements.yaml index 59c3442b5a5..3f29ad7dfdc 100644 --- a/infra/charts/feast/requirements.yaml +++ b/infra/charts/feast/requirements.yaml @@ -1,12 +1,12 @@ dependencies: - name: feature-server alias: feature-server - version: 0.60.0 + version: 0.65.0 condition: feature-server.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: transformation-service alias: transformation-service - version: 0.60.0 + version: 0.65.0 condition: transformation-service.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: redis diff --git a/infra/feast-operator/.golangci.bck.yml b/infra/feast-operator/.golangci.bck.yml new file mode 100644 index 00000000000..6c104980d43 --- /dev/null +++ b/infra/feast-operator/.golangci.bck.yml @@ -0,0 +1,55 @@ +run: + timeout: 5m + allow-parallel-runners: true + +issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + - path: "api/*" + linters: + - lll + - path: "internal/*" + linters: + - dupl + - lll + - path: "test/*" + linters: + - lll + - path: "upgrade/*" + linters: + - lll + - path: "previous-version/*" + linters: + - lll +linters: + disable-all: true + enable: + - dupl + - errcheck + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - ginkgolinter + - prealloc + - revive + - staticcheck + - typecheck + - unconvert + - unparam + - unused + +linters-settings: + revive: + rules: + - name: comment-spacings diff --git a/infra/feast-operator/.golangci.yml b/infra/feast-operator/.golangci.yml index 6c104980d43..95be6d14177 100644 --- a/infra/feast-operator/.golangci.yml +++ b/infra/feast-operator/.golangci.yml @@ -1,55 +1,67 @@ +version: "2" run: - timeout: 5m allow-parallel-runners: true - -issues: - # don't skip warning about doc comments - # don't exclude the default set of lint - exclude-use-default: false - # restore some of the defaults - # (fill in the rest as needed) - exclude-rules: - - path: "api/*" - linters: - - lll - - path: "internal/*" - linters: - - dupl - - lll - - path: "test/*" - linters: - - lll - - path: "upgrade/*" - linters: - - lll - - path: "previous-version/*" - linters: - - lll linters: - disable-all: true + default: none enable: - dupl - errcheck + - ginkgolinter - goconst - gocyclo - - gofmt - - goimports - - gosimple - govet - ineffassign - lll - misspell - nakedret - - ginkgolinter - prealloc - revive - staticcheck - - typecheck - unconvert - unparam - unused - -linters-settings: - revive: + settings: + revive: + rules: + - name: comment-spacings + staticcheck: + checks: + - "all" + - "-QF*" + - "-ST*" + exclusions: + generated: lax + presets: [] rules: - - name: comment-spacings + - linters: + - lll + path: api/* + - linters: + - dupl + - goconst + - lll + path: internal/* + - linters: + - goconst + - lll + path: test/* + - linters: + - lll + path: upgrade/* + - linters: + - lll + path: previous-version/* + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/infra/feast-operator/Dockerfile b/infra/feast-operator/Dockerfile index f0814b25576..a7ad3fa044d 100644 --- a/infra/feast-operator/Dockerfile +++ b/infra/feast-operator/Dockerfile @@ -1,28 +1,29 @@ # Build the manager binary -FROM registry.access.redhat.com/ubi9/go-toolset:1.22.9 AS builder +FROM registry.access.redhat.com/ubi9/go-toolset:1.25 AS builder ARG TARGETOS ARG TARGETARCH +ENV GOTOOLCHAIN=auto # Copy the Go Modules manifests -COPY go.mod go.mod -COPY go.sum go.sum +COPY --chown=1001:0 go.mod go.mod +COPY --chown=1001:0 go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the go source -COPY cmd/main.go cmd/main.go -COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY --chown=1001:0 cmd/ cmd/ +COPY --chown=1001:0 api/ api/ +COPY --chown=1001:0 internal/controller/ internal/controller/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager ./cmd/ -FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5 +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.8 WORKDIR / COPY --from=builder /opt/app-root/src/manager . USER 65532:65532 diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile index f017154d39d..b70608a389e 100644 --- a/infra/feast-operator/Makefile +++ b/infra/feast-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.60.0 +VERSION ?= 0.65.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") @@ -48,7 +48,7 @@ endif # Set the Operator SDK version to use. By default, what is installed on the system is used. # This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. -OPERATOR_SDK_VERSION ?= v1.38.0 +OPERATOR_SDK_VERSION ?= v1.41.0 # Image URL to use all building/pushing image targets # During development and testing, and before make deploy we need to export FS_IMG to point to # the dev image generated using command `make build-feature-server-dev-docker` @@ -56,7 +56,7 @@ IMG ?= $(IMAGE_TAG_BASE):$(VERSION) FS_IMG ?= quay.io/feastdev/feature-server:$(VERSION) CJ_IMG ?= quay.io/openshift/origin-cli:4.17 # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.30.0 +ENVTEST_K8S_VERSION = 1.31.0 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -116,7 +116,7 @@ vet: ## Run go vet against code. .PHONY: test test: build-installer vet lint envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path --use-deprecated-gcs=false)" go test $$(go list ./... | grep -v test/e2e | grep -v test/data-source-types | grep -v test/upgrade | grep -v test/previous-version) -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" GOTOOLCHAIN=go$(shell go env GOVERSION | sed 's/^go//') go test $$(go list ./... | grep -v test/e2e | grep -v test/data-source-types | grep -v test/upgrade | grep -v test/previous-version) -coverprofile cover.out # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. @@ -239,11 +239,11 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ENVSUBST = $(LOCALBIN)/envsubst ## Tool Versions -KUSTOMIZE_VERSION ?= v5.4.2 -CONTROLLER_TOOLS_VERSION ?= v0.15.0 -CRD_REF_DOCS_VERSION ?= v0.1.0 -ENVTEST_VERSION ?= release-0.18 -GOLANGCI_LINT_VERSION ?= v1.59.1 +KUSTOMIZE_VERSION ?= v5.4.3 +CONTROLLER_TOOLS_VERSION ?= v0.18.0 +CRD_REF_DOCS_VERSION ?= v0.2.0 +ENVTEST_VERSION ?= release-0.21 +GOLANGCI_LINT_VERSION ?= v2.12.2 ENVSUBST_VERSION ?= v1.4.2 .PHONY: kustomize @@ -264,7 +264,7 @@ $(ENVTEST): $(LOCALBIN) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) - $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) .PHONY: envsubst envsubst: $(ENVSUBST) ## Download envsubst locally if necessary. @@ -338,7 +338,7 @@ ifeq (,$(shell which opm 2>/dev/null)) set -e ;\ mkdir -p $(dir $(OPM)) ;\ OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ - curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\ + curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.55.0/$${OS}-$${ARCH}-opm ;\ chmod +x $(OPM) ;\ } else diff --git a/infra/feast-operator/README.md b/infra/feast-operator/README.md index 6c0ef634e78..f879dff1cc1 100644 --- a/infra/feast-operator/README.md +++ b/infra/feast-operator/README.md @@ -3,6 +3,18 @@ This is a K8s Operator that can be used to deploy and manage **Feast**, an open ### **[FeatureStore CR API Reference](docs/api/markdown/ref.md)** +### **[Operator Configuration Guides](https://docs.feast.dev/how-to-guides/feast-operator)** + +| Guide | Topic | +|-------|-------| +| [1 — Project Provisioning](https://docs.feast.dev/how-to-guides/feast-operator/01-project-provisioning) | `feastProjectDir`: git clone, `feast init`, or a repository packaged in an image | +| [2 — Persistence](https://docs.feast.dev/how-to-guides/feast-operator/02-persistence) | File (path + PVC) vs DB store for offline/online/registry; Secret format | +| [3 — Serving & Observability](https://docs.feast.dev/how-to-guides/feast-operator/03-serving-and-observability) | Workers, log level, Prometheus metrics, offline push batching, MCP | +| [4 — Registry Topology](https://docs.feast.dev/how-to-guides/feast-operator/04-registry-topology) | Local, remote, cross-namespace `feastRef` | +| [5 — Security](https://docs.feast.dev/how-to-guides/feast-operator/05-security) | Kubernetes RBAC roles vs OIDC auth; TLS for all servers | +| [6 — Batch & Jobs](https://docs.feast.dev/how-to-guides/feast-operator/06-batch-and-jobs) | `batchEngine` ConfigMap, `cronJob` for scheduled materialization | +| [7 — OpenLineage & Materialization](https://docs.feast.dev/how-to-guides/feast-operator/07-openlineage-and-materialization) | Lineage transports, API key Secret, materialization batch size | + ## Getting Started ### Prerequisites @@ -24,6 +36,7 @@ kubectl apply --server-side --force-conflicts -f https://raw.githubusercontent.c ``` > **NOTE**: Server-Side Apply (`--server-side`) is required because the CRD includes both v1alpha1 and v1 API versions, making it too large for the standard `kubectl apply` annotation limit. If you encounter annotation size errors, use `--server-side --force-conflicts` flags. + ##### Feast Operator Demo Videos [![](https://img.youtube.com/vi/48cb4AHxPR4/0.jpg)](https://www.youtube.com/playlist?list=PLPzVNzik7rsAN-amQLZckd0so3cIr7blX) diff --git a/infra/feast-operator/api/feastversion/version.go b/infra/feast-operator/api/feastversion/version.go index f80338fb9fc..deabd34ac38 100644 --- a/infra/feast-operator/api/feastversion/version.go +++ b/infra/feast-operator/api/feastversion/version.go @@ -17,4 +17,4 @@ limitations under the License. package feastversion // Feast release version. Keep on line #20, this is critical to release CI -const FeastVersion = "0.60.0" +const FeastVersion = "0.65.0" diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index 8928fe74ce8..3372e74f63a 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -22,6 +22,7 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) const ( @@ -51,6 +52,7 @@ const ( ClientFailedReason = "ClientDeploymentFailed" CronJobFailedReason = "CronJobDeploymentFailed" KubernetesAuthzFailedReason = "KubernetesAuthorizationDeploymentFailed" + OidcAuthzFailedReason = "OidcAuthorizationDeploymentFailed" // Feast condition messages: ReadyMessage = "FeatureStore installation complete" @@ -61,12 +63,85 @@ const ( ClientReadyMessage = "Client installation complete" CronJobReadyMessage = "CronJob installation complete" KubernetesAuthzReadyMessage = "Kubernetes authorization installation complete" + OidcAuthzReadyMessage = "OIDC authorization installation complete" DeploymentNotAvailableMessage = "Deployment is not available" // entity_key_serialization_version SerializationVersion = 3 ) +// MaterializationConfig controls feature materialization behavior written into feature_store.yaml. +type MaterializationConfig struct { + // Number of rows per batch when writing to the online store during materialization. + // Prevents OOM for large feature views. Supported engines: local, spark, ray. + // If unset, all rows are written in a single batch. + // +kubebuilder:validation:Minimum=1 + // +optional + OnlineWriteBatchSize *int32 `json:"onlineWriteBatchSize,omitempty"` + // ExtraConfig passes additional materialization key-value settings inline into + // feature_store.yaml. + // +optional + ExtraConfig map[string]string `json:"extraConfig,omitempty"` +} + +// OpenLineageConfig enables OpenLineage data lineage tracking for Feast operations. +// Lineage events are emitted during feast apply and materialization when enabled. +type OpenLineageConfig struct { + // Enable OpenLineage integration. + Enabled bool `json:"enabled"` + // Transport type for lineage events. + // +kubebuilder:validation:Enum=http;console;file;kafka + // +optional + TransportType *string `json:"transportType,omitempty"` + // URL for HTTP transport (e.g. http://marquez:5000). Required when transportType is "http". + // +optional + TransportUrl *string `json:"transportUrl,omitempty"` + // API endpoint path appended to transportUrl. Defaults to "api/v1/lineage". + // +optional + TransportEndpoint *string `json:"transportEndpoint,omitempty"` + // Reference to a Secret containing the key "api_key" for lineage server authentication. + // +optional + ApiKeySecretRef *corev1.LocalObjectReference `json:"apiKeySecretRef,omitempty"` + // ExtraConfig holds additional OpenLineage key-value settings written inline into + // the openlineage block of feature_store.yaml alongside the typed fields above. + // Use this for non-core settings (e.g. namespace, producer, emit_on_apply, + // emit_on_materialize) and transport-specific options (e.g. kafka + // bootstrap_servers, topic; file path). Boolean values ("true"/"false") and + // integer values are automatically coerced to their native YAML types. + // Keys must be valid Feast OpenLineageConfig YAML field names. + // +optional + ExtraConfig map[string]string `json:"extraConfig,omitempty"` + // Consumer configures the OpenLineage consumer (event receiver) that enables + // Feast to receive and display lineage from external producers (Airflow, Spark, dbt, etc.). + // +optional + Consumer *OpenLineageConsumerConfig `json:"consumer,omitempty"` +} + +// OpenLineageConsumerConfig configures the OpenLineage consumer (event receiver). +// When enabled, the Feast REST server exposes POST /api/v1/lineage to receive +// OpenLineage events from any producer, storing them for visualization in the Feast UI. +type OpenLineageConsumerConfig struct { + // Enable the OpenLineage consumer. + Enabled bool `json:"enabled"` + // StoreType is the storage backend for lineage events. Currently only "sql" is supported. + // +kubebuilder:default="sql" + // +kubebuilder:validation:Enum=sql + // +optional + StoreType *string `json:"storeType,omitempty"` + // Reference to a Secret containing the key "connection_string" for a separate + // lineage database. If omitted, the SQL registry database is reused. + // +optional + ConnectionStringSecretRef *corev1.LocalObjectReference `json:"connectionStringSecretRef,omitempty"` + // Reference to a Secret containing the key "api_key" that producers must + // provide in the X-API-Key header when sending events. + // +optional + ApiKeySecretRef *corev1.LocalObjectReference `json:"apiKeySecretRef,omitempty"` + // NamespaceMapping maps OpenLineage namespaces to Feast projects for + // RBAC-based filtering of lineage data in the UI. + // +optional + NamespaceMapping map[string]string `json:"namespaceMapping,omitempty"` +} + // FeatureStoreSpec defines the desired state of FeatureStore // +kubebuilder:validation:XValidation:rule="self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) || !has(self.services.scaling.autoscaling)",message="replicas > 1 and services.scaling.autoscaling are mutually exclusive." // +kubebuilder:validation:XValidation:rule="self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) || !has(self.services.scaling.autoscaling)) || (has(self.services) && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) && has(self.services.onlineStore.persistence.store))",message="Scaling requires DB-backed persistence for the online store. Configure services.onlineStore.persistence.store when using replicas > 1 or autoscaling." @@ -81,18 +156,41 @@ type FeatureStoreSpec struct { AuthzConfig *AuthzConfig `json:"authz,omitempty"` CronJob *FeastCronJob `json:"cronJob,omitempty"` BatchEngine *BatchEngineConfig `json:"batchEngine,omitempty"` + // DataQualityMonitoring configures Data Quality Monitoring behaviour. + // +optional + DataQualityMonitoring *DataQualityMonitoringConfig `json:"dataQualityMonitoring,omitempty"` // Replicas is the desired number of pod replicas. Used by the scale sub-resource. // Mutually exclusive with services.scaling.autoscaling. // +kubebuilder:default=1 // +kubebuilder:validation:Minimum=1 - Replicas *int32 `json:"replicas"` + Replicas *int32 `json:"replicas,omitempty"` + // Materialization controls feature materialization behavior (batch size, pull strategy). + // Written into feature_store.yaml for all service pods. + // +optional + Materialization *MaterializationConfig `json:"materialization,omitempty"` + // OpenLineage enables OpenLineage data lineage tracking for Feast operations. + // Written into feature_store.yaml for all service pods. + // +optional + OpenLineage *OpenLineageConfig `json:"openlineage,omitempty"` } // FeastProjectDir defines how to create the feast project directory. -// +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init)].exists_one(c, c)",message="One selection required between init or git." +// +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init), has(self.packaged)].exists_one(c, c)",message="One selection required between init, git, or packaged." type FeastProjectDir struct { - Git *GitCloneOptions `json:"git,omitempty"` - Init *FeastInitOptions `json:"init,omitempty"` + Git *GitCloneOptions `json:"git,omitempty"` + Init *FeastInitOptions `json:"init,omitempty"` + Packaged *FeastPackagedOptions `json:"packaged,omitempty"` +} + +// FeastPackagedOptions describes a feature repository packaged in a feature server image. +// +kubebuilder:validation:XValidation:rule="self.featureRepoPath.startsWith('/') && self.featureRepoPath != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..')",message="FeatureRepoPath must be a canonical absolute, non-root path without dot segments or repeated separators." +type FeastPackagedOptions struct { + // Image containing the packaged feature repository. When set, this image is used by the + // repository initialization and feast apply containers and as the default service image. + // When omitted, the operator's configured feature server image is used. + Image string `json:"image,omitempty"` + // FeatureRepoPath is the canonical absolute path to the feature repository in the image. + FeatureRepoPath string `json:"featureRepoPath"` } // GitCloneOptions describes how a clone should be performed. @@ -116,7 +214,7 @@ type GitCloneOptions struct { type FeastInitOptions struct { Minimal bool `json:"minimal,omitempty"` // Template for the created project - // +kubebuilder:validation:Enum=local;gcp;aws;snowflake;spark;postgres;hbase;cassandra;hazelcast;ikv;couchbase;clickhouse + // +kubebuilder:validation:Enum=local;gcp;aws;snowflake;spark;postgres;hbase;cassandra;hazelcast;couchbase;clickhouse;milvus;ray;ray_rag;pytorch_nlp Template string `json:"template,omitempty"` } @@ -175,6 +273,13 @@ type BatchEngineConfig struct { ConfigMapKey string `json:"configMapKey,omitempty"` } +// DataQualityMonitoringConfig defines the Data Quality Monitoring configuration. +type DataQualityMonitoringConfig struct { + // AutoBaseline controls whether baseline distribution is computed automatically on feast apply. Defaults to true. + // +kubebuilder:default=true + AutoBaseline *bool `json:"autoBaseline,omitempty"` +} + // JobSpec describes how the job execution will look like. type JobSpec struct { // PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate @@ -307,13 +412,50 @@ type FeatureStoreServices struct { UI *ServerConfigs `json:"ui,omitempty"` DeploymentStrategy *appsv1.DeploymentStrategy `json:"deploymentStrategy,omitempty"` SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` + // PodAnnotations are annotations to be applied to the Deployment's PodTemplate metadata. + // This enables annotation-driven integrations like OpenTelemetry auto-instrumentation, + // Istio sidecar injection, Vault agent injection, etc. + // +optional + PodAnnotations map[string]string `json:"podAnnotations,omitempty"` // Disable the 'feast repo initialization' initContainer DisableInitContainers bool `json:"disableInitContainers,omitempty"` + // InitImage overrides the image for init containers (feast-init, feast-apply). + // Resolution order: InitImage → FeastProjectDir.Packaged.Image → RELATED_IMAGE_FEATURE_SERVER → DefaultImage. + // +optional + InitImage *string `json:"initImage,omitempty"` + // Runs feast apply on pod start to populate the registry. Defaults to true. Ignored when DisableInitContainers is true. + RunFeastApplyOnInit *bool `json:"runFeastApplyOnInit,omitempty"` // Volumes specifies the volumes to mount in the FeatureStore deployment. A corresponding `VolumeMount` should be added to whichever feast service(s) require access to said volume(s). Volumes []corev1.Volume `json:"volumes,omitempty"` // Scaling configures horizontal scaling for the FeatureStore deployment (e.g. HPA autoscaling). // For static replicas, use spec.replicas instead. Scaling *ScalingConfig `json:"scaling,omitempty"` + // PodDisruptionBudgets configures a PodDisruptionBudget for the FeatureStore deployment. + // Only created when scaling is enabled (replicas > 1 or autoscaling). + // +optional + PodDisruptionBudgets *PDBConfig `json:"podDisruptionBudgets,omitempty"` + // TopologySpreadConstraints defines how pods are spread across topology domains. + // When scaling is enabled and this is not set, the operator auto-injects a soft + // zone-spread constraint (whenUnsatisfiable: ScheduleAnyway). + // Set to an empty array to disable auto-injection. + // +optional + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + // Affinity defines the pod scheduling constraints for the FeatureStore deployment. + // When scaling is enabled and this is not set, the operator auto-injects a soft + // pod anti-affinity rule to prefer spreading pods across nodes. + // +optional + Affinity *corev1.Affinity `json:"affinity,omitempty"` + // ResourceClaims defines which ResourceClaims must be allocated + // and reserved before the Pod is allowed to start. The resources + // will be made available to those containers which consume them + // by name. + // + // +patchMergeKey=name + // +patchStrategy=merge,retainKeys + // +listType=map + // +listMapKey=name + // +optional + ResourceClaims []corev1.PodResourceClaim `json:"resourceClaims,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name"` } // ScalingConfig configures horizontal scaling for the FeatureStore deployment. @@ -342,6 +484,20 @@ type AutoscalingConfig struct { Behavior *autoscalingv2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"` } +// PDBConfig configures a PodDisruptionBudget for the FeatureStore deployment. +// Exactly one of minAvailable or maxUnavailable must be set. +// +kubebuilder:validation:XValidation:rule="[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, c)",message="Exactly one of minAvailable or maxUnavailable must be set." +type PDBConfig struct { + // MinAvailable specifies the minimum number/percentage of pods that must remain available. + // Mutually exclusive with maxUnavailable. + // +optional + MinAvailable *intstr.IntOrString `json:"minAvailable,omitempty"` + // MaxUnavailable specifies the maximum number/percentage of pods that can be unavailable. + // Mutually exclusive with minAvailable. + // +optional + MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"` +} + // OfflineStore configures the offline store service type OfflineStore struct { // Creates a remote offline server container @@ -372,7 +528,7 @@ var ValidOfflineStoreFilePersistenceTypes = []string{ // OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service type OfflineStoreDBStorePersistence struct { // Type of the persistence type you want to use. - // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;trino;athena;mssql;couchbase.offline;clickhouse;ray + // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;trino;athena;mssql;couchbase.offline;clickhouse;ray;oracle Type string `json:"type"` // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` @@ -392,6 +548,7 @@ var ValidOfflineStoreDBStorePersistenceTypes = []string{ "couchbase.offline", "clickhouse", "ray", + "oracle", } // OnlineStore configures the online store service @@ -399,6 +556,76 @@ type OnlineStore struct { // Creates a feature server container Server *ServerConfigs `json:"server,omitempty"` Persistence *OnlineStorePersistence `json:"persistence,omitempty"` + // Serving configures the Feast feature_server section written into feature_store.yaml for the online serve pod. + // Controls metrics granularity, offline push batching, and MCP. + // +optional + Serving *ServingConfig `json:"serving,omitempty"` + // Disabled skips deploying the online store service entirely, including its + // serving pod and persistence. Omitting the online store block, or setting + // this to false, deploys the online store with defaults as before. + // +optional + Disabled bool `json:"disabled,omitempty"` +} + +// ServingConfig configures the feature_server section of the generated feature_store.yaml. +// When Mcp is set, the feature server type is switched to "mcp"; otherwise "local" is used. +type ServingConfig struct { + // Metrics configures per-category Prometheus metrics for the feature server. + // Coexists with the server.metrics bool flag — both can be set simultaneously. + // +optional + Metrics *ServingMetricsConfig `json:"metrics,omitempty"` + // OfflinePushBatching batches writes to the offline store via the /push endpoint. + // +optional + OfflinePushBatching *OfflinePushBatchingConfig `json:"offlinePushBatching,omitempty"` + // Mcp enables MCP (Model Context Protocol) server support. When set, feature server type is "mcp". + // +optional + Mcp *McpConfig `json:"mcp,omitempty"` +} + +// ServingMetricsConfig controls per-category Prometheus metrics for the feature server. +// Setting Enabled to true activates the metrics HTTP server on port 8000. +// All metric categories default to true when enabled; use Categories to selectively disable them. +type ServingMetricsConfig struct { + // Enable the Prometheus metrics endpoint on port 8000. + Enabled bool `json:"enabled"` + // Categories selectively enables or disables individual Feast metric categories. + // Keys are Feast MetricsConfig field names (e.g. "resource", "request", + // "online_features", "push", "materialization", "freshness"). Omitted keys + // default to true when metrics is enabled. + // +optional + Categories map[string]bool `json:"categories,omitempty"` +} + +// OfflinePushBatchingConfig controls batching of writes to the offline store via the /push endpoint. +// Recommended for high-throughput push workloads (streaming pipelines, IoT) to prevent OOM. +type OfflinePushBatchingConfig struct { + // Enable offline push batching. + Enabled bool `json:"enabled"` + // Maximum number of rows per offline write batch. + // +kubebuilder:validation:Minimum=1 + // +optional + BatchSize *int32 `json:"batchSize,omitempty"` + // Seconds between batch flushes to the offline store. + // +kubebuilder:validation:Minimum=1 + // +optional + BatchIntervalSeconds *int32 `json:"batchIntervalSeconds,omitempty"` +} + +// McpConfig enables MCP (Model Context Protocol) server support in the feature server. +// When this field is set on ServingConfig, the feature server type is switched to "mcp". +type McpConfig struct { + // Enable the MCP server. + Enabled bool `json:"enabled"` + // MCP server name for identification. Defaults to "feast-mcp-server". + // +optional + ServerName *string `json:"serverName,omitempty"` + // MCP server version string. Defaults to "1.0.0". + // +optional + ServerVersion *string `json:"serverVersion,omitempty"` + // MCP transport protocol. + // +kubebuilder:validation:Enum=sse;http + // +optional + Transport *string `json:"transport,omitempty"` } // OnlineStorePersistence configures the persistence settings for the online store service @@ -420,7 +647,7 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the online store service type OnlineStoreDBStorePersistence struct { // Type of the persistence type you want to use. - // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase.online;milvus;hybrid + // +kubebuilder:validation:Enum=snowflake.online;redis;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase.online;milvus;hybrid;mongodb;aerospike;scylladb Type string `json:"type"` // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` @@ -431,7 +658,6 @@ type OnlineStoreDBStorePersistence struct { var ValidOnlineStoreDBStorePersistenceTypes = []string{ "snowflake.online", "redis", - "ikv", "datastore", "dynamodb", "bigtable", @@ -446,6 +672,9 @@ var ValidOnlineStoreDBStorePersistenceTypes = []string{ "couchbase.online", "milvus", "hybrid", + "mongodb", + "aerospike", + "scylladb", } // LocalRegistryConfig configures the registry service @@ -614,6 +843,7 @@ type WorkerConfigs struct { // RegistryServerConfigs creates a registry server for the feast service, with specified container configurations. // +kubebuilder:validation:XValidation:rule="self.restAPI == true || self.grpc == true || !has(self.grpc)", message="At least one of restAPI or grpc must be true" +// +kubebuilder:validation:XValidation:rule="!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) && self.restAPI == true)", message="MCP requires restAPI to be true" type RegistryServerConfigs struct { ServerConfigs `json:",inline"` @@ -622,6 +852,11 @@ type RegistryServerConfigs struct { // Enable gRPC registry server. Defaults to true if unset. GRPC *bool `json:"grpc,omitempty"` + + // Mcp enables MCP (Model Context Protocol) on the REST registry server. + // Requires restAPI to be true. Reuses the same McpConfig struct as the online store. + // +optional + Mcp *McpConfig `json:"mcp,omitempty"` } // CronJobContainerConfigs k8s container settings for the CronJob @@ -675,7 +910,34 @@ type KubernetesAuthz struct { // OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. // https://auth0.com/docs/authenticate/protocols/openid-connect-protocol type OidcAuthz struct { - SecretRef corev1.LocalObjectReference `json:"secretRef"` + // OIDC issuer URL. The operator appends /.well-known/openid-configuration to derive the discovery endpoint. + // +optional + // +kubebuilder:validation:Pattern=`^https://\S+$` + IssuerUrl string `json:"issuerUrl,omitempty"` + // Secret with OIDC properties (auth_discovery_url, client_id, client_secret). issuerUrl takes precedence. + // +optional + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` + // Key in the Secret containing all OIDC properties as a YAML value. If unset, each key is a property. + // +optional + SecretKeyName string `json:"secretKeyName,omitempty"` + // Env var name for client pods to read an OIDC token from. Sets token_env_var in client config. + // +optional + TokenEnvVar *string `json:"tokenEnvVar,omitempty"` + // Verify SSL certificates for the OIDC provider. Defaults to true. + // +optional + VerifySSL *bool `json:"verifySSL,omitempty"` + // ConfigMap with the CA certificate for self-signed OIDC providers. Auto-detected on RHOAI/ODH. + // +optional + CACertConfigMap *OidcCACertConfigMap `json:"caCertConfigMap,omitempty"` +} + +// OidcCACertConfigMap references a ConfigMap containing a CA certificate for OIDC provider TLS. +type OidcCACertConfigMap struct { + // ConfigMap name. + Name string `json:"name"` + // Key in the ConfigMap holding the PEM certificate. Defaults to "ca-bundle.crt". + // +optional + Key string `json:"key,omitempty"` } // TlsConfigs configures server TLS for a feast service. in an openshift cluster, this is configured by default using service serving certificates. diff --git a/infra/feast-operator/api/v1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1/zz_generated.deepcopy.go index 6b12020435b..3035ed066cf 100644 --- a/infra/feast-operator/api/v1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1/zz_generated.deepcopy.go @@ -27,6 +27,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -40,7 +41,7 @@ func (in *AuthzConfig) DeepCopyInto(out *AuthzConfig) { if in.OidcAuthz != nil { in, out := &in.OidcAuthz, &out.OidcAuthz *out = new(OidcAuthz) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -144,6 +145,26 @@ func (in *CronJobContainerConfigs) DeepCopy() *CronJobContainerConfigs { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataQualityMonitoringConfig) DeepCopyInto(out *DataQualityMonitoringConfig) { + *out = *in + if in.AutoBaseline != nil { + in, out := &in.AutoBaseline, &out.AutoBaseline + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataQualityMonitoringConfig. +func (in *DataQualityMonitoringConfig) DeepCopy() *DataQualityMonitoringConfig { + if in == nil { + return nil + } + out := new(DataQualityMonitoringConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DefaultCtrConfigs) DeepCopyInto(out *DefaultCtrConfigs) { *out = *in @@ -236,6 +257,21 @@ func (in *FeastInitOptions) DeepCopy() *FeastInitOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeastPackagedOptions) DeepCopyInto(out *FeastPackagedOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastPackagedOptions. +func (in *FeastPackagedOptions) DeepCopy() *FeastPackagedOptions { + if in == nil { + return nil + } + out := new(FeastPackagedOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeastProjectDir) DeepCopyInto(out *FeastProjectDir) { *out = *in @@ -249,6 +285,11 @@ func (in *FeastProjectDir) DeepCopyInto(out *FeastProjectDir) { *out = new(FeastInitOptions) **out = **in } + if in.Packaged != nil { + in, out := &in.Packaged, &out.Packaged + *out = new(FeastPackagedOptions) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastProjectDir. @@ -368,6 +409,23 @@ func (in *FeatureStoreServices) DeepCopyInto(out *FeatureStoreServices) { *out = new(corev1.PodSecurityContext) (*in).DeepCopyInto(*out) } + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.InitImage != nil { + in, out := &in.InitImage, &out.InitImage + *out = new(string) + **out = **in + } + if in.RunFeastApplyOnInit != nil { + in, out := &in.RunFeastApplyOnInit, &out.RunFeastApplyOnInit + *out = new(bool) + **out = **in + } if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = make([]corev1.Volume, len(*in)) @@ -380,6 +438,30 @@ func (in *FeatureStoreServices) DeepCopyInto(out *FeatureStoreServices) { *out = new(ScalingConfig) (*in).DeepCopyInto(*out) } + if in.PodDisruptionBudgets != nil { + in, out := &in.PodDisruptionBudgets, &out.PodDisruptionBudgets + *out = new(PDBConfig) + (*in).DeepCopyInto(*out) + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]corev1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.ResourceClaims != nil { + in, out := &in.ResourceClaims, &out.ResourceClaims + *out = make([]corev1.PodResourceClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreServices. @@ -420,11 +502,26 @@ func (in *FeatureStoreSpec) DeepCopyInto(out *FeatureStoreSpec) { *out = new(BatchEngineConfig) (*in).DeepCopyInto(*out) } + if in.DataQualityMonitoring != nil { + in, out := &in.DataQualityMonitoring, &out.DataQualityMonitoring + *out = new(DataQualityMonitoringConfig) + (*in).DeepCopyInto(*out) + } if in.Replicas != nil { in, out := &in.Replicas, &out.Replicas *out = new(int32) **out = **in } + if in.Materialization != nil { + in, out := &in.Materialization, &out.Materialization + *out = new(MaterializationConfig) + (*in).DeepCopyInto(*out) + } + if in.OpenLineage != nil { + in, out := &in.OpenLineage, &out.OpenLineage + *out = new(OpenLineageConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreSpec. @@ -632,6 +729,88 @@ func (in *LocalRegistryConfig) DeepCopy() *LocalRegistryConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MaterializationConfig) DeepCopyInto(out *MaterializationConfig) { + *out = *in + if in.OnlineWriteBatchSize != nil { + in, out := &in.OnlineWriteBatchSize, &out.OnlineWriteBatchSize + *out = new(int32) + **out = **in + } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MaterializationConfig. +func (in *MaterializationConfig) DeepCopy() *MaterializationConfig { + if in == nil { + return nil + } + out := new(MaterializationConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *McpConfig) DeepCopyInto(out *McpConfig) { + *out = *in + if in.ServerName != nil { + in, out := &in.ServerName, &out.ServerName + *out = new(string) + **out = **in + } + if in.ServerVersion != nil { + in, out := &in.ServerVersion, &out.ServerVersion + *out = new(string) + **out = **in + } + if in.Transport != nil { + in, out := &in.Transport, &out.Transport + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new McpConfig. +func (in *McpConfig) DeepCopy() *McpConfig { + if in == nil { + return nil + } + out := new(McpConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflinePushBatchingConfig) DeepCopyInto(out *OfflinePushBatchingConfig) { + *out = *in + if in.BatchSize != nil { + in, out := &in.BatchSize, &out.BatchSize + *out = new(int32) + **out = **in + } + if in.BatchIntervalSeconds != nil { + in, out := &in.BatchIntervalSeconds, &out.BatchIntervalSeconds + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflinePushBatchingConfig. +func (in *OfflinePushBatchingConfig) DeepCopy() *OfflinePushBatchingConfig { + if in == nil { + return nil + } + out := new(OfflinePushBatchingConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OfflineStore) DeepCopyInto(out *OfflineStore) { *out = *in @@ -721,7 +900,26 @@ func (in *OfflineStorePersistence) DeepCopy() *OfflineStorePersistence { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OidcAuthz) DeepCopyInto(out *OidcAuthz) { *out = *in - out.SecretRef = in.SecretRef + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.TokenEnvVar != nil { + in, out := &in.TokenEnvVar, &out.TokenEnvVar + *out = new(string) + **out = **in + } + if in.VerifySSL != nil { + in, out := &in.VerifySSL, &out.VerifySSL + *out = new(bool) + **out = **in + } + if in.CACertConfigMap != nil { + in, out := &in.CACertConfigMap, &out.CACertConfigMap + *out = new(OidcCACertConfigMap) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OidcAuthz. @@ -734,6 +932,21 @@ func (in *OidcAuthz) DeepCopy() *OidcAuthz { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OidcCACertConfigMap) DeepCopyInto(out *OidcCACertConfigMap) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OidcCACertConfigMap. +func (in *OidcCACertConfigMap) DeepCopy() *OidcCACertConfigMap { + if in == nil { + return nil + } + out := new(OidcCACertConfigMap) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { *out = *in @@ -747,6 +960,11 @@ func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { *out = new(OnlineStorePersistence) (*in).DeepCopyInto(*out) } + if in.Serving != nil { + in, out := &in.Serving, &out.Serving + *out = new(ServingConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStore. @@ -820,6 +1038,90 @@ func (in *OnlineStorePersistence) DeepCopy() *OnlineStorePersistence { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenLineageConfig) DeepCopyInto(out *OpenLineageConfig) { + *out = *in + if in.TransportType != nil { + in, out := &in.TransportType, &out.TransportType + *out = new(string) + **out = **in + } + if in.TransportUrl != nil { + in, out := &in.TransportUrl, &out.TransportUrl + *out = new(string) + **out = **in + } + if in.TransportEndpoint != nil { + in, out := &in.TransportEndpoint, &out.TransportEndpoint + *out = new(string) + **out = **in + } + if in.ApiKeySecretRef != nil { + in, out := &in.ApiKeySecretRef, &out.ApiKeySecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Consumer != nil { + in, out := &in.Consumer, &out.Consumer + *out = new(OpenLineageConsumerConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenLineageConfig. +func (in *OpenLineageConfig) DeepCopy() *OpenLineageConfig { + if in == nil { + return nil + } + out := new(OpenLineageConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenLineageConsumerConfig) DeepCopyInto(out *OpenLineageConsumerConfig) { + *out = *in + if in.StoreType != nil { + in, out := &in.StoreType, &out.StoreType + *out = new(string) + **out = **in + } + if in.ConnectionStringSecretRef != nil { + in, out := &in.ConnectionStringSecretRef, &out.ConnectionStringSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.ApiKeySecretRef != nil { + in, out := &in.ApiKeySecretRef, &out.ApiKeySecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.NamespaceMapping != nil { + in, out := &in.NamespaceMapping, &out.NamespaceMapping + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenLineageConsumerConfig. +func (in *OpenLineageConsumerConfig) DeepCopy() *OpenLineageConsumerConfig { + if in == nil { + return nil + } + out := new(OpenLineageConsumerConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OptionalCtrConfigs) DeepCopyInto(out *OptionalCtrConfigs) { *out = *in @@ -878,6 +1180,31 @@ func (in *OptionalCtrConfigs) DeepCopy() *OptionalCtrConfigs { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDBConfig) DeepCopyInto(out *PDBConfig) { + *out = *in + if in.MinAvailable != nil { + in, out := &in.MinAvailable, &out.MinAvailable + *out = new(intstr.IntOrString) + **out = **in + } + if in.MaxUnavailable != nil { + in, out := &in.MaxUnavailable, &out.MaxUnavailable + *out = new(intstr.IntOrString) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDBConfig. +func (in *PDBConfig) DeepCopy() *PDBConfig { + if in == nil { + return nil + } + out := new(PDBConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PvcConfig) DeepCopyInto(out *PvcConfig) { *out = *in @@ -1050,6 +1377,11 @@ func (in *RegistryServerConfigs) DeepCopyInto(out *RegistryServerConfigs) { *out = new(bool) **out = **in } + if in.Mcp != nil { + in, out := &in.Mcp, &out.Mcp + *out = new(McpConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryServerConfigs. @@ -1200,6 +1532,58 @@ func (in *ServiceHostnames) DeepCopy() *ServiceHostnames { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServingConfig) DeepCopyInto(out *ServingConfig) { + *out = *in + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = new(ServingMetricsConfig) + (*in).DeepCopyInto(*out) + } + if in.OfflinePushBatching != nil { + in, out := &in.OfflinePushBatching, &out.OfflinePushBatching + *out = new(OfflinePushBatchingConfig) + (*in).DeepCopyInto(*out) + } + if in.Mcp != nil { + in, out := &in.Mcp, &out.Mcp + *out = new(McpConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServingConfig. +func (in *ServingConfig) DeepCopy() *ServingConfig { + if in == nil { + return nil + } + out := new(ServingConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServingMetricsConfig) DeepCopyInto(out *ServingMetricsConfig) { + *out = *in + if in.Categories != nil { + in, out := &in.Categories, &out.Categories + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServingMetricsConfig. +func (in *ServingMetricsConfig) DeepCopy() *ServingMetricsConfig { + if in == nil { + return nil + } + out := new(ServingMetricsConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TlsConfigs) DeepCopyInto(out *TlsConfigs) { *out = *in diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 27b151bbb77..8ccde377e77 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -78,10 +78,22 @@ type FeatureStoreSpec struct { } // FeastProjectDir defines how to create the feast project directory. -// +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init)].exists_one(c, c)",message="One selection required between init or git." +// +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init), has(self.packaged)].exists_one(c, c)",message="One selection required between init, git, or packaged." type FeastProjectDir struct { - Git *GitCloneOptions `json:"git,omitempty"` - Init *FeastInitOptions `json:"init,omitempty"` + Git *GitCloneOptions `json:"git,omitempty"` + Init *FeastInitOptions `json:"init,omitempty"` + Packaged *FeastPackagedOptions `json:"packaged,omitempty"` +} + +// FeastPackagedOptions describes a feature repository packaged in a feature server image. +// +kubebuilder:validation:XValidation:rule="self.featureRepoPath.startsWith('/') && self.featureRepoPath != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..')",message="FeatureRepoPath must be a canonical absolute, non-root path without dot segments or repeated separators." +type FeastPackagedOptions struct { + // Image containing the packaged feature repository. When set, this image is used by the + // repository initialization and feast apply containers and as the default service image. + // When omitted, the operator's configured feature server image is used. + Image string `json:"image,omitempty"` + // FeatureRepoPath is the canonical absolute path to the feature repository in the image. + FeatureRepoPath string `json:"featureRepoPath"` } // GitCloneOptions describes how a clone should be performed. @@ -105,7 +117,7 @@ type GitCloneOptions struct { type FeastInitOptions struct { Minimal bool `json:"minimal,omitempty"` // Template for the created project - // +kubebuilder:validation:Enum=local;gcp;aws;snowflake;spark;postgres;hbase;cassandra;hazelcast;ikv;couchbase;clickhouse + // +kubebuilder:validation:Enum=local;gcp;aws;snowflake;spark;postgres;hbase;cassandra;hazelcast;couchbase;clickhouse;milvus;ray;ray_rag;pytorch_nlp Template string `json:"template,omitempty"` } @@ -289,6 +301,8 @@ type FeatureStoreServices struct { SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` // Disable the 'feast repo initialization' initContainer DisableInitContainers bool `json:"disableInitContainers,omitempty"` + // Runs feast apply on pod start to populate the registry. Defaults to true. Ignored when DisableInitContainers is true. + RunFeastApplyOnInit *bool `json:"runFeastApplyOnInit,omitempty"` // Volumes specifies the volumes to mount in the FeatureStore deployment. A corresponding `VolumeMount` should be added to whichever feast service(s) require access to said volume(s). Volumes []corev1.Volume `json:"volumes,omitempty"` } @@ -371,7 +385,7 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the online store service type OnlineStoreDBStorePersistence struct { // Type of the persistence type you want to use. - // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase.online;milvus;hybrid + // +kubebuilder:validation:Enum=snowflake.online;redis;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase.online;milvus;hybrid;mongodb;aerospike;scylladb Type string `json:"type"` // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` @@ -382,7 +396,6 @@ type OnlineStoreDBStorePersistence struct { var ValidOnlineStoreDBStorePersistenceTypes = []string{ "snowflake.online", "redis", - "ikv", "datastore", "dynamodb", "bigtable", @@ -397,6 +410,9 @@ var ValidOnlineStoreDBStorePersistenceTypes = []string{ "couchbase.online", "milvus", "hybrid", + "mongodb", + "aerospike", + "scylladb", } // LocalRegistryConfig configures the registry service diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index fa7c6f210dd..17ae4841966 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -183,6 +183,21 @@ func (in *FeastInitOptions) DeepCopy() *FeastInitOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeastPackagedOptions) DeepCopyInto(out *FeastPackagedOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastPackagedOptions. +func (in *FeastPackagedOptions) DeepCopy() *FeastPackagedOptions { + if in == nil { + return nil + } + out := new(FeastPackagedOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeastProjectDir) DeepCopyInto(out *FeastProjectDir) { *out = *in @@ -196,6 +211,11 @@ func (in *FeastProjectDir) DeepCopyInto(out *FeastProjectDir) { *out = new(FeastInitOptions) **out = **in } + if in.Packaged != nil { + in, out := &in.Packaged, &out.Packaged + *out = new(FeastPackagedOptions) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastProjectDir. @@ -315,6 +335,11 @@ func (in *FeatureStoreServices) DeepCopyInto(out *FeatureStoreServices) { *out = new(v1.PodSecurityContext) (*in).DeepCopyInto(*out) } + if in.RunFeastApplyOnInit != nil { + in, out := &in.RunFeastApplyOnInit, &out.RunFeastApplyOnInit + *out = new(bool) + **out = **in + } if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = make([]v1.Volume, len(*in)) diff --git a/infra/feast-operator/bundle.Dockerfile b/infra/feast-operator/bundle.Dockerfile index 685b137b92a..eda73b6494e 100644 --- a/infra/feast-operator/bundle.Dockerfile +++ b/infra/feast-operator/bundle.Dockerfile @@ -6,7 +6,7 @@ LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ LABEL operators.operatorframework.io.bundle.package.v1=feast-operator LABEL operators.operatorframework.io.bundle.channels.v1=alpha -LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.38.0 +LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.41.0 LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4 diff --git a/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml b/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml index 913517e198a..5749c2042b5 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml @@ -14,6 +14,7 @@ spec: protocol: TCP targetPort: 8443 selector: + app.kubernetes.io/name: feast-operator control-plane: controller-manager status: loadBalancer: {} diff --git a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml index aff4d1f9840..b1f88ff62c3 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -5,6 +5,8 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: feast-operator + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" name: feast-operator-featurestore-editor-role rules: - apiGroups: diff --git a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml index bcf9699fc1a..c64d255eed7 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -5,6 +5,7 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: feast-operator + rbac.authorization.k8s.io/aggregate-to-view: "true" name: feast-operator-featurestore-viewer-role rules: - apiGroups: diff --git a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml index b0ff79ec692..19af99046e9 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml @@ -4,6 +4,35 @@ metadata: annotations: alm-examples: |- [ + { + "apiVersion": "feast.dev/v1", + "kind": "FeatureStore", + "metadata": { + "name": "sample-materialization-openlineage", + "namespace": "feast" + }, + "spec": { + "feastProject": "my_project", + "materialization": { + "onlineWriteBatchSize": 10000 + }, + "openlineage": { + "apiKeySecretRef": { + "name": "openlineage-secret" + }, + "enabled": true, + "extraConfig": { + "emit_on_apply": "true", + "emit_on_materialize": "true", + "namespace": "my-feast-project", + "producer": "feast-operator" + }, + "transportEndpoint": "api/v1/lineage", + "transportType": "http", + "transportUrl": "http://marquez.feast.svc.cluster.local:5000" + } + } + }, { "apiVersion": "feast.dev/v1", "kind": "FeatureStore", @@ -14,6 +43,39 @@ metadata: "feastProject": "my_project" } }, + { + "apiVersion": "feast.dev/v1", + "kind": "FeatureStore", + "metadata": { + "name": "sample-mcp" + }, + "spec": { + "feastProject": "my_project", + "services": { + "onlineStore": { + "server": {}, + "serving": { + "mcp": { + "enabled": true, + "serverName": "feast-mcp-server", + "serverVersion": "1.0.0", + "transport": "sse" + } + } + }, + "registry": { + "local": { + "server": { + "mcp": { + "enabled": true + }, + "restAPI": true + } + } + } + } + } + }, { "apiVersion": "feast.dev/v1", "kind": "FeatureStore", @@ -35,6 +97,41 @@ metadata: } } }, + { + "apiVersion": "feast.dev/v1", + "kind": "FeatureStore", + "metadata": { + "name": "sample-serving" + }, + "spec": { + "feastProject": "my_project", + "services": { + "onlineStore": { + "server": {}, + "serving": { + "metrics": { + "categories": { + "audit_logging": false, + "freshness": false, + "materialization": true, + "offline_features": true, + "online_features": true, + "push": true, + "request": true, + "resource": true + }, + "enabled": true + }, + "offlinePushBatching": { + "batchIntervalSeconds": 10, + "batchSize": 1000, + "enabled": true + } + } + } + } + } + }, { "apiVersion": "feast.dev/v1", "kind": "FeatureStore", @@ -50,10 +147,10 @@ metadata: } ] capabilities: Basic Install - createdAt: "2026-02-17T13:52:39Z" - operators.operatorframework.io/builder: operator-sdk-v1.38.0 + createdAt: "2026-07-20T13:27:58Z" + operators.operatorframework.io/builder: operator-sdk-v1.41.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 - name: feast-operator.v0.60.0 + name: feast-operator.v0.65.0 namespace: placeholder spec: apiservicedefinitions: {} @@ -78,6 +175,63 @@ spec: spec: clusterPermissions: - rules: + - apiGroups: + - "" + resources: + - configmaps + - persistentvolumeclaims + - services + verbs: + - create + - delete + - deletecollection + - get + - list + - update + - watch + - apiGroups: + - "" + resources: + - namespaces + - secrets + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - deletecollection + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods/exec + verbs: + - create + - apiGroups: + - "" + resources: + - pods/log + verbs: + - get + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - update + - watch - apiGroups: - apps resources: @@ -96,9 +250,9 @@ spec: verbs: - create - apiGroups: - - batch + - autoscaling resources: - - cronjobs + - horizontalpodautoscalers verbs: - create - delete @@ -108,35 +262,25 @@ spec: - update - watch - apiGroups: - - "" + - batch resources: - - configmaps - - persistentvolumeclaims - - serviceaccounts - - services + - cronjobs verbs: - create - delete - get - list + - patch - update - watch - apiGroups: - - "" + - config.openshift.io resources: - - namespaces - - pods - - secrets + - apiservers verbs: - get - list - watch - - apiGroups: - - "" - resources: - - pods/exec - verbs: - - create - apiGroups: - feast.dev resources: @@ -163,6 +307,29 @@ spec: - get - patch - update + - apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - watch + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: @@ -189,6 +356,14 @@ spec: - list - update - watch + - apiGroups: + - sparkoperator.k8s.io + resources: + - sparkapplications + verbs: + - create + - delete + - get - apiGroups: - authentication.k8s.io resources: @@ -212,6 +387,7 @@ spec: replicas: 1 selector: matchLabels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager strategy: {} template: @@ -219,6 +395,7 @@ spec: annotations: kubectl.kubernetes.io/default-container: manager labels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager spec: containers: @@ -230,10 +407,13 @@ spec: - /manager env: - name: RELATED_IMAGE_FEATURE_SERVER - value: quay.io/feastdev/feature-server:0.60.0 + value: quay.io/feastdev/feature-server:0.65.0 - name: RELATED_IMAGE_CRON_JOB value: quay.io/openshift/origin-cli:4.17 - image: quay.io/feastdev/feast-operator:0.60.0 + - name: GOMEMLIMIT + value: 230MiB + - name: OIDC_ISSUER_URL + image: quay.io/feastdev/feast-operator:0.65.0 livenessProbe: httpGet: path: /healthz @@ -323,8 +503,8 @@ spec: name: Feast Community url: https://lf-aidata.atlassian.net/wiki/spaces/FEAST/ relatedImages: - - image: quay.io/feastdev/feature-server:0.60.0 + - image: quay.io/feastdev/feature-server:0.65.0 name: feature-server - image: quay.io/openshift/origin-cli:4.17 name: cron-job - version: 0.60.0 + version: 0.65.0 diff --git a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml index f69971c1c4c..0ab08afef51 100644 --- a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml +++ b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml @@ -2,7 +2,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null name: featurestores.feast.dev spec: @@ -62,10 +62,32 @@ spec: OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. https://auth0. properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM certificate. + Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. properties: name: default: "" @@ -76,8 +98,14 @@ spec: type: string type: object x-kubernetes-map-type: atomic - required: - - secretRef + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean type: object type: object x-kubernetes-validations: @@ -133,14 +161,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -184,6 +212,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -239,7 +296,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -258,8 +315,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -305,6 +362,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -346,7 +407,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -440,7 +501,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -498,6 +558,16 @@ spec: description: The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. type: string type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object feastProject: description: FeastProject is the Feast project id. pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ @@ -523,14 +593,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -574,6 +644,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -629,7 +728,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -648,8 +747,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -703,624 +802,1026 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' - services: - description: FeatureStoreServices defines the desired feast services. - An ephemeral onlineStore feature server is deployed by default. + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. properties: - deploymentStrategy: - description: DeploymentStrategy describes how to replace existing - pods with new ones. + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. properties: - rollingUpdate: + name: + default: "" description: |- - Rolling update config params. Present only if DeploymentStrategyType = - RollingUpdate. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... properties: - maxSurge: - anyOf: - - type: integer - - type: string + name: + default: "" description: |- - The maximum number of pods that can be scheduled above the desired number of - pods. - x-kubernetes-int-or-string: true - maxUnavailable: - anyOf: - - type: integer - - type: string - description: The maximum number of pods that can be unavailable - during the update. - x-kubernetes-int-or-string: true + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string type: object - type: - description: Type of deployment. Can be "Recreate" or "RollingUpdate". - Default is RollingUpdate. + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql type: string + required: + - enabled type: object - disableInitContainers: - description: Disable the 'feast repo initialization' initContainer + enabled: + description: Enable OpenLineage integration. type: boolean - offlineStore: - description: OfflineStore configures the offline store service + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints for + the FeatureStore deployment. properties: - persistence: - description: OfflineStorePersistence configures the persistence - settings for the offline store service + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. properties: - file: - description: OfflineStoreFilePersistence configures the - file-based persistence for the offline store service - properties: - pvc: - description: PvcConfig defines the settings for a - persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent volume - access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true + key: + description: The label key that the selector + applies to. + type: string + operator: description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum - amount of compute resources required. - type: object + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which this - persistent volume belongs. - type: string - type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. - type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath - type: object - x-kubernetes-validations: - - message: One selection is required between ref and - create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and must - not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - type: - enum: - - file - - dask - - duckdb - type: string - type: object - store: - description: OfflineStoreDBStorePersistence configures - the DB store persistence for the offline store service - properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName - type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the secret - key. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you want - to use. - enum: - - snowflake.offline - - bigquery - - redshift - - spark - - postgres - - trino - - athena - - mssql - - couchbase.offline - - clickhouse - - ray - type: string + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic required: - - secretRef - - type + - nodeSelectorTerms type: object + x-kubernetes-map-type: atomic type: object - x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, c)' - server: - description: Creates a remote offline server container + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). properties: - env: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... items: - description: EnvVar represents an environment variable - present in a Container. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports - metadata.name, metadata.namespace, `metadata.labels['''']`, - `metadata.' + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - resourceFieldRef: + matchLabelKeys: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace - properties: - key: - description: The key of the secret to select - from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer required: - - name + - podAffinityTerm + - weight type: object type: array - envFrom: + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... items: - description: EnvFromSource represents the source of - a set of ConfigMaps + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... properties: - configMapRef: - description: The ConfigMap to select from + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey type: object type: array - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - logLevel: + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible metrics - for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - tls: - description: TlsConfigs configures server TLS for a feast - service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, where - TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret key - names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... items: - description: VolumeMount describes a mounting of a Volume - within a container. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: - description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. - type: string - subPath: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: Expanded path within the volume from - which the container's volume should be mounted. - type: string + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer required: - - mountPath - - name + - podAffinityTerm + - weight type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number of seconds - after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores (2 * - CPU + 1). - format: int32 - minimum: -1 - type: integer - type: object - type: object - type: object - onlineStore: - description: OnlineStore configures the online store service - properties: - persistence: - description: OnlineStorePersistence configures the persistence - settings for the online store service - properties: - file: - description: OnlineStoreFilePersistence configures the - file-based persistence for the online store service - properties: - path: - type: string - pvc: - description: PvcConfig defines the settings for a - persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent volume - access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true + key: + description: key is the label key that + the selector applies to. + type: string + operator: description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum - amount of compute resources required. - type: object + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which this - persistent volume belongs. + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: type: string - type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be unavailable + during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or "RollingUpdate". + Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures the + file-based persistence for the offline store service + properties: + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string type: object x-kubernetes-map-type: atomic @@ -1335,21 +1836,16 @@ spec: - message: Mount path must start with '/' and must not contain ':' rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string type: object - x-kubernetes-validations: - - message: Ephemeral stores must have absolute paths. - rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') - : true' - - message: PVC path must be a file name only, with no - slashes. - rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') - : true' - - message: Online store does not support S3 or GS buckets. - rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') - || self.path.startsWith(''gs://'')) : true' store: - description: OnlineStoreDBStorePersistence configures - the DB store persistence for the online store service + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service properties: secretKeyName: description: By default, the selected store "type" @@ -1373,23 +1869,18 @@ spec: description: Type of the persistence type you want to use. enum: - - snowflake.online - - redis - - ikv - - datastore - - dynamodb - - bigtable + - snowflake.offline + - bigquery + - redshift + - spark - postgres - - cassandra - - mysql - - hazelcast - - singlestore - - hbase - - elasticsearch - - qdrant - - couchbase.online - - milvus - - hybrid + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle type: string required: - secretRef @@ -1400,7 +1891,7 @@ spec: - message: One selection required between file or store. rule: '[has(self.file), has(self.store)].exists_one(c, c)' server: - description: Creates a feature server container + description: Creates a remote offline server container properties: env: items: @@ -1408,14 +1899,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1459,6 +1950,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1515,7 +2036,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1534,8 +2055,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1597,6 +2118,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1756,151 +2281,79 @@ spec: type: object type: object type: object - registry: - description: Registry configures the registry service. One selection - is required. Local is the default setting. + onlineStore: + description: OnlineStore configures the online store service properties: - local: - description: LocalRegistryConfig configures the registry service + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service properties: - persistence: - description: RegistryPersistence configures the persistence - settings for the registry service + file: + description: OnlineStoreFilePersistence configures the + file-based persistence for the online store service properties: - file: - description: RegistryFilePersistence configures the - file-based persistence for the registry service + path: + type: string + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. properties: - cache_mode: - description: |- - CacheMode defines the registry cache update strategy. - Allowed values are "sync" and "thread". - enum: - - none - - sync - - thread - type: string - cache_ttl_seconds: - description: CacheTTLSeconds defines the TTL (in - seconds) for the registry cache. - format: int32 - minimum: 0 - type: integer - path: - type: string - pvc: - description: PvcConfig defines the settings for - a persistent file store based on PVCs. + create: + description: Settings for creating a new PVC properties: - create: - description: Settings for creating a new PVC + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. properties: - accessModes: - description: AccessModes k8s persistent - volume access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the - minimum amount of compute resources - required. - type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which - this persistent volume belongs. - type: string type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath type: object x-kubernetes-validations: - - message: One selection is required between ref - and create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and - must not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - s3_additional_kwargs: - additionalProperties: - type: string - type: object - type: object - x-kubernetes-validations: - - message: Registry files must use absolute paths - or be S3 ('s3://') or GS ('gs://') object store - URIs. - rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') - || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) - : true' - - message: PVC path must be a file name only, with - no slashes. - rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') - : true' - - message: PVC persistence does not support S3 or - GS object store URIs. - rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') - || self.path.startsWith(''gs://'')) : true' - - message: Additional S3 settings are available only - for S3 object store URIs. - rule: '(has(self.s3_additional_kwargs) && has(self.path)) - ? self.path.startsWith(''s3://'') : true' - store: - description: RegistryDBStorePersistence configures - the DB store persistence for the registry service - properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the - secret key. + ref: + description: Reference to an existing field properties: name: default: "" @@ -1911,146 +2364,111 @@ spec: type: string type: object x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you - want to use. - enum: - - sql - - snowflake.registry - type: string required: - - secretRef - - type + - mountPath type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') type: object x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, - c)' - server: - description: Creates a registry server container + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with no + slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service properties: - env: - items: - description: EnvVar represents an environment variable - present in a Container. + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: - supports metadata.name, metadata.namespace, - `metadata.labels['''']`, `metadata.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source - of a set of ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: + key: + description: The key to select. + type: string name: default: "" description: |- @@ -2060,17 +2478,92 @@ spec: type: string optional: description: Specify whether the ConfigMap - must be defined + or its key must be defined type: boolean + required: + - key type: object x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string name: default: "" description: |- @@ -2080,374 +2573,1744 @@ spec: type: string optional: description: Specify whether the Secret - must be defined + or its key must be defined type: boolean + required: + - key type: object x-kubernetes-map-type: atomic type: object - type: array - grpc: - description: Enable gRPC registry server. Defaults - to true if unset. - type: boolean - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible - metrics for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - restAPI: - description: Enable REST API registry server. - type: boolean - tls: - description: TlsConfigs configures server TLS for - a feast service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, - where TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret - key names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. - items: - description: VolumeMount describes a mounting of - a Volume within a container. + secretRef: + description: The Secret to select from properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - type: string name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: + default: "" description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - subPath: + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. type: string - subPathExpr: - description: Expanded path within the volume - from which the container's volume should be - mounted. + request: + description: Request is the name chosen for + a request in the referenced claim. type: string required: - - mountPath - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number - of seconds after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores - (2 * CPU + 1). - format: int32 - minimum: -1 - type: integer + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string type: object + x-kubernetes-map-type: atomic type: object x-kubernetes-validations: - - message: At least one of restAPI or grpc must be true - rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object - remote: - description: RemoteRegistryConfig points to a remote feast - registry server. + serving: + description: Serving configures the Feast feature_server section + written into feature_store.yaml for the online serve pod. properties: - feastRef: - description: Reference to an existing `FeatureStore` CR - in the same k8s cluster. + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is "mcp". properties: - name: - description: Name of the FeatureStore + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. Defaults + to "feast-mcp-server". type: string - namespace: - description: Namespace of the FeatureStore + serverVersion: + description: MCP server version string. Defaults to + "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http type: string required: - - name + - enabled type: object - hostname: - description: Host address of the remote registry service - - :, e.g. `registry..svc.cluster.local:80` - type: string - tls: - description: TlsRemoteRegistryConfigs configures client - TLS for a remote feast registry. + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. properties: - certName: - description: defines the configmap key name for the - client TLS cert. - type: string - configMapRef: - description: references the local k8s configmap where - the TLS cert resides - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or disables + individual Feast metric categories. type: object - x-kubernetes-map-type: atomic + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean required: - - certName - - configMapRef + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to the + offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to the + offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline write + batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled type: object type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, - c)' + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied to the + Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true type: object x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.local), has(self.remote)].exists_one(c, c)' - securityContext: - description: PodSecurityContext holds pod-level security attributes - and common container settings. + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One selection + is required. Local is the default setting. properties: - appArmorProfile: - description: appArmorProfile is the AppArmor options to use - by the containers in this pod. - properties: - localhostProfile: - description: localhostProfile indicates a profile loaded - on the node that should be used. - type: string - type: - description: type indicates which kind of AppArmor profile - will be applied. - type: string - required: - - type - type: object - fsGroup: - description: A special supplemental group that applies to - all containers in a pod. - format: int64 - type: integer - fsGroupChangePolicy: - description: |- - fsGroupChangePolicy defines behavior of changing ownership and permission of the volume - before being exposed inside Pod. - type: string - runAsGroup: - description: |- - The GID to run the entrypoint of the container process. - Uses runtime default if unset. - format: int64 - type: integer - runAsNonRoot: - description: Indicates that the container must run as a non-root - user. - type: boolean - runAsUser: - description: |- - The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - format: int64 - type: integer - seLinuxOptions: - description: The SELinux context to be applied to all containers. - properties: - level: - description: Level is SELinux level label that applies - to the container. - type: string - role: - description: Role is a SELinux role label that applies - to the container. - type: string - type: - description: Type is a SELinux type label that applies - to the container. - type: string - user: - description: User is a SELinux user label that applies - to the container. - type: string - type: object - seccompProfile: - description: |- - The seccomp options to use by the containers in this pod. - Note that this field cannot be set when spec.os. + local: + description: LocalRegistryConfig configures the registry service properties: - localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. - type: string - type: - description: type indicates which kind of seccomp profile - will be applied. - type: string - required: - - type - type: object - supplementalGroups: - description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG - items: - format: int64 - type: integer - type: array - x-kubernetes-list-type: atomic - sysctls: - description: Sysctls hold a list of namespaced sysctls used - for the pod. - items: - description: Sysctl defines a kernel parameter to be set + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures the + file-based persistence for the registry service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object store + URIs. + rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') + || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 or + GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available only + for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be true + rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` CR + in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the FeatureStore + deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: scaleUp is scaling policy for scaling + Up. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the number + of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for which + to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... + properties: + container: + description: container is the name of the container + in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the descriptions + of a object,such as kind,name apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent + type: string + kind: + description: 'kind is the kind of the referent; + More info: https://git.k8s.' + type: string + name: + description: 'name is the name of the referent; + More info: https://kubernetes.' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. + type: string + required: + - type + type: object + type: array + minReplicas: + description: MinReplicas is the lower limit for the number + of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object + type: object + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to use + by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile loaded + on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor profile + will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies to + all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as a non-root + user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string + seLinuxOptions: + description: The SELinux context to be applied to all containers. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile defined + in a file on the node should be used. + type: string + type: + description: type indicates which kind of seccomp profile + will be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string + sysctls: + description: Sysctls hold a list of namespaced sysctls used + for the pod. + items: + description: Sysctl defines a kernel parameter to be set properties: name: description: Name of a property to set @@ -2484,6 +4347,96 @@ spec: type: string type: object type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are spread + across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of eligible + domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array ui: description: Creates a UI server container properties: @@ -2493,14 +4446,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -2544,6 +4497,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -2599,7 +4581,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -2618,8 +4600,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -2680,6 +4662,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -2848,7 +4834,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the volume @@ -2890,6 +4876,7 @@ spec: blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -2898,9 +4885,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -2931,7 +4919,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: monitors: description: |- @@ -2979,7 +4967,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -3025,7 +5013,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -3065,7 +5053,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver that @@ -3077,7 +5065,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -3139,7 +5127,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3388,9 +5376,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -3425,7 +5413,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -3446,7 +5434,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -3456,7 +5444,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -3485,7 +5473,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -3503,14 +5491,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that details + Glusterfs topology. type: string path: description: |- @@ -3545,6 +5531,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -3570,6 +5572,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -3660,7 +5663,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -3677,7 +5680,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -3707,10 +5710,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected along - with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod to @@ -3790,7 +5796,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -3861,7 +5867,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3903,6 +5909,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -3910,7 +5962,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -3976,7 +6028,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: group: description: |- @@ -3991,12 +6043,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -4012,9 +6064,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the volume @@ -4026,6 +6077,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -4040,333 +6092,832 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - More info: https://examples.k8s. - type: boolean - secretRef: - description: |- - secretRef is name of the authentication secret for RBDUser. If provided - overrides keyring. - Default is nil. + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s. + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes. + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes. + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope of + the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline store. + Configure services.offlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure registry.local.persistence.store + or use a remote registry when using replicas > 1 or autoscaling. S3/GCS-backed + registry is also allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + properties: + applied: + description: Shows the currently applied feast configuration, including + any pertinent defaults + properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed + in the same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM + certificate. Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine + configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" + if not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch + engine configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable + present in a Container. properties: name: - default: "" description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string - type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is the rados user name. - Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume - attached and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - gateway: - description: gateway is the host address of the ScaleIO - API Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO - Protection Domain for the configured storage. - type: string - readOnly: - description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef references to the secret for ScaleIO user and other - sensitive information. - properties: - name: - default: "" + value: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... type: string - type: object - x-kubernetes-map-type: atomic - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: storageMode indicates whether the storage - for a volume should be ThickProvisioned or ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool - associated with the protection domain. - type: string - system: - description: system is the name of the storage system - as configured in ScaleIO. - type: string - volumeName: - description: |- - volumeName is the name of a volume already created in the ScaleIO system - that is associated with this volume source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: |- - secret represents a secret that should populate this volume. - More info: https://kubernetes. - properties: - defaultMode: - description: 'defaultMode is Optional: mode bits used - to set permissions on created files by default.' - format: int32 - type: integer + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: items: - description: |- - items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a - items: - description: Maps a string key to a path within a - volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used - to set permissions on this file.' - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - type: string - required: - - key - - path - type: object - type: array - x-kubernetes-list-type: atomic - optional: - description: optional field specify whether the Secret - or its keys must be defined - type: boolean - secretName: - description: |- - secretName is the name of the secret in the pod's namespace to use. - More info: https://kubernetes. - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef specifies the secret to use for obtaining the StorageOS API - credentials. + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic type: object - x-kubernetes-map-type: atomic - volumeName: - description: |- - volumeName is the human-readable name of the StorageOS volume. Volume - names are only unique within a namespace. - type: string - volumeNamespace: - description: volumeNamespace specifies the scope of - the volume within StorageOS. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based - Management (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy - Based Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies - vSphere volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object - type: array - type: object - required: - - feastProject - type: object - status: - description: FeatureStoreStatus defines the observed state of FeatureStore - properties: - applied: - description: Shows the currently applied feast configuration, including - any pertinent defaults - properties: - authz: - description: AuthzConfig defines the authorization settings for - the deployed Feast services. - properties: - kubernetes: - description: |- - KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. - https://kubernetes. - properties: - roles: - description: The Kubernetes RBAC roles to be deployed - in the same namespace of the FeatureStore. - items: - type: string type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object type: object - oidc: - description: |- - OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. - https://auth0. + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. properties: - secretRef: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules type: object - x-kubernetes-map-type: atomic - required: - - secretRef + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string type: object - x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, - c)' - batchEngine: - description: BatchEngineConfig defines the batch compute engine - configuration. + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. properties: - configMapKey: - description: Key name in the ConfigMap. Defaults to "config" - if not specified. - type: string - configMapRef: - description: Reference to a ConfigMap containing the batch - engine configuration. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean type: object - cronJob: - description: FeastCronJob defines a CronJob to execute against - a Feature Store deployment. + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. properties: - annotations: - additionalProperties: - type: string - description: Annotations to be added to the CronJob metadata. - type: object - concurrencyPolicy: - description: Specifies how to treat concurrent executions - of a Job. - type: string - containerConfigs: - description: CronJobContainerConfigs k8s container settings - for the CronJob + git: + description: GitCloneOptions describes how a clone should + be performed. properties: - commands: - description: Array of commands to be executed (in order) - against a Feature Store deployment. - items: + configs: + additionalProperties: type: string - type: array + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object env: items: description: EnvVar represents an environment variable present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4410,6 +6961,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4466,7 +7047,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4485,8 +7066,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -4506,449 +7087,959 @@ spec: x-kubernetes-map-type: atomic type: object type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast + init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string image: + description: Image containing the packaged feature repository. type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - nodeSelector: + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: additionalProperties: type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints + for the FeatureStore deployment. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. properties: - claims: + preferredDuringSchedulingIgnoredDuringExecution: description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in the + range 1-100. + format: int32 + type: integer required: - - name + - preference + - weight type: object type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - type: object - failedJobsHistoryLimit: - description: The number of failed finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - jobSpec: - description: Specification of the desired behavior of a job. - properties: - activeDeadlineSeconds: - description: |- - Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr - format: int64 - type: integer - backoffLimit: - description: Specifies the number of retries before marking - this job failed. - format: int32 - type: integer - backoffLimitPerIndex: - description: |- - Specifies the limit for the number of retries within an - index before marking this index as failed. - format: int32 - type: integer - completionMode: - description: |- - completionMode specifies how Pod completions are tracked. It can be - `NonIndexed` (default) or `Indexed`. - type: string - completions: - description: |- - Specifies the desired number of successfully finished pods the - job should be run with. - format: int32 - type: integer - maxFailedIndexes: - description: |- - Specifies the maximal number of failed indexes before marking the Job as - failed, when backoffLimitPerIndex is set. - format: int32 - type: integer - parallelism: - description: |- - Specifies the maximum desired number of pods the job should - run at any given time. - format: int32 - type: integer - podFailurePolicy: - description: Specifies the policy of handling failed pods. + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). properties: - rules: - description: A list of pod failure policy rules. The - rules are evaluated in order. + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... items: - description: PodFailurePolicyRule describes how - a pod failure is handled when the requirements - are met. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - action: - description: Specifies the action taken on a - pod failure when the requirements are satisfied. - type: string - onExitCodes: - description: Represents the requirement on the - container exit codes. + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. properties: - containerName: + labelSelector: description: |- - Restricts the check for exit codes to the container with the - specified name. - type: string - operator: + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: description: |- - Represents the relationship between the container exit code(s) and the - specified values. - type: string - values: - description: Specifies the set of values. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. items: - format: int32 - type: integer - type: array - x-kubernetes-list-type: set - required: - - operator - - values - type: object - onPodConditions: - description: |- - Represents the requirement on the pod conditions. The requirement is represented - as a list of pod condition patterns. - items: - description: |- - PodFailurePolicyOnPodConditionsPattern describes a pattern for matching - an actual pod condition type. - properties: - status: - description: Specifies the required Pod - condition status. type: string - type: - description: Specifies the required Pod - condition type. + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: type: string - required: - - status - - type - type: object - type: array - x-kubernetes-list-type: atomic - required: - - action - type: object - type: array - x-kubernetes-list-type: atomic - required: - - rules - type: object - podReplacementPolicy: - description: podReplacementPolicy specifies when to create - replacement Pods. - type: string - podTemplateAnnotations: - additionalProperties: - type: string - description: |- - PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate - metadata. - type: object - suspend: - description: suspend specifies whether the Job controller - should create Pods or not. - type: boolean - ttlSecondsAfterFinished: - description: |- - ttlSecondsAfterFinished limits the lifetime of a Job that has finished - execution (either Complete or Failed). - format: int32 - type: integer - type: object - schedule: - description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. - type: string - startingDeadlineSeconds: - description: |- - Optional deadline in seconds for starting the job if it misses scheduled - time for any reason. - format: int64 - type: integer - successfulJobsHistoryLimit: - description: The number of successful finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - suspend: - description: |- - This flag tells the controller to suspend subsequent executions, it does - not apply to already started executions. - type: boolean - timeZone: - description: The time zone name for the given schedule, see - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. - type: string - type: object - feastProject: - description: FeastProject is the Feast project id. - pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ - type: string - feastProjectDir: - description: FeastProjectDir defines how to create the feast project - directory. - properties: - git: - description: GitCloneOptions describes how a clone should - be performed. - properties: - configs: - additionalProperties: - type: string - description: |- - Configs passed to git via `-c` - e.g. http.sslVerify: 'false' - OR 'url."https://api:\${TOKEN}@github.com/". - type: object - env: - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean required: - - key + - topologyKey type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports - metadata.name, metadata.namespace, `metadata.labels['''']`, - `metadata.' + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - resourceFieldRef: + matchLabelKeys: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, + etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. properties: - key: - description: The key of the secret to select - from. Must be a valid secret key. - type: string - name: - default: "" + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean required: - - key + - topologyKey type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source of - a set of ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from - properties: - name: - default: "" + weight: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight type: object - x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... properties: - name: - default: "" + labelSelector: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean + required: + - topologyKey type: object - x-kubernetes-map-type: atomic - type: object - type: array - featureRepoPath: - description: FeatureRepoPath is the relative path to the - feature repo subdirectory. Default is 'feature_repo'. - type: string - ref: - description: Reference to a branch / tag / commit - type: string - url: - description: The repository URL to clone from. - type: string - required: - - url - type: object - x-kubernetes-validations: - - message: RepoPath must be a file name only, with no slashes. - rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') - : true' - init: - description: FeastInitOptions defines how to run a `feast - init`. - properties: - minimal: - type: boolean - template: - description: Template for the created project - enum: - - local - - gcp - - aws - - snowflake - - spark - - postgres - - hbase - - cassandra - - hazelcast - - ikv - - couchbase - - clickhouse - type: string + type: array + x-kubernetes-list-type: atomic + type: object type: object - type: object - x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' - services: - description: FeatureStoreServices defines the desired feast services. - An ephemeral onlineStore feature server is deployed by default. - properties: deploymentStrategy: description: DeploymentStrategy describes how to replace existing pods with new ones. @@ -4982,6 +8073,10 @@ spec: disableInitContainers: description: Disable the 'feast repo initialization' initContainer type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string offlineStore: description: OfflineStore configures the offline store service properties: @@ -5115,6 +8210,7 @@ spec: - couchbase.offline - clickhouse - ray + - oracle type: string required: - secretRef @@ -5134,14 +8230,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -5186,6 +8282,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5244,7 +8370,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5263,8 +8389,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5326,6 +8452,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -5489,6 +8619,11 @@ spec: onlineStore: description: OnlineStore configures the online store service properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean persistence: description: OnlineStorePersistence configures the persistence settings for the online store service @@ -5618,7 +8753,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -5633,6 +8767,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -5652,14 +8789,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -5696,12 +8833,42 @@ spec: FieldPath is written in terms of, defaults to "v1". type: string - fieldPath: - description: Path of the field to select - in the specified API version. + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. type: string required: - - fieldPath + - key + - path + - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: @@ -5762,7 +8929,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5781,8 +8948,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5844,6 +9011,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6003,7 +9174,108 @@ spec: type: integer type: object type: object + serving: + description: Serving configures the Feast feature_server + section written into feature_store.yaml for the online + serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is + "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or + disables individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to + the offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to + the offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline + write batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied + to the Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' registry: description: Registry configures the registry service. One selection is required. Local is the default setting. @@ -6192,14 +9464,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -6245,6 +9517,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -6304,7 +9606,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6323,9 +9625,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -6366,6 +9667,31 @@ spec: - error - critical type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object metrics: description: Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. @@ -6391,6 +9717,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6562,6 +9892,9 @@ spec: true rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' type: object remote: description: RemoteRegistryConfig points to a remote feast @@ -6610,15 +9943,636 @@ spec: - configMapRef type: object type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, - c)' + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, + c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the + FeatureStore deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential + scaling polices which can be used during + scaling. + items: + description: HPAScalingPolicy is a single + policy which must hold true for a specified + past interval. + properties: + periodSeconds: + description: periodSeconds specifies + the window of time for which the policy + should hold true. + format: int32 + type: integer + type: + description: type is used to specify + the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: scaleUp is scaling policy for scaling + Up. + properties: + policies: + description: policies is a list of potential + scaling polices which can be used during + scaling. + items: + description: HPAScalingPolicy is a single + policy which must hold true for a specified + past interval. + properties: + periodSeconds: + description: periodSeconds specifies + the window of time for which the policy + should hold true. + format: int32 + type: integer + type: + description: type is used to specify + the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the + number of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for + which to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... + properties: + container: + description: container is the name of the + container in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the + descriptions of a object,such as kind,name + apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent + type: string + kind: + description: 'kind is the kind of the + referent; More info: https://git.k8s.' + type: string + name: + description: 'name is the name of the + referent; More info: https://kubernetes.' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. + type: string + required: + - type + type: object + type: array + minReplicas: + description: MinReplicas is the lower limit for the + number of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.local), has(self.remote)].exists_one(c, - c)' securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -6664,6 +10618,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -6703,13 +10662,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -6752,6 +10716,98 @@ spec: type: string type: object type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are + spread across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array ui: description: Creates a UI server container properties: @@ -6761,14 +10817,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -6812,6 +10868,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -6868,7 +10954,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6887,8 +10973,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -6950,6 +11036,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -7118,7 +11208,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -7160,6 +11250,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -7168,9 +11259,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -7201,7 +11293,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -7250,7 +11342,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -7296,7 +11388,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -7336,7 +11428,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -7349,7 +11441,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -7413,7 +11505,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -7665,9 +11757,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -7702,7 +11794,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -7723,7 +11815,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -7733,7 +11825,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -7762,7 +11854,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -7780,14 +11872,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -7822,6 +11912,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -7847,6 +11953,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -7938,7 +12045,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -7955,7 +12062,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -7985,10 +12092,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -8069,7 +12179,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -8142,7 +12252,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -8184,6 +12294,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -8191,7 +12347,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -8257,7 +12413,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -8272,12 +12428,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -8293,9 +12449,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -8307,6 +12462,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -8321,6 +12477,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -8348,6 +12505,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -8362,6 +12520,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -8399,6 +12558,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -8433,7 +12593,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -8508,7 +12668,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -8540,6 +12700,37 @@ spec: required: - feastProject type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline + store. Configure services.offlineStore.persistence.store when + using replicas > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure + registry.local.persistence.store or use a remote registry when + using replicas > 1 or autoscaling. S3/GCS-backed registry is also + allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && + (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) clientConfigMap: description: ConfigMap in this namespace containing a client `feature_store.yaml` for this feast deployment @@ -8582,10 +12773,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -8604,6 +12792,28 @@ spec: type: string phase: type: string + replicas: + description: Replicas is the current number of ready pod replicas + (used by the scale sub-resource). + format: int32 + type: integer + scalingStatus: + description: ScalingStatus reports the current scaling state of the + FeatureStore deployment. + properties: + currentReplicas: + description: CurrentReplicas is the current number of pod replicas. + format: int32 + type: integer + desiredReplicas: + description: DesiredReplicas is the desired number of pod replicas. + format: int32 + type: integer + type: object + selector: + description: Selector is the label selector for pods managed by the + FeatureStore deployment (used by the scale sub-resource). + type: string serviceHostnames: description: ServiceHostnames defines the service hostnames in the format of :, e.g. example.svc.cluster.local:80 @@ -8624,6 +12834,10 @@ spec: served: true storage: true subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} - additionalPrinterColumns: - jsonPath: .status.phase @@ -8724,14 +12938,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -8775,6 +12989,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -8830,7 +13073,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -8849,8 +13092,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -8896,6 +13139,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -8937,7 +13184,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -9031,7 +13278,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -9114,14 +13360,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -9165,6 +13411,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -9220,7 +13495,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -9239,8 +13514,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -9294,15 +13569,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -9489,14 +13789,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -9540,6 +13840,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -9596,7 +13926,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -9615,8 +13945,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -9678,6 +14008,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -9966,7 +14300,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -9981,6 +14314,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -9999,14 +14335,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -10050,6 +14386,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -10106,7 +14472,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -10125,8 +14491,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -10188,6 +14554,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -10527,14 +14897,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -10579,6 +14949,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -10637,7 +15037,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -10656,8 +15056,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -10723,6 +15123,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -10944,6 +15348,10 @@ spec: x-kubernetes-validations: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -10989,6 +15397,10 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. properties: @@ -11027,13 +15439,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -11084,14 +15501,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -11135,6 +15552,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -11190,7 +15636,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -11209,8 +15655,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -11271,6 +15717,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -11439,7 +15889,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the volume @@ -11481,6 +15931,7 @@ spec: blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -11489,9 +15940,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -11522,7 +15974,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: monitors: description: |- @@ -11570,7 +16022,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -11616,7 +16068,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -11656,7 +16108,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver that @@ -11668,7 +16120,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -11730,7 +16182,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -11979,9 +16431,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -12016,7 +16468,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -12037,7 +16489,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -12047,7 +16499,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -12076,7 +16528,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -12094,14 +16546,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that details + Glusterfs topology. type: string path: description: |- @@ -12136,6 +16586,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -12161,6 +16627,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -12251,7 +16718,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -12268,7 +16735,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -12298,10 +16765,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected along - with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod to @@ -12381,7 +16851,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -12452,7 +16922,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -12494,6 +16964,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -12501,7 +17017,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -12567,7 +17083,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: group: description: |- @@ -12582,12 +17098,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -12603,9 +17119,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the volume @@ -12617,6 +17132,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -12631,6 +17147,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -12658,6 +17175,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -12672,6 +17190,7 @@ spec: attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -12709,6 +17228,7 @@ spec: with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -12743,7 +17263,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -12818,7 +17338,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine + and mounted on kubelets host machine. properties: fsType: description: |- @@ -12928,14 +17448,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -12979,6 +17499,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -13035,7 +17585,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -13054,8 +17604,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -13102,6 +17652,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -13143,7 +17697,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -13238,7 +17792,6 @@ spec: condition type. type: string required: - - status - type type: object type: array @@ -13323,14 +17876,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -13374,6 +17927,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -13430,7 +18013,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -13449,8 +18032,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -13505,15 +18088,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -13703,14 +18311,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -13755,6 +18363,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -13813,7 +18451,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -13832,8 +18470,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -13895,6 +18533,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -14187,7 +18829,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -14202,6 +18843,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -14221,14 +18865,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -14273,6 +18917,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -14331,7 +19005,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -14350,8 +19024,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -14413,6 +19087,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -14761,14 +19439,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -14814,6 +19492,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -14873,7 +19581,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -14892,9 +19600,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -14960,6 +19667,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -15188,6 +19899,11 @@ spec: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -15233,6 +19949,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -15272,13 +19993,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -15330,14 +20056,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -15381,6 +20107,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -15437,7 +20193,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -15456,8 +20212,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -15519,6 +20275,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -15687,7 +20447,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -15729,6 +20489,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -15737,9 +20498,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -15770,7 +20532,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -15819,7 +20581,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -15865,7 +20627,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -15905,7 +20667,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -15918,7 +20680,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -15982,7 +20744,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -16234,9 +20996,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -16271,7 +21033,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -16292,7 +21054,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -16302,7 +21064,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -16331,7 +21093,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -16349,14 +21111,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -16391,6 +21151,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -16416,6 +21192,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -16507,7 +21284,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -16524,7 +21301,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -16554,10 +21331,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -16638,7 +21418,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -16711,7 +21491,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -16753,6 +21533,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -16760,7 +21586,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -16826,7 +21652,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -16841,12 +21667,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -16862,9 +21688,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -16876,6 +21701,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -16890,6 +21716,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -16917,6 +21744,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -16931,6 +21759,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -16968,6 +21797,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -17002,7 +21832,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -17077,7 +21907,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -17151,10 +21981,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string diff --git a/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml b/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml new file mode 100644 index 00000000000..40483cc0c43 --- /dev/null +++ b/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: openlineage-secret +stringData: + api_key: your-marquez-api-key diff --git a/infra/feast-operator/bundle/metadata/annotations.yaml b/infra/feast-operator/bundle/metadata/annotations.yaml index 5e280a43e24..2c6d3383343 100644 --- a/infra/feast-operator/bundle/metadata/annotations.yaml +++ b/infra/feast-operator/bundle/metadata/annotations.yaml @@ -5,7 +5,7 @@ annotations: operators.operatorframework.io.bundle.metadata.v1: metadata/ operators.operatorframework.io.bundle.package.v1: feast-operator operators.operatorframework.io.bundle.channels.v1: alpha - operators.operatorframework.io.metrics.builder: operator-sdk-v1.38.0 + operators.operatorframework.io.metrics.builder: operator-sdk-v1.41.0 operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 diff --git a/infra/feast-operator/cmd/main.go b/infra/feast-operator/cmd/main.go index 4be1777ac72..5d2bbece7dc 100644 --- a/infra/feast-operator/cmd/main.go +++ b/infra/feast-operator/cmd/main.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "context" "crypto/tls" "flag" "os" @@ -25,11 +26,20 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + configv1 "github.com/openshift/api/config/v1" + tlspkg "github.com/openshift/controller-runtime-common/pkg/tls" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -42,6 +52,7 @@ import ( routev1 "github.com/openshift/api/route/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller" + feastmetrics "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" // +kubebuilder:scaffold:imports ) @@ -53,19 +64,43 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(configv1.Install(scheme)) utilruntime.Must(routev1.AddToScheme(scheme)) utilruntime.Must(feastdevv1alpha1.AddToScheme(scheme)) utilruntime.Must(feastdevv1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } +func newCacheOptions() cache.Options { + managedBySelector := labels.SelectorFromSet(labels.Set{ + services.ManagedByLabelKey: services.ManagedByLabelValue, + }) + managedByFilter := cache.ByObject{Label: managedBySelector} + + return cache.Options{ + DefaultTransform: cache.TransformStripManagedFields(), + ByObject: map[client.Object]cache.ByObject{ + &corev1.ConfigMap{}: managedByFilter, + &appsv1.Deployment{}: managedByFilter, + &corev1.Service{}: managedByFilter, + &corev1.ServiceAccount{}: managedByFilter, + &corev1.PersistentVolumeClaim{}: managedByFilter, + &rbacv1.RoleBinding{}: managedByFilter, + &rbacv1.Role{}: managedByFilter, + &batchv1.CronJob{}: managedByFilter, + &autoscalingv2.HorizontalPodAutoscaler{}: managedByFilter, + &policyv1.PodDisruptionBudget{}: managedByFilter, + }, + } +} + func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string var secureMetrics bool - var enableHTTP2 bool - var tlsOpts []func(*tls.Config) + var featureStoreMetrics bool + tlsOpts := make([]func(*tls.Config), 0, 2) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -74,8 +109,9 @@ func main() { "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.BoolVar(&enableHTTP2, "enable-http2", false, - "If set, HTTP/2 will be enabled for the metrics and webhook servers") + flag.BoolVar(&featureStoreMetrics, "feature-store-metrics", true, + "Enable Prometheus gauges exposing online/offline store and registry configuration per FeatureStore. "+ + "Disable with --feature-store-metrics=false.") opts := zap.Options{ Development: true, } @@ -84,20 +120,20 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - // if the enable-http2 flag is false (the default), http/2 should be disabled - // due to its vulnerabilities. More specifically, disabling http/2 will - // prevent from being vulnerable to the HTTP/2 Stream Cancellation and - // Rapid Reset CVEs. For more information see: - // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 - // - https://github.com/advisories/GHSA-4374-p667-p6c8 - disableHTTP2 := func(c *tls.Config) { - setupLog.Info("disabling http/2") - c.NextProtos = []string{"http/1.1"} + // Fetch cluster TLS profile from apiservers.config.openshift.io/cluster + cfg := ctrl.GetConfigOrDie() + bootstrapClient, err := client.New(cfg, client.Options{Scheme: scheme}) + if err != nil { + setupLog.Error(err, "unable to create bootstrap client for TLS profile fetch") + os.Exit(1) } - if !enableHTTP2 { - tlsOpts = append(tlsOpts, disableHTTP2) + tlsResult, err := bootstrapTLS(context.Background(), bootstrapClient) + if err != nil { + setupLog.Error(err, "TLS bootstrap failed") + os.Exit(1) } + tlsOpts = append(tlsOpts, tlsResult.TLSOpts...) webhookServer := webhook.NewServer(webhook.Options{ TLSOpts: tlsOpts, @@ -105,7 +141,7 @@ func main() { // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: - // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/server + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server // - https://book.kubebuilder.io/reference/metrics.html metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, @@ -123,11 +159,11 @@ func main() { // FilterProvider is used to protect the metrics endpoint with authn/authz. // These configurations ensure that only authorized users and service accounts // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: - // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, @@ -145,11 +181,26 @@ func main() { // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, + Cache: newCacheOptions(), Client: client.Options{ Cache: &client.CacheOptions{ + // Bypass the label-filtered informer cache for all reads so that + // pre-existing resources without the managed-by label are still + // visible to the reconciler. The ByObject cache filter above still + // restricts the watch to managed-by-labeled objects, limiting + // memory usage while avoiding upgrade deadlocks. DisableFor: []client.Object{ &corev1.ConfigMap{}, &corev1.Secret{}, + &appsv1.Deployment{}, + &corev1.Service{}, + &corev1.ServiceAccount{}, + &corev1.PersistentVolumeClaim{}, + &rbacv1.RoleBinding{}, + &rbacv1.Role{}, + &batchv1.CronJob{}, + &autoscalingv2.HorizontalPodAutoscaler{}, + &policyv1.PodDisruptionBudget{}, }, }, }, @@ -161,15 +212,51 @@ func main() { services.SetIsOpenShift(mgr.GetConfig()) + var fsMetrics *feastmetrics.FeatureStoreMetrics + if featureStoreMetrics { + fsMetrics = feastmetrics.NewFeatureStoreMetrics() + fsMetrics.Register() + setupLog.Info("FeatureStore installation metrics enabled") + } else { + setupLog.Info("FeatureStore installation metrics disabled (--feature-store-metrics=false)") + } + if err = (&controller.FeatureStoreReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Metrics: fsMetrics, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "FeatureStore") os.Exit(1) } // +kubebuilder:scaffold:builder + // Register SecurityProfileWatcher to restart on TLS profile changes + ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler()) + defer cancel() + + if tlsResult.ProfileFetched { + watcher := &tlspkg.SecurityProfileWatcher{ + Client: mgr.GetClient(), + InitialTLSProfileSpec: tlsResult.ProfileSpec, + OnProfileChange: func(_ context.Context, _, _ configv1.TLSProfileSpec) { + setupLog.Info("TLS profile changed, initiating shutdown to reload") + cancel() + }, + } + if tlsResult.AdherenceFetched { + watcher.InitialTLSAdherencePolicy = tlsResult.AdherencePolicy + watcher.OnAdherencePolicyChange = func(_ context.Context, _, _ configv1.TLSAdherencePolicy) { + setupLog.Info("TLS adherence policy changed, initiating shutdown to reload") + cancel() + } + } + if err := watcher.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to set up TLS profile watcher") + os.Exit(1) + } + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) @@ -180,7 +267,7 @@ func main() { } setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } diff --git a/infra/feast-operator/cmd/tls_bootstrap.go b/infra/feast-operator/cmd/tls_bootstrap.go new file mode 100644 index 00000000000..6fe33631e02 --- /dev/null +++ b/infra/feast-operator/cmd/tls_bootstrap.go @@ -0,0 +1,134 @@ +/* +Copyright 2024 Feast Community. + +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 + + http://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 main + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "time" + + configv1 "github.com/openshift/api/config/v1" + tlspkg "github.com/openshift/controller-runtime-common/pkg/tls" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + tlsFetchTimeout = 10 * time.Second + alpnH2 = "h2" + alpnHTTP11 = "http/1.1" +) + +type tlsBootstrapResult struct { + TLSOpts []func(*tls.Config) + ProfileFetched bool + ProfileSpec configv1.TLSProfileSpec + AdherenceFetched bool + AdherencePolicy configv1.TLSAdherencePolicy + UnsupportedCiphers []string +} + +func fetchTLSProfile(ctx context.Context, k8sClient client.Client) (configv1.TLSProfileSpec, bool, error) { + fetchCtx, cancel := context.WithTimeout(ctx, tlsFetchTimeout) + defer cancel() + + profile, err := tlspkg.FetchAPIServerTLSProfile(fetchCtx, k8sClient) + if err != nil { + return classifyTLSProfileError(err) + } + return profile, true, nil +} + +func classifyTLSProfileError(err error) (configv1.TLSProfileSpec, bool, error) { + intermediate := *configv1.TLSProfiles[configv1.TLSProfileIntermediateType] + + switch { + case apimeta.IsNoMatchError(err): + return intermediate, false, nil + case apierrors.IsNotFound(err): + return intermediate, false, nil + case isTransientError(err): + return intermediate, true, nil + default: + return configv1.TLSProfileSpec{}, false, fmt.Errorf("unable to read APIServer TLS profile: %w", err) + } +} + +func fetchTLSAdherencePolicy(ctx context.Context, k8sClient client.Client) (configv1.TLSAdherencePolicy, bool, error) { + fetchCtx, cancel := context.WithTimeout(ctx, tlsFetchTimeout) + defer cancel() + + policy, err := tlspkg.FetchAPIServerTLSAdherencePolicy(fetchCtx, k8sClient) + if err == nil { + return policy, true, nil + } + + switch { + case apimeta.IsNoMatchError(err), + apierrors.IsNotFound(err), + isTransientError(err): + return "", false, nil + default: + return "", false, fmt.Errorf("unable to read APIServer TLS adherence policy: %w", err) + } +} + +func bootstrapTLS(ctx context.Context, k8sClient client.Client) (*tlsBootstrapResult, error) { + logger := log.FromContext(ctx) + result := &tlsBootstrapResult{ + TLSOpts: make([]func(*tls.Config), 0, 2), + } + + profile, profileFetched, err := fetchTLSProfile(ctx, k8sClient) + if err != nil { + return nil, err + } + result.ProfileFetched = profileFetched + result.ProfileSpec = profile + + tlsConfigFn, unsupported := tlspkg.NewTLSConfigFromProfile(profile) + result.UnsupportedCiphers = unsupported + if len(unsupported) > 0 { + logger.Info("TLS profile contains ciphers unsupported by Go", "unsupported", unsupported) + } + result.TLSOpts = append(result.TLSOpts, tlsConfigFn) + + adherence, adherenceFetched, err := fetchTLSAdherencePolicy(ctx, k8sClient) + if err != nil { + return nil, err + } + result.AdherenceFetched = adherenceFetched + result.AdherencePolicy = adherence + + result.TLSOpts = append(result.TLSOpts, func(c *tls.Config) { + c.NextProtos = []string{alpnH2, alpnHTTP11} + }) + + return result, nil +} + +func isTransientError(err error) bool { + return apierrors.IsServiceUnavailable(err) || + apierrors.IsTimeout(err) || + apierrors.IsServerTimeout(err) || + apierrors.IsTooManyRequests(err) || + errors.Is(err, context.DeadlineExceeded) +} diff --git a/infra/feast-operator/cmd/tls_bootstrap_test.go b/infra/feast-operator/cmd/tls_bootstrap_test.go new file mode 100644 index 00000000000..0bb31c14bf6 --- /dev/null +++ b/infra/feast-operator/cmd/tls_bootstrap_test.go @@ -0,0 +1,347 @@ +/* +Copyright 2024 Feast Community. + +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 + + http://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 main + +import ( + "context" + "crypto/tls" + "errors" + "testing" + + configv1 "github.com/openshift/api/config/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func intermediateProfile() configv1.TLSProfileSpec { + return *configv1.TLSProfiles[configv1.TLSProfileIntermediateType] +} + +func TestClassifyTLSProfileError(t *testing.T) { + tests := []struct { + name string + err error + wantProfileFetched bool + wantError bool + wantIntermediate bool + }{ + { + name: "NoMatchError returns Intermediate defaults, profileFetched=false", + err: &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "config.openshift.io"}}, + wantProfileFetched: false, + wantError: false, + wantIntermediate: true, + }, + { + name: "NotFound returns Intermediate defaults, profileFetched=false", + err: apierrors.NewNotFound(schema.GroupResource{Group: "config.openshift.io", Resource: "apiservers"}, "cluster"), + wantProfileFetched: false, + wantError: false, + wantIntermediate: true, + }, + { + name: "ServiceUnavailable is transient, profileFetched=true", + err: apierrors.NewServiceUnavailable("api server down"), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "Timeout is transient, profileFetched=true", + err: apierrors.NewTimeoutError("timed out", 5), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "ServerTimeout is transient, profileFetched=true", + err: apierrors.NewServerTimeout(schema.GroupResource{Group: "config.openshift.io", Resource: "apiservers"}, "GET", 5), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "TooManyRequests is transient, profileFetched=true", + err: apierrors.NewTooManyRequests("throttled", 5), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "DeadlineExceeded is transient, profileFetched=true", + err: context.DeadlineExceeded, + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "Forbidden is fatal, returns error", + err: apierrors.NewForbidden(schema.GroupResource{Group: "config.openshift.io", Resource: "apiservers"}, "cluster", errors.New("RBAC")), + wantProfileFetched: false, + wantError: true, + wantIntermediate: false, + }, + { + name: "Unauthorized is fatal, returns error", + err: apierrors.NewUnauthorized("no token"), + wantProfileFetched: false, + wantError: true, + wantIntermediate: false, + }, + { + name: "InternalServerError is fatal, returns error", + err: apierrors.NewInternalError(errors.New("crash")), + wantProfileFetched: false, + wantError: true, + wantIntermediate: false, + }, + { + name: "Generic error is fatal, returns error", + err: errors.New("something unexpected"), + wantProfileFetched: false, + wantError: true, + wantIntermediate: false, + }, + { + name: "Wrapped DeadlineExceeded is transient", + err: errors.Join(errors.New("fetch failed"), context.DeadlineExceeded), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + profile, fetched, err := classifyTLSProfileError(tt.err) + + if tt.wantError && err == nil { + t.Errorf("expected error, got nil") + } + if !tt.wantError && err != nil { + t.Errorf("unexpected error: %v", err) + } + if fetched != tt.wantProfileFetched { + t.Errorf("profileFetched = %v, want %v", fetched, tt.wantProfileFetched) + } + if tt.wantIntermediate { + intermediate := intermediateProfile() + if profile.MinTLSVersion != intermediate.MinTLSVersion { + t.Errorf("MinTLSVersion = %v, want %v (Intermediate)", profile.MinTLSVersion, intermediate.MinTLSVersion) + } + } + }) + } +} + +func TestIsTransientError(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + {"ServiceUnavailable", apierrors.NewServiceUnavailable("down"), true}, + {"Timeout", apierrors.NewTimeoutError("slow", 5), true}, + {"ServerTimeout", apierrors.NewServerTimeout(schema.GroupResource{}, "GET", 5), true}, + {"TooManyRequests", apierrors.NewTooManyRequests("throttled", 5), true}, + {"DeadlineExceeded", context.DeadlineExceeded, true}, + {"Wrapped DeadlineExceeded", errors.Join(errors.New("wrapper"), context.DeadlineExceeded), true}, + {"NotFound", apierrors.NewNotFound(schema.GroupResource{}, "x"), false}, + {"Forbidden", apierrors.NewForbidden(schema.GroupResource{}, "x", errors.New("RBAC")), false}, + {"Unauthorized", apierrors.NewUnauthorized("no token"), false}, + {"InternalError", apierrors.NewInternalError(errors.New("crash")), false}, + {"Generic error", errors.New("oops"), false}, + {"Nil", nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isTransientError(tt.err); got != tt.want { + t.Errorf("isTransientError() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIntermediateProfileHasExpectedDefaults(t *testing.T) { + profile := intermediateProfile() + + if profile.MinTLSVersion != configv1.VersionTLS12 { + t.Errorf("Intermediate MinTLSVersion = %v, want %v", profile.MinTLSVersion, configv1.VersionTLS12) + } + if len(profile.Ciphers) == 0 { + t.Error("Intermediate profile should have non-empty cipher list") + } +} + +func TestTLSConfigFromIntermediateProfile(t *testing.T) { + profile := intermediateProfile() + tlsConfigFn := configv1ToTLSConfig(profile) + + cfg := &tls.Config{} + tlsConfigFn(cfg) + + if cfg.MinVersion != tls.VersionTLS12 { + t.Errorf("MinVersion = %v, want %v (TLS 1.2)", cfg.MinVersion, tls.VersionTLS12) + } + if len(cfg.CipherSuites) == 0 { + t.Error("CipherSuites should not be empty for Intermediate profile") + } +} + +func configv1ToTLSConfig(profile configv1.TLSProfileSpec) func(*tls.Config) { + // Thin wrapper to test the actual conversion without importing tlspkg in tests. + // tlspkg.NewTLSConfigFromProfile is what main.go uses. + var minVersion uint16 + switch profile.MinTLSVersion { + case configv1.VersionTLS10: + minVersion = tls.VersionTLS10 + case configv1.VersionTLS11: + minVersion = tls.VersionTLS11 + case configv1.VersionTLS12: + minVersion = tls.VersionTLS12 + case configv1.VersionTLS13: + minVersion = tls.VersionTLS13 + } + + return func(c *tls.Config) { + c.MinVersion = minVersion + c.CipherSuites = mapCiphers(profile.Ciphers) + } +} + +func mapCiphers(names []string) []uint16 { + cipherMap := map[string]uint16{ + "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, + "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, + "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + } + var ids []uint16 + for _, name := range names { + if id, ok := cipherMap[name]; ok { + ids = append(ids, id) + } + } + return ids +} + +func TestClassifyTLSProfileError_AllTransientErrorsSetProfileFetched(t *testing.T) { + transientErrors := []error{ + apierrors.NewServiceUnavailable("down"), + apierrors.NewTimeoutError("slow", 5), + apierrors.NewServerTimeout(schema.GroupResource{Group: "config.openshift.io", Resource: "apiservers"}, "GET", 5), + apierrors.NewTooManyRequests("throttled", 5), + context.DeadlineExceeded, + } + + for _, err := range transientErrors { + _, fetched, classifyErr := classifyTLSProfileError(err) + if classifyErr != nil { + t.Errorf("transient error %T should not return error, got: %v", err, classifyErr) + } + if !fetched { + t.Errorf("transient error %T should set profileFetched=true", err) + } + } +} + +func TestClassifyTLSProfileError_NonTransientErrorsDoNotSetProfileFetched(t *testing.T) { + nonTransientErrors := []error{ + &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "config.openshift.io"}}, + apierrors.NewNotFound(schema.GroupResource{}, "cluster"), + } + + for _, err := range nonTransientErrors { + _, fetched, classifyErr := classifyTLSProfileError(err) + if classifyErr != nil { + t.Errorf("graceful error %T should not return error, got: %v", err, classifyErr) + } + if fetched { + t.Errorf("graceful error %T should set profileFetched=false", err) + } + } +} + +func TestClassifyTLSProfileError_FatalErrorsReturnError(t *testing.T) { + fatalErrors := []error{ + apierrors.NewForbidden(schema.GroupResource{}, "cluster", errors.New("RBAC")), + apierrors.NewUnauthorized("no token"), + apierrors.NewInternalError(errors.New("crash")), + errors.New("unexpected"), + } + + for _, err := range fatalErrors { + _, _, classifyErr := classifyTLSProfileError(err) + if classifyErr == nil { + t.Errorf("fatal error %T should return error", err) + } + } +} + +func TestClassifyTLSProfileError_IntermediateProfileAlwaysApplied(t *testing.T) { + allNonFatalErrors := []error{ + &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "config.openshift.io"}}, + apierrors.NewNotFound(schema.GroupResource{}, "cluster"), + apierrors.NewServiceUnavailable("down"), + apierrors.NewTimeoutError("slow", 5), + apierrors.NewServerTimeout(schema.GroupResource{}, "GET", 5), + apierrors.NewTooManyRequests("throttled", 5), + context.DeadlineExceeded, + } + + intermediate := intermediateProfile() + for _, err := range allNonFatalErrors { + profile, _, classifyErr := classifyTLSProfileError(err) + if classifyErr != nil { + t.Fatalf("unexpected error for %T: %v", err, classifyErr) + } + if profile.MinTLSVersion != intermediate.MinTLSVersion { + t.Errorf("for error %T: MinTLSVersion = %v, want Intermediate (%v)", err, profile.MinTLSVersion, intermediate.MinTLSVersion) + } + if len(profile.Ciphers) != len(intermediate.Ciphers) { + t.Errorf("for error %T: got %d ciphers, want %d (Intermediate)", err, len(profile.Ciphers), len(intermediate.Ciphers)) + } + } +} + +func TestTLSBootstrapResult_NextProtosAlwaysSet(t *testing.T) { + // Verify that the TLSOpts from bootstrapTLS always include ALPN with h2 and http/1.1. + // We can't call bootstrapTLS without a real client, but we can verify the function + // in tls_bootstrap.go sets NextProtos. + result := &tlsBootstrapResult{ + TLSOpts: make([]func(*tls.Config), 0, 2), + } + result.TLSOpts = append(result.TLSOpts, func(c *tls.Config) { + c.NextProtos = []string{"h2", alpnHTTP11} + }) + + cfg := &tls.Config{} + for _, opt := range result.TLSOpts { + opt(cfg) + } + + if len(cfg.NextProtos) != 2 || cfg.NextProtos[0] != "h2" || cfg.NextProtos[1] != alpnHTTP11 { + t.Errorf("NextProtos = %v, want [h2, %s]", cfg.NextProtos, alpnHTTP11) + } +} diff --git a/infra/feast-operator/config/component_metadata.yaml b/infra/feast-operator/config/component_metadata.yaml index fad77d7090e..7ee38fdb165 100644 --- a/infra/feast-operator/config/component_metadata.yaml +++ b/infra/feast-operator/config/component_metadata.yaml @@ -1,5 +1,5 @@ # This file is required to configure Feast release information for ODH/RHOAI Operator releases: - name: Feast - version: 0.60.0 + version: 0.65.0 repoUrl: https://github.com/feast-dev/feast diff --git a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml index 00a26ef5b58..8184906e14d 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: featurestores.feast.dev spec: group: feast.dev @@ -62,10 +62,32 @@ spec: OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. https://auth0. properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM certificate. + Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. properties: name: default: "" @@ -76,8 +98,14 @@ spec: type: string type: object x-kubernetes-map-type: atomic - required: - - secretRef + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean type: object type: object x-kubernetes-validations: @@ -133,14 +161,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -184,6 +212,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -239,7 +296,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -258,8 +315,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -305,6 +362,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -346,7 +407,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -440,7 +501,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -498,6 +558,16 @@ spec: description: The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. type: string type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object feastProject: description: FeastProject is the Feast project id. pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ @@ -523,14 +593,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -574,6 +644,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -629,7 +728,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -648,8 +747,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -703,15 +802,160 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object replicas: default: 1 description: |- @@ -724,558 +968,807 @@ spec: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. properties: - deploymentStrategy: - description: DeploymentStrategy describes how to replace existing - pods with new ones. + affinity: + description: Affinity defines the pod scheduling constraints for + the FeatureStore deployment. properties: - rollingUpdate: - description: |- - Rolling update config params. Present only if DeploymentStrategyType = - RollingUpdate. + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. properties: - maxSurge: - anyOf: - - type: integer - - type: string + preferredDuringSchedulingIgnoredDuringExecution: description: |- - The maximum number of pods that can be scheduled above the desired number of - pods. - x-kubernetes-int-or-string: true - maxUnavailable: - anyOf: - - type: integer - - type: string - description: The maximum number of pods that can be unavailable - during the update. - x-kubernetes-int-or-string: true - type: object - type: - description: Type of deployment. Can be "Recreate" or "RollingUpdate". - Default is RollingUpdate. - type: string - type: object - disableInitContainers: - description: Disable the 'feast repo initialization' initContainer - type: boolean - offlineStore: - description: OfflineStore configures the offline store service - properties: - persistence: - description: OfflineStorePersistence configures the persistence - settings for the offline store service - properties: - file: - description: OfflineStoreFilePersistence configures the - file-based persistence for the offline store service - properties: - pvc: - description: PvcConfig defines the settings for a - persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent volume - access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true + key: + description: The label key that the selector + applies to. + type: string + operator: description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum - amount of compute resources required. - type: object + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which this - persistent volume belongs. - type: string - type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. - type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath - type: object - x-kubernetes-validations: - - message: One selection is required between ref and - create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and must - not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - type: - enum: - - file - - dask - - duckdb - type: string - type: object - store: - description: OfflineStoreDBStorePersistence configures - the DB store persistence for the offline store service + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName - type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the secret - key. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you want - to use. - enum: - - snowflake.offline - - bigquery - - redshift - - spark - - postgres - - trino - - athena - - mssql - - couchbase.offline - - clickhouse - - ray - type: string + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic required: - - secretRef - - type + - nodeSelectorTerms type: object + x-kubernetes-map-type: atomic type: object - x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, c)' - server: - description: Creates a remote offline server container + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). properties: - env: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... items: - description: EnvVar represents an environment variable - present in a Container. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports - metadata.name, metadata.namespace, `metadata.labels['''']`, - `metadata.' + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - resourceFieldRef: + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. - properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace - properties: - key: - description: The key of the secret to select - from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer required: - - name + - podAffinityTerm + - weight type: object type: array - envFrom: + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... items: - description: EnvFromSource represents the source of - a set of ConfigMaps + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... properties: - configMapRef: - description: The ConfigMap to select from + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey type: object type: array - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - logLevel: + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible metrics - for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - tls: - description: TlsConfigs configures server TLS for a feast - service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, where - TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret key - names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... items: - description: VolumeMount describes a mounting of a Volume - within a container. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. - type: string - subPath: + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: Expanded path within the volume from - which the container's volume should be mounted. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string required: - - mountPath - - name + - topologyKey type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number of seconds - after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores (2 * - CPU + 1). - format: int32 - minimum: -1 - type: integer - type: object + x-kubernetes-list-type: atomic type: object type: object - onlineStore: - description: OnlineStore configures the online store service + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. properties: - persistence: - description: OnlineStorePersistence configures the persistence - settings for the online store service + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. properties: - file: - description: OnlineStoreFilePersistence configures the - file-based persistence for the online store service - properties: - path: - type: string - pvc: - description: PvcConfig defines the settings for a - persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent volume + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be unavailable + during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or "RollingUpdate". + Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures the + file-based persistence for the offline store service + properties: + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume access modes. Defaults to ["ReadWriteOnce"]. items: type: string @@ -1343,21 +1836,16 @@ spec: - message: Mount path must start with '/' and must not contain ':' rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string type: object - x-kubernetes-validations: - - message: Ephemeral stores must have absolute paths. - rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') - : true' - - message: PVC path must be a file name only, with no - slashes. - rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') - : true' - - message: Online store does not support S3 or GS buckets. - rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') - || self.path.startsWith(''gs://'')) : true' store: - description: OnlineStoreDBStorePersistence configures - the DB store persistence for the online store service + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service properties: secretKeyName: description: By default, the selected store "type" @@ -1381,23 +1869,18 @@ spec: description: Type of the persistence type you want to use. enum: - - snowflake.online - - redis - - ikv - - datastore - - dynamodb - - bigtable + - snowflake.offline + - bigquery + - redshift + - spark - postgres - - cassandra - - mysql - - hazelcast - - singlestore - - hbase - - elasticsearch - - qdrant - - couchbase.online - - milvus - - hybrid + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle type: string required: - secretRef @@ -1408,7 +1891,7 @@ spec: - message: One selection required between file or store. rule: '[has(self.file), has(self.store)].exists_one(c, c)' server: - description: Creates a feature server container + description: Creates a remote offline server container properties: env: items: @@ -1416,14 +1899,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1467,6 +1950,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1523,7 +2036,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1542,8 +2055,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1605,6 +2118,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1764,151 +2281,79 @@ spec: type: object type: object type: object - registry: - description: Registry configures the registry service. One selection - is required. Local is the default setting. + onlineStore: + description: OnlineStore configures the online store service properties: - local: - description: LocalRegistryConfig configures the registry service + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service properties: - persistence: - description: RegistryPersistence configures the persistence - settings for the registry service + file: + description: OnlineStoreFilePersistence configures the + file-based persistence for the online store service properties: - file: - description: RegistryFilePersistence configures the - file-based persistence for the registry service + path: + type: string + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. properties: - cache_mode: - description: |- - CacheMode defines the registry cache update strategy. - Allowed values are "sync" and "thread". - enum: - - none - - sync - - thread - type: string - cache_ttl_seconds: - description: CacheTTLSeconds defines the TTL (in - seconds) for the registry cache. - format: int32 - minimum: 0 - type: integer - path: - type: string - pvc: - description: PvcConfig defines the settings for - a persistent file store based on PVCs. + create: + description: Settings for creating a new PVC properties: - create: - description: Settings for creating a new PVC + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. properties: - accessModes: - description: AccessModes k8s persistent - volume access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the - minimum amount of compute resources - required. - type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which - this persistent volume belongs. - type: string type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath type: object x-kubernetes-validations: - - message: One selection is required between ref - and create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and - must not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - s3_additional_kwargs: - additionalProperties: - type: string - type: object - type: object - x-kubernetes-validations: - - message: Registry files must use absolute paths - or be S3 ('s3://') or GS ('gs://') object store - URIs. - rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') - || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) - : true' - - message: PVC path must be a file name only, with - no slashes. - rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') - : true' - - message: PVC persistence does not support S3 or - GS object store URIs. - rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') - || self.path.startsWith(''gs://'')) : true' - - message: Additional S3 settings are available only - for S3 object store URIs. - rule: '(has(self.s3_additional_kwargs) && has(self.path)) - ? self.path.startsWith(''s3://'') : true' - store: - description: RegistryDBStorePersistence configures - the DB store persistence for the registry service - properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the - secret key. + ref: + description: Reference to an existing field properties: name: default: "" @@ -1919,166 +2364,206 @@ spec: type: string type: object x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you - want to use. - enum: - - sql - - snowflake.registry - type: string required: - - secretRef - - type + - mountPath type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') type: object x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, - c)' - server: - description: Creates a registry server container + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with no + slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service properties: - env: - items: - description: EnvVar represents an environment variable - present in a Container. + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: - supports metadata.name, metadata.namespace, - `metadata.labels['''']`, `metadata.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source - of a set of ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + containerName: + description: 'Container name: required for + volumes, optional for env vars' type: string - optional: - description: Specify whether the ConfigMap - must be defined - type: boolean + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource type: object x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string name: default: "" description: |- @@ -2088,256 +2573,158 @@ spec: type: string optional: description: Specify whether the Secret - must be defined + or its key must be defined type: boolean + required: + - key type: object x-kubernetes-map-type: atomic type: object - type: array - grpc: - description: Enable gRPC registry server. Defaults - to true if unset. - type: boolean - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible - metrics for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - restAPI: - description: Enable REST API registry server. - type: boolean - tls: - description: TlsConfigs configures server TLS for - a feast service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, - where TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret - key names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. - items: - description: VolumeMount describes a mounting of - a Volume within a container. + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: + name: + default: "" description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: name: - description: This must match the Name of a Volume. - type: string - readOnly: + default: "" description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: - description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - subPath: + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. type: string - subPathExpr: - description: Expanded path within the volume - from which the container's volume should be - mounted. + request: + description: Request is the name chosen for + a request in the referenced claim. type: string required: - - mountPath - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number - of seconds after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores - (2 * CPU + 1). - format: int32 - minimum: -1 - type: integer + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. type: object type: object - x-kubernetes-validations: - - message: At least one of restAPI or grpc must be true - rule: self.restAPI == true || self.grpc == true || !has(self.grpc) - type: object - remote: - description: RemoteRegistryConfig points to a remote feast - registry server. - properties: - feastRef: - description: Reference to an existing `FeatureStore` CR - in the same k8s cluster. - properties: - name: - description: Name of the FeatureStore - type: string - namespace: - description: Namespace of the FeatureStore - type: string - required: - - name - type: object - hostname: - description: Host address of the remote registry service - - :, e.g. `registry..svc.cluster.local:80` - type: string tls: - description: TlsRemoteRegistryConfigs configures client - TLS for a remote feast registry. + description: TlsConfigs configures server TLS for a feast + service. properties: - certName: - description: defines the configmap key name for the - client TLS cert. - type: string - configMapRef: - description: references the local k8s configmap where - the TLS cert resides + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside properties: name: default: "" @@ -2348,78 +2735,964 @@ spec: type: string type: object x-kubernetes-map-type: atomic - required: - - certName - - configMapRef type: object - type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, - c)' - type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.local), has(self.remote)].exists_one(c, c)' - scaling: - description: Scaling configures horizontal scaling for the FeatureStore - deployment (e.g. HPA autoscaling). - properties: - autoscaling: - description: |- - Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. - Mutually exclusive with spec.replicas. - properties: - behavior: - description: Behavior configures the scaling behavior - of the target. - properties: - scaleDown: - description: scaleDown is scaling policy for scaling - Down. - properties: - policies: - description: policies is a list of potential scaling - polices which can be used during scaling. - items: - description: HPAScalingPolicy is a single policy - which must hold true for a specified past - interval. - properties: - periodSeconds: - description: periodSeconds specifies the - window of time for which the policy should - hold true. - format: int32 - type: integer - type: - description: type is used to specify the - scaling policy. - type: string - value: - description: |- - value contains the amount of change which is permitted by the policy. - It must be greater than zero - format: int32 - type: integer - required: - - periodSeconds - - type - - value - type: object - type: array - x-kubernetes-list-type: atomic - selectPolicy: - description: |- - selectPolicy is used to specify which policy should be used. - If not set, the default value Max is used. - type: string - stabilizationWindowSeconds: - description: |- - stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up - format: int32 + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + serving: + description: Serving configures the Feast feature_server section + written into feature_store.yaml for the online serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. Defaults + to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults to + "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or disables + individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to the + offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to the + offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline write + batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied to the + Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true + type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One selection + is required. Local is the default setting. + properties: + local: + description: LocalRegistryConfig configures the registry service + properties: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures the + file-based persistence for the registry service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object store + URIs. + rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') + || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 or + GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available only + for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be true + rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` CR + in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the FeatureStore + deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true type: object scaleUp: description: scaleUp is scaling policy for scaling @@ -2464,9 +3737,18 @@ spec: stabilizationWindowSeconds: description: |- stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up + considered while scaling... format: int32 type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true type: object type: object maxReplicas: @@ -2481,12 +3763,12 @@ spec: items: description: |- MetricSpec specifies how to scale based on a single metric - (only `type` and one other matching field should be set at on + (only `type` and one other matching field should be set at... properties: containerResource: description: |- containerResource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes descr + requests and limits) known to Kubernetes... properties: container: description: container is the name of the container @@ -2501,10 +3783,9 @@ spec: for the given metric properties: averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -2551,10 +3832,9 @@ spec: metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label selector - for the given metric\nWhen set, it is - passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is a list @@ -2605,10 +3885,9 @@ spec: for the given metric properties: averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -2674,10 +3953,9 @@ spec: metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label selector - for the given metric\nWhen set, it is - passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is a list @@ -2728,10 +4006,9 @@ spec: for the given metric properties: averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -2767,7 +4044,7 @@ spec: pods: description: |- pods refers to a metric describing each pod in the current scale target - (for example, transactions-processed-per-second) + (for example,... properties: metric: description: metric identifies the target metric @@ -2778,10 +4055,9 @@ spec: metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label selector - for the given metric\nWhen set, it is - passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is a list @@ -2832,10 +4108,9 @@ spec: for the given metric properties: averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -2870,7 +4145,7 @@ spec: resource: description: |- resource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes describing eac + requests and limits) known to Kubernetes describing... properties: name: description: name is the name of the resource @@ -2881,10 +4156,9 @@ spec: for the given metric properties: averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -2978,6 +4252,10 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. properties: @@ -3016,13 +4294,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -3064,6 +4347,96 @@ spec: type: string type: object type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are spread + across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of eligible + domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array ui: description: Creates a UI server container properties: @@ -3073,14 +4446,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -3124,6 +4497,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -3179,7 +4581,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -3198,8 +4600,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -3260,6 +4662,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -3428,7 +4834,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the volume @@ -3470,6 +4876,7 @@ spec: blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -3478,9 +4885,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -3511,7 +4919,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: monitors: description: |- @@ -3559,7 +4967,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -3605,7 +5013,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -3645,7 +5053,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver that @@ -3657,7 +5065,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -3719,7 +5127,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3968,9 +5376,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -4005,7 +5413,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -4026,7 +5434,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -4036,7 +5444,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -4065,7 +5473,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -4083,14 +5491,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that details + Glusterfs topology. type: string path: description: |- @@ -4125,6 +5531,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -4150,6 +5572,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -4240,7 +5663,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -4257,7 +5680,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -4287,10 +5710,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected along - with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod to @@ -4370,7 +5796,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -4441,7 +5867,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -4483,6 +5909,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -4490,7 +5962,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -4556,7 +6028,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: group: description: |- @@ -4571,12 +6043,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -4592,9 +6064,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the volume @@ -4606,6 +6077,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -4620,6 +6092,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -4647,6 +6120,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -4661,6 +6135,7 @@ spec: attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -4698,6 +6173,7 @@ spec: with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -4732,7 +6208,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -4807,7 +6283,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine + and mounted on kubelets host machine. properties: fsType: description: |- @@ -4837,7 +6313,6 @@ spec: type: object required: - feastProject - - replicas type: object x-kubernetes-validations: - message: replicas > 1 and services.scaling.autoscaling are mutually @@ -4884,99 +6359,565 @@ spec: KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. https://kubernetes. properties: - roles: - description: The Kubernetes RBAC roles to be deployed - in the same namespace of the FeatureStore. + roles: + description: The Kubernetes RBAC roles to be deployed + in the same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM + certificate. Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine + configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" + if not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch + engine configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. items: type: string type: array + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object type: object - oidc: - description: |- - OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. - https://auth0. + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. properties: - secretRef: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules type: object - x-kubernetes-map-type: atomic - required: - - secretRef + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string type: object - x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, - c)' - batchEngine: - description: BatchEngineConfig defines the batch compute engine - configuration. + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. properties: - configMapKey: - description: Key name in the ConfigMap. Defaults to "config" - if not specified. - type: string - configMapRef: - description: Reference to a ConfigMap containing the batch - engine configuration. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean type: object - cronJob: - description: FeastCronJob defines a CronJob to execute against - a Feature Store deployment. + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. properties: - annotations: - additionalProperties: - type: string - description: Annotations to be added to the CronJob metadata. - type: object - concurrencyPolicy: - description: Specifies how to treat concurrent executions - of a Job. - type: string - containerConfigs: - description: CronJobContainerConfigs k8s container settings - for the CronJob + git: + description: GitCloneOptions describes how a clone should + be performed. properties: - commands: - description: Array of commands to be executed (in order) - against a Feature Store deployment. - items: + configs: + additionalProperties: type: string - type: array + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object env: items: description: EnvVar represents an environment variable present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -5020,6 +6961,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5076,7 +7047,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5095,8 +7066,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -5116,457 +7087,959 @@ spec: x-kubernetes-map-type: atomic type: object type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast + init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string image: + description: Image containing the packaged feature repository. type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - nodeSelector: + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: additionalProperties: type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints + for the FeatureStore deployment. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. properties: - claims: + preferredDuringSchedulingIgnoredDuringExecution: description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in the + range 1-100. + format: int32 + type: integer required: - - name + - preference + - weight type: object type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - type: object - failedJobsHistoryLimit: - description: The number of failed finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - jobSpec: - description: Specification of the desired behavior of a job. - properties: - activeDeadlineSeconds: - description: |- - Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr - format: int64 - type: integer - backoffLimit: - description: Specifies the number of retries before marking - this job failed. - format: int32 - type: integer - backoffLimitPerIndex: - description: |- - Specifies the limit for the number of retries within an - index before marking this index as failed. - format: int32 - type: integer - completionMode: - description: |- - completionMode specifies how Pod completions are tracked. It can be - `NonIndexed` (default) or `Indexed`. - type: string - completions: - description: |- - Specifies the desired number of successfully finished pods the - job should be run with. - format: int32 - type: integer - maxFailedIndexes: - description: |- - Specifies the maximal number of failed indexes before marking the Job as - failed, when backoffLimitPerIndex is set. - format: int32 - type: integer - parallelism: - description: |- - Specifies the maximum desired number of pods the job should - run at any given time. - format: int32 - type: integer - podFailurePolicy: - description: Specifies the policy of handling failed pods. + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). properties: - rules: - description: A list of pod failure policy rules. The - rules are evaluated in order. + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... items: - description: PodFailurePolicyRule describes how - a pod failure is handled when the requirements - are met. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - action: - description: Specifies the action taken on a - pod failure when the requirements are satisfied. - type: string - onExitCodes: - description: Represents the requirement on the - container exit codes. + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. properties: - containerName: + labelSelector: description: |- - Restricts the check for exit codes to the container with the - specified name. - type: string - operator: + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: description: |- - Represents the relationship between the container exit code(s) and the - specified values. - type: string - values: - description: Specifies the set of values. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. items: - format: int32 - type: integer - type: array - x-kubernetes-list-type: set - required: - - operator - - values - type: object - onPodConditions: - description: |- - Represents the requirement on the pod conditions. The requirement is represented - as a list of pod condition patterns. - items: - description: |- - PodFailurePolicyOnPodConditionsPattern describes a pattern for matching - an actual pod condition type. - properties: - status: - description: Specifies the required Pod - condition status. type: string - type: - description: Specifies the required Pod - condition type. + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: type: string - required: - - status - - type - type: object - type: array - x-kubernetes-list-type: atomic - required: - - action - type: object - type: array - x-kubernetes-list-type: atomic - required: - - rules - type: object - podReplacementPolicy: - description: podReplacementPolicy specifies when to create - replacement Pods. - type: string - podTemplateAnnotations: - additionalProperties: - type: string - description: |- - PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate - metadata. - type: object - suspend: - description: suspend specifies whether the Job controller - should create Pods or not. - type: boolean - ttlSecondsAfterFinished: - description: |- - ttlSecondsAfterFinished limits the lifetime of a Job that has finished - execution (either Complete or Failed). - format: int32 - type: integer - type: object - schedule: - description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. - type: string - startingDeadlineSeconds: - description: |- - Optional deadline in seconds for starting the job if it misses scheduled - time for any reason. - format: int64 - type: integer - successfulJobsHistoryLimit: - description: The number of successful finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - suspend: - description: |- - This flag tells the controller to suspend subsequent executions, it does - not apply to already started executions. - type: boolean - timeZone: - description: The time zone name for the given schedule, see - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. - type: string - type: object - feastProject: - description: FeastProject is the Feast project id. - pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ - type: string - feastProjectDir: - description: FeastProjectDir defines how to create the feast project - directory. - properties: - git: - description: GitCloneOptions describes how a clone should - be performed. - properties: - configs: - additionalProperties: - type: string - description: |- - Configs passed to git via `-c` - e.g. http.sslVerify: 'false' - OR 'url."https://api:\${TOKEN}@github.com/". - type: object - env: - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean required: - - key + - topologyKey type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports - metadata.name, metadata.namespace, `metadata.labels['''']`, - `metadata.' + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - resourceFieldRef: + matchLabelKeys: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, + etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. properties: - key: - description: The key of the secret to select - from. Must be a valid secret key. - type: string - name: - default: "" + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean required: - - key + - topologyKey type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source of - a set of ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from - properties: - name: - default: "" + weight: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight type: object - x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... properties: - name: - default: "" + labelSelector: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean + required: + - topologyKey type: object - x-kubernetes-map-type: atomic - type: object - type: array - featureRepoPath: - description: FeatureRepoPath is the relative path to the - feature repo subdirectory. Default is 'feature_repo'. - type: string - ref: - description: Reference to a branch / tag / commit - type: string - url: - description: The repository URL to clone from. - type: string - required: - - url - type: object - x-kubernetes-validations: - - message: RepoPath must be a file name only, with no slashes. - rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') - : true' - init: - description: FeastInitOptions defines how to run a `feast - init`. - properties: - minimal: - type: boolean - template: - description: Template for the created project - enum: - - local - - gcp - - aws - - snowflake - - spark - - postgres - - hbase - - cassandra - - hazelcast - - ikv - - couchbase - - clickhouse - type: string + type: array + x-kubernetes-list-type: atomic + type: object type: object - type: object - x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' - replicas: - default: 1 - description: |- - Replicas is the desired number of pod replicas. Used by the scale sub-resource. - Mutually exclusive with services. - format: int32 - minimum: 1 - type: integer - services: - description: FeatureStoreServices defines the desired feast services. - An ephemeral onlineStore feature server is deployed by default. - properties: deploymentStrategy: description: DeploymentStrategy describes how to replace existing pods with new ones. @@ -5600,6 +8073,10 @@ spec: disableInitContainers: description: Disable the 'feast repo initialization' initContainer type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string offlineStore: description: OfflineStore configures the offline store service properties: @@ -5733,6 +8210,7 @@ spec: - couchbase.offline - clickhouse - ray + - oracle type: string required: - secretRef @@ -5752,14 +8230,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -5804,6 +8282,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5862,7 +8370,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5881,8 +8389,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5944,6 +8452,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6107,6 +8619,11 @@ spec: onlineStore: description: OnlineStore configures the online store service properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean persistence: description: OnlineStorePersistence configures the persistence settings for the online store service @@ -6236,7 +8753,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -6251,6 +8767,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -6270,14 +8789,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -6314,12 +8833,42 @@ spec: FieldPath is written in terms of, defaults to "v1". type: string - fieldPath: - description: Path of the field to select - in the specified API version. + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. type: string required: - - fieldPath + - key + - path + - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: @@ -6380,7 +8929,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6399,8 +8948,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -6462,6 +9011,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6621,7 +9174,108 @@ spec: type: integer type: object type: object + serving: + description: Serving configures the Feast feature_server + section written into feature_store.yaml for the online + serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is + "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or + disables individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to + the offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to + the offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline + write batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied + to the Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' registry: description: Registry configures the registry service. One selection is required. Local is the default setting. @@ -6810,14 +9464,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -6863,6 +9517,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -6922,7 +9606,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6941,9 +9625,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -6984,6 +9667,31 @@ spec: - error - critical type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object metrics: description: Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. @@ -7009,6 +9717,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -7180,6 +9892,9 @@ spec: true rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' type: object remote: description: RemoteRegistryConfig points to a remote feast @@ -7237,6 +9952,42 @@ spec: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean scaling: description: Scaling configures horizontal scaling for the FeatureStore deployment (e.g. HPA autoscaling). @@ -7294,9 +10045,18 @@ spec: stabilizationWindowSeconds: description: |- stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up + considered while scaling... format: int32 type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true type: object scaleUp: description: scaleUp is scaling policy for scaling @@ -7342,9 +10102,18 @@ spec: stabilizationWindowSeconds: description: |- stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up + considered while scaling... format: int32 type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true type: object type: object maxReplicas: @@ -7359,12 +10128,12 @@ spec: items: description: |- MetricSpec specifies how to scale based on a single metric - (only `type` and one other matching field should be set at on + (only `type` and one other matching field should be set at... properties: containerResource: description: |- containerResource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes descr + requests and limits) known to Kubernetes... properties: container: description: container is the name of the @@ -7379,10 +10148,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7429,10 +10197,9 @@ spec: given metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label - selector for the given metric\nWhen - set, it is passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is @@ -7483,10 +10250,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7553,10 +10319,9 @@ spec: given metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label - selector for the given metric\nWhen - set, it is passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is @@ -7607,10 +10372,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7646,7 +10410,7 @@ spec: pods: description: |- pods refers to a metric describing each pod in the current scale target - (for example, transactions-processed-per-second) + (for example,... properties: metric: description: metric identifies the target @@ -7657,10 +10421,9 @@ spec: given metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label - selector for the given metric\nWhen - set, it is passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is @@ -7711,10 +10474,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7749,7 +10511,7 @@ spec: resource: description: |- resource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes describing eac + requests and limits) known to Kubernetes describing... properties: name: description: name is the name of the resource @@ -7760,10 +10522,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7857,6 +10618,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -7896,13 +10662,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -7945,6 +10716,98 @@ spec: type: string type: object type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are + spread across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array ui: description: Creates a UI server container properties: @@ -7954,14 +10817,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -8005,6 +10868,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -8061,7 +10954,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -8080,8 +10973,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -8143,6 +11036,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -8311,7 +11208,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -8353,6 +11250,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -8361,9 +11259,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -8394,7 +11293,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -8443,7 +11342,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -8489,7 +11388,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -8529,7 +11428,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -8542,7 +11441,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -8606,7 +11505,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -8858,9 +11757,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -8895,7 +11794,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -8916,7 +11815,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -8926,7 +11825,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -8955,7 +11854,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -8973,14 +11872,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -9015,6 +11912,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -9040,6 +11953,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -9131,7 +12045,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -9148,7 +12062,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -9178,10 +12092,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -9262,7 +12179,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -9335,7 +12252,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -9377,6 +12294,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -9384,7 +12347,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -9450,7 +12413,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -9465,12 +12428,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -9486,9 +12449,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -9500,6 +12462,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -9514,6 +12477,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -9541,6 +12505,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -9555,6 +12520,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -9592,6 +12558,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -9626,7 +12593,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -9701,7 +12668,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -9732,7 +12699,6 @@ spec: type: object required: - feastProject - - replicas type: object x-kubernetes-validations: - message: replicas > 1 and services.scaling.autoscaling are mutually @@ -9807,10 +12773,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -9975,14 +12938,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -10026,6 +12989,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -10081,7 +13073,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -10100,8 +13092,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -10147,6 +13139,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -10188,7 +13184,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -10282,7 +13278,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -10365,14 +13360,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -10416,6 +13411,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -10471,7 +13495,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -10490,8 +13514,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -10545,15 +13569,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -10740,14 +13789,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -10791,6 +13840,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -10847,7 +13926,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -10866,8 +13945,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -10929,6 +14008,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -11217,7 +14300,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -11232,6 +14314,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -11250,14 +14335,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -11301,6 +14386,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -11357,7 +14472,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -11376,8 +14491,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -11439,6 +14554,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -11778,14 +14897,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -11830,6 +14949,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -11888,7 +15037,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -11907,8 +15056,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -11974,6 +15123,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -12195,6 +15348,10 @@ spec: x-kubernetes-validations: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -12240,6 +15397,10 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. properties: @@ -12278,13 +15439,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -12335,14 +15501,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -12386,6 +15552,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -12441,7 +15636,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -12460,8 +15655,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -12522,6 +15717,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -12690,7 +15889,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the volume @@ -12732,6 +15931,7 @@ spec: blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -12740,9 +15940,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -12773,7 +15974,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: monitors: description: |- @@ -12821,7 +16022,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -12867,7 +16068,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -12907,7 +16108,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver that @@ -12919,7 +16120,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -12981,7 +16182,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -13230,9 +16431,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -13267,7 +16468,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -13288,7 +16489,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -13298,7 +16499,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -13327,7 +16528,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -13345,14 +16546,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that details + Glusterfs topology. type: string path: description: |- @@ -13387,6 +16586,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -13412,6 +16627,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -13502,7 +16718,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -13519,7 +16735,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -13549,10 +16765,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected along - with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod to @@ -13632,7 +16851,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -13703,7 +16922,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -13745,6 +16964,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -13752,7 +17017,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -13818,7 +17083,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: group: description: |- @@ -13833,12 +17098,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -13854,9 +17119,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the volume @@ -13868,6 +17132,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -13882,6 +17147,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -13909,6 +17175,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -13923,6 +17190,7 @@ spec: attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -13960,6 +17228,7 @@ spec: with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -13994,7 +17263,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -14069,7 +17338,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine + and mounted on kubelets host machine. properties: fsType: description: |- @@ -14179,14 +17448,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -14230,6 +17499,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -14286,7 +17585,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -14305,8 +17604,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -14353,6 +17652,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -14394,7 +17697,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -14489,7 +17792,6 @@ spec: condition type. type: string required: - - status - type type: object type: array @@ -14574,14 +17876,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -14625,6 +17927,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -14681,7 +18013,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -14700,8 +18032,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -14756,15 +18088,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -14954,14 +18311,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -15006,6 +18363,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -15064,7 +18451,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -15083,8 +18470,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -15146,6 +18533,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -15438,7 +18829,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -15453,6 +18843,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -15472,14 +18865,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -15524,6 +18917,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -15582,7 +19005,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -15601,8 +19024,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -15664,6 +19087,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -16012,14 +19439,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -16065,6 +19492,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -16124,7 +19581,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -16143,9 +19600,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -16211,6 +19667,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -16439,6 +19899,11 @@ spec: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -16484,6 +19949,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -16523,13 +19993,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -16581,14 +20056,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -16632,6 +20107,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -16688,7 +20193,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -16707,8 +20212,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -16770,6 +20275,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -16938,7 +20447,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -16980,6 +20489,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -16988,9 +20498,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -17021,7 +20532,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -17070,7 +20581,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -17116,7 +20627,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -17156,7 +20667,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -17169,7 +20680,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -17233,7 +20744,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -17485,9 +20996,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -17522,7 +21033,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -17543,7 +21054,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -17553,7 +21064,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -17582,7 +21093,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -17600,14 +21111,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -17642,6 +21151,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -17667,6 +21192,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -17758,7 +21284,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -17775,7 +21301,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -17805,10 +21331,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -17889,7 +21418,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -17962,7 +21491,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -18004,6 +21533,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -18011,7 +21586,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -18077,7 +21652,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -18092,12 +21667,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -18113,9 +21688,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -18127,6 +21701,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -18141,6 +21716,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -18168,6 +21744,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -18182,6 +21759,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -18219,6 +21797,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -18253,7 +21832,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -18328,7 +21907,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -18402,10 +21981,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string diff --git a/infra/feast-operator/config/default/metrics_service.yaml b/infra/feast-operator/config/default/metrics_service.yaml index 0207c0469d4..fbf17dd96ec 100644 --- a/infra/feast-operator/config/default/metrics_service.yaml +++ b/infra/feast-operator/config/default/metrics_service.yaml @@ -14,4 +14,5 @@ spec: protocol: TCP targetPort: 8443 selector: + app.kubernetes.io/name: feast-operator control-plane: controller-manager diff --git a/infra/feast-operator/config/default/related_image_fs_patch.tmpl b/infra/feast-operator/config/default/related_image_fs_patch.tmpl index 23bf80c98ba..11e127dab39 100644 --- a/infra/feast-operator/config/default/related_image_fs_patch.tmpl +++ b/infra/feast-operator/config/default/related_image_fs_patch.tmpl @@ -1,10 +1,14 @@ -- op: replace - path: "/spec/template/spec/containers/0/env/0" - value: - name: RELATED_IMAGE_FEATURE_SERVER - value: ${FS_IMG} -- op: replace - path: "/spec/template/spec/containers/0/env/1" - value: - name: RELATED_IMAGE_CRON_JOB - value: ${CJ_IMG} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager +spec: + template: + spec: + containers: + - name: manager + env: + - name: RELATED_IMAGE_FEATURE_SERVER + value: ${FS_IMG} + - name: RELATED_IMAGE_CRON_JOB + value: ${CJ_IMG} diff --git a/infra/feast-operator/config/default/related_image_fs_patch.yaml b/infra/feast-operator/config/default/related_image_fs_patch.yaml index 172afcfd075..5ab07fd91e1 100644 --- a/infra/feast-operator/config/default/related_image_fs_patch.yaml +++ b/infra/feast-operator/config/default/related_image_fs_patch.yaml @@ -1,10 +1,14 @@ -- op: replace - path: "/spec/template/spec/containers/0/env/0" - value: - name: RELATED_IMAGE_FEATURE_SERVER - value: quay.io/feastdev/feature-server:0.60.0 -- op: replace - path: "/spec/template/spec/containers/0/env/1" - value: - name: RELATED_IMAGE_CRON_JOB - value: quay.io/openshift/origin-cli:4.17 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager +spec: + template: + spec: + containers: + - name: manager + env: + - name: RELATED_IMAGE_FEATURE_SERVER + value: quay.io/feastdev/feature-server:0.65.0 + - name: RELATED_IMAGE_CRON_JOB + value: quay.io/openshift/origin-cli:4.17 diff --git a/infra/feast-operator/config/manager/kustomization.yaml b/infra/feast-operator/config/manager/kustomization.yaml index 844c53ae757..5f3ce6cadda 100644 --- a/infra/feast-operator/config/manager/kustomization.yaml +++ b/infra/feast-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: quay.io/feastdev/feast-operator - newTag: 0.60.0 + newTag: 0.65.0 diff --git a/infra/feast-operator/config/manager/manager.yaml b/infra/feast-operator/config/manager/manager.yaml index 242144e2b03..4213787e075 100644 --- a/infra/feast-operator/config/manager/manager.yaml +++ b/infra/feast-operator/config/manager/manager.yaml @@ -19,6 +19,7 @@ metadata: spec: selector: matchLabels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager replicas: 1 template: @@ -26,6 +27,7 @@ spec: annotations: kubectl.kubernetes.io/default-container: manager labels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager spec: # TODO(user): Uncomment the following code to configure the nodeAffinity expression @@ -71,10 +73,16 @@ spec: drop: - "ALL" env: + - name: GOMEMLIMIT + value: "230MiB" - name: RELATED_IMAGE_FEATURE_SERVER value: feast:latest - name: RELATED_IMAGE_CRON_JOB value: origin-cli:latest + # Injected from params.env via kustomize replacements (ODH/RHOAI overlays). + # Open Data Hub operator sets this from GatewayConfig when the cluster uses external OIDC. + - name: OIDC_ISSUER_URL + value: "" livenessProbe: httpGet: path: /healthz diff --git a/infra/feast-operator/config/overlays/odh/kustomization.yaml b/infra/feast-operator/config/overlays/odh/kustomization.yaml index cf751d178bd..044614f01fe 100644 --- a/infra/feast-operator/config/overlays/odh/kustomization.yaml +++ b/infra/feast-operator/config/overlays/odh/kustomization.yaml @@ -52,3 +52,13 @@ replacements: name: controller-manager fieldPaths: - spec.template.spec.containers.[name=manager].env.[name=RELATED_IMAGE_CRON_JOB].value + - source: + kind: ConfigMap + name: feast-operator-parameters + fieldPath: data.OIDC_ISSUER_URL + targets: + - select: + kind: Deployment + name: controller-manager + fieldPaths: + - spec.template.spec.containers.[name=manager].env.[name=OIDC_ISSUER_URL].value diff --git a/infra/feast-operator/config/overlays/odh/params.env b/infra/feast-operator/config/overlays/odh/params.env index c3cb4bab64b..b0d55d6bd70 100644 --- a/infra/feast-operator/config/overlays/odh/params.env +++ b/infra/feast-operator/config/overlays/odh/params.env @@ -1,3 +1,5 @@ -RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.60.0 -RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.60.0 +RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.65.0 +RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.65.0 RELATED_IMAGE_CRON_JOB=quay.io/openshift/origin-cli:4.17 +# Set at deploy time by the Open Data Hub operator from GatewayConfig (external OIDC). +OIDC_ISSUER_URL= diff --git a/infra/feast-operator/config/overlays/rhoai/kustomization.yaml b/infra/feast-operator/config/overlays/rhoai/kustomization.yaml index 4917579ef28..b9d075bdf39 100644 --- a/infra/feast-operator/config/overlays/rhoai/kustomization.yaml +++ b/infra/feast-operator/config/overlays/rhoai/kustomization.yaml @@ -52,3 +52,13 @@ replacements: name: controller-manager fieldPaths: - spec.template.spec.containers.[name=manager].env.[name=RELATED_IMAGE_CRON_JOB].value + - source: + kind: ConfigMap + name: feast-operator-parameters + fieldPath: data.OIDC_ISSUER_URL + targets: + - select: + kind: Deployment + name: controller-manager + fieldPaths: + - spec.template.spec.containers.[name=manager].env.[name=OIDC_ISSUER_URL].value diff --git a/infra/feast-operator/config/overlays/rhoai/params.env b/infra/feast-operator/config/overlays/rhoai/params.env index afae8c9bea4..dabacfd458c 100644 --- a/infra/feast-operator/config/overlays/rhoai/params.env +++ b/infra/feast-operator/config/overlays/rhoai/params.env @@ -1,3 +1,5 @@ -RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.60.0 -RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.60.0 -RELATED_IMAGE_CRON_JOB=registry.redhat.io/openshift4/ose-cli@sha256:bc35a9fc663baf0d6493cc57e89e77a240a36c43cf38fb78d8e61d3b87cf5cc5 \ No newline at end of file +RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.65.0 +RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.65.0 +RELATED_IMAGE_CRON_JOB=registry.redhat.io/openshift4/ose-cli@sha256:bc35a9fc663baf0d6493cc57e89e77a240a36c43cf38fb78d8e61d3b87cf5cc5 +# Set at deploy time by the Open Data Hub operator from GatewayConfig (external OIDC). +OIDC_ISSUER_URL= \ No newline at end of file diff --git a/infra/feast-operator/config/prometheus/monitor.yaml b/infra/feast-operator/config/prometheus/monitor.yaml index e76479a1305..50f2ea8e448 100644 --- a/infra/feast-operator/config/prometheus/monitor.yaml +++ b/infra/feast-operator/config/prometheus/monitor.yaml @@ -27,4 +27,5 @@ spec: insecureSkipVerify: true selector: matchLabels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager diff --git a/infra/feast-operator/config/rbac/featurestore_editor_role.yaml b/infra/feast-operator/config/rbac/featurestore_editor_role.yaml index 37c38e6f618..e2e3acf1b60 100644 --- a/infra/feast-operator/config/rbac/featurestore_editor_role.yaml +++ b/infra/feast-operator/config/rbac/featurestore_editor_role.yaml @@ -5,6 +5,8 @@ metadata: labels: app.kubernetes.io/name: feast-operator app.kubernetes.io/managed-by: kustomize + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" name: featurestore-editor-role rules: - apiGroups: diff --git a/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml b/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml index b4444cbe60a..bca67d9788d 100644 --- a/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml +++ b/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml @@ -5,6 +5,7 @@ metadata: labels: app.kubernetes.io/name: feast-operator app.kubernetes.io/managed-by: kustomize + rbac.authorization.k8s.io/aggregate-to-view: "true" name: featurestore-viewer-role rules: - apiGroups: diff --git a/infra/feast-operator/config/rbac/role.yaml b/infra/feast-operator/config/rbac/role.yaml index 3fa228afc6b..a79dca283ed 100644 --- a/infra/feast-operator/config/rbac/role.yaml +++ b/infra/feast-operator/config/rbac/role.yaml @@ -5,76 +5,111 @@ metadata: name: manager-role rules: - apiGroups: - - apps + - "" resources: - - deployments + - configmaps + - persistentvolumeclaims + - services verbs: - create - delete + - deletecollection - get - list - update - watch - apiGroups: - - authentication.k8s.io + - "" resources: - - tokenreviews + - namespaces + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods verbs: - create + - delete + - deletecollection + - get + - list + - watch - apiGroups: - - autoscaling + - "" resources: - - horizontalpodautoscalers + - pods/exec + verbs: + - create +- apiGroups: + - "" + resources: + - pods/log + verbs: + - get +- apiGroups: + - "" + resources: + - serviceaccounts verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - - batch + - apps resources: - - cronjobs + - deployments verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - - "" + - authentication.k8s.io resources: - - configmaps - - persistentvolumeclaims - - serviceaccounts - - services + - tokenreviews + verbs: + - create +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers verbs: - create - delete - get - list + - patch - update - watch - apiGroups: - - "" + - batch resources: - - namespaces - - pods - - secrets + - cronjobs verbs: + - create + - delete - get - list + - patch + - update - watch - apiGroups: - - "" + - config.openshift.io resources: - - pods/exec + - apiservers verbs: - - create + - get + - list + - watch - apiGroups: - feast.dev resources: @@ -101,6 +136,29 @@ rules: - get - patch - update +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: @@ -127,3 +185,11 @@ rules: - list - update - watch +- apiGroups: + - sparkoperator.k8s.io + resources: + - sparkapplications + verbs: + - create + - delete + - get diff --git a/infra/feast-operator/config/samples/kustomization.yaml b/infra/feast-operator/config/samples/kustomization.yaml index 127bd5894b4..65061a2b265 100644 --- a/infra/feast-operator/config/samples/kustomization.yaml +++ b/infra/feast-operator/config/samples/kustomization.yaml @@ -3,4 +3,7 @@ resources: - v1_featurestore.yaml - v1_featurestore_with_ui.yaml - v1_featurestore_all_remote_servers.yaml +- v1_featurestore_serving.yaml +- v1_featurestore_mcp.yaml +- v1_featurestore_materialization_openlineage.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/infra/feast-operator/config/samples/v1_featurestore_materialization_openlineage.yaml b/infra/feast-operator/config/samples/v1_featurestore_materialization_openlineage.yaml new file mode 100644 index 00000000000..f2fa585b1f8 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_materialization_openlineage.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: Secret +metadata: + name: openlineage-secret + namespace: feast +stringData: + # api_key is read from this Secret and written into feature_store.yaml + api_key: "your-marquez-api-key" #pragma: allowlist secret +--- +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-materialization-openlineage + namespace: feast +spec: + feastProject: my_project + # materialization controls how features are written to the online store. + # Written into feature_store.yaml for all service pods. + materialization: + # onlineWriteBatchSize limits rows per batch to prevent OOM during materialization. + # Supported by local, Spark, and Ray engines. + onlineWriteBatchSize: 10000 + # extraConfig passes additional materialization settings inline into feature_store.yaml. + # Use for fields not typed above (e.g. pull_latest_features) or fields added + # in newer Feast SDK versions without waiting for an operator update. + # Boolean ("true"/"false") and integer strings are coerced to native YAML types. + # extraConfig: + # pull_latest_features: "false" + # openlineage emits data lineage events during feast apply and materialization. + openlineage: + enabled: true + transportType: http + # transportUrl is the base URL of your Marquez or OpenLineage-compatible server. + transportUrl: "http://marquez.feast.svc.cluster.local:5000" + transportEndpoint: "api/v1/lineage" + # apiKeySecretRef references a Secret with key "api_key" for bearer auth. + apiKeySecretRef: + name: openlineage-secret + # extraConfig passes any additional OpenLineage settings inline into feature_store.yaml. + # Use it for non-core options (namespace, producer, emit_on_apply, + # emit_on_materialize) and transport-specific settings (e.g. kafka + # bootstrap_servers, topic, sasl_mechanism; file path). + # Boolean values ("true"/"false") and integers are automatically coerced to + # their native YAML types so Feast Pydantic validators accept them. + extraConfig: + namespace: "my-feast-project" + producer: "feast-operator" + emit_on_apply: "true" + emit_on_materialize: "true" + # kafka transport example: + # bootstrap_servers: "kafka.svc:9092" + # topic: "openlineage" + # sasl_mechanism: "PLAIN" diff --git a/infra/feast-operator/config/samples/v1_featurestore_mcp.yaml b/infra/feast-operator/config/samples/v1_featurestore_mcp.yaml new file mode 100644 index 00000000000..67dd1c6947d --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_mcp.yaml @@ -0,0 +1,26 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-mcp +spec: + feastProject: my_project + services: + onlineStore: + server: {} + # serving.mcp switches the feature server to MCP (Model Context Protocol) mode. + # When mcp is set, the feature_server type in feature_store.yaml is "mcp". + # MCP enables LLM-friendly tool-based access to Feast feature views. + serving: + mcp: + enabled: true + serverName: feast-mcp-server + serverVersion: "1.0.0" + # transport can be "sse" (default) or "http" + transport: sse + registry: + local: + server: + # MCP on the registry requires REST API to be enabled. + restAPI: true + mcp: + enabled: true diff --git a/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml b/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml index 7ef676d0297..97f325bb0bd 100644 --- a/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml @@ -19,3 +19,7 @@ stringData: client_secret: client_secret username: username password: password + # Optional: enable audience/issuer claim verification on the servers. + # Values must match the claims in the tokens your IdP issues. + # audience: api://feast-feature-server + # issuer: https://idp.example.com/realms/feast diff --git a/infra/feast-operator/config/samples/v1_featurestore_openlineage_consumer.yaml b/infra/feast-operator/config/samples/v1_featurestore_openlineage_consumer.yaml new file mode 100644 index 00000000000..9e242896135 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_openlineage_consumer.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: Secret +metadata: + name: openlineage-producer-secret + namespace: feast +stringData: + api_key: "your-marquez-api-key" #pragma: allowlist secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: openlineage-consumer-secret + namespace: feast +stringData: + api_key: "consumer-api-key-for-producers" #pragma: allowlist secret +--- +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-openlineage-consumer + namespace: feast +spec: + feastProject: my_project + services: + registry: + local: + persistence: + store: + type: sql + secretRef: + name: registry-db-secret + openlineage: + enabled: true + transportType: http + transportUrl: "http://localhost:8080/api" + transportEndpoint: "v1/lineage" + apiKeySecretRef: + name: openlineage-producer-secret + extraConfig: + namespace: "my_project" + producer: "feast-operator" + emit_on_apply: "true" + emit_on_materialize: "true" + # consumer enables Feast as an OpenLineage event receiver. + # External producers (Airflow, Spark, dbt) can POST events to + # the Feast REST server at POST /api/v1/lineage. + # The Feast UI then displays lineage from all producers in + # Registry, OpenLineage, and Merged views. + consumer: + enabled: true + storeType: sql + # Optional: use a separate database for lineage storage. + # If omitted, the SQL registry database is reused. + # connectionStringSecretRef: + # name: lineage-db-secret + apiKeySecretRef: + name: openlineage-consumer-secret + # namespaceMapping maps OL namespaces to Feast projects + # for RBAC-scoped visibility in the UI. + namespaceMapping: + airflow_production: my_project + spark_etl: my_project + dbt_analytics: my_project diff --git a/infra/feast-operator/config/samples/v1_featurestore_packaged.yaml b/infra/feast-operator/config/samples/v1_featurestore_packaged.yaml new file mode 100644 index 00000000000..4b334b9d3d8 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_packaged.yaml @@ -0,0 +1,10 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-packaged +spec: + feastProject: sample_packaged + feastProjectDir: + packaged: + image: registry.example.com/feature-server@sha256:0123456789abcdef + featureRepoPath: /opt/feast/feature_repo diff --git a/infra/feast-operator/config/samples/v1_featurestore_scaling_hpa.yaml b/infra/feast-operator/config/samples/v1_featurestore_scaling_hpa.yaml index 4380b28d018..af4a9fd1d02 100644 --- a/infra/feast-operator/config/samples/v1_featurestore_scaling_hpa.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_scaling_hpa.yaml @@ -30,7 +30,7 @@ stringData: user: ${POSTGRES_USER} password: ${POSTGRES_PASSWORD} --- -# HPA autoscaling: 2-10 replicas with DB-backed persistence +# HPA autoscaling: 2-10 replicas with DB-backed persistence and HA apiVersion: feast.dev/v1 kind: FeatureStore metadata: @@ -50,6 +50,8 @@ spec: target: type: Utilization averageUtilization: 70 + podDisruptionBudgets: + maxUnavailable: 1 onlineStore: persistence: store: diff --git a/infra/feast-operator/config/samples/v1_featurestore_scaling_static.yaml b/infra/feast-operator/config/samples/v1_featurestore_scaling_static.yaml index c0f0f21cd6a..e4df5a6245a 100644 --- a/infra/feast-operator/config/samples/v1_featurestore_scaling_static.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_scaling_static.yaml @@ -30,7 +30,13 @@ stringData: user: ${POSTGRES_USER} password: ${POSTGRES_PASSWORD} --- -# Static scaling: 3 replicas with DB-backed persistence +# Static scaling: 3 replicas with DB-backed persistence, PDB, and HA +# +# By default the operator auto-injects: +# - Soft pod anti-affinity (prefer different nodes) +# - Soft zone topology spread (prefer different zones, e.g. us-east-1a, us-east-1b, us-east-1c) +# +# To enforce strict zone spreading on AWS (DoNotSchedule), uncomment topologySpreadConstraints below. apiVersion: feast.dev/v1 kind: FeatureStore metadata: @@ -40,6 +46,16 @@ spec: feastProject: my_project replicas: 3 services: + podDisruptionBudgets: + maxUnavailable: 1 + # Uncomment to enforce strict spreading across AWS availability zones: + # topologySpreadConstraints: + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # whenUnsatisfiable: DoNotSchedule + # labelSelector: + # matchLabels: + # feast.dev/name: sample-scaling-static onlineStore: persistence: store: diff --git a/infra/feast-operator/config/samples/v1_featurestore_serving.yaml b/infra/feast-operator/config/samples/v1_featurestore_serving.yaml new file mode 100644 index 00000000000..412499412e6 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_serving.yaml @@ -0,0 +1,34 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-serving +spec: + feastProject: my_project + services: + onlineStore: + server: {} + # serving configures the Feast feature_server section in feature_store.yaml. + # It controls fine-grained metrics and offline push batching. + # This section only applies to the online feature server (feast serve). + serving: + metrics: + enabled: true + # categories selectively enables or disables Feast metric categories. + # All categories default to true when metrics is enabled. + # Keys must match Feast MetricsConfig field names for your SDK version. + # When a newer Feast SDK adds a metric category you can toggle it here + # immediately — no operator update needed, the key passes through to + # feature_store.yaml and is validated by the running Feast SDK. + categories: + resource: true # CPU / memory gauges + request: true # per-endpoint latency and request counters + online_features: true # online feature retrieval metrics + push: true # push/write request counters + materialization: true # materialization counters and duration histograms + freshness: false # feature freshness gauges (can be expensive at scale) + offline_features: true # offline store retrieval counters, latency, row count + audit_logging: false # structured JSON audit logs via the feast.audit logger + offlinePushBatching: + enabled: true + batchSize: 1000 # max rows per offline write batch + batchIntervalSeconds: 10 # flush batch every 10 s diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 0c0b05be388..be85a29a7b2 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -11,7 +11,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: featurestores.feast.dev spec: group: feast.dev @@ -70,10 +70,32 @@ spec: OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. https://auth0. properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM certificate. + Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string secretRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. properties: name: default: "" @@ -84,8 +106,14 @@ spec: type: string type: object x-kubernetes-map-type: atomic - required: - - secretRef + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean type: object type: object x-kubernetes-validations: @@ -141,14 +169,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -192,6 +220,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -247,7 +304,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -266,8 +323,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -313,6 +370,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -354,7 +415,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -448,7 +509,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -506,6 +566,16 @@ spec: description: The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. type: string type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object feastProject: description: FeastProject is the Feast project id. pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ @@ -531,14 +601,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -582,6 +652,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -637,7 +736,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -656,8 +755,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -711,15 +810,160 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object replicas: default: 1 description: |- @@ -732,558 +976,807 @@ spec: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. properties: - deploymentStrategy: - description: DeploymentStrategy describes how to replace existing - pods with new ones. + affinity: + description: Affinity defines the pod scheduling constraints for + the FeatureStore deployment. properties: - rollingUpdate: - description: |- - Rolling update config params. Present only if DeploymentStrategyType = - RollingUpdate. + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. properties: - maxSurge: - anyOf: - - type: integer - - type: string + preferredDuringSchedulingIgnoredDuringExecution: description: |- - The maximum number of pods that can be scheduled above the desired number of - pods. - x-kubernetes-int-or-string: true - maxUnavailable: - anyOf: - - type: integer - - type: string - description: The maximum number of pods that can be unavailable - during the update. - x-kubernetes-int-or-string: true - type: object - type: - description: Type of deployment. Can be "Recreate" or "RollingUpdate". - Default is RollingUpdate. - type: string - type: object - disableInitContainers: - description: Disable the 'feast repo initialization' initContainer - type: boolean - offlineStore: - description: OfflineStore configures the offline store service - properties: - persistence: - description: OfflineStorePersistence configures the persistence - settings for the offline store service - properties: - file: - description: OfflineStoreFilePersistence configures the - file-based persistence for the offline store service - properties: - pvc: - description: PvcConfig defines the settings for a - persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent volume - access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true + key: + description: The label key that the selector + applies to. + type: string + operator: description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum - amount of compute resources required. - type: object + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which this - persistent volume belongs. - type: string - type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. - type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath - type: object - x-kubernetes-validations: - - message: One selection is required between ref and - create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and must - not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - type: - enum: - - file - - dask - - duckdb - type: string - type: object - store: - description: OfflineStoreDBStorePersistence configures - the DB store persistence for the offline store service + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName - type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the secret - key. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you want - to use. - enum: - - snowflake.offline - - bigquery - - redshift - - spark - - postgres - - trino - - athena - - mssql - - couchbase.offline - - clickhouse - - ray - type: string + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic required: - - secretRef - - type + - nodeSelectorTerms type: object + x-kubernetes-map-type: atomic type: object - x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, c)' - server: - description: Creates a remote offline server container + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). properties: - env: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... items: - description: EnvVar represents an environment variable - present in a Container. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports - metadata.name, metadata.namespace, `metadata.labels['''']`, - `metadata.' + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - resourceFieldRef: + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. - properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace - properties: - key: - description: The key of the secret to select - from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer required: - - name + - podAffinityTerm + - weight type: object type: array - envFrom: + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... items: - description: EnvFromSource represents the source of - a set of ConfigMaps + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... properties: - configMapRef: - description: The ConfigMap to select from + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey type: object type: array - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - logLevel: + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible metrics - for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - tls: - description: TlsConfigs configures server TLS for a feast - service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, where - TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret key - names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... items: - description: VolumeMount describes a mounting of a Volume - within a container. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. - type: string - subPath: + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: Expanded path within the volume from - which the container's volume should be mounted. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string required: - - mountPath - - name + - topologyKey type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number of seconds - after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores (2 * - CPU + 1). - format: int32 - minimum: -1 - type: integer - type: object + x-kubernetes-list-type: atomic type: object type: object - onlineStore: - description: OnlineStore configures the online store service + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. properties: - persistence: - description: OnlineStorePersistence configures the persistence - settings for the online store service + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. properties: - file: - description: OnlineStoreFilePersistence configures the - file-based persistence for the online store service - properties: - path: - type: string - pvc: - description: PvcConfig defines the settings for a - persistent file store based on PVCs. - properties: - create: - description: Settings for creating a new PVC - properties: - accessModes: - description: AccessModes k8s persistent volume + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be unavailable + during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or "RollingUpdate". + Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures the + file-based persistence for the offline store service + properties: + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume access modes. Defaults to ["ReadWriteOnce"]. items: type: string @@ -1351,21 +1844,16 @@ spec: - message: Mount path must start with '/' and must not contain ':' rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string type: object - x-kubernetes-validations: - - message: Ephemeral stores must have absolute paths. - rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') - : true' - - message: PVC path must be a file name only, with no - slashes. - rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') - : true' - - message: Online store does not support S3 or GS buckets. - rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') - || self.path.startsWith(''gs://'')) : true' store: - description: OnlineStoreDBStorePersistence configures - the DB store persistence for the online store service + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service properties: secretKeyName: description: By default, the selected store "type" @@ -1389,23 +1877,18 @@ spec: description: Type of the persistence type you want to use. enum: - - snowflake.online - - redis - - ikv - - datastore - - dynamodb - - bigtable + - snowflake.offline + - bigquery + - redshift + - spark - postgres - - cassandra - - mysql - - hazelcast - - singlestore - - hbase - - elasticsearch - - qdrant - - couchbase.online - - milvus - - hybrid + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle type: string required: - secretRef @@ -1416,7 +1899,7 @@ spec: - message: One selection required between file or store. rule: '[has(self.file), has(self.store)].exists_one(c, c)' server: - description: Creates a feature server container + description: Creates a remote offline server container properties: env: items: @@ -1424,14 +1907,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1475,6 +1958,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1531,7 +2044,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1550,8 +2063,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1613,6 +2126,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1772,151 +2289,79 @@ spec: type: object type: object type: object - registry: - description: Registry configures the registry service. One selection - is required. Local is the default setting. + onlineStore: + description: OnlineStore configures the online store service properties: - local: - description: LocalRegistryConfig configures the registry service + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service properties: - persistence: - description: RegistryPersistence configures the persistence - settings for the registry service + file: + description: OnlineStoreFilePersistence configures the + file-based persistence for the online store service properties: - file: - description: RegistryFilePersistence configures the - file-based persistence for the registry service + path: + type: string + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. properties: - cache_mode: - description: |- - CacheMode defines the registry cache update strategy. - Allowed values are "sync" and "thread". - enum: - - none - - sync - - thread - type: string - cache_ttl_seconds: - description: CacheTTLSeconds defines the TTL (in - seconds) for the registry cache. - format: int32 - minimum: 0 - type: integer - path: - type: string - pvc: - description: PvcConfig defines the settings for - a persistent file store based on PVCs. + create: + description: Settings for creating a new PVC properties: - create: - description: Settings for creating a new PVC + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. properties: - accessModes: - description: AccessModes k8s persistent - volume access modes. Defaults to ["ReadWriteOnce"]. - items: - type: string - type: array - resources: - description: Resources describes the storage - resource requirements for a volume. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the - minimum amount of compute resources - required. - type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. type: object - storageClassName: - description: StorageClassName is the name - of an existing StorageClass to which - this persistent volume belongs. - type: string type: object - x-kubernetes-validations: - - message: PvcCreate is immutable - rule: self == oldSelf - mountPath: - description: |- - MountPath within the container at which the volume should be mounted. - Must start by "/" and cannot contain ':'. + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. type: string - ref: - description: Reference to an existing field - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - mountPath type: object x-kubernetes-validations: - - message: One selection is required between ref - and create. - rule: '[has(self.ref), has(self.create)].exists_one(c, - c)' - - message: Mount path must start with '/' and - must not contain ':' - rule: self.mountPath.matches('^/[^:]*$') - s3_additional_kwargs: - additionalProperties: - type: string - type: object - type: object - x-kubernetes-validations: - - message: Registry files must use absolute paths - or be S3 ('s3://') or GS ('gs://') object store - URIs. - rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') - || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) - : true' - - message: PVC path must be a file name only, with - no slashes. - rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') - : true' - - message: PVC persistence does not support S3 or - GS object store URIs. - rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') - || self.path.startsWith(''gs://'')) : true' - - message: Additional S3 settings are available only - for S3 object store URIs. - rule: '(has(self.s3_additional_kwargs) && has(self.path)) - ? self.path.startsWith(''s3://'') : true' - store: - description: RegistryDBStorePersistence configures - the DB store persistence for the registry service - properties: - secretKeyName: - description: By default, the selected store "type" - is used as the SecretKeyName + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. type: string - secretRef: - description: Data store parameters should be placed - as-is from the "feature_store.yaml" under the - secret key. + ref: + description: Reference to an existing field properties: name: default: "" @@ -1927,166 +2372,206 @@ spec: type: string type: object x-kubernetes-map-type: atomic - type: - description: Type of the persistence type you - want to use. - enum: - - sql - - snowflake.registry - type: string required: - - secretRef - - type + - mountPath type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') type: object x-kubernetes-validations: - - message: One selection required between file or store. - rule: '[has(self.file), has(self.store)].exists_one(c, - c)' - server: - description: Creates a registry server container + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with no + slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service properties: - env: - items: - description: EnvVar represents an environment variable - present in a Container. + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: - supports metadata.name, metadata.namespace, - `metadata.labels['''']`, `metadata.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source - of a set of ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + containerName: + description: 'Container name: required for + volumes, optional for env vars' type: string - optional: - description: Specify whether the ConfigMap - must be defined - type: boolean + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource type: object x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string name: default: "" description: |- @@ -2096,256 +2581,158 @@ spec: type: string optional: description: Specify whether the Secret - must be defined + or its key must be defined type: boolean + required: + - key type: object x-kubernetes-map-type: atomic type: object - type: array - grpc: - description: Enable gRPC registry server. Defaults - to true if unset. - type: boolean - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible - metrics for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object - type: object - restAPI: - description: Enable REST API registry server. - type: boolean - tls: - description: TlsConfigs configures server TLS for - a feast service. - properties: - disable: - description: will disable TLS for the feast service. - useful in an openshift cluster, for example, - where TLS is configured by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret - key names for the TLS key and cert. - properties: - tlsCrt: - description: defaults to "tls.crt" - type: string - tlsKey: - description: defaults to "tls.key" - type: string - type: object - secretRef: - description: references the local k8s secret where - the TLS key and cert reside - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - type: object - x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes - that should be mounted into the feast container. - items: - description: VolumeMount describes a mounting of - a Volume within a container. + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: + name: + default: "" description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: name: - description: This must match the Name of a Volume. - type: string - readOnly: + default: "" description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: - description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - subPath: + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. type: string - subPathExpr: - description: Expanded path within the volume - from which the container's volume should be - mounted. + request: + description: Request is the name chosen for + a request in the referenced claim. type: string required: - - mountPath - name type: object type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number - of seconds after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores - (2 * CPU + 1). - format: int32 - minimum: -1 - type: integer + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. type: object type: object - x-kubernetes-validations: - - message: At least one of restAPI or grpc must be true - rule: self.restAPI == true || self.grpc == true || !has(self.grpc) - type: object - remote: - description: RemoteRegistryConfig points to a remote feast - registry server. - properties: - feastRef: - description: Reference to an existing `FeatureStore` CR - in the same k8s cluster. - properties: - name: - description: Name of the FeatureStore - type: string - namespace: - description: Namespace of the FeatureStore - type: string - required: - - name - type: object - hostname: - description: Host address of the remote registry service - - :, e.g. `registry..svc.cluster.local:80` - type: string tls: - description: TlsRemoteRegistryConfigs configures client - TLS for a remote feast registry. + description: TlsConfigs configures server TLS for a feast + service. properties: - certName: - description: defines the configmap key name for the - client TLS cert. - type: string - configMapRef: - description: references the local k8s configmap where - the TLS cert resides + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside properties: name: default: "" @@ -2356,2085 +2743,3124 @@ spec: type: string type: object x-kubernetes-map-type: atomic - required: - - certName - - configMapRef type: object - type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, - c)' - type: object - x-kubernetes-validations: - - message: One selection required. - rule: '[has(self.local), has(self.remote)].exists_one(c, c)' - scaling: - description: Scaling configures horizontal scaling for the FeatureStore - deployment (e.g. HPA autoscaling). - properties: - autoscaling: - description: |- - Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. - Mutually exclusive with spec.replicas. - properties: - behavior: - description: Behavior configures the scaling behavior - of the target. - properties: - scaleDown: - description: scaleDown is scaling policy for scaling - Down. - properties: - policies: - description: policies is a list of potential scaling - polices which can be used during scaling. - items: - description: HPAScalingPolicy is a single policy - which must hold true for a specified past - interval. - properties: - periodSeconds: - description: periodSeconds specifies the - window of time for which the policy should - hold true. - format: int32 - type: integer - type: - description: type is used to specify the - scaling policy. - type: string - value: - description: |- - value contains the amount of change which is permitted by the policy. - It must be greater than zero - format: int32 - type: integer - required: - - periodSeconds - - type - - value - type: object - type: array - x-kubernetes-list-type: atomic - selectPolicy: - description: |- - selectPolicy is used to specify which policy should be used. - If not set, the default value Max is used. - type: string - stabilizationWindowSeconds: - description: |- - stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up - format: int32 - type: integer + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + serving: + description: Serving configures the Feast feature_server section + written into feature_store.yaml for the online serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. Defaults + to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults to + "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or disables + individual Feast metric categories. type: object - scaleUp: - description: scaleUp is scaling policy for scaling - Up. + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to the + offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to the + offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline write + batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied to the + Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true + type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One selection + is required. Local is the default setting. + properties: + local: + description: LocalRegistryConfig configures the registry service + properties: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures the + file-based persistence for the registry service properties: - policies: - description: policies is a list of potential scaling - polices which can be used during scaling. - items: - description: HPAScalingPolicy is a single policy - which must hold true for a specified past - interval. - properties: - periodSeconds: - description: periodSeconds specifies the - window of time for which the policy should - hold true. - format: int32 - type: integer - type: - description: type is used to specify the - scaling policy. - type: string - value: - description: |- - value contains the amount of change which is permitted by the policy. - It must be greater than zero - format: int32 - type: integer - required: - - periodSeconds - - type - - value - type: object - type: array - x-kubernetes-list-type: atomic - selectPolicy: + cache_mode: description: |- - selectPolicy is used to specify which policy should be used. - If not set, the default value Max is used. + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread type: string - stabilizationWindowSeconds: - description: |- - stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. format: int32 + minimum: 0 type: integer - type: object - type: object - maxReplicas: - description: MaxReplicas is the upper limit for the number - of replicas. Required. - format: int32 - minimum: 1 - type: integer - metrics: - description: Metrics contains the specifications for which - to use to calculate the desired replica count. - items: - description: |- - MetricSpec specifies how to scale based on a single metric - (only `type` and one other matching field should be set at on - properties: - containerResource: - description: |- - containerResource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes descr - properties: - container: - description: container is the name of the container - in the pods of the scaling target - type: string - name: - description: name is the name of the resource - in question. - type: string - target: - description: target specifies the target value - for the given metric - properties: - averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the - metric type is Utilization, Value, or - AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of - the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - container - - name - - target - type: object - external: - description: |- - external refers to a global metric that is not associated - with any Kubernetes object. + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object store + URIs. + rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') + || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 or + GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available only + for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. properties: - metric: - description: metric identifies the target metric - by name and selector + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: name is the name of the given - metric - type: string - selector: - description: "selector is the string-encoded - form of a standard kubernetes label selector - for the given metric\nWhen set, it is - passed " + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The - requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label - key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of - {key,value} pairs. - type: object + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object x-kubernetes-map-type: atomic - required: - - name - type: object - target: - description: target specifies the target value - for the given metric - properties: - averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the - metric type is Utilization, Value, or - AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of - the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type type: object required: - - metric - - target + - name type: object - object: - description: |- - object refers to a metric describing a single kubernetes object - (for example, hits-per-second on an Ingress object). + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets properties: - describedObject: - description: describedObject specifies the descriptions - of a object,such as kind,name apiVersion + configMapRef: + description: The ConfigMap to select from properties: - apiVersion: - description: apiVersion is the API version - of the referent - type: string - kind: - description: 'kind is the kind of the referent; - More info: https://git.k8s.' + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: name: - description: 'name is the name of the referent; - More info: https://kubernetes.' + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. type: string - required: - - kind - - name + optional: + description: Specify whether the Secret + must be defined + type: boolean type: object - metric: - description: metric identifies the target metric - by name and selector + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. properties: name: - description: name is the name of the given - metric + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. type: string - selector: - description: "selector is the string-encoded - form of a standard kubernetes label selector - for the given metric\nWhen set, it is - passed " - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The - requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label - key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of - {key,value} pairs. - type: object - type: object - x-kubernetes-map-type: atomic required: - name type: object - target: - description: target specifies the target value - for the given metric - properties: - averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the - metric type is Utilization, Value, or - AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of - the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - describedObject - - metric - - target - type: object - pods: - description: |- - pods refers to a metric describing each pod in the current scale target - (for example, transactions-processed-per-second) - properties: - metric: - description: metric identifies the target metric - by name and selector - properties: - name: - description: name is the name of the given - metric - type: string - selector: - description: "selector is the string-encoded - form of a standard kubernetes label selector - for the given metric\nWhen set, it is - passed " - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The - requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label - key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of - {key,value} pairs. - type: object - type: object - x-kubernetes-map-type: atomic - required: - - name - type: object - target: - description: target specifies the target value - for the given metric - properties: - averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the - metric type is Utilization, Value, or - AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of - the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - metric - - target - type: object - resource: - description: |- - resource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes describing eac + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string name: - description: name is the name of the resource - in question. + description: This must match the Name of a Volume. type: string - target: - description: target specifies the target value - for the given metric - properties: - averageUtilization: - description: "averageUtilization is the - target value of the average of the\nresource - metric across all relevant pods, represented - as a " - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the - metric type is Utilization, Value, or - AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of - the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - name - - target - type: object - type: - description: type is the type of metric source. - type: string - required: - - type - type: object - type: array - minReplicas: - description: MinReplicas is the lower limit for the number - of replicas. Defaults to 1. - format: int32 - minimum: 1 - type: integer - required: - - maxReplicas - type: object - type: object - securityContext: - description: PodSecurityContext holds pod-level security attributes - and common container settings. - properties: - appArmorProfile: - description: appArmorProfile is the AppArmor options to use - by the containers in this pod. - properties: - localhostProfile: - description: localhostProfile indicates a profile loaded - on the node that should be used. - type: string - type: - description: type indicates which kind of AppArmor profile - will be applied. - type: string - required: - - type - type: object - fsGroup: - description: A special supplemental group that applies to - all containers in a pod. - format: int64 - type: integer - fsGroupChangePolicy: - description: |- - fsGroupChangePolicy defines behavior of changing ownership and permission of the volume - before being exposed inside Pod. - type: string - runAsGroup: - description: |- - The GID to run the entrypoint of the container process. - Uses runtime default if unset. - format: int64 - type: integer - runAsNonRoot: - description: Indicates that the container must run as a non-root - user. - type: boolean - runAsUser: - description: |- - The UID to run the entrypoint of the container process. - Defaults to user specified in image metadata if unspecified. - format: int64 - type: integer - seLinuxOptions: - description: The SELinux context to be applied to all containers. - properties: - level: - description: Level is SELinux level label that applies - to the container. - type: string - role: - description: Role is a SELinux role label that applies - to the container. - type: string - type: - description: Type is a SELinux type label that applies - to the container. - type: string - user: - description: User is a SELinux user label that applies - to the container. - type: string - type: object - seccompProfile: - description: |- - The seccomp options to use by the containers in this pod. - Note that this field cannot be set when spec.os. - properties: - localhostProfile: - description: localhostProfile indicates a profile defined - in a file on the node should be used. - type: string - type: - description: type indicates which kind of seccomp profile - will be applied. - type: string - required: - - type - type: object - supplementalGroups: - description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG - items: - format: int64 - type: integer - type: array - x-kubernetes-list-type: atomic - sysctls: - description: Sysctls hold a list of namespaced sysctls used - for the pod. - items: - description: Sysctl defines a kernel parameter to be set - properties: - name: - description: Name of a property to set - type: string - value: - description: Value of a property to set - type: string - required: - - name - - value - type: object - type: array - x-kubernetes-list-type: atomic - windowsOptions: - description: The Windows specific settings applied to all - containers. - properties: - gmsaCredentialSpec: - description: |- - GMSACredentialSpec is where the GMSA admission webhook - (https://github. - type: string - gmsaCredentialSpecName: - description: GMSACredentialSpecName is the name of the - GMSA credential spec to use. - type: string - hostProcess: - description: HostProcess determines if a container should - be run as a 'Host Process' container. - type: boolean - runAsUserName: - description: The UserName in Windows to run the entrypoint - of the container process. - type: string - type: object - type: object - ui: - description: Creates a UI server container - properties: - env: - items: - description: EnvVar represents an environment variable present - in a Container. - properties: - name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's value. - Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" + readOnly: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap or - its key must be defined + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports - metadata.name, metadata.namespace, `metadata.labels['''']`, - `metadata.' - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in - the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of - the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's - namespace - properties: - key: - description: The key of the secret to select - from. Must be a valid secret key. + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. type: string - name: - default: "" + subPath: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. type: string - optional: - description: Specify whether the Secret or its - key must be defined - type: boolean required: - - key + - mountPath + - name type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source of a set - of ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret must be - defined - type: boolean - type: object - x-kubernetes-map-type: atomic - type: object - type: array - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when to - pull a container image - type: string - logLevel: - description: |- - LogLevel sets the logging level for the server - Allowed values: "debug", "info", "warning", "error", "critical". - enum: - - debug - - info - - warning - - error - - critical - type: string - metrics: - description: Metrics exposes Prometheus-compatible metrics - for the Feast server when enabled. - type: boolean - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute resource - requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount of - compute resources required. + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be true + rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' type: object - tls: - description: TlsConfigs configures server TLS for a feast - service. + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. properties: - disable: - description: will disable TLS for the feast service. useful - in an openshift cluster, for example, where TLS is configured - by default - type: boolean - secretKeyNames: - description: SecretKeyNames defines the secret key names - for the TLS key and cert. + feastRef: + description: Reference to an existing `FeatureStore` CR + in the same k8s cluster. properties: - tlsCrt: - description: defaults to "tls.crt" + name: + description: Name of the FeatureStore type: string - tlsKey: - description: defaults to "tls.key" + namespace: + description: Namespace of the FeatureStore type: string + required: + - name type: object - secretRef: - description: references the local k8s secret where the - TLS key and cert reside + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + certName: + description: defines the configmap key name for the + client TLS cert. type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef type: object - x-kubernetes-map-type: atomic type: object x-kubernetes-validations: - - message: '`secretRef` required if `disable` is false.' - rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) - : true' - volumeMounts: - description: VolumeMounts defines the list of volumes that - should be mounted into the feast container. - items: - description: VolumeMount describes a mounting of a Volume - within a container. - properties: - mountPath: - description: |- - Path within the container at which the volume should be mounted. Must - not contain ':'. - type: string - mountPropagation: - description: |- - mountPropagation determines how mounts are propagated from the host - to container and the other way around. - type: string - name: - description: This must match the Name of a Volume. - type: string - readOnly: - description: |- - Mounted read-only if true, read-write otherwise (false or unspecified). - Defaults to false. - type: boolean - recursiveReadOnly: - description: |- - RecursiveReadOnly specifies whether read-only mounts should be handled - recursively. - type: string - subPath: - description: |- - Path within the volume from which the container's volume should be mounted. - Defaults to "" (volume's root). - type: string - subPathExpr: - description: Expanded path within the volume from which - the container's volume should be mounted. - type: string - required: - - mountPath - - name - type: object - type: array - workerConfigs: - description: WorkerConfigs defines the worker configuration - for the Feast server. - properties: - keepAliveTimeout: - description: |- - KeepAliveTimeout is the timeout for keep-alive connections in seconds. - Defaults to 30. - format: int32 - minimum: 1 - type: integer - maxRequests: - description: |- - MaxRequests is the maximum number of requests a worker will process before restarting. - This helps prevent memory leaks. - format: int32 - minimum: 0 - type: integer - maxRequestsJitter: - description: |- - MaxRequestsJitter is the maximum jitter to add to max-requests to prevent - thundering herd effect on worker restart. - format: int32 - minimum: 0 - type: integer - registryTTLSeconds: - description: RegistryTTLSeconds is the number of seconds - after which the registry is refreshed. - format: int32 - minimum: 0 - type: integer - workerConnections: - description: |- - WorkerConnections is the maximum number of simultaneous clients per worker process. - Defaults to 1000. - format: int32 - minimum: 1 - type: integer - workers: - description: Workers is the number of worker processes. - Use -1 to auto-calculate based on CPU cores (2 * CPU - + 1). - format: int32 - minimum: -1 - type: integer - type: object + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' type: object - volumes: - description: Volumes specifies the volumes to mount in the FeatureStore - deployment. + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. items: - description: Volume represents a named volume in a pod that - may be accessed by any container in the pod. + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... properties: - awsElasticBlockStore: + name: description: |- - awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th - properties: - fsType: - description: fsType is the filesystem type of the volume - that you want to mount. - type: string - partition: - description: |- - partition is the partition in the volume that you want to mount. - If omitted, the default is to mount by volume name. - format: int32 - type: integer - readOnly: - description: |- - readOnly value true will force the readOnly setting in VolumeMounts. - More info: https://kubernetes. - type: boolean - volumeID: - description: |- - volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). - More info: https://kubernetes. - type: string - required: - - volumeID - type: object - azureDisk: - description: azureDisk represents an Azure Data Disk mount - on the host and bind mount to the pod. - properties: - cachingMode: - description: 'cachingMode is the Host Caching mode: - None, Read Only, Read Write.' - type: string - diskName: - description: diskName is the Name of the data disk in - the blob storage - type: string - diskURI: - description: diskURI is the URI of data disk in the - blob storage - type: string - fsType: - description: |- - fsType is Filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - kind: - description: 'kind expected values are Shared: multiple - blob disks per storage account Dedicated: single - blob disk per storage accoun' - type: string - readOnly: - description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - required: - - diskName - - diskURI - type: object - azureFile: - description: azureFile represents an Azure File Service - mount on the host and bind mount to the pod. - properties: - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretName: - description: secretName is the name of secret that - contains Azure Storage Account Name and Key - type: string - shareName: - description: shareName is the azure share Name - type: string - required: - - secretName - - shareName - type: object - cephfs: - description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime - properties: - monitors: - description: |- - monitors is Required: Monitors is a collection of Ceph monitors - More info: https://examples.k8s. - items: - type: string - type: array - x-kubernetes-list-type: atomic - path: - description: 'path is Optional: Used as the mounted - root, rather than the full Ceph tree, default is /' - type: string - readOnly: - description: |- - readOnly is Optional: Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretFile: - description: 'secretFile is Optional: SecretFile is - the path to key ring for User, default is /etc/ceph/user.' - type: string - secretRef: - description: 'secretRef is Optional: SecretRef is reference - to the authentication secret for User, default is - empty.' - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is optional: User is the rados user name, default is admin - More info: https://examples.k8s. - type: string - required: - - monitors - type: object - cinder: + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: description: |- - cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef is optional: points to a secret object containing parameters used to connect - to OpenStack. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - volumeID: - description: |- - volumeID used to identify the volume in cinder. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md - type: string - required: - - volumeID - type: object - configMap: - description: configMap represents a configMap that should - populate this volume - properties: - defaultMode: - description: 'defaultMode is optional: mode bits used - to set permissions on created files by default.' - format: int32 - type: integer - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum - items: - description: Maps a string key to a path within a - volume. - properties: - key: - description: key is the key to project. + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the FeatureStore + deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. type: string - mode: - description: 'mode is Optional: mode bits used - to set permissions on this file.' + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... format: int32 type: integer - path: + tolerance: + anyOf: + - type: integer + - type: string description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - type: string - required: - - key - - path + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true type: object - type: array - x-kubernetes-list-type: atomic - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: optional specify whether the ConfigMap - or its keys must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - csi: - description: csi (Container Storage Interface) represents - ephemeral storage that is handled by certain external - CSI drivers (Beta fea - properties: - driver: - description: driver is the name of the CSI driver that - handles this volume. - type: string - fsType: - description: fsType to mount. Ex. "ext4", "xfs", "ntfs". - type: string - nodePublishSecretRef: - description: |- - nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - readOnly: - description: |- - readOnly specifies a read-only configuration for the volume. - Defaults to false (read/write). - type: boolean - volumeAttributes: - additionalProperties: - type: string - description: |- - volumeAttributes stores driver-specific properties that are passed to the CSI - driver. - type: object - required: - - driver - type: object - downwardAPI: - description: downwardAPI represents downward API about the - pod that should populate this volume - properties: - defaultMode: - description: 'Optional: mode bits to use on created - files by default.' - format: int32 - type: integer - items: - description: Items is a list of downward API volume - file - items: - description: DownwardAPIVolumeFile represents information - to create the file containing the pod field + scaleUp: + description: scaleUp is scaling policy for scaling + Up. properties: - fieldRef: - description: 'Required: Selects a field of the - pod: only annotations, labels, name, namespace - and uid are supported.' - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in - the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - mode: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: description: |- - Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... format: int32 type: integer - path: - description: 'Required: Path is the relative - path name of the file to be created. Must not - be absolute or contain the ''..'' path.' - type: string - resourceFieldRef: + tolerance: + anyOf: + - type: integer + - type: string description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, requests. - properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of - the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - required: - - path + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true type: object - type: array - x-kubernetes-list-type: atomic - type: object - emptyDir: - description: |- - emptyDir represents a temporary directory that shares a pod's lifetime. - More info: https://kubernetes. - properties: - medium: - description: medium represents what type of storage - medium should back this directory. - type: string - sizeLimit: - anyOf: - - type: integer - - type: string - description: sizeLimit is the total amount of local - storage required for this EmptyDir volume. - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - ephemeral: - description: ephemeral represents a volume that is handled - by a cluster storage driver. - properties: - volumeClaimTemplate: - description: Will be used to create a stand-alone PVC - to provision the volume. + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the number + of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for which + to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... properties: - metadata: + containerResource: description: |- - May contain labels and annotations that will be copied into the PVC - when creating it. - type: object - spec: - description: The specification for the PersistentVolumeClaim. + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... properties: - accessModes: - description: |- - accessModes contains the desired access modes the volume should have. - More info: https://kubernetes. - items: - type: string - type: array - x-kubernetes-list-type: atomic - dataSource: - description: |- - dataSource field can be used to specify either: - * An existing VolumeSnapshot object (snapshot.storage.k8s. + container: + description: container is the name of the container + in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric properties: - apiGroup: - description: APIGroup is the group for the - resource being referenced. - type: string - kind: - description: Kind is the type of resource - being referenced + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: name: - description: Name is the name of resource - being referenced + description: name is the name of the given + metric type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic required: - - kind - name type: object - x-kubernetes-map-type: atomic - dataSourceRef: - description: |- - dataSourceRef specifies the object from which to populate the volume with data, if a non-empty - volume is desired. + target: + description: target specifies the target value + for the given metric properties: - apiGroup: - description: APIGroup is the group for the - resource being referenced. + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the descriptions + of a object,such as kind,name apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent type: string kind: - description: Kind is the type of resource - being referenced + description: 'kind is the kind of the referent; + More info: https://git.k8s.' type: string name: - description: Name is the name of resource - being referenced - type: string - namespace: - description: |- - Namespace is the namespace of resource being referenced - Note that when a namespace is specified, a gateway.networking. + description: 'name is the name of the referent; + More info: https://kubernetes.' type: string required: - kind - name type: object - resources: - description: resources represents the minimum - resources the volume should have. + metric: + description: metric identifies the target metric + by name and selector properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true + name: + description: name is the name of the given + metric + type: string + selector: description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum - amount of compute resources required. - type: object - type: object - selector: - description: selector is a label query over - volumes to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. + description: matchLabels is a map of + {key,value} pairs. + type: object type: object + x-kubernetes-map-type: atomic + required: + - name type: object - x-kubernetes-map-type: atomic - storageClassName: - description: |- - storageClassName is the name of the StorageClass required by the claim. - More info: https://kubernetes. - type: string - volumeAttributesClassName: - description: volumeAttributesClassName may be - used to set the VolumeAttributesClass used - by this claim. - type: string - volumeMode: - description: volumeMode defines what type of - volume is required by the claim. - type: string - volumeName: - description: volumeName is the binding reference - to the PersistentVolume backing this claim. - type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target type: object - required: - - spec - type: object - type: object - fc: - description: fc represents a Fibre Channel resource that - is attached to a kubelet's host machine and then exposed - to the pod. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - lun: - description: 'lun is Optional: FC target lun number' - format: int32 - type: integer - readOnly: - description: |- - readOnly is Optional: Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - targetWWNs: - description: 'targetWWNs is Optional: FC target worldwide - names (WWNs)' - items: - type: string - type: array - x-kubernetes-list-type: atomic - wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - flexVolume: - description: |- - flexVolume represents a generic volume resource that is - provisioned/attached using an exec based plugin. - properties: - driver: - description: driver is the name of the driver to use - for this volume. - type: string - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - options: - additionalProperties: - type: string - description: 'options is Optional: this field holds - extra command options if any.' - type: object - readOnly: - description: |- - readOnly is Optional: defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi - properties: - name: - default: "" + pods: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - driver - type: object - flocker: - description: flocker represents a Flocker volume attached - to a kubelet's host machine. - properties: - datasetName: - description: |- - datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca - type: string - datasetUUID: - description: datasetUUID is the UUID of the dataset. - This is unique identifier of a Flocker dataset - type: string - type: object - gcePersistentDisk: - description: |- - gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po - properties: - fsType: - description: fsType is filesystem type of the volume - that you want to mount. - type: string - partition: - description: |- - partition is the partition in the volume that you want to mount. - If omitted, the default is to mount by volume name. - format: int32 - type: integer - pdName: - description: |- - pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. - More info: https://kubernetes. - type: string - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - More info: https://kubernetes. - type: boolean - required: - - pdName - type: object - gitRepo: - description: |- - gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. - properties: - directory: - description: |- - directory is the target directory name. - Must not contain or start with '..'. If '. - type: string - repository: - description: repository is the URL - type: string - revision: - description: revision is the commit hash for the specified - revision. - type: string - required: - - repository - type: object - glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. - properties: - endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. - type: string - path: - description: |- - path is the Glusterfs volume path. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod - type: string - readOnly: - description: |- - readOnly here will force the Glusterfs volume to be mounted with read-only permissions. - Defaults to false. - type: boolean - required: - - endpoints - - path - type: object - hostPath: - description: |- - hostPath represents a pre-existing file or directory on the host - machine that is directly exposed to the container. - properties: - path: - description: |- - path of the directory on the host. - If the path is a symlink, it will follow the link to the real path. - type: string - type: - description: |- - type for HostPath Volume - Defaults to "" - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - type: string - required: - - path - type: object - iscsi: - description: |- - iscsi represents an ISCSI Disk resource that is attached to a - kubelet's host machine and then exposed to the pod. - properties: - chapAuthDiscovery: - description: chapAuthDiscovery defines whether support - iSCSI Discovery CHAP authentication - type: boolean - chapAuthSession: - description: chapAuthSession defines whether support - iSCSI Session CHAP authentication - type: boolean - fsType: - description: fsType is the filesystem type of the volume - that you want to mount. - type: string - initiatorName: - description: initiatorName is the custom iSCSI Initiator - Name. - type: string - iqn: - description: iqn is the target iSCSI Qualified Name. - type: string - iscsiInterface: - description: |- - iscsiInterface is the interface Name that uses an iSCSI transport. - Defaults to 'default' (tcp). - type: string - lun: - description: lun represents iSCSI Target Lun number. - format: int32 - type: integer - portals: - description: portals is the iSCSI Target Portal List. - items: - type: string - type: array - x-kubernetes-list-type: atomic - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - type: boolean - secretRef: - description: secretRef is the CHAP Secret for iSCSI - target and initiator authentication - properties: - name: - default: "" + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. type: string + required: + - type type: object - x-kubernetes-map-type: atomic - targetPortal: - description: targetPortal is iSCSI Target Portal. - type: string - required: - - iqn - - lun - - targetPortal - type: object - name: - description: |- - name of the volume. - Must be a DNS_LABEL and unique within the pod. - More info: https://kubernetes. - type: string - nfs: - description: |- - nfs represents an NFS mount on the host that shares a pod's lifetime - More info: https://kubernetes. + type: array + minReplicas: + description: MinReplicas is the lower limit for the number + of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object + type: object + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to use + by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile loaded + on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor profile + will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies to + all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as a non-root + user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string + seLinuxOptions: + description: The SELinux context to be applied to all containers. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile defined + in a file on the node should be used. + type: string + type: + description: type indicates which kind of seccomp profile + will be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string + sysctls: + description: Sysctls hold a list of namespaced sysctls used + for the pod. + items: + description: Sysctl defines a kernel parameter to be set properties: - path: - description: |- - path that is exported by the NFS server. - More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs - type: string - readOnly: - description: |- - readOnly here will force the NFS export to be mounted with read-only permissions. - Defaults to false. - type: boolean - server: - description: |- - server is the hostname or IP address of the NFS server. - More info: https://kubernetes. + name: + description: Name of a property to set type: string - required: - - path - - server - type: object - persistentVolumeClaim: - description: |- - persistentVolumeClaimVolumeSource represents a reference to a - PersistentVolumeClaim in the same namespace. - properties: - claimName: - description: claimName is the name of a PersistentVolumeClaim - in the same namespace as the pod using this volume. + value: + description: Value of a property to set type: string - readOnly: - description: |- - readOnly Will force the ReadOnly setting in VolumeMounts. - Default false. - type: boolean required: - - claimName + - name + - value type: object - photonPersistentDisk: - description: photonPersistentDisk represents a PhotonController - persistent disk attached and mounted on kubelets host - machine + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: The Windows specific settings applied to all + containers. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container should + be run as a 'Host Process' container. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. + type: string + type: object + type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are spread + across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching pods. properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - pdID: - description: pdID is the ID that identifies Photon Controller - persistent disk - type: string - required: - - pdID + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object type: object - portworxVolume: - description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of eligible + domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + ui: + description: Creates a UI server container + properties: + env: + items: + description: EnvVar represents an environment variable present + in a Container. properties: - fsType: + name: description: |- - fSType represents the filesystem type to mount - Must be a filesystem type supported by the host operating system. - Ex. + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string - readOnly: + value: description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - volumeID: - description: volumeID uniquely identifies a Portworx - volume + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... type: string - required: - - volumeID - type: object - projected: - description: projected items for all in one resources secrets, - configmaps, and downward API + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets properties: - defaultMode: - description: defaultMode are the mode bits used to set - permissions on created files by default. - format: int32 - type: integer - sources: - description: sources is the list of volume projections - items: - description: Projection that may be projected along - with other supported volume types - properties: - clusterTrustBundle: - description: ClusterTrustBundle allows a pod to - access the `.spec. - properties: - labelSelector: - description: |- - Select all ClusterTrustBundles that match this label selector. Only has - effect if signerName is set. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The - requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. - type: object - type: object - x-kubernetes-map-type: atomic - name: - description: |- - Select a single ClusterTrustBundle by object name. Mutually-exclusive - with signerName and labelSelector. - type: string - optional: - description: |- - If true, don't block pod startup if the referenced ClusterTrustBundle(s) - aren't available. - type: boolean - path: - description: Relative path from the volume - root to write the bundle. - type: string - signerName: - description: |- - Select all ClusterTrustBundles that match this signer name. - Mutually-exclusive with name. - type: string - required: - - path - type: object - configMap: - description: configMap information about the configMap - data to project - properties: - items: - description: |- - items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum - items: - description: Maps a string key to a path - within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode - bits used to set permissions on this - file.' - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - type: string - required: - - key - - path - type: object - type: array - x-kubernetes-list-type: atomic - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: optional specify whether the - ConfigMap or its keys must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - downwardAPI: - description: downwardAPI information about the - downwardAPI data to project - properties: - items: - description: Items is a list of DownwardAPIVolume - file - items: - description: DownwardAPIVolumeFile represents - information to create the file containing - the pod field - properties: - fieldRef: - description: 'Required: Selects a field - of the pod: only annotations, labels, - name, namespace and uid are supported.' - properties: - apiVersion: - description: Version of the schema + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. useful + in an openshift cluster, for example, where TLS is configured + by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key names + for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where the + TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes that + should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from which + the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * CPU + + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + volumes: + description: Volumes specifies the volumes to mount in the FeatureStore + deployment. + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to... + properties: + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes. + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes. + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk mount + on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in + the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the + blob storage + type: string + fsType: + default: ext4 + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage...' + type: string + readOnly: + default: false + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the host + that shares a pod's lifetime. + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s. + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile is + the path to key ring for User, default is /etc/ceph/user.' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is reference + to the authentication secret for User, default is + empty.' + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s. + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + Deprecated: Cinder is deprecated. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers. + properties: + driver: + description: driver is the name of the CSI driver that + handles this volume. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", "ntfs". + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the + pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the + pod: only annotations, labels, name, namespace + and uid are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must not + be absolute or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes. + properties: + medium: + description: medium represents what type of storage + medium should back this directory. + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: sizeLimit is the total amount of local + storage required for this EmptyDir volume. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: ephemeral represents a volume that is handled + by a cluster storage driver. + properties: + volumeClaimTemplate: + description: Will be used to create a stand-alone PVC + to provision the volume. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. + type: object + spec: + description: The specification for the PersistentVolumeClaim. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s. + properties: + apiGroup: + description: APIGroup is the group for the + resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. + properties: + apiGroup: + description: APIGroup is the group for the + resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking. + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum + resources the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may be + used to set the VolumeAttributesClass used + by this claim. + type: string + volumeMode: + description: volumeMode defines what type of + volume is required by the claim. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that + is attached to a kubelet's host machine and then exposed + to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use + for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as... + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the... + properties: + fsType: + description: fsType is filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes. + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes. + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + Deprecated: GitRepo is deprecated. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. + properties: + endpoints: + description: endpoints is the endpoint name that details + Glusterfs topology. + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + default: default + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal List. + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: targetPortal is iSCSI Target Portal. + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes. + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes. + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes. + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + properties: + claimName: + description: claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used to set + permissions on created files by default. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: ClusterTrustBundle allows a pod to + access the `.spec. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode + bits used to set permissions on this + file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name, namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath is written in terms of, defaults to "v1". type: string @@ -4449,7 +5875,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -4491,423 +5917,1300 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project properties: items: description: |- - items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a - items: - description: Maps a string key to a path - within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode - bits used to set permissions on this - file.' - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - type: string - required: - - key - - path - type: object - type: array - x-kubernetes-list-type: atomic - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: optional field specify whether - the Secret or its key must be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - serviceAccountToken: - description: serviceAccountToken is information - about the serviceAccountToken data to project - properties: - audience: - description: audience is the intended audience - of the token. - type: string - expirationSeconds: - description: |- - expirationSeconds is the requested duration of validity of the service - account token. - format: int64 - type: integer - path: - description: |- - path is the path relative to the mount point of the file to project the - token into. - type: string - required: - - path - type: object - type: object - type: array - x-kubernetes-list-type: atomic - type: object - quobyte: - description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime - properties: - group: - description: |- - group to map volume access to - Default is no group - type: string - readOnly: - description: |- - readOnly here will force the Quobyte volume to be mounted with read-only permissions. - Defaults to false. - type: boolean - registry: - description: |- - registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent - type: string - tenant: - description: |- - tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by - type: string - user: - description: |- - user to map volume access to - Defaults to serivceaccount user - type: string - volume: - description: volume is a string that references an already - created Quobyte volume by name. - type: string - required: - - registry - - volume - type: object - rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. - properties: - fsType: - description: fsType is the filesystem type of the volume - that you want to mount. - type: string - image: - description: |- - image is the rados image name. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - keyring: - description: |- - keyring is the path to key ring for RBDUser. - Default is /etc/ceph/keyring. - More info: https://examples.k8s. + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode + bits used to set permissions on this + file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended audience + of the token. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime. + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple... + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set... + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. + properties: + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + default: /etc/ceph/keyring + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s. + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s. + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes. + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes. + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope of + the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline store. + Configure services.offlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure registry.local.persistence.store + or use a remote registry when using replicas > 1 or autoscaling. S3/GCS-backed + registry is also allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + properties: + applied: + description: Shows the currently applied feast configuration, including + any pertinent defaults + properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed + in the same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM + certificate. Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine + configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" + if not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch + engine configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: type: string - monitors: - description: |- - monitors is a collection of Ceph monitors. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - items: - type: string - type: array - x-kubernetes-list-type: atomic - pool: - description: |- - pool is the rados pool name. - Default is rbd. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. + properties: + git: + description: GitCloneOptions describes how a clone should + be performed. + properties: + configs: + additionalProperties: type: string - readOnly: - description: |- - readOnly here will force the ReadOnly setting in VolumeMounts. - Defaults to false. - More info: https://examples.k8s. - type: boolean - secretRef: - description: |- - secretRef is name of the authentication secret for RBDUser. If provided - overrides keyring. - Default is nil. + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object + env: + items: + description: EnvVar represents an environment variable + present in a Container. properties: name: - default: "" description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string - type: object - x-kubernetes-map-type: atomic - user: - description: |- - user is the rados user name. - Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume - attached and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - gateway: - description: gateway is the host address of the ScaleIO - API Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO - Protection Domain for the configured storage. - type: string - readOnly: - description: |- - readOnly Defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef references to the secret for ScaleIO user and other - sensitive information. - properties: - name: - default: "" + value: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name type: object - x-kubernetes-map-type: atomic - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: storageMode indicates whether the storage - for a volume should be ThickProvisioned or ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool - associated with the protection domain. - type: string - system: - description: system is the name of the storage system - as configured in ScaleIO. - type: string - volumeName: - description: |- - volumeName is the name of a volume already created in the ScaleIO system - that is associated with this volume source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: |- - secret represents a secret that should populate this volume. - More info: https://kubernetes. - properties: - defaultMode: - description: 'defaultMode is Optional: mode bits used - to set permissions on created files by default.' - format: int32 - type: integer + type: array + envFrom: items: - description: |- - items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a - items: - description: Maps a string key to a path within a - volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used - to set permissions on this file.' - format: int32 - type: integer - path: - description: |- - path is the relative path of the file to map the key to. - May not be an absolute path. - type: string - required: - - key - - path - type: object - type: array - x-kubernetes-list-type: atomic - optional: - description: optional field specify whether the Secret - or its keys must be defined - type: boolean - secretName: - description: |- - secretName is the name of the secret in the pod's namespace to use. - More info: https://kubernetes. - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: |- - fsType is the filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - readOnly: - description: |- - readOnly defaults to false (read/write). ReadOnly here will force - the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: |- - secretRef specifies the secret to use for obtaining the StorageOS API - credentials. + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic type: object - x-kubernetes-map-type: atomic - volumeName: - description: |- - volumeName is the human-readable name of the StorageOS volume. Volume - names are only unique within a namespace. - type: string - volumeNamespace: - description: volumeNamespace specifies the scope of - the volume within StorageOS. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based - Management (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy - Based Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies - vSphere volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object - type: array - type: object - required: - - feastProject - - replicas - type: object - x-kubernetes-validations: - - message: replicas > 1 and services.scaling.autoscaling are mutually - exclusive. - rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) - || !has(self.services.scaling.autoscaling) - - message: Scaling requires DB-backed persistence for the online store. - Configure services.onlineStore.persistence.store when using replicas - > 1 or autoscaling. - rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) - || !has(self.services.scaling.autoscaling)) || (has(self.services) - && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) - && has(self.services.onlineStore.persistence.store)) - - message: Scaling requires DB-backed persistence for the offline store. - Configure services.offlineStore.persistence.store when using replicas - > 1 or autoscaling. - rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) - || !has(self.services.scaling.autoscaling)) || (!has(self.services) - || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) - && has(self.services.offlineStore.persistence.store))) - - message: Scaling requires DB-backed or remote registry. Configure registry.local.persistence.store - or use a remote registry when using replicas > 1 or autoscaling. S3/GCS-backed - registry is also allowed. - rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) - || !has(self.services.scaling.autoscaling)) || (has(self.services) - && has(self.services.registry) && (has(self.services.registry.remote) - || (has(self.services.registry.local) && has(self.services.registry.local.persistence) - && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) - && has(self.services.registry.local.persistence.file.path) && (self.services.registry.local.persistence.file.path.startsWith('s3://') - || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) - status: - description: FeatureStoreStatus defines the observed state of FeatureStore - properties: - applied: - description: Shows the currently applied feast configuration, including - any pertinent defaults - properties: - authz: - description: AuthzConfig defines the authorization settings for - the deployed Feast services. + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast + init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. properties: - kubernetes: + extraConfig: + additionalProperties: + type: string description: |- - KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. - https://kubernetes. + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. properties: - roles: - description: The Kubernetes RBAC roles to be deployed - in the same namespace of the FeatureStore. - items: - type: string - type: array + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string type: object - oidc: + x-kubernetes-map-type: atomic + consumer: description: |- - OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. - https://auth0. + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... properties: - secretRef: + apiKeySecretRef: description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... properties: name: default: "" @@ -4918,663 +7221,833 @@ spec: type: string type: object x-kubernetes-map-type: atomic - required: - - secretRef - type: object - type: object - x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, - c)' - batchEngine: - description: BatchEngineConfig defines the batch compute engine - configuration. - properties: - configMapKey: - description: Key name in the ConfigMap. Defaults to "config" - if not specified. - type: string - configMapRef: - description: Reference to a ConfigMap containing the batch - engine configuration. - properties: - name: - default: "" + connectionStringSecretRef: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql type: string + required: + - enabled type: object - x-kubernetes-map-type: atomic - type: object - cronJob: - description: FeastCronJob defines a CronJob to execute against - a Feature Store deployment. - properties: - annotations: + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: additionalProperties: type: string - description: Annotations to be added to the CronJob metadata. + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. type: object - concurrencyPolicy: - description: Specifies how to treat concurrent executions - of a Job. + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". type: string - containerConfigs: - description: CronJobContainerConfigs k8s container settings - for the CronJob + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints + for the FeatureStore deployment. properties: - commands: - description: Array of commands to be executed (in order) - against a Feature Store deployment. - items: - type: string - type: array - env: - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. - type: string - value: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports - metadata.name, metadata.namespace, `metadata.labels['''']`, - `metadata.' + preference: + description: A node selector term, associated + with the corresponding weight. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic - resourceFieldRef: + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in the + range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. + A null or empty node selector term matches no objects. The requirements of + them are ANDed. properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. properties: - key: - description: The key of the secret to select - from. Must be a valid secret key. - type: string - name: - default: "" + labelSelector: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean required: - - key + - topologyKey type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source of - a set of ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from - properties: - name: - default: "" + weight: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight type: object - x-kubernetes-map-type: atomic - type: object - type: array - image: - type: string - imagePullPolicy: - description: PullPolicy describes a policy for if/when - to pull a container image - type: string - nodeSelector: - additionalProperties: - type: string - type: object - resources: - description: ResourceRequirements describes the compute - resource requirements. - properties: - claims: + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... properties: - name: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string required: - - name + - topologyKey type: object type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes. - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: Requests describes the minimum amount - of compute resources required. - type: object + x-kubernetes-list-type: atomic type: object - type: object - failedJobsHistoryLimit: - description: The number of failed finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - jobSpec: - description: Specification of the desired behavior of a job. - properties: - activeDeadlineSeconds: - description: |- - Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr - format: int64 - type: integer - backoffLimit: - description: Specifies the number of retries before marking - this job failed. - format: int32 - type: integer - backoffLimitPerIndex: - description: |- - Specifies the limit for the number of retries within an - index before marking this index as failed. - format: int32 - type: integer - completionMode: - description: |- - completionMode specifies how Pod completions are tracked. It can be - `NonIndexed` (default) or `Indexed`. - type: string - completions: - description: |- - Specifies the desired number of successfully finished pods the - job should be run with. - format: int32 - type: integer - maxFailedIndexes: - description: |- - Specifies the maximal number of failed indexes before marking the Job as - failed, when backoffLimitPerIndex is set. - format: int32 - type: integer - parallelism: - description: |- - Specifies the maximum desired number of pods the job should - run at any given time. - format: int32 - type: integer - podFailurePolicy: - description: Specifies the policy of handling failed pods. + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, + etc. properties: - rules: - description: A list of pod failure policy rules. The - rules are evaluated in order. + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... items: - description: PodFailurePolicyRule describes how - a pod failure is handled when the requirements - are met. + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) properties: - action: - description: Specifies the action taken on a - pod failure when the requirements are satisfied. - type: string - onExitCodes: - description: Represents the requirement on the - container exit codes. + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. properties: - containerName: + labelSelector: description: |- - Restricts the check for exit codes to the container with the - specified name. - type: string - operator: + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: description: |- - Represents the relationship between the container exit code(s) and the - specified values. - type: string - values: - description: Specifies the set of values. + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. items: - format: int32 - type: integer + type: string type: array - x-kubernetes-list-type: set + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string required: - - operator - - values + - topologyKey type: object - onPodConditions: + weight: description: |- - Represents the requirement on the pod conditions. The requirement is represented - as a list of pod condition patterns. - items: - description: |- - PodFailurePolicyOnPodConditionsPattern describes a pattern for matching - an actual pod condition type. - properties: - status: - description: Specifies the required Pod - condition status. - type: string - type: - description: Specifies the required Pod - condition type. - type: string - required: - - status - - type - type: object - type: array - x-kubernetes-list-type: atomic + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer required: - - action + - podAffinityTerm + - weight type: object type: array x-kubernetes-list-type: atomic - required: - - rules - type: object - podReplacementPolicy: - description: podReplacementPolicy specifies when to create - replacement Pods. - type: string - podTemplateAnnotations: - additionalProperties: - type: string - description: |- - PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate - metadata. - type: object - suspend: - description: suspend specifies whether the Job controller - should create Pods or not. - type: boolean - ttlSecondsAfterFinished: - description: |- - ttlSecondsAfterFinished limits the lifetime of a Job that has finished - execution (either Complete or Failed). - format: int32 - type: integer - type: object - schedule: - description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. - type: string - startingDeadlineSeconds: - description: |- - Optional deadline in seconds for starting the job if it misses scheduled - time for any reason. - format: int64 - type: integer - successfulJobsHistoryLimit: - description: The number of successful finished jobs to retain. - Value must be non-negative integer. - format: int32 - type: integer - suspend: - description: |- - This flag tells the controller to suspend subsequent executions, it does - not apply to already started executions. - type: boolean - timeZone: - description: The time zone name for the given schedule, see - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. - type: string - type: object - feastProject: - description: FeastProject is the Feast project id. - pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ - type: string - feastProjectDir: - description: FeastProjectDir defines how to create the feast project - directory. - properties: - git: - description: GitCloneOptions describes how a clone should - be performed. - properties: - configs: - additionalProperties: - type: string - description: |- - Configs passed to git via `-c` - e.g. http.sslVerify: 'false' - OR 'url."https://api:\${TOKEN}@github.com/". - type: object - env: - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. - type: string - value: + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: 'Selects a field of the pod: supports - metadata.name, metadata.namespace, `metadata.labels['''']`, - `metadata.' - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + labelSelector: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits. + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. properties: - containerName: - description: 'Container name: required for - volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the - pod's namespace + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. properties: - key: - description: The key of the secret to select - from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object type: object x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - envFrom: - items: - description: EnvFromSource represents the source of - a set of ConfigMaps - properties: - configMapRef: - description: The ConfigMap to select from - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string - optional: - description: Specify whether the ConfigMap must - be defined - type: boolean - type: object - x-kubernetes-map-type: atomic - prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. - type: string - secretRef: - description: The Secret to select from - properties: - name: - default: "" + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... type: string - optional: - description: Specify whether the Secret must - be defined - type: boolean + required: + - topologyKey type: object - x-kubernetes-map-type: atomic - type: object - type: array - featureRepoPath: - description: FeatureRepoPath is the relative path to the - feature repo subdirectory. Default is 'feature_repo'. - type: string - ref: - description: Reference to a branch / tag / commit - type: string - url: - description: The repository URL to clone from. - type: string - required: - - url - type: object - x-kubernetes-validations: - - message: RepoPath must be a file name only, with no slashes. - rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') - : true' - init: - description: FeastInitOptions defines how to run a `feast - init`. - properties: - minimal: - type: boolean - template: - description: Template for the created project - enum: - - local - - gcp - - aws - - snowflake - - spark - - postgres - - hbase - - cassandra - - hazelcast - - ikv - - couchbase - - clickhouse - type: string + type: array + x-kubernetes-list-type: atomic + type: object type: object - type: object - x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' - replicas: - default: 1 - description: |- - Replicas is the desired number of pod replicas. Used by the scale sub-resource. - Mutually exclusive with services. - format: int32 - minimum: 1 - type: integer - services: - description: FeatureStoreServices defines the desired feast services. - An ephemeral onlineStore feature server is deployed by default. - properties: deploymentStrategy: description: DeploymentStrategy describes how to replace existing pods with new ones. @@ -5608,6 +8081,10 @@ spec: disableInitContainers: description: Disable the 'feast repo initialization' initContainer type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string offlineStore: description: OfflineStore configures the offline store service properties: @@ -5741,6 +8218,7 @@ spec: - couchbase.offline - clickhouse - ray + - oracle type: string required: - secretRef @@ -5760,14 +8238,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -5812,6 +8290,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5870,7 +8378,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5889,8 +8397,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5952,6 +8460,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6115,6 +8627,11 @@ spec: onlineStore: description: OnlineStore configures the online store service properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean persistence: description: OnlineStorePersistence configures the persistence settings for the online store service @@ -6244,7 +8761,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -6259,6 +8775,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -6278,14 +8797,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -6330,6 +8849,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -6388,7 +8937,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6407,8 +8956,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -6470,6 +9019,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6629,7 +9182,108 @@ spec: type: integer type: object type: object + serving: + description: Serving configures the Feast feature_server + section written into feature_store.yaml for the online + serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is + "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or + disables individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to + the offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to + the offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline + write batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied + to the Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' registry: description: Registry configures the registry service. One selection is required. Local is the default setting. @@ -6818,14 +9472,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -6871,6 +9525,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -6930,7 +9614,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6949,9 +9633,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -6992,6 +9675,31 @@ spec: - error - critical type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object metrics: description: Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. @@ -7017,6 +9725,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -7188,6 +9900,9 @@ spec: true rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' type: object remote: description: RemoteRegistryConfig points to a remote feast @@ -7245,6 +9960,42 @@ spec: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean scaling: description: Scaling configures horizontal scaling for the FeatureStore deployment (e.g. HPA autoscaling). @@ -7302,9 +10053,18 @@ spec: stabilizationWindowSeconds: description: |- stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up + considered while scaling... format: int32 type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true type: object scaleUp: description: scaleUp is scaling policy for scaling @@ -7350,9 +10110,18 @@ spec: stabilizationWindowSeconds: description: |- stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up + considered while scaling... format: int32 type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true type: object type: object maxReplicas: @@ -7367,12 +10136,12 @@ spec: items: description: |- MetricSpec specifies how to scale based on a single metric - (only `type` and one other matching field should be set at on + (only `type` and one other matching field should be set at... properties: containerResource: description: |- containerResource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes descr + requests and limits) known to Kubernetes... properties: container: description: container is the name of the @@ -7387,10 +10156,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7437,10 +10205,9 @@ spec: given metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label - selector for the given metric\nWhen - set, it is passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is @@ -7491,10 +10258,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7561,10 +10327,9 @@ spec: given metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label - selector for the given metric\nWhen - set, it is passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is @@ -7615,10 +10380,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7654,7 +10418,7 @@ spec: pods: description: |- pods refers to a metric describing each pod in the current scale target - (for example, transactions-processed-per-second) + (for example,... properties: metric: description: metric identifies the target @@ -7665,10 +10429,9 @@ spec: given metric type: string selector: - description: "selector is the string-encoded - form of a standard kubernetes label - selector for the given metric\nWhen - set, it is passed " + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... properties: matchExpressions: description: matchExpressions is @@ -7719,10 +10482,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7757,7 +10519,7 @@ spec: resource: description: |- resource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes describing eac + requests and limits) known to Kubernetes describing... properties: name: description: name is the name of the resource @@ -7768,10 +10530,9 @@ spec: value for the given metric properties: averageUtilization: - description: "averageUtilization is - the target value of the average of - the\nresource metric across all relevant - pods, represented as a " + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... format: int32 type: integer averageValue: @@ -7865,6 +10626,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -7904,13 +10670,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -7953,6 +10724,98 @@ spec: type: string type: object type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are + spread across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array ui: description: Creates a UI server container properties: @@ -7962,14 +10825,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -8013,6 +10876,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -8069,7 +10962,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -8088,8 +10981,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -8151,6 +11044,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -8319,7 +11216,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -8361,6 +11258,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -8369,9 +11267,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -8402,7 +11301,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -8451,7 +11350,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -8497,7 +11396,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -8537,7 +11436,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -8550,7 +11449,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -8614,7 +11513,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -8866,9 +11765,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -8903,7 +11802,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -8924,7 +11823,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -8934,7 +11833,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -8963,7 +11862,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -8981,14 +11880,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -9023,6 +11920,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -9048,6 +11961,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -9139,7 +12053,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -9156,7 +12070,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -9186,10 +12100,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -9270,7 +12187,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -9343,7 +12260,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -9385,6 +12302,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -9392,7 +12355,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -9458,7 +12421,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -9473,12 +12436,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -9494,9 +12457,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -9508,6 +12470,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -9522,6 +12485,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -9549,6 +12513,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -9563,6 +12528,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -9600,6 +12566,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -9634,7 +12601,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -9709,7 +12676,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -9740,7 +12707,6 @@ spec: type: object required: - feastProject - - replicas type: object x-kubernetes-validations: - message: replicas > 1 and services.scaling.autoscaling are mutually @@ -9815,10 +12781,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -9983,14 +12946,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -10034,6 +12997,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -10089,7 +13081,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -10108,8 +13100,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -10155,6 +13147,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -10196,7 +13192,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -10290,7 +13286,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -10373,14 +13368,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -10424,6 +13419,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -10479,7 +13503,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -10498,8 +13522,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -10553,15 +13577,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -10748,14 +13797,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -10799,6 +13848,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -10855,7 +13934,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -10874,8 +13953,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -10937,6 +14016,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -11225,7 +14308,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -11240,6 +14322,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -11258,14 +14343,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -11309,6 +14394,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -11365,7 +14480,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -11384,8 +14499,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -11447,6 +14562,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -11786,14 +14905,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -11838,6 +14957,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -11896,7 +15045,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -11915,8 +15064,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -11982,6 +15131,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -12203,6 +15356,10 @@ spec: x-kubernetes-validations: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -12248,6 +15405,10 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. properties: @@ -12286,13 +15447,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -12343,14 +15509,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -12394,6 +15560,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -12449,7 +15644,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -12468,8 +15663,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -12530,6 +15725,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -12698,7 +15897,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the volume @@ -12740,6 +15939,7 @@ spec: blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -12748,9 +15948,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -12781,7 +15982,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: monitors: description: |- @@ -12829,7 +16030,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -12875,7 +16076,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -12915,7 +16116,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver that @@ -12927,7 +16128,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -12989,7 +16190,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -13238,9 +16439,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -13275,7 +16476,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -13296,7 +16497,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -13306,7 +16507,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -13335,7 +16536,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -13353,14 +16554,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that details + Glusterfs topology. type: string path: description: |- @@ -13395,6 +16594,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -13420,6 +16635,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -13510,7 +16726,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -13527,7 +16743,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -13557,10 +16773,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected along - with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod to @@ -13640,7 +16859,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -13711,7 +16930,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -13753,6 +16972,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -13760,7 +17025,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -13826,7 +17091,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: group: description: |- @@ -13841,12 +17106,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -13862,9 +17127,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the volume @@ -13876,6 +17140,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -13890,6 +17155,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -13917,6 +17183,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -13931,6 +17198,7 @@ spec: attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -13968,6 +17236,7 @@ spec: with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -14002,7 +17271,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -14077,7 +17346,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine + and mounted on kubelets host machine. properties: fsType: description: |- @@ -14187,14 +17456,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -14238,6 +17507,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -14294,7 +17593,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -14313,8 +17612,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -14361,6 +17660,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -14402,7 +17705,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -14497,7 +17800,6 @@ spec: condition type. type: string required: - - status - type type: object type: array @@ -14582,14 +17884,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -14633,6 +17935,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -14689,7 +18021,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -14708,8 +18040,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -14764,15 +18096,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -14962,14 +18319,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -15014,6 +18371,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -15072,7 +18459,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -15091,8 +18478,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -15154,6 +18541,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -15446,7 +18837,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -15461,6 +18851,9 @@ spec: - couchbase.online - milvus - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -15480,14 +18873,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -15532,6 +18925,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -15590,7 +19013,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -15609,8 +19032,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -15672,6 +19095,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -16020,14 +19447,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -16073,6 +19500,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -16132,7 +19589,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -16151,9 +19608,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -16219,6 +19675,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -16447,6 +19907,11 @@ spec: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -16492,6 +19957,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -16531,13 +20001,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -16589,14 +20064,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -16640,6 +20115,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -16696,7 +20201,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -16715,8 +20220,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -16778,6 +20283,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -16946,7 +20455,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -16988,6 +20497,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -16996,9 +20506,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -17029,7 +20540,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -17078,7 +20589,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -17124,7 +20635,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -17164,7 +20675,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -17177,7 +20688,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -17241,7 +20752,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -17493,9 +21004,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -17530,7 +21041,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -17551,7 +21062,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -17561,7 +21072,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -17590,7 +21101,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -17608,14 +21119,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -17650,6 +21159,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -17675,6 +21200,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -17766,7 +21292,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -17783,7 +21309,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -17813,10 +21339,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -17897,7 +21426,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -17970,7 +21499,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -18012,6 +21541,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -18019,7 +21594,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -18085,7 +21660,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -18100,12 +21675,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -18121,9 +21696,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -18135,6 +21709,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -18149,6 +21724,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -18176,6 +21752,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -18190,6 +21767,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -18227,6 +21805,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -18261,7 +21840,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -18336,7 +21915,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -18410,10 +21989,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -18510,6 +22086,8 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: feast-operator + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" name: feast-operator-featurestore-editor-role rules: - apiGroups: @@ -18537,6 +22115,7 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: feast-operator + rbac.authorization.k8s.io/aggregate-to-view: "true" name: feast-operator-featurestore-viewer-role rules: - apiGroups: @@ -18560,76 +22139,111 @@ metadata: name: feast-operator-manager-role rules: - apiGroups: - - apps + - "" resources: - - deployments + - configmaps + - persistentvolumeclaims + - services verbs: - create - delete + - deletecollection - get - list - update - watch - apiGroups: - - authentication.k8s.io + - "" resources: - - tokenreviews + - namespaces + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - deletecollection + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods/exec verbs: - create - apiGroups: - - autoscaling + - "" resources: - - horizontalpodautoscalers + - pods/log + verbs: + - get +- apiGroups: + - "" + resources: + - serviceaccounts verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - - batch + - apps resources: - - cronjobs + - deployments verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - - "" + - authentication.k8s.io resources: - - configmaps - - persistentvolumeclaims - - serviceaccounts - - services + - tokenreviews + verbs: + - create +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers verbs: - create - delete - get - list + - patch - update - watch - apiGroups: - - "" + - batch resources: - - namespaces - - pods - - secrets + - cronjobs verbs: + - create + - delete - get - list + - patch + - update - watch - apiGroups: - - "" + - config.openshift.io resources: - - pods/exec + - apiservers verbs: - - create + - get + - list + - watch - apiGroups: - feast.dev resources: @@ -18656,6 +22270,29 @@ rules: - get - patch - update +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: @@ -18682,6 +22319,14 @@ rules: - list - update - watch +- apiGroups: + - sparkoperator.k8s.io + resources: + - sparkapplications + verbs: + - create + - delete + - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -18782,6 +22427,7 @@ spec: protocol: TCP targetPort: 8443 selector: + app.kubernetes.io/name: feast-operator control-plane: controller-manager --- apiVersion: apps/v1 @@ -18797,12 +22443,14 @@ spec: replicas: 1 selector: matchLabels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager spec: containers: @@ -18814,10 +22462,14 @@ spec: - /manager env: - name: RELATED_IMAGE_FEATURE_SERVER - value: quay.io/feastdev/feature-server:0.60.0 + value: quay.io/feastdev/feature-server:0.65.0 - name: RELATED_IMAGE_CRON_JOB value: quay.io/openshift/origin-cli:4.17 - image: quay.io/feastdev/feast-operator:0.60.0 + - name: GOMEMLIMIT + value: 230MiB + - name: OIDC_ISSUER_URL + value: "" + image: quay.io/feastdev/feast-operator:0.65.0 livenessProbe: httpGet: path: /healthz diff --git a/infra/feast-operator/dist/operator-e2e-tests b/infra/feast-operator/dist/operator-e2e-tests index c9b11c0c3ea..0d5ff42aef8 100755 Binary files a/infra/feast-operator/dist/operator-e2e-tests and b/infra/feast-operator/dist/operator-e2e-tests differ diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index 1e2367a583f..cb911ffae22 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -104,6 +104,20 @@ _Appears in:_ Defaults to "feast apply" & "feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')" | +#### DataQualityMonitoringConfig + + + +DataQualityMonitoringConfig defines the Data Quality Monitoring configuration. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `autoBaseline` _boolean_ | AutoBaseline controls whether baseline distribution is computed automatically on feast apply. Defaults to true. | + + #### DefaultCtrConfigs @@ -150,7 +164,6 @@ time for any reason. Missed jobs executions will be counted as failed ones. | | `concurrencyPolicy` _[ConcurrencyPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#concurrencypolicy-v1-batch)_ | Specifies how to treat concurrent executions of a Job. Valid values are: - - "Allow" (default): allows CronJobs to run concurrently; - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; - "Replace": cancels currently running job and replaces it with a new one | @@ -175,6 +188,23 @@ _Appears in:_ | `template` _string_ | Template for the created project | +#### FeastPackagedOptions + + + +FeastPackagedOptions describes a feature repository packaged in a feature server image. + +_Appears in:_ +- [FeastProjectDir](#feastprojectdir) + +| Field | Description | +| --- | --- | +| `image` _string_ | Image containing the packaged feature repository. When set, this image is used by the +repository initialization and feast apply containers and as the default service image. +When omitted, the operator's configured feature server image is used. | +| `featureRepoPath` _string_ | FeatureRepoPath is the canonical absolute path to the feature repository in the image. | + + #### FeastProjectDir @@ -188,6 +218,7 @@ _Appears in:_ | --- | --- | | `git` _[GitCloneOptions](#gitcloneoptions)_ | | | `init` _[FeastInitOptions](#feastinitoptions)_ | | +| `packaged` _[FeastPackagedOptions](#feastpackagedoptions)_ | | #### FeatureStore @@ -239,10 +270,29 @@ _Appears in:_ | `ui` _[ServerConfigs](#serverconfigs)_ | Creates a UI server container | | `deploymentStrategy` _[DeploymentStrategy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#deploymentstrategy-v1-apps)_ | | | `securityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podsecuritycontext-v1-core)_ | | +| `podAnnotations` _object (keys:string, values:string)_ | PodAnnotations are annotations to be applied to the Deployment's PodTemplate metadata. +This enables annotation-driven integrations like OpenTelemetry auto-instrumentation, +Istio sidecar injection, Vault agent injection, etc. | | `disableInitContainers` _boolean_ | Disable the 'feast repo initialization' initContainer | +| `initImage` _string_ | InitImage overrides the image for init containers (feast-init, feast-apply). +Resolution order: InitImage → FeastProjectDir.Packaged.Image → RELATED_IMAGE_FEATURE_SERVER → DefaultImage. | +| `runFeastApplyOnInit` _boolean_ | Runs feast apply on pod start to populate the registry. Defaults to true. Ignored when DisableInitContainers is true. | | `volumes` _[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volume-v1-core) array_ | Volumes specifies the volumes to mount in the FeatureStore deployment. A corresponding `VolumeMount` should be added to whichever feast service(s) require access to said volume(s). | | `scaling` _[ScalingConfig](#scalingconfig)_ | Scaling configures horizontal scaling for the FeatureStore deployment (e.g. HPA autoscaling). For static replicas, use spec.replicas instead. | +| `podDisruptionBudgets` _[PDBConfig](#pdbconfig)_ | PodDisruptionBudgets configures a PodDisruptionBudget for the FeatureStore deployment. +Only created when scaling is enabled (replicas > 1 or autoscaling). | +| `topologySpreadConstraints` _[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#topologyspreadconstraint-v1-core) array_ | TopologySpreadConstraints defines how pods are spread across topology domains. +When scaling is enabled and this is not set, the operator auto-injects a soft +zone-spread constraint (whenUnsatisfiable: ScheduleAnyway). +Set to an empty array to disable auto-injection. | +| `affinity` _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#affinity-v1-core)_ | Affinity defines the pod scheduling constraints for the FeatureStore deployment. +When scaling is enabled and this is not set, the operator auto-injects a soft +pod anti-affinity rule to prefer spreading pods across nodes. | +| `resourceClaims` _[PodResourceClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podresourceclaim-v1-core) array_ | ResourceClaims defines which ResourceClaims must be allocated +and reserved before the Pod is allowed to start. The resources +will be made available to those containers which consume them +by name. | #### FeatureStoreSpec @@ -263,8 +313,13 @@ _Appears in:_ | `authz` _[AuthzConfig](#authzconfig)_ | | | `cronJob` _[FeastCronJob](#feastcronjob)_ | | | `batchEngine` _[BatchEngineConfig](#batchengineconfig)_ | | +| `dataQualityMonitoring` _[DataQualityMonitoringConfig](#dataqualitymonitoringconfig)_ | DataQualityMonitoring configures Data Quality Monitoring behaviour. | | `replicas` _integer_ | Replicas is the desired number of pod replicas. Used by the scale sub-resource. Mutually exclusive with services.scaling.autoscaling. | +| `materialization` _[MaterializationConfig](#materializationconfig)_ | Materialization controls feature materialization behavior (batch size, pull strategy). +Written into feature_store.yaml for all service pods. | +| `openlineage` _[OpenLineageConfig](#openlineageconfig)_ | OpenLineage enables OpenLineage data lineage tracking for Feast operations. +Written into feature_store.yaml for all service pods. | #### FeatureStoreStatus @@ -349,7 +404,6 @@ represented by the jobs's .status.failed field, is incremented and it is checked against the backoffLimit. This field cannot be used in combination with restartPolicy=OnFailure. - This field is beta-level. It can be used when the `JobPodFailurePolicy` feature gate is enabled (enabled by default). | | `backoffLimit` _integer_ | Specifies the number of retries before marking this job failed. | @@ -381,12 +435,10 @@ the Job becomes eligible to be deleted immediately after it finishes. | | `completionMode` _[CompletionMode](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#completionmode-v1-batch)_ | completionMode specifies how Pod completions are tracked. It can be `NonIndexed` (default) or `Indexed`. - `NonIndexed` means that the Job is considered complete when there have been .spec.completions successfully completed Pods. Each Pod completion is homologous to each other. - `Indexed` means that the Pods of a Job get an associated completion index from 0 to (.spec.completions - 1), available in the annotation batch.kubernetes.io/job-completion-index. @@ -398,7 +450,6 @@ In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`. - More completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller @@ -417,7 +468,6 @@ Possible values are: - Failed means to wait until a previously created Pod is fully terminated (has phase Failed or Succeeded) before creating a replacement Pod. - When using podFailurePolicy, Failed is the the only allowed value. TerminatingOrFailed and Failed are allowed values when podFailurePolicy is not in use. This is an beta field. To use this, enable the JobPodReplacementPolicy feature toggle. @@ -459,6 +509,60 @@ _Appears in:_ | `persistence` _[RegistryPersistence](#registrypersistence)_ | | +#### MaterializationConfig + + + +MaterializationConfig controls feature materialization behavior written into feature_store.yaml. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `onlineWriteBatchSize` _integer_ | Number of rows per batch when writing to the online store during materialization. +Prevents OOM for large feature views. Supported engines: local, spark, ray. +If unset, all rows are written in a single batch. | +| `extraConfig` _object (keys:string, values:string)_ | ExtraConfig passes additional materialization key-value settings inline into +feature_store.yaml. | + + +#### McpConfig + + + +McpConfig enables MCP (Model Context Protocol) server support in the feature server. +When this field is set on ServingConfig, the feature server type is switched to "mcp". + +_Appears in:_ +- [RegistryServerConfigs](#registryserverconfigs) +- [ServingConfig](#servingconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable the MCP server. | +| `serverName` _string_ | MCP server name for identification. Defaults to "feast-mcp-server". | +| `serverVersion` _string_ | MCP server version string. Defaults to "1.0.0". | +| `transport` _string_ | MCP transport protocol. | + + +#### OfflinePushBatchingConfig + + + +OfflinePushBatchingConfig controls batching of writes to the offline store via the /push endpoint. +Recommended for high-throughput push workloads (streaming pipelines, IoT) to prevent OOM. + +_Appears in:_ +- [ServingConfig](#servingconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable offline push batching. | +| `batchSize` _integer_ | Maximum number of rows per offline write batch. | +| `batchIntervalSeconds` _integer_ | Seconds between batch flushes to the offline store. | + + #### OfflineStore @@ -532,7 +636,27 @@ _Appears in:_ | Field | Description | | --- | --- | -| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | +| `issuerUrl` _string_ | OIDC issuer URL. The operator appends /.well-known/openid-configuration to derive the discovery endpoint. | +| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Secret with OIDC properties (auth_discovery_url, client_id, client_secret). issuerUrl takes precedence. | +| `secretKeyName` _string_ | Key in the Secret containing all OIDC properties as a YAML value. If unset, each key is a property. | +| `tokenEnvVar` _string_ | Env var name for client pods to read an OIDC token from. Sets token_env_var in client config. | +| `verifySSL` _boolean_ | Verify SSL certificates for the OIDC provider. Defaults to true. | +| `caCertConfigMap` _[OidcCACertConfigMap](#oidccacertconfigmap)_ | ConfigMap with the CA certificate for self-signed OIDC providers. Auto-detected on RHOAI/ODH. | + + +#### OidcCACertConfigMap + + + +OidcCACertConfigMap references a ConfigMap containing a CA certificate for OIDC provider TLS. + +_Appears in:_ +- [OidcAuthz](#oidcauthz) + +| Field | Description | +| --- | --- | +| `name` _string_ | ConfigMap name. | +| `key` _string_ | Key in the ConfigMap holding the PEM certificate. Defaults to "ca-bundle.crt". | #### OnlineStore @@ -548,6 +672,11 @@ _Appears in:_ | --- | --- | | `server` _[ServerConfigs](#serverconfigs)_ | Creates a feature server container | | `persistence` _[OnlineStorePersistence](#onlinestorepersistence)_ | | +| `serving` _[ServingConfig](#servingconfig)_ | Serving configures the Feast feature_server section written into feature_store.yaml for the online serve pod. +Controls metrics granularity, offline push batching, and MCP. | +| `disabled` _boolean_ | Disabled skips deploying the online store service entirely, including its +serving pod and persistence. Omitting the online store block, or setting +this to false, deploys the online store with defaults as before. | #### OnlineStoreDBStorePersistence @@ -596,6 +725,57 @@ _Appears in:_ | `store` _[OnlineStoreDBStorePersistence](#onlinestoredbstorepersistence)_ | | +#### OpenLineageConfig + + + +OpenLineageConfig enables OpenLineage data lineage tracking for Feast operations. +Lineage events are emitted during feast apply and materialization when enabled. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable OpenLineage integration. | +| `transportType` _string_ | Transport type for lineage events. | +| `transportUrl` _string_ | URL for HTTP transport (e.g. http://marquez:5000). Required when transportType is "http". | +| `transportEndpoint` _string_ | API endpoint path appended to transportUrl. Defaults to "api/v1/lineage". | +| `apiKeySecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Reference to a Secret containing the key "api_key" for lineage server authentication. | +| `extraConfig` _object (keys:string, values:string)_ | ExtraConfig holds additional OpenLineage key-value settings written inline into +the openlineage block of feature_store.yaml alongside the typed fields above. +Use this for non-core settings (e.g. namespace, producer, emit_on_apply, +emit_on_materialize) and transport-specific options (e.g. kafka +bootstrap_servers, topic; file path). Boolean values ("true"/"false") and +integer values are automatically coerced to their native YAML types. +Keys must be valid Feast OpenLineageConfig YAML field names. | +| `consumer` _[OpenLineageConsumerConfig](#openlineageconsumerconfig)_ | Consumer configures the OpenLineage consumer (event receiver) that enables +Feast to receive and display lineage from external producers (Airflow, Spark, dbt, etc.). | + + +#### OpenLineageConsumerConfig + + + +OpenLineageConsumerConfig configures the OpenLineage consumer (event receiver). +When enabled, the Feast REST server exposes POST /api/v1/lineage to receive +OpenLineage events from any producer, storing them for visualization in the Feast UI. + +_Appears in:_ +- [OpenLineageConfig](#openlineageconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable the OpenLineage consumer. | +| `storeType` _string_ | StoreType is the storage backend for lineage events. Currently only "sql" is supported. | +| `connectionStringSecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Reference to a Secret containing the key "connection_string" for a separate +lineage database. If omitted, the SQL registry database is reused. | +| `apiKeySecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Reference to a Secret containing the key "api_key" that producers must +provide in the X-API-Key header when sending events. | +| `namespaceMapping` _object (keys:string, values:string)_ | NamespaceMapping maps OpenLineage namespaces to Feast projects for +RBAC-based filtering of lineage data in the UI. | + + #### OptionalCtrConfigs @@ -617,6 +797,24 @@ _Appears in:_ | `nodeSelector` _map[string]string_ | | +#### PDBConfig + + + +PDBConfig configures a PodDisruptionBudget for the FeatureStore deployment. +Exactly one of minAvailable or maxUnavailable must be set. + +_Appears in:_ +- [FeatureStoreServices](#featurestoreservices) + +| Field | Description | +| --- | --- | +| `minAvailable` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#intorstring-intstr-util)_ | MinAvailable specifies the minimum number/percentage of pods that must remain available. +Mutually exclusive with maxUnavailable. | +| `maxUnavailable` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#intorstring-intstr-util)_ | MaxUnavailable specifies the maximum number/percentage of pods that can be unavailable. +Mutually exclusive with minAvailable. | + + #### PvcConfig @@ -753,6 +951,8 @@ volume definition in the Volumes field. | These options are primarily used for production deployments to optimize performance. | | `restAPI` _boolean_ | Enable REST API registry server. | | `grpc` _boolean_ | Enable gRPC registry server. Defaults to true if unset. | +| `mcp` _[McpConfig](#mcpconfig)_ | Mcp enables MCP (Model Context Protocol) on the REST registry server. +Requires restAPI to be true. Reuses the same McpConfig struct as the online store. | #### RemoteRegistryConfig @@ -867,6 +1067,44 @@ _Appears in:_ | `ui` _string_ | | +#### ServingConfig + + + +ServingConfig configures the feature_server section of the generated feature_store.yaml. +When Mcp is set, the feature server type is switched to "mcp"; otherwise "local" is used. + +_Appears in:_ +- [OnlineStore](#onlinestore) + +| Field | Description | +| --- | --- | +| `metrics` _[ServingMetricsConfig](#servingmetricsconfig)_ | Metrics configures per-category Prometheus metrics for the feature server. +Coexists with the server.metrics bool flag — both can be set simultaneously. | +| `offlinePushBatching` _[OfflinePushBatchingConfig](#offlinepushbatchingconfig)_ | OfflinePushBatching batches writes to the offline store via the /push endpoint. | +| `mcp` _[McpConfig](#mcpconfig)_ | Mcp enables MCP (Model Context Protocol) server support. When set, feature server type is "mcp". | + + +#### ServingMetricsConfig + + + +ServingMetricsConfig controls per-category Prometheus metrics for the feature server. +Setting Enabled to true activates the metrics HTTP server on port 8000. +All metric categories default to true when enabled; use Categories to selectively disable them. + +_Appears in:_ +- [ServingConfig](#servingconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable the Prometheus metrics endpoint on port 8000. | +| `categories` _object (keys:string, values:boolean)_ | Categories selectively enables or disables individual Feast metric categories. +Keys are Feast MetricsConfig field names (e.g. "resource", "request", +"online_features", "push", "materialization", "freshness"). Omitted keys +default to true when metrics is enabled. | + + #### TlsConfigs diff --git a/infra/feast-operator/docs/odh-operator-parameters.md b/infra/feast-operator/docs/odh-operator-parameters.md new file mode 100644 index 00000000000..8b29d4d1ca8 --- /dev/null +++ b/infra/feast-operator/docs/odh-operator-parameters.md @@ -0,0 +1,15 @@ +# Open Data Hub / RHOAI operator parameters + +These values are supplied through the Feast operator **`params.env`** files in the **ODH** and **RHOAI** overlays (`config/overlays/odh/params.env`, `config/overlays/rhoai/params.env`). The Open Data Hub operator updates keys in `params.env` before rendering; Kustomize **`replacements`** copy them into the controller Deployment. + +## `OIDC_ISSUER_URL` + +**Purpose:** OIDC issuer URL when the OpenShift cluster uses external OIDC (for example Keycloak). The Feast operator process receives it as the **`OIDC_ISSUER_URL`** environment variable. An empty value means the cluster is not using external OIDC in this integration path (OpenShift OAuth / default behavior). + +**Manifest parameter:** `OIDC_ISSUER_URL` in `params.env`. + +**Injected into:** `controller-manager` Deployment, `manager` container. + +**Set by:** Open Data Hub operator (Feast component reconcile), from `GatewayConfig.spec.oidc.issuerURL` when cluster authentication is OIDC. + +**Consumption:** Operator code should read `os.Getenv("OIDC_ISSUER_URL")` (or equivalent) where JWKS / OIDC discovery is required for managed workloads. diff --git a/infra/feast-operator/go.mod b/infra/feast-operator/go.mod index 3e41f468d68..ab19a1de20a 100644 --- a/infra/feast-operator/go.mod +++ b/infra/feast-operator/go.mod @@ -1,97 +1,108 @@ module github.com/feast-dev/feast/infra/feast-operator -go 1.22.9 +go 1.25.0 require ( - github.com/onsi/ginkgo/v2 v2.17.1 - github.com/onsi/gomega v1.32.0 - github.com/openshift/api v0.0.0-20240912201240-0a8800162826 // release-4.17 + github.com/onsi/ginkgo/v2 v2.28.1 + github.com/onsi/gomega v1.39.1 + github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb // release-4.17 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.30.1 - k8s.io/apimachinery v0.30.1 - k8s.io/client-go v0.30.1 - sigs.k8s.io/controller-runtime v0.18.4 + k8s.io/api v0.35.2 + k8s.io/apimachinery v0.35.2 + k8s.io/client-go v0.35.2 + sigs.k8s.io/controller-runtime v0.23.3 ) require ( - github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect + github.com/openshift/controller-runtime-common v0.0.0-20260428152732-64ee174f5e2e + github.com/prometheus-operator/prometheus-operator/pkg/client v0.83.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 + k8s.io/apiextensions-apiserver v0.35.1 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 +) + +require ( + cel.dev/expr v0.25.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.17.8 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.18.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.45.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stoewer/go-strcase v1.2.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/sdk v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect - go.opentelemetry.io/proto/otlp v1.0.0 // indirect + github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.83.0 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/spf13/cobra v1.10.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/oauth2 v0.12.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.44.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/grpc v1.58.3 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.30.1 // indirect - k8s.io/apiserver v0.30.1 // indirect - k8s.io/component-base v0.30.1 // indirect - k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + k8s.io/apiserver v0.35.1 // indirect + k8s.io/component-base v0.35.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/infra/feast-operator/go.sum b/infra/feast-operator/go.sum index ef5d6204916..b642252f7d3 100644 --- a/infra/feast-operator/go.sum +++ b/infra/feast-operator/go.sum @@ -1,253 +1,263 @@ -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= -github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.17.8 h1:j9m730pMZt1Fc4oKhCLUHfjj6527LuhYcYw0Rl8gqto= -github.com/google/cel-go v0.17.8/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= -github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= -github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= -github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= -github.com/openshift/api v0.0.0-20240912201240-0a8800162826 h1:A8D9SN/hJUwAbdO0rPCVTqmuBOctdgurr53gK701SYo= -github.com/openshift/api v0.0.0-20240912201240-0a8800162826/go.mod h1:OOh6Qopf21pSzqNVCB5gomomBXb8o5sGKZxG2KNpaXM= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb h1:iwBR3mzmyE3EMFx7R3CQ9lOccTS0dNht8TW82aGITg0= +github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= +github.com/openshift/controller-runtime-common v0.0.0-20260428152732-64ee174f5e2e h1:k89oIo2EjX0PRSdi1kesktCyWp50SC9WwKurvupvRGs= +github.com/openshift/controller-runtime-common v0.0.0-20260428152732-64ee174f5e2e/go.mod h1:XGabTMnNbz0M5Oa7IbscZp/jmcc7aHobvOCUWwkzKvM= +github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 h1:9Pe6iVOMjt9CdA/vaKBNUSoEIjIe1po5Ha3ABRYXLJI= +github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5/go.mod h1:K3FoNLgNBFYbFuG+Kr8usAnQxj1w84XogyUp2M8rK8k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.83.0 h1:j9Ce3W6X6Tzi0QnSap+YzGwpqJLJGP/7xV6P9f86jjM= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.83.0/go.mod h1:sSxwdmprUfmRfTknPc4KIjUd2ZIc/kirw4UdXNhOauM= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.83.0 h1:odshP0+Jo6iUNGpK8MOFA6p5Yj0QOV4yLgiqFU5MVuI= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.83.0/go.mod h1:6Ndhfow0psSp7dV1qp9zK5h++CDKz4eSFWPbrHd5Iic= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0= +github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= +github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= -google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e h1:z3vDksarJxsAKM5dmEGv0GHwE2hKJ096wZra71Vs4sw= -google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= -k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= -k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= -k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= -k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= -k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/apiserver v0.30.1 h1:BEWEe8bzS12nMtDKXzCF5Q5ovp6LjjYkSp8qOPk8LZ8= -k8s.io/apiserver v0.30.1/go.mod h1:i87ZnQ+/PGAmSbD/iEKM68bm1D5reX8fO4Ito4B01mo= -k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= -k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= -k8s.io/component-base v0.30.1 h1:bvAtlPh1UrdaZL20D9+sWxsJljMi0QZ3Lmw+kmZAaxQ= -k8s.io/component-base v0.30.1/go.mod h1:e/X9kDiOebwlI41AvBHuWdqFriSRrX50CdwA9TFaHLI= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 h1:/U5vjBbQn3RChhv7P11uhYvCSm5G2GaIi5AIGBS6r4c= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmGM2dfIiLRfrC6jb5kV2Mq/sK1ZP303cxzkV5Y4= -sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= -sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= +k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= +k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= +k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= +k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= +k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= +k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= +k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/infra/feast-operator/internal/controller/authz/authz.go b/infra/feast-operator/internal/controller/authz/authz.go index 9cb5b7c9554..9ab8d10c55c 100644 --- a/infra/feast-operator/internal/controller/authz/authz.go +++ b/infra/feast-operator/internal/controller/authz/authz.go @@ -15,16 +15,41 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +const ( + authenticationAPIGroup = "authentication.k8s.io" + verbCreate = "create" +) + // Deploy the feast authorization func (authz *FeastAuthorization) Deploy() error { if authz.isKubernetesAuth() { + authz.cleanupOidcRbac() return authz.deployKubernetesAuth() } + // Clean up namespace-scoped Kubernetes auth resources authz.removeOrphanedRoles() _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRole()) _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRoleBinding()) apimeta.RemoveStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue].Type) + + // Clean up cluster-scoped Kubernetes auth CRB (handles Kubernetes→OIDC or Kubernetes→no-auth transitions) + authz.cleanupKubernetesClusterRbac() + + if authz.isOidcAuth() { + if err := authz.createOidcClusterRole(); err != nil { + return authz.setFeastOidcAuthCondition(err) + } + if err := authz.createOidcClusterRoleBinding(); err != nil { + return authz.setFeastOidcAuthCondition(err) + } + return authz.setFeastOidcAuthCondition(nil) + } + + // No auth - clean up OIDC RBAC and remove condition + authz.cleanupOidcRbac() + apimeta.RemoveStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastOidcAuthConditions[metav1.ConditionTrue].Type) + return nil } @@ -33,6 +58,11 @@ func (authz *FeastAuthorization) isKubernetesAuth() bool { return authzConfig != nil && authzConfig.KubernetesAuthz != nil } +func (authz *FeastAuthorization) isOidcAuth() bool { + authzConfig := authz.Handler.FeatureStore.Status.Applied.AuthzConfig + return authzConfig != nil && authzConfig.OidcAuthz != nil +} + func (authz *FeastAuthorization) deployKubernetesAuth() error { if authz.isKubernetesAuth() { authz.removeOrphanedRoles() @@ -127,32 +157,32 @@ func (authz *FeastAuthorization) setFeastClusterRole(clusterRole *rbacv1.Cluster { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"rolebindings"}, - Verbs: []string{"list"}, + Verbs: []string{verbList}, }, { - APIGroups: []string{"authentication.k8s.io"}, - Resources: []string{"tokenreviews"}, - Verbs: []string{"create"}, + APIGroups: []string{authenticationAPIGroup}, + Resources: []string{resourceTokenReviews}, + Verbs: []string{verbCreate}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"subjectaccessreviews"}, - Verbs: []string{"create"}, + Verbs: []string{verbCreate}, }, { APIGroups: []string{""}, Resources: []string{"namespaces"}, - Verbs: []string{"get", "list", "watch"}, + Verbs: []string{verbGet, verbList, verbWatch}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"clusterroles"}, - Verbs: []string{"get", "list"}, + Verbs: []string{verbGet, verbList}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"clusterrolebindings"}, - Verbs: []string{"get", "list"}, + Verbs: []string{verbGet, verbList}, }, } // Don't set controller reference for shared ClusterRole @@ -213,32 +243,32 @@ func (authz *FeastAuthorization) setFeastRole(role *rbacv1.Role) error { { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"roles", "rolebindings"}, - Verbs: []string{"get", "list", "watch"}, + Verbs: []string{verbGet, verbList, verbWatch}, }, { - APIGroups: []string{"authentication.k8s.io"}, - Resources: []string{"tokenreviews"}, - Verbs: []string{"create"}, + APIGroups: []string{authenticationAPIGroup}, + Resources: []string{resourceTokenReviews}, + Verbs: []string{verbCreate}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"subjectaccessreviews"}, - Verbs: []string{"create"}, + Verbs: []string{verbCreate}, }, { APIGroups: []string{""}, Resources: []string{"namespaces"}, - Verbs: []string{"get", "list", "watch"}, + Verbs: []string{verbGet, verbList, verbWatch}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"clusterroles"}, - Verbs: []string{"get", "list"}, + Verbs: []string{verbGet, verbList}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"clusterrolebindings"}, - Verbs: []string{"get", "list"}, + Verbs: []string{verbGet, verbList}, }, } @@ -312,24 +342,120 @@ func (authz *FeastAuthorization) setAuthRole(role *rbacv1.Role) error { return controllerutil.SetControllerReference(authz.Handler.FeatureStore, role, authz.Handler.Scheme) } +func (authz *FeastAuthorization) createOidcClusterRole() error { + logger := log.FromContext(authz.Handler.Context) + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleName()}, + } + clusterRole.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRole")) + if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, clusterRole, controllerutil.MutateFn(func() error { + clusterRole.Labels = authz.getSharedOidcClusterRoleLabels() + clusterRole.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{authenticationAPIGroup}, + Resources: []string{resourceTokenReviews}, + Verbs: []string{verbCreate}, + }, + } + return nil + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "ClusterRole", clusterRole.Name, "operation", op) + } + return nil +} + +func (authz *FeastAuthorization) createOidcClusterRoleBinding() error { + logger := log.FromContext(authz.Handler.Context) + crb := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleBindingName()}, + } + crb.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) + if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, crb, controllerutil.MutateFn(func() error { + crb.Labels = authz.getLabels() + crb.Subjects = []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: authz.getFeastServiceAccountName(), + Namespace: authz.Handler.FeatureStore.Namespace, + }, + } + crb.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: authz.getOidcClusterRoleName(), + } + return nil + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "ClusterRoleBinding", crb.Name, "operation", op) + } + return nil +} + +func (authz *FeastAuthorization) cleanupOidcRbac() { + crb := &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleBindingName()}} + crb.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) + _ = authz.Handler.Client.Delete(authz.Handler.Context, crb) +} + +func (authz *FeastAuthorization) cleanupKubernetesClusterRbac() { + crb := &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: authz.getFeastClusterRoleBindingName()}} + crb.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) + if err := authz.Handler.Client.Get(authz.Handler.Context, client.ObjectKeyFromObject(crb), crb); err != nil { + return + } + _ = authz.Handler.Client.Delete(authz.Handler.Context, crb) +} + +func (authz *FeastAuthorization) getOidcClusterRoleName() string { + return "feast-oidc-token-review" +} + +func (authz *FeastAuthorization) getOidcClusterRoleBindingName() string { + return services.GetFeastName(authz.Handler.FeatureStore) + "-oidc-token-review" +} + func (authz *FeastAuthorization) getLabels() map[string]string { return map[string]string{ services.NameLabelKey: authz.Handler.FeatureStore.Name, services.ServiceTypeLabelKey: string(services.AuthzFeastType), + services.ManagedByLabelKey: services.ManagedByLabelValue, + } +} + +func (authz *FeastAuthorization) getSharedOidcClusterRoleLabels() map[string]string { + return map[string]string{ + services.ServiceTypeLabelKey: string(services.AuthzFeastType), + services.ManagedByLabelKey: services.ManagedByLabelValue, } } +func (authz *FeastAuthorization) setFeastOidcAuthCondition(err error) error { + if err != nil { + logger := log.FromContext(authz.Handler.Context) + cond := feastOidcAuthConditions[metav1.ConditionFalse] + cond.Message = services.ErrorMessagePrefix + err.Error() + apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, cond) + logger.Error(err, "Error deploying the OIDC authorization") + return err + } + apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastOidcAuthConditions[metav1.ConditionTrue]) + return nil +} + func (authz *FeastAuthorization) setFeastKubernetesAuthCondition(err error) error { if err != nil { logger := log.FromContext(authz.Handler.Context) cond := feastKubernetesAuthConditions[metav1.ConditionFalse] - cond.Message = "Error: " + err.Error() + cond.Message = services.ErrorMessagePrefix + err.Error() apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, cond) logger.Error(err, "Error deploying the Kubernetes authorization") return err - } else { - apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue]) } + apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue]) return nil } diff --git a/infra/feast-operator/internal/controller/authz/authz_types.go b/infra/feast-operator/internal/controller/authz/authz_types.go index aea5e5f7a65..388ab179e7e 100644 --- a/infra/feast-operator/internal/controller/authz/authz_types.go +++ b/infra/feast-operator/internal/controller/authz/authz_types.go @@ -11,6 +11,16 @@ type FeastAuthorization struct { Handler handler.FeastHandler } +const ( + // RBAC verbs + verbGet = "get" + verbList = "list" + verbWatch = "watch" + + // RBAC resources + resourceTokenReviews = "tokenreviews" +) + var ( feastKubernetesAuthConditions = map[metav1.ConditionStatus]metav1.Condition{ metav1.ConditionTrue: { @@ -25,4 +35,17 @@ var ( Reason: feastdevv1.KubernetesAuthzFailedReason, }, } + feastOidcAuthConditions = map[metav1.ConditionStatus]metav1.Condition{ + metav1.ConditionTrue: { + Type: feastdevv1.AuthorizationReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.OidcAuthzReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1.AuthorizationReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1.OidcAuthzFailedReason, + }, + } ) diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go index d73b30c0175..b94808c0df5 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller.go +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -25,11 +25,14 @@ import ( autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,6 +43,7 @@ import ( feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz" feasthandler "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + feastmetrics "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" routev1 "github.com/openshift/api/route/v1" ) @@ -52,21 +56,29 @@ const ( // FeatureStoreReconciler reconciles a FeatureStore object type FeatureStoreReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + Metrics *feastmetrics.FeatureStoreMetrics } +// +kubebuilder:rbac:groups=config.openshift.io,resources=apiservers,verbs=get;list;watch // +kubebuilder:rbac:groups=feast.dev,resources=featurestores,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=feast.dev,resources=featurestores/status,verbs=get;update;patch // +kubebuilder:rbac:groups=feast.dev,resources=featurestores/finalizers,verbs=update // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;create;update;watch;delete -// +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims;serviceaccounts,verbs=get;list;create;update;watch;delete +// +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims,verbs=get;list;create;update;watch;delete;deletecollection +// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;create;update;watch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings;clusterroles;clusterrolebindings;subjectaccessreviews,verbs=get;list;create;update;watch;delete -// +kubebuilder:rbac:groups=core,resources=secrets;pods;namespaces,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=secrets;namespaces,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;delete;deletecollection // +kubebuilder:rbac:groups=core,resources=pods/exec,verbs=create +// +kubebuilder:rbac:groups=core,resources=pods/log,verbs=get +// +kubebuilder:rbac:groups=sparkoperator.k8s.io,resources=sparkapplications,verbs=create;get;delete // +kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create // +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;create;update;watch;delete // +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;list;watch;create;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -82,6 +94,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request if apierrors.IsNotFound(err) { // CR deleted since request queued, child objects getting GC'd, no requeue logger.V(1).Info("FeatureStore CR not found, has been deleted") + if r.Metrics != nil { + r.Metrics.DeleteFeatureStore(req.NamespacedName.Namespace, req.NamespacedName.Name) + } // Clean up namespace registry entry even if the CR is not found if err := r.cleanupNamespaceRegistry(ctx, &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ @@ -102,6 +117,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request // Handle deletion - clean up namespace registry entry if cr.DeletionTimestamp != nil { logger.Info("FeatureStore is being deleted, cleaning up namespace registry entry") + if r.Metrics != nil { + r.Metrics.DeleteFeatureStore(cr.Namespace, cr.Name) + } if err := r.cleanupNamespaceRegistry(ctx, cr); err != nil { logger.Error(err, "Failed to clean up namespace registry entry") return ctrl.Result{}, err @@ -110,6 +128,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request } result, recErr = r.deployFeast(ctx, cr) + if recErr == nil && r.Metrics != nil { + r.Metrics.RecordFeatureStore(cr) + } if cr.DeletionTimestamp == nil && !reflect.DeepEqual(currentStatus, cr.Status) { if err = r.Client.Status().Update(ctx, cr); err != nil { if apierrors.IsConflict(err) { @@ -194,11 +215,15 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 } else { isDeployAvailable := services.IsDeploymentAvailable(deployment.Status.Conditions) if !isDeployAvailable { + msg := feastdevv1.DeploymentNotAvailableMessage + if podMsg := feast.GetPodContainerFailureMessage(deployment); podMsg != "" { + msg = msg + ": " + podMsg + } condition = metav1.Condition{ Type: feastdevv1.ReadyType, Status: metav1.ConditionUnknown, Reason: feastdevv1.DeploymentNotAvailableReason, - Message: feastdevv1.DeploymentNotAvailableMessage, + Message: msg, } result = errResult @@ -232,11 +257,21 @@ func (r *FeatureStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&rbacv1.Role{}). Owns(&batchv1.CronJob{}). Owns(&autoscalingv2.HorizontalPodAutoscaler{}). + Owns(&policyv1.PodDisruptionBudget{}). Watches(&feastdevv1.FeatureStore{}, handler.EnqueueRequestsFromMapFunc(r.mapFeastRefsToFeastRequests)) if services.IsOpenShift() { bldr = bldr.Owns(&routev1.Route{}) } + if services.HasServiceMonitorCRD() { + sm := &unstructured.Unstructured{} + sm.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "monitoring.coreos.com", + Version: "v1", + Kind: "ServiceMonitor", + }) + bldr = bldr.Owns(sm) + } return bldr.Complete(r) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go b/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go index c329d70f06d..11ae2af7777 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go @@ -98,6 +98,8 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { Expect(resource.Status).NotTo(BeNil()) Expect(resource.Status.CronJob).To(Equal(objMeta.Name)) Expect(resource.Status.Applied.CronJob.Schedule).NotTo(BeEmpty()) + Expect(resource.Status.Applied.Services.RunFeastApplyOnInit).NotTo(BeNil()) + Expect(*resource.Status.Applied.Services.RunFeastApplyOnInit).To(BeTrue()) Expect(resource.Status.Conditions).NotTo(BeEmpty()) cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.CronJobReadyType) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index d17bffb2377..08276293032 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -132,22 +132,22 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { typeNamespacedName := types.NamespacedName{ Name: resourceName, - Namespace: "default", + Namespace: services.DefaultNs, } offlineSecretNamespacedName := types.NamespacedName{ - Name: "offline-store-secret", - Namespace: "default", + Name: services.OfflineStoreSecretName, + Namespace: services.DefaultNs, } onlineSecretNamespacedName := types.NamespacedName{ - Name: "online-store-secret", - Namespace: "default", + Name: services.OnlineStoreSecretName, + Namespace: services.DefaultNs, } registrySecretNamespacedName := types.NamespacedName{ - Name: "registry-store-secret", - Namespace: "default", + Name: services.RegistryStoreSecretName, + Namespace: services.DefaultNs, } featurestore := &feastdevv1.FeatureStore{} @@ -212,7 +212,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { DBPersistence: &feastdevv1.OfflineStoreDBStorePersistence{ Type: string(offlineType), SecretRef: corev1.LocalObjectReference{ - Name: "offline-store-secret", + Name: services.OfflineStoreSecretName, }, }, } @@ -220,7 +220,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ Type: string(onlineType), SecretRef: corev1.LocalObjectReference{ - Name: "online-store-secret", + Name: services.OnlineStoreSecretName, }, }, } @@ -228,7 +228,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { DBPersistence: &feastdevv1.RegistryDBStorePersistence{ Type: string(registryType), SecretRef: corev1.LocalObjectReference{ - Name: "registry-store-secret", + Name: services.RegistryStoreSecretName, }, SecretKeyName: "sql_custom_registry_key", }, @@ -291,7 +291,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: services.OnlineStoreSecretName} // pragma: allowlist secret resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "invalid.secret.key" Expect(k8sClient.Update(ctx, resource)).To(Succeed()) resource = &feastdevv1.FeatureStore{} @@ -316,7 +316,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) - resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: services.OnlineStoreSecretName} // pragma: allowlist secret resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" Expect(k8sClient.Update(ctx, resource)).To(Succeed()) resource = &feastdevv1.FeatureStore{} @@ -364,7 +364,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.Type).To(Equal(string(offlineType))) - Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "offline-store-secret"})) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: services.OfflineStoreSecretName})) Expect(resource.Status.Applied.Services.OfflineStore.Server.ImagePullPolicy).To(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Server.Resources).To(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Server.Image).To(Equal(&services.DefaultImage)) @@ -372,7 +372,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(resource.Status.Applied.Services.OnlineStore.Persistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.Type).To(Equal(string(onlineType))) - Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "online-store-secret"})) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: services.OnlineStoreSecretName})) Expect(resource.Status.Applied.Services.OnlineStore.Server.ImagePullPolicy).To(Equal(&pullPolicy)) Expect(resource.Status.Applied.Services.OnlineStore.Server.Resources).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore.Server.Image).To(Equal(&image)) @@ -381,7 +381,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(resource.Status.Applied.Services.Registry.Local.Persistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.Type).To(Equal(string(registryType))) - Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "registry-store-secret"})) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: services.RegistryStoreSecretName})) Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName).To(Equal("sql_custom_registry_key")) Expect(resource.Status.Applied.Services.Registry.Local.Server.ImagePullPolicy).To(BeNil()) Expect(resource.Status.Applied.Services.Registry.Local.Server.Resources).To(BeNil()) @@ -461,7 +461,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(secretContainingValidTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) - resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: services.OnlineStoreSecretName} // pragma: allowlist secret resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" Expect(k8sClient.Update(ctx, resource)).To(Succeed()) resource = &feastdevv1.FeatureStore{} @@ -492,7 +492,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret.Data[string(services.RegistryDBPersistenceSQLConfigType)] = []byte(invalidSecretRegistryTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) - resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "registry-store-secret"} + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: services.RegistryStoreSecretName} // pragma: allowlist secret resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName = "" Expect(k8sClient.Update(ctx, resource)).To(Succeed()) resource = &feastdevv1.FeatureStore{} @@ -676,7 +676,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { // change paths and reconcile resourceNew := resource.DeepCopy() - newOnlineSecretName := "offline-store-secret" + newOnlineSecretName := services.OfflineStoreSecretName // pragma: allowlist secret newOnlineDBPersistenceType := services.OnlineDBPersistenceSnowflakeConfigType resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.Type = string(newOnlineDBPersistenceType) resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: newOnlineSecretName} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go index 37d22094147..a326752a78f 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go @@ -185,7 +185,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(2)) Expect(services.GetRegistryContainer(*deploy)).NotTo(BeNil()) Expect(services.GetOnlineContainer(*deploy)).NotTo(BeNil()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go index bb5cc4fb4b2..0f99f6d0479 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go @@ -77,7 +77,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { if err != nil && errors.IsNotFound(err) { resource := createFeatureStoreResource(resourceName, image, pullPolicy, &[]corev1.EnvVar{}, withEnvFrom()) resource.Spec.AuthzConfig = &feastdevv1.AuthzConfig{OidcAuthz: &feastdevv1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ + SecretRef: &corev1.LocalObjectReference{ Name: oidcSecretName, }, }} @@ -134,7 +134,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) expectedAuthzConfig := &feastdevv1.AuthzConfig{ OidcAuthz: &feastdevv1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ + SecretRef: &corev1.LocalObjectReference{ Name: oidcSecretName, }, }, @@ -179,7 +179,10 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Message).To(Equal(feastdevv1.OidcAuthzReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) @@ -221,7 +224,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) Expect(services.GetOfflineContainer(*deploy).VolumeMounts).To(HaveLen(1)) @@ -476,7 +479,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(cond.Status).To(Equal(metav1.ConditionFalse)) Expect(cond.Reason).To(Equal(feastdevv1.FailedReason)) Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) - Expect(cond.Message).To(ContainSubstring("missing OIDC")) + Expect(cond.Message).To(ContainSubstring("OIDC discovery URL")) }) }) }) @@ -490,18 +493,14 @@ func expectedServerOidcAuthorizConfig() services.AuthzConfig { string(services.OidcClientSecret): "client-secret", string(services.OidcUsername): "username", string(services.OidcPassword): "password", + string(services.OidcAudience): "api://feast-feature-server", + string(services.OidcIssuer): "https://keycloak.example.com/realms/test", }, } } func expectedClientOidcAuthorizConfig() services.AuthzConfig { return services.AuthzConfig{ Type: services.OidcAuthType, - OidcParameters: map[string]interface{}{ - string(services.OidcClientId): "client-id", - string(services.OidcAuthDiscoveryUrl): "auth-discovery-url", - string(services.OidcClientSecret): "client-secret", - string(services.OidcUsername): "username", - string(services.OidcPassword): "password"}, } } @@ -512,6 +511,8 @@ func validOidcSecretMap() map[string]string { string(services.OidcClientSecret): "client-secret", string(services.OidcUsername): "username", string(services.OidcPassword): "password", + string(services.OidcAudience): "api://feast-feature-server", + string(services.OidcIssuer): "https://keycloak.example.com/realms/test", } } @@ -529,7 +530,7 @@ func createValidOidcSecret(secretName string) *corev1.Secret { func createInvalidOidcSecret(secretName string) *corev1.Secret { oidcProperties := validOidcSecretMap() - delete(oidcProperties, string(services.OidcClientId)) + delete(oidcProperties, string(services.OidcAuthDiscoveryUrl)) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, diff --git a/infra/feast-operator/internal/controller/featurestore_controller_packaged_test.go b/infra/feast-operator/internal/controller/featurestore_controller_packaged_test.go new file mode 100644 index 00000000000..9c534a624bd --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_packaged_test.go @@ -0,0 +1,237 @@ +/* +Copyright 2026 Feast Community. + +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 + + http://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 controller + +import ( + "context" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("Packaged feature repositories", func() { + const ( + resourceName = "packaged-feature-repo" + packagedImage = "registry.example.com/feature-server@sha256:0123456789abcdef" + packagedRepoDir = "/opt/feast/feature_repo" + ) + + ctx := context.Background() + key := types.NamespacedName{Name: resourceName, Namespace: "default"} + + newFeatureStore := func() *feastdevv1.FeatureStore { + return &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: key.Name, Namespace: key.Namespace}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: feastProject, + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{ + Image: packagedImage, + FeatureRepoPath: packagedRepoDir, + }, + }, + }, + } + } + + reconcileFeatureStore := func() (*feastdevv1.FeatureStore, *appsv1.Deployment) { + reconciler := &FeatureStoreReconciler{Client: k8sClient, Scheme: k8sClient.Scheme()} + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) + Expect(err).NotTo(HaveOccurred()) + + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + feastServices := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: k8sClient, + Context: ctx, + Scheme: k8sClient.Scheme(), + FeatureStore: featureStore, + }, + } + deployment := &appsv1.Deployment{} + meta := feastServices.GetObjectMeta() + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: meta.Name, Namespace: meta.Namespace}, deployment)).To(Succeed()) + return featureStore, deployment + } + + BeforeEach(func() { + Expect(k8sClient.Create(ctx, newFeatureStore())).To(Succeed()) + }) + + AfterEach(func() { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }) + + It("stages the packaged repository and applies it from the shared directory", func() { + featureStore, deployment := reconcileFeatureStore() + + canonicalRepoDir := services.EphemeralPath + "/" + feastProject + "/" + services.FeatureRepoDir + Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(2)) + initContainer := deployment.Spec.Template.Spec.InitContainers[0] + Expect(initContainer.Name).To(Equal("feast-init")) + Expect(initContainer.Image).To(Equal(packagedImage)) + Expect(initContainer.WorkingDir).To(Equal(services.EphemeralPath)) + Expect(initContainer.Env).To(ContainElements( + corev1.EnvVar{Name: "FEAST_PACKAGED_FEATURE_REPO_PATH", Value: packagedRepoDir}, + corev1.EnvVar{Name: "FEAST_STAGED_FEATURE_REPO_PATH", Value: canonicalRepoDir}, + )) + Expect(initContainer.Args).To(HaveLen(1)) + Expect(initContainer.Args[0]).To(ContainSubstring(`rm -rf -- "${FEAST_STAGED_FEATURE_REPO_PATH}"`)) + Expect(initContainer.Args[0]).To(ContainSubstring(`cp -a -- "${FEAST_PACKAGED_FEATURE_REPO_PATH}/." "${FEAST_STAGED_FEATURE_REPO_PATH}/"`)) + Expect(initContainer.Args[0]).To(ContainSubstring(`printf '%s' "${TMP_FEATURE_STORE_YAML_BASE64}" | base64 -d`)) + Expect(initContainer.Args[0]).To(ContainSubstring(`"${FEAST_STAGED_FEATURE_REPO_PATH}/feature_store.yaml"`)) + + applyContainer := deployment.Spec.Template.Spec.InitContainers[1] + Expect(applyContainer.Name).To(Equal("feast-apply")) + Expect(applyContainer.Image).To(Equal(packagedImage)) + Expect(applyContainer.Command).To(Equal([]string{"feast", "apply"})) + Expect(applyContainer.WorkingDir).To(Equal(canonicalRepoDir)) + + online := services.GetOnlineContainer(*deployment) + Expect(online.Image).To(Equal(packagedImage)) + Expect(online.WorkingDir).To(Equal(canonicalRepoDir)) + Expect(*featureStore.Status.Applied.Services.OnlineStore.Server.Image).To(Equal(packagedImage)) + }) + + It("supports staging without applying and direct use of the baked repository", func() { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{RunFeastApplyOnInit: ptr(false)} + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + featureStore, deployment := reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.InitContainers[0].Name).To(Equal("feast-init")) + + featureStore.Spec.Services.DisableInitContainers = true + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + _, deployment = reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers).To(BeEmpty()) + online := services.GetOnlineContainer(*deployment) + Expect(online.Image).To(Equal(packagedImage)) + Expect(online.WorkingDir).To(Equal(packagedRepoDir)) + }) + + It("keeps explicit service images ahead of the packaged image", func() { + const serviceImage = "registry.example.com/online-server:custom" + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{Image: ptr(serviceImage)}, + }, + }, + }, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + _, deployment := reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers[0].Image).To(Equal(packagedImage)) + Expect(services.GetOnlineContainer(*deployment).Image).To(Equal(serviceImage)) + }) + + It("keeps an explicit init image ahead of the packaged image", func() { + const initImage = "registry.example.com/feast-init:custom" + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + InitImage: ptr(initImage), + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + _, deployment := reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(2)) + Expect(deployment.Spec.Template.Spec.InitContainers[0].Image).To(Equal(initImage)) + Expect(deployment.Spec.Template.Spec.InitContainers[1].Image).To(Equal(initImage)) + Expect(services.GetOnlineContainer(*deployment).Image).To(Equal(packagedImage)) + }) + + It("supports path-only direct mode with an explicit service image", func() { + const serviceImage = "registry.example.com/online-server:air-gapped" + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.FeastProjectDir.Packaged.Image = "" + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + DisableInitContainers: true, + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{Image: ptr(serviceImage)}, + }, + }, + }, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + _, deployment := reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers).To(BeEmpty()) + online := services.GetOnlineContainer(*deployment) + Expect(online.Image).To(Equal(serviceImage)) + Expect(online.WorkingDir).To(Equal(packagedRepoDir)) + }) + + It("retains the operator image fallback when the packaged image is omitted", func() { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.FeastProjectDir.Packaged.Image = "" + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + _, deployment := reconcileFeatureStore() + initImage := deployment.Spec.Template.Spec.InitContainers[0].Image + Expect(initImage).NotTo(BeEmpty()) + Expect(services.GetOnlineContainer(*deployment).Image).To(Equal(initImage)) + }) + + DescribeTable("rejects packaged and staged repository path overlap", + func(featureRepoPath string) { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.FeastProjectDir.Packaged.FeatureRepoPath = featureRepoPath + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + reconciler := &FeatureStoreReconciler{Client: k8sClient, Scheme: k8sClient.Scheme()} + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) + Expect(err).To(MatchError(ContainSubstring("overlaps staged repository path"))) + }, + Entry("equal paths", services.EphemeralPath+"/"+feastProject+"/"+services.FeatureRepoDir), + Entry("packaged path is an ancestor", services.EphemeralPath+"/"+feastProject), + Entry("packaged path is a descendant", services.EphemeralPath+"/"+feastProject+"/"+services.FeatureRepoDir+"/baked"), + ) + + It("allows similar path prefixes that do not overlap", func() { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.FeastProjectDir.Packaged.FeatureRepoPath = + services.EphemeralPath + "/" + feastProject + "/" + services.FeatureRepoDir + "-image" + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + reconcileFeatureStore() + }) +}) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index a70cd476679..712644f7a0c 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -19,6 +19,7 @@ package controller import ( "context" "encoding/base64" + "encoding/json" "fmt" "reflect" "strings" @@ -210,8 +211,10 @@ var _ = Describe("FeatureStore Controller", func() { Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("feast init")) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Name).To(Equal("feast-apply")) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Command).To(Equal([]string{"feast", "apply"})) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) deploy.Spec.Replicas = int32Ptr(3) @@ -264,7 +267,7 @@ var _ = Describe("FeatureStore Controller", func() { }, deploy) Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("git -c http.sslVerify=false clone")) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("git checkout " + ref)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring(featureRepoPath)) @@ -294,8 +297,43 @@ var _ = Describe("FeatureStore Controller", func() { Namespace: objMeta.Namespace, }, deploy) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("feast init -t spark")) + + // initImage is independent of server images: init containers use initImage, + // main containers keep their own server.image. + initImage := "quay.io/org/feast-init:custom" + serverImage := "quay.io/org/feast-online:server" + if resource.Spec.Services == nil { + resource.Spec.Services = &feastdevv1.FeatureStoreServices{} + } + resource.Spec.Services.InitImage = &initImage + if resource.Spec.Services.OnlineStore == nil { + resource.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{} + } + if resource.Spec.Services.OnlineStore.Server == nil { + resource.Spec.Services.OnlineStore.Server = &feastdevv1.ServerConfigs{} + } + resource.Spec.Services.OnlineStore.Server.Image = &serverImage + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) + Expect(deploy.Spec.Template.Spec.InitContainers[0].Image).To(Equal(initImage)) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Image).To(Equal(initImage)) + online = services.GetOnlineContainer(*deploy) + Expect(online).NotTo(BeNil()) + Expect(online.Image).To(Equal(serverImage)) + Expect(online.Image).NotTo(Equal(initImage)) }) It("should properly encode a feature_store.yaml config", func() { @@ -737,6 +775,12 @@ var _ = Describe("FeatureStore Controller", func() { }, deploy) Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Name).To(Equal("feast-apply")) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Env).To(ContainElements( + corev1.EnvVar{Name: testEnvVarName, Value: testEnvVarValue}, + )) + Expect(deploy.Spec.Template.Spec.InitContainers[1].EnvFrom).NotTo(BeEmpty()) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) registryContainer := services.GetRegistryContainer(*deploy) Expect(registryContainer.Env).To(HaveLen(1)) @@ -1145,7 +1189,7 @@ var _ = Describe("FeatureStore Controller", func() { Namespace: objMeta.Namespace, }, deploy) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) // check client config cm := &corev1.ConfigMap{} @@ -1225,6 +1269,127 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cond.Message).To(Equal("Error: Remote feast registry of referenced FeatureStore '" + referencedRegistry.Name + "' is not ready")) }) + It("should allow cross-project registry references with different feastProject names", func() { + By("Reconciling the primary local registry FeatureStore") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + primaryStore := &feastdevv1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, primaryStore) + Expect(err).NotTo(HaveOccurred()) + Expect(primaryStore.Status.Applied.FeastProject).To(Equal(feastProject)) + + By("Creating a second FeatureStore with a DIFFERENT feastProject name referencing the first") + crossProjectName := "cross-project-ref" + crossProjectFeastName := "different_project" + crossProjectResource := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: crossProjectName, + Namespace: primaryStore.Namespace, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: crossProjectFeastName, + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{}, + }, + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + FeastRef: &feastdevv1.FeatureStoreRef{ + Name: primaryStore.Name, + }, + }, + }, + }, + }, + } + crossProjectResource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) + crossProjectNsName := client.ObjectKeyFromObject(crossProjectResource) + err = k8sClient.Create(ctx, crossProjectResource) + Expect(err).NotTo(HaveOccurred()) + + By("Reconciling the cross-project FeatureStore — should succeed without error") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: crossProjectNsName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, crossProjectNsName, crossProjectResource) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying the cross-project FeatureStore is ready and uses its own project name") + Expect(crossProjectResource.Status.Applied.FeastProject).To(Equal(crossProjectFeastName)) + Expect(crossProjectResource.Status.ServiceHostnames.Registry).To(Equal(primaryStore.Status.ServiceHostnames.Registry)) + Expect(apimeta.IsStatusConditionTrue(crossProjectResource.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) + + By("Verifying the cross-project client ConfigMap uses the correct project name and shared registry") + crossFeast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: crossProjectResource, + }, + } + crossCm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: crossFeast.GetFeastServiceName(services.ClientFeastType), + Namespace: crossProjectResource.Namespace, + }, crossCm) + Expect(err).NotTo(HaveOccurred()) + crossRepoConfig := &services.RepoConfig{} + err = yaml.Unmarshal([]byte(crossCm.Data[services.FeatureStoreYamlCmKey]), crossRepoConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(crossRepoConfig.Project).To(Equal(crossProjectFeastName)) + Expect(crossRepoConfig.Registry.Path).To(ContainSubstring(primaryStore.Name)) + + By("Verifying the primary store client ConfigMap still uses its own project name") + primaryFeast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: primaryStore, + }, + } + primaryCm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: primaryFeast.GetFeastServiceName(services.ClientFeastType), + Namespace: primaryStore.Namespace, + }, primaryCm) + Expect(err).NotTo(HaveOccurred()) + primaryRepoConfig := &services.RepoConfig{} + err = yaml.Unmarshal([]byte(primaryCm.Data[services.FeatureStoreYamlCmKey]), primaryRepoConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(primaryRepoConfig.Project).To(Equal(feastProject)) + + By("Verifying both stores share the same registry path") + Expect(crossRepoConfig.Registry.Path).To(Equal(primaryRepoConfig.Registry.Path)) + + By("Verifying the namespace registry ConfigMap lists both client configs") + registryCm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: services.NamespaceRegistryConfigMapName, + Namespace: services.DefaultKubernetesNamespace, + }, registryCm) + Expect(err).NotTo(HaveOccurred()) + var registryData services.NamespaceRegistryData + err = json.Unmarshal([]byte(registryCm.Data[services.NamespaceRegistryDataKey]), ®istryData) + Expect(err).NotTo(HaveOccurred()) + ns := primaryStore.Namespace + Expect(registryData.Namespaces[ns]).To(ContainElement(primaryFeast.GetFeastServiceName(services.ClientFeastType))) + Expect(registryData.Namespaces[ns]).To(ContainElement(crossFeast.GetFeastServiceName(services.ClientFeastType))) + + By("Cleaning up the cross-project FeatureStore") + Expect(k8sClient.Delete(ctx, crossProjectResource)).To(Succeed()) + }) + It("should correctly set container command args for grpc/rest modes", func() { controllerReconciler := &FeatureStoreReconciler{ Client: k8sClient, @@ -1361,6 +1526,109 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err.Error()).To(ContainSubstring("At least one of restAPI or grpc must be true")) }) + It("should generate correct feature_store.yaml when registry MCP is enabled", func() { + const mcpName = "mcp-registry" + mcpNsName := types.NamespacedName{ + Name: mcpName, + Namespace: "default", + } + + resource := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: mcpName, + Namespace: "default", + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: feastProject, + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + RestAPI: ptr(true), + Mcp: &feastdevv1.McpConfig{ + Enabled: true, + }, + }, + }, + }, + }, + }, + } + resource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: mcpNsName}) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, mcpNsName, resource) + Expect(err).NotTo(HaveOccurred()) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + deploy := &appsv1.Deployment{} + objMeta := feast.GetObjectMeta() + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, deploy) + Expect(err).NotTo(HaveOccurred()) + + registryContainer := services.GetRegistryContainer(*deploy) + Expect(registryContainer).NotTo(BeNil()) + + env := getFeatureStoreYamlEnvVar(registryContainer.Env) + Expect(env).NotTo(BeNil()) + + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Registry.Mcp).NotTo(BeNil()) + Expect(repoConfig.Registry.Mcp.Enabled).To(BeTrue()) + }) + + It("should reject registry MCP without restAPI enabled", func() { + mcpNoRestResource := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mcp-no-rest", + Namespace: "default", + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: feastProject, + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + RestAPI: ptr(false), + GRPC: ptr(true), + Mcp: &feastdevv1.McpConfig{ + Enabled: true, + }, + }, + }, + }, + }, + }, + } + mcpNoRestResource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) + + err := k8sClient.Create(ctx, mcpNoRestResource) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("MCP requires restAPI to be true")) + }) + It("should error on reconcile", func() { By("Trying to set the controller OwnerRef of a Deployment that already has a controller") controllerReconciler := &FeatureStoreReconciler{ diff --git a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go index 0af097120ce..bc60aed4374 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go @@ -193,6 +193,16 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) + + // verify init containers have TLS volume mounts after reconciliation + Expect(deploy.Spec.Template.Spec.InitContainers).NotTo(BeEmpty()) + for _, initContainer := range deploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement(SatisfyAll( + HaveField("MountPath", services.GetTlsPath(services.RegistryFeastType)), + HaveField("ReadOnly", true), + )), "init container %s should have registry TLS volume mount", initContainer.Name) + } + svc := &corev1.Service{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: feast.GetFeastServiceName(services.RegistryFeastType), @@ -401,6 +411,14 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(2)) + // verify init containers have remote registry TLS volume mounts + for _, initContainer := range deploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement(SatisfyAll( + HaveField("MountPath", services.GetTlsPath(services.RegistryFeastType)), + HaveField("ReadOnly", true), + )), "init container %s should have remote registry TLS volume mount", initContainer.Name) + } + // check offline config offlineContainer = services.GetOfflineContainer(*deploy) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) @@ -530,6 +548,12 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { HaveField("MountPath", tlsPathCustomCABundle), ))) } + for _, initContainer := range deploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement(SatisfyAll( + HaveField("Name", configMapName), + HaveField("MountPath", tlsPathCustomCABundle), + )), "init container %s should have CA bundle volume mount", initContainer.Name) + } }) It("should not mount CA bundle volume or container mounts when ConfigMap is absent", func() { @@ -570,5 +594,10 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { for _, container := range deploy.Spec.Template.Spec.Containers { Expect(container.VolumeMounts).NotTo(ContainElement(HaveField("Name", configMapName))) } + for _, initContainer := range deploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).NotTo(ContainElement( + HaveField("Name", configMapName), + ), "init container %s should not have CA bundle mount when ConfigMap is absent", initContainer.Name) + } }) }) diff --git a/infra/feast-operator/internal/controller/metrics/metrics.go b/infra/feast-operator/internal/controller/metrics/metrics.go new file mode 100644 index 00000000000..c29342aab2a --- /dev/null +++ b/infra/feast-operator/internal/controller/metrics/metrics.go @@ -0,0 +1,137 @@ +/* +Copyright 2026 Feast Community. + +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 + + http://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 metrics provides a Prometheus info gauge that records the store +// types configured for each FeatureStore CR (online store, offline store, +// registry). These operator-level metrics are distinct from the Feast +// feature-server application metrics (feast_feature_server_*) and are useful +// for usage telemetry and assessing the impact of removing store type support. +package metrics + +import ( + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + "github.com/prometheus/client_golang/prometheus" + ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +const ( + typeNone = "none" + labelName = "name" + labelNamespace = "namespace" +) + +// FeatureStoreMetrics holds the Prometheus GaugeVec for feast-operator +// installation telemetry. +type FeatureStoreMetrics struct { + FeatureStoreInfo *prometheus.GaugeVec +} + +// NewFeatureStoreMetrics creates a new FeatureStoreMetrics with the GaugeVec +// initialised but not yet registered. Call Register() before starting the manager. +func NewFeatureStoreMetrics() *FeatureStoreMetrics { + return &FeatureStoreMetrics{ + FeatureStoreInfo: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "feast_operator_feature_store_info", + Help: "Information about a deployed FeatureStore. " + + "Value is always 1. Labels carry the configured store types: " + + "'online_store_type', 'offline_store_type', and 'registry_type' " + + "are set to the persistence type (e.g. redis, snowflake.offline, local) " + + "or 'none' when that component is not configured.", + }, + []string{labelNamespace, labelName, "online_store_type", "offline_store_type", "registry_type"}, + ), + } +} + +// Register registers the metric with the controller-runtime metrics registry +// so it is exposed on the manager's /metrics endpoint. +func (m *FeatureStoreMetrics) Register() { + ctrlmetrics.Registry.MustRegister(m.FeatureStoreInfo) +} + +// RecordFeatureStore updates the gauge for the given FeatureStore using the +// applied configuration stored in status.Applied (which has operator defaults +// applied). The previous label set for this FeatureStore is deleted first so +// that store type changes are reflected cleanly on the next scrape. +func (m *FeatureStoreMetrics) RecordFeatureStore(fs *feastdevv1.FeatureStore) { + svcs := fs.Status.Applied.Services + m.FeatureStoreInfo.DeletePartialMatch(prometheus.Labels{ + labelNamespace: fs.Namespace, + labelName: fs.Name, + }) + m.FeatureStoreInfo.WithLabelValues( + fs.Namespace, + fs.Name, + onlineStoreType(svcs), + offlineStoreType(svcs), + registryType(svcs), + ).Set(1) +} + +// DeleteFeatureStore removes the metric label set for the given FeatureStore. +// Safe to call when the CR has already been deleted from the API server. +func (m *FeatureStoreMetrics) DeleteFeatureStore(namespace, name string) { + m.FeatureStoreInfo.DeletePartialMatch(prometheus.Labels{ + "namespace": namespace, + "name": name, + }) +} + +// onlineStoreType returns the online store persistence type or "none". +func onlineStoreType(svcs *feastdevv1.FeatureStoreServices) string { + if svcs == nil || svcs.OnlineStore == nil { + return typeNone + } + if p := svcs.OnlineStore.Persistence; p != nil && p.DBPersistence != nil { + return p.DBPersistence.Type + } + return "file" +} + +// offlineStoreType returns the offline store persistence type or "none". +func offlineStoreType(svcs *feastdevv1.FeatureStoreServices) string { + if svcs == nil || svcs.OfflineStore == nil { + return typeNone + } + if p := svcs.OfflineStore.Persistence; p != nil { + if p.DBPersistence != nil { + return p.DBPersistence.Type + } + if p.FilePersistence != nil && p.FilePersistence.Type != "" { + return p.FilePersistence.Type + } + } + return "file" +} + +// registryType returns "local", "remote", "remote_feastref", or "none". +func registryType(svcs *feastdevv1.FeatureStoreServices) string { + if svcs == nil || svcs.Registry == nil { + return typeNone + } + switch { + case svcs.Registry.Local != nil: + return "local" + case svcs.Registry.Remote != nil: + if svcs.Registry.Remote.FeastRef != nil { + return "remote_feastref" + } + return "remote" + default: + return typeNone + } +} diff --git a/infra/feast-operator/internal/controller/metrics/metrics_test.go b/infra/feast-operator/internal/controller/metrics/metrics_test.go new file mode 100644 index 00000000000..480a861560d --- /dev/null +++ b/infra/feast-operator/internal/controller/metrics/metrics_test.go @@ -0,0 +1,275 @@ +/* +Copyright 2026 Feast Community. + +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 + + http://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 metrics_test + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + . "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics" +) + +const testNamespace = "test-ns" + +// gaugeValue reads the float64 value for the given label values. +// Returns -1 if the metric is not found. +func gaugeValue(gv *prometheus.GaugeVec, labels ...string) float64 { + g, err := gv.GetMetricWithLabelValues(labels...) + if err != nil { + return -1 + } + m := &dto.Metric{} + if err := g.Write(m); err != nil { + return -1 + } + return m.GetGauge().GetValue() +} + +func featureStore(name string, svcs *feastdevv1.FeatureStoreServices) *feastdevv1.FeatureStore { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: name}, + } + fs.Status.Applied.Services = svcs + return fs +} + +func TestRecordFeatureStore_NoServices(t *testing.T) { + m := NewFeatureStoreMetrics() + m.RecordFeatureStore(featureStore("fs", nil)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "none", "none"); v != 1 { + t.Errorf("expected 1 for all-absent store, got %v", v) + } +} + +func TestRecordFeatureStore_OnlineStore_File(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{}, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "file", "none", "none"); v != 1 { + t.Errorf("expected 1 for file online store, got %v", v) + } +} + +func TestRecordFeatureStore_OnlineStore_Redis(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "none", "none"); v != 1 { + t.Errorf("expected 1 for redis online store, got %v", v) + } +} + +func TestRecordFeatureStore_OfflineStore_DB(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + DBPersistence: &feastdevv1.OfflineStoreDBStorePersistence{Type: "snowflake.offline"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "snowflake.offline", "none"); v != 1 { + t.Errorf("expected 1 for snowflake offline store, got %v", v) + } +} + +func TestRecordFeatureStore_OfflineStore_FilePersistenceType(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{Type: "duckdb"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "duckdb", "none"); v != 1 { + t.Errorf("expected 1 for duckdb offline store, got %v", v) + } +} + +func TestRecordFeatureStore_Registry_Local(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{}, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "none", "local"); v != 1 { + t.Errorf("expected 1 for local registry, got %v", v) + } +} + +func TestRecordFeatureStore_Registry_RemoteHostname(t *testing.T) { + hostname := "registry.example.com:443" + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{Hostname: &hostname}, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "none", "remote"); v != 1 { + t.Errorf("expected 1 for remote registry, got %v", v) + } +} + +func TestRecordFeatureStore_Registry_RemoteFeastRef(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + FeastRef: &feastdevv1.FeatureStoreRef{Name: "other-fs", Namespace: "other-ns"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "none", "remote_feastref"); v != 1 { + t.Errorf("expected 1 for remote_feastref registry, got %v", v) + } +} + +func TestRecordFeatureStore_AllComponents(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + DBPersistence: &feastdevv1.OfflineStoreDBStorePersistence{Type: "snowflake.offline"}, + }, + }, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{}, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "snowflake.offline", "local"); v != 1 { + t.Errorf("expected 1 for full store config, got %v", v) + } +} + +func TestRecordFeatureStore_TypeChange(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs1 := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + } + svcs2 := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "postgres"}, + }, + }, + } + + m.RecordFeatureStore(featureStore("fs", svcs1)) + m.RecordFeatureStore(featureStore("fs", svcs2)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "none", "none"); v != 0 { + t.Errorf("old label set (redis) should be removed after type change, got %v", v) + } + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "postgres", "none", "none"); v != 1 { + t.Errorf("new label set (postgres) should be 1 after type change, got %v", v) + } +} + +func TestDeleteFeatureStore_RemovesMetric(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "none", "none"); v != 1 { + t.Fatalf("setup: expected 1 before delete, got %v", v) + } + + m.DeleteFeatureStore(testNamespace, "fs") + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "none", "none"); v != 0 { + t.Errorf("expected 0 after DeleteFeatureStore, got %v", v) + } +} + +func TestMultipleFeatureStores_IndependentLabelSets(t *testing.T) { + m := NewFeatureStoreMetrics() + + svcs1 := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + } + svcs2 := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "postgres"}, + }, + }, + } + + m.RecordFeatureStore(featureStore("fs-1", svcs1)) + m.RecordFeatureStore(featureStore("fs-2", svcs2)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs-1", "redis", "none", "none"); v != 1 { + t.Errorf("fs-1: expected redis=1, got %v", v) + } + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs-2", "postgres", "none", "none"); v != 1 { + t.Errorf("fs-2: expected postgres=1, got %v", v) + } + + m.DeleteFeatureStore(testNamespace, "fs-1") + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs-2", "postgres", "none", "none"); v != 1 { + t.Errorf("fs-2 should be unaffected after fs-1 deletion, got %v", v) + } +} diff --git a/infra/feast-operator/internal/controller/services/batch_engine_rbac.go b/infra/feast-operator/internal/controller/services/batch_engine_rbac.go new file mode 100644 index 00000000000..97afcd3b48d --- /dev/null +++ b/infra/feast-operator/internal/controller/services/batch_engine_rbac.go @@ -0,0 +1,285 @@ +/* +Copyright 2024 Feast Community. + +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 + + http://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 services + +import ( + "embed" + "fmt" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/yaml" +) + +const ( + BatchEngineFeastType FeastServiceType = "batch-engine" + BatchDriverFeastType FeastServiceType = "batch-driver" +) + +//go:embed rbac_templates/*.yaml +var batchEngineRBACTemplates embed.FS + +// BatchEngineRBACTemplate declares RBAC requirements for a batch compute engine. +type BatchEngineRBACTemplate struct { + EngineType string `json:"engine_type" yaml:"engine_type"` + Server *RBACRoleSpec `json:"server,omitempty" yaml:"server,omitempty"` + Driver *DriverRBACSpec `json:"driver,omitempty" yaml:"driver,omitempty"` +} + +// RBACRoleSpec defines policy rules for a Role. +type RBACRoleSpec struct { + Rules []rbacv1.PolicyRule `json:"rules" yaml:"rules"` +} + +// DriverRBACSpec defines policy rules and optional SA creation for a driver Role. +type DriverRBACSpec struct { + CreateServiceAccount bool `json:"create_service_account" yaml:"create_service_account"` + Rules []rbacv1.PolicyRule `json:"rules" yaml:"rules"` +} + +func loadBatchEngineTemplate(engineType string) (*BatchEngineRBACTemplate, error) { + data, err := batchEngineRBACTemplates.ReadFile( + "rbac_templates/" + engineType + ".yaml", + ) + if err != nil { + return nil, nil + } + var tmpl BatchEngineRBACTemplate + if err := yaml.Unmarshal(data, &tmpl); err != nil { + return nil, fmt.Errorf("failed to parse RBAC template for engine %q: %w", engineType, err) + } + return &tmpl, nil +} + +func (feast *FeastServices) reconcileBatchEngineRBAC() error { + config, ok := feast.getBatchEngineConfig() + if !ok { + return feast.deleteBatchEngineRBAC() + } + + engineType, _ := config["type"].(string) + if engineType == "" { + return feast.deleteBatchEngineRBAC() + } + + tmpl, err := loadBatchEngineTemplate(engineType) + if err != nil { + return err + } + if tmpl == nil { + return feast.deleteBatchEngineRBAC() + } + + if tmpl.Server != nil { + if err := feast.ensureBatchEngineRole(BatchEngineFeastType, tmpl.Server.Rules); err != nil { + return err + } + if err := feast.ensureBatchEngineRoleBinding(BatchEngineFeastType, feast.initFeastSA().Name); err != nil { + return err + } + } + + if tmpl.Driver != nil { + driverSAName := resolveBatchDriverSAName(feast.Handler.FeatureStore, config) + if tmpl.Driver.CreateServiceAccount { + if err := feast.ensureBatchDriverServiceAccount(driverSAName); err != nil { + return err + } + } + if err := feast.ensureBatchEngineRole(BatchDriverFeastType, tmpl.Driver.Rules); err != nil { + return err + } + if err := feast.ensureBatchEngineRoleBinding(BatchDriverFeastType, driverSAName); err != nil { + return err + } + } + + return nil +} + +// getBatchEngineConfig returns the parsed batch-engine ConfigMap data. +// ok=false means no batch engine is configured or the ConfigMap is unreadable. +func (feast *FeastServices) getBatchEngineConfig() (map[string]interface{}, bool) { + appliedSpec := feast.Handler.FeatureStore.Status.Applied + if appliedSpec.BatchEngine == nil || appliedSpec.BatchEngine.ConfigMapRef == nil { + return nil, false + } + + configMapKey := appliedSpec.BatchEngine.ConfigMapKey + if configMapKey == "" { + configMapKey = "config" + } + + cm, err := feast.getConfigMap(appliedSpec.BatchEngine.ConfigMapRef.Name) + if err != nil { + return nil, false + } + + data, found := cm.Data[configMapKey] + if !found { + return nil, false + } + + var config map[string]interface{} + if err := yaml.Unmarshal([]byte(data), &config); err != nil { + return nil, false + } + return config, true +} + +// resolveBatchDriverSAName returns the ServiceAccount name for the Spark driver. +// If batch engine config sets a non-empty service_account, that value wins. +// Otherwise defaults to feast--batch-driver (same name used for RBAC). +func resolveBatchDriverSAName(featureStore *feastdevv1.FeatureStore, config map[string]interface{}) string { + if sa, ok := config["service_account"].(string); ok && sa != "" { + return sa + } + return GetFeastServiceName(featureStore, BatchDriverFeastType) +} + +func (feast *FeastServices) ensureBatchEngineRole(feastType FeastServiceType, rules []rbacv1.PolicyRule) error { + logger := log.FromContext(feast.Handler.Context) + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: feast.GetFeastServiceName(feastType), + Namespace: feast.Handler.FeatureStore.Namespace, + }, + } + role.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + + op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, role, func() error { + role.Labels = feast.getFeastTypeLabels(feastType) + role.Rules = rules + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, role, feast.Handler.Scheme) + }) + if err != nil { + return err + } + if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "Role", role.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) ensureBatchEngineRoleBinding(feastType FeastServiceType, saName string) error { + logger := log.FromContext(feast.Handler.Context) + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: feast.GetFeastServiceName(feastType), + Namespace: feast.Handler.FeatureStore.Namespace, + }, + } + roleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + + op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, roleBinding, func() error { + roleBinding.Labels = feast.getFeastTypeLabels(feastType) + roleBinding.Subjects = []rbacv1.Subject{{ + Kind: rbacv1.ServiceAccountKind, + Name: saName, + Namespace: feast.Handler.FeatureStore.Namespace, + }} + roleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: feast.GetFeastServiceName(feastType), + } + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, roleBinding, feast.Handler.Scheme) + }) + if err != nil { + return err + } + if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "RoleBinding", roleBinding.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) ensureBatchDriverServiceAccount(saName string) error { + logger := log.FromContext(feast.Handler.Context) + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: saName, + Namespace: feast.Handler.FeatureStore.Namespace, + }, + } + sa.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount")) + + op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, sa, func() error { + if sa.Labels == nil { + sa.Labels = map[string]string{} + } + for k, v := range feast.getFeastTypeLabels(BatchDriverFeastType) { + sa.Labels[k] = v + } + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, sa, feast.Handler.Scheme) + }) + if err != nil { + return err + } + if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "ServiceAccount", sa.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) deleteBatchEngineRBAC() error { + serverRoleName := feast.GetFeastServiceName(BatchEngineFeastType) + driverRoleName := feast.GetFeastServiceName(BatchDriverFeastType) + ns := feast.Handler.FeatureStore.Namespace + + serverRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: serverRoleName, Namespace: ns}, + } + serverRoleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + if err := feast.Handler.DeleteOwnedFeastObj(serverRoleBinding); err != nil { + return err + } + + serverRole := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: serverRoleName, Namespace: ns}, + } + serverRole.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + if err := feast.Handler.DeleteOwnedFeastObj(serverRole); err != nil { + return err + } + + driverRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: driverRoleName, Namespace: ns}, + } + driverRoleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + if err := feast.Handler.DeleteOwnedFeastObj(driverRoleBinding); err != nil { + return err + } + + driverRole := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: driverRoleName, Namespace: ns}, + } + driverRole.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + if err := feast.Handler.DeleteOwnedFeastObj(driverRole); err != nil { + return err + } + + driverSA := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: driverRoleName, Namespace: ns}, + } + driverSA.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount")) + return feast.Handler.DeleteOwnedFeastObj(driverSA) +} diff --git a/infra/feast-operator/internal/controller/services/batch_engine_rbac_test.go b/infra/feast-operator/internal/controller/services/batch_engine_rbac_test.go new file mode 100644 index 00000000000..9d792c6f923 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/batch_engine_rbac_test.go @@ -0,0 +1,195 @@ +/* +Copyright 2024 Feast Community. + +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 + + http://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 services + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" +) + +var _ = Describe("Batch Engine RBAC", func() { + + Describe("loadBatchEngineTemplate", func() { + It("should load spark_application template", func() { + tmpl, err := loadBatchEngineTemplate("spark_application") + Expect(err).NotTo(HaveOccurred()) + Expect(tmpl).NotTo(BeNil()) + Expect(tmpl.EngineType).To(Equal("spark_application")) + Expect(tmpl.Server).NotTo(BeNil()) + Expect(tmpl.Server.Rules).NotTo(BeEmpty()) + Expect(tmpl.Driver).NotTo(BeNil()) + Expect(tmpl.Driver.CreateServiceAccount).To(BeTrue()) + Expect(tmpl.Driver.Rules).NotTo(BeEmpty()) + }) + + It("should return nil for unknown engine type", func() { + tmpl, err := loadBatchEngineTemplate("nonexistent_engine") + Expect(err).NotTo(HaveOccurred()) + Expect(tmpl).To(BeNil()) + }) + + It("should contain correct server rules for spark_application", func() { + tmpl, err := loadBatchEngineTemplate("spark_application") + Expect(err).NotTo(HaveOccurred()) + + serverRules := tmpl.Server.Rules + Expect(serverRules).To(HaveLen(4)) + + hasConfigMapRule := false + hasSparkAppRule := false + hasPodListRule := false + hasPodLogRule := false + + for _, rule := range serverRules { + if containsResource(rule, "configmaps") && containsVerb(rule, "create") && containsVerb(rule, "delete") { + hasConfigMapRule = true + } + if containsResource(rule, "sparkapplications") && containsVerb(rule, "create") && containsVerb(rule, "get") && containsVerb(rule, "delete") { + hasSparkAppRule = true + } + if containsResource(rule, "pods") && containsVerb(rule, "list") { + hasPodListRule = true + } + if containsResource(rule, "pods/log") && containsVerb(rule, "get") { + hasPodLogRule = true + } + } + + Expect(hasConfigMapRule).To(BeTrue(), "should have configmaps create/delete rule") + Expect(hasSparkAppRule).To(BeTrue(), "should have sparkapplications create/get/delete rule") + Expect(hasPodListRule).To(BeTrue(), "should have pods list rule") + Expect(hasPodLogRule).To(BeTrue(), "should have pods/log get rule") + }) + + It("should contain correct driver rules for spark_application", func() { + tmpl, err := loadBatchEngineTemplate("spark_application") + Expect(err).NotTo(HaveOccurred()) + + driverRules := tmpl.Driver.Rules + Expect(driverRules).To(HaveLen(2)) + + hasPodRule := false + hasResourceRule := false + for _, rule := range driverRules { + if containsResource(rule, "pods") && + containsVerb(rule, "create") && + containsVerb(rule, "deletecollection") { + hasPodRule = true + } + if containsResource(rule, "services") && + containsResource(rule, "configmaps") && + containsResource(rule, "persistentvolumeclaims") && + containsVerb(rule, "deletecollection") { + hasResourceRule = true + } + } + Expect(hasPodRule).To(BeTrue(), "should have pods CRUD + deletecollection rule") + Expect(hasResourceRule).To(BeTrue(), "should have services/configmaps/PVCs CRUD + deletecollection rule") + }) + }) + + Describe("BatchEngineRBACTemplate YAML parsing", func() { + It("should correctly unmarshal a template", func() { + yamlData := ` +engine_type: test_engine +server: + rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] +driver: + create_service_account: true + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get"] +` + var tmpl BatchEngineRBACTemplate + err := yaml.Unmarshal([]byte(yamlData), &tmpl) + Expect(err).NotTo(HaveOccurred()) + Expect(tmpl.EngineType).To(Equal("test_engine")) + Expect(tmpl.Server).NotTo(BeNil()) + Expect(tmpl.Server.Rules).To(HaveLen(1)) + Expect(tmpl.Driver).NotTo(BeNil()) + Expect(tmpl.Driver.CreateServiceAccount).To(BeTrue()) + Expect(tmpl.Driver.Rules).To(HaveLen(1)) + }) + + It("should handle server-only template (no driver)", func() { + yamlData := ` +engine_type: server_only +server: + rules: + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["create", "delete"] +` + var tmpl BatchEngineRBACTemplate + err := yaml.Unmarshal([]byte(yamlData), &tmpl) + Expect(err).NotTo(HaveOccurred()) + Expect(tmpl.Server).NotTo(BeNil()) + Expect(tmpl.Driver).To(BeNil()) + }) + }) +}) + +var _ = Describe("resolveBatchDriverSAName", func() { + It("defaults to feast--batch-driver when service_account is omitted", func() { + fs := &feastdevv1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: "spark-pg-e2e", Namespace: "feast-spark"}} + Expect(resolveBatchDriverSAName(fs, map[string]interface{}{ + "type": "spark_application", + "image": "quay.io/example/driver:v1", + })).To(Equal("feast-spark-pg-e2e-batch-driver")) + }) + + It("defaults when service_account is empty string", func() { + fs := &feastdevv1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: "spark-pg-e2e"}} + Expect(resolveBatchDriverSAName(fs, map[string]interface{}{ + "service_account": "", + })).To(Equal("feast-spark-pg-e2e-batch-driver")) + }) + + It("keeps an explicit service_account override", func() { + fs := &feastdevv1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: "spark-pg-e2e"}} + Expect(resolveBatchDriverSAName(fs, map[string]interface{}{ + "service_account": "my-custom-driver", + })).To(Equal("my-custom-driver")) + }) +}) + +func containsResource(rule rbacv1.PolicyRule, resource string) bool { + for _, r := range rule.Resources { + if r == resource { + return true + } + } + return false +} + +func containsVerb(rule rbacv1.PolicyRule, verb string) bool { + for _, v := range rule.Verbs { + if v == verb { + return true + } + } + return false +} diff --git a/infra/feast-operator/internal/controller/services/client.go b/infra/feast-operator/internal/controller/services/client.go index fbd972368fb..4fcd9b894e7 100644 --- a/infra/feast-operator/internal/controller/services/client.go +++ b/infra/feast-operator/internal/controller/services/client.go @@ -47,7 +47,7 @@ func (feast *FeastServices) createClientConfigMap() error { func (feast *FeastServices) setClientConfigMap(cm *corev1.ConfigMap) error { cm.Labels = feast.getFeastTypeLabels(ClientFeastType) - clientYaml, err := feast.getClientFeatureStoreYaml(feast.extractConfigFromSecret) + clientYaml, err := feast.getClientFeatureStoreYaml() if err != nil { return err } @@ -74,7 +74,7 @@ func (feast *FeastServices) setCaConfigMap(cm *corev1.ConfigMap) error { if len(cm.Annotations) == 0 { cm.Annotations = map[string]string{} } - cm.Annotations["service.beta.openshift.io/inject-cabundle"] = "true" + cm.Annotations[openshiftInjectCaBundleAnnotation] = stringTrue return controllerutil.SetControllerReference(feast.Handler.FeatureStore, cm, feast.Handler.Scheme) } diff --git a/infra/feast-operator/internal/controller/services/namespace_registry.go b/infra/feast-operator/internal/controller/services/namespace_registry.go index 64cdaebd6f0..122e7ba9e98 100644 --- a/infra/feast-operator/internal/controller/services/namespace_registry.go +++ b/infra/feast-operator/internal/controller/services/namespace_registry.go @@ -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 { @@ -163,42 +177,38 @@ func (feast *FeastServices) createNamespaceRegistryRoleBinding(targetNamespace s // setNamespaceRegistryRoleBinding sets the RoleBinding for namespace registry access func (feast *FeastServices) setNamespaceRegistryRoleBinding(rb *rbacv1.RoleBinding) error { - // Create a Role that allows reading the ConfigMap + roleName := NamespaceRegistryConfigMapName + "-reader" + + desiredRules := []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + ResourceNames: []string{NamespaceRegistryConfigMapName}, + Verbs: []string{"get", "list"}, + }, + } + role := &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ - Name: NamespaceRegistryConfigMapName + "-reader", + Name: roleName, Namespace: rb.Namespace, }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"configmaps"}, - ResourceNames: []string{NamespaceRegistryConfigMapName}, - Verbs: []string{"get", "list"}, - }, - }, } + role.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) - // Create or update the Role - if _, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, role, controllerutil.MutateFn(func() error { - role.Rules = []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"configmaps"}, - ResourceNames: []string{NamespaceRegistryConfigMapName}, - Verbs: []string{"get", "list"}, - }, - } + if _, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, role, func() error { + role.Labels = feast.getLabels() + role.Rules = desiredRules return nil - })); err != nil { - return err + }); err != nil { + return fmt.Errorf("failed to reconcile namespace registry Role: %w", err) } - // Set the RoleBinding + rb.Labels = feast.getLabels() rb.RoleRef = rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "Role", - Name: role.Name, + Name: roleName, } rb.Subjects = []rbacv1.Subject{ @@ -234,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 { diff --git a/infra/feast-operator/internal/controller/services/rbac_templates/spark_application.yaml b/infra/feast-operator/internal/controller/services/rbac_templates/spark_application.yaml new file mode 100644 index 00000000000..c03e0f3db57 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/rbac_templates/spark_application.yaml @@ -0,0 +1,26 @@ +engine_type: spark_application + +server: + rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["create", "delete"] + - apiGroups: ["sparkoperator.k8s.io"] + resources: ["sparkapplications"] + verbs: ["create", "get", "delete"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["list"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] + +driver: + create_service_account: true + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["create", "get", "list", "watch", "delete", "deletecollection"] + - apiGroups: [""] + resources: ["services", "configmaps", "persistentvolumeclaims"] + verbs: ["create", "get", "list", "watch", "delete", "deletecollection"] diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 9b20955f324..b4a01b312b1 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -19,6 +19,7 @@ package services import ( "encoding/base64" "fmt" + "os" "path" "strings" @@ -26,6 +27,8 @@ import ( "gopkg.in/yaml.v3" ) +const oidcIssuerUrlEnvVar = "OIDC_ISSUER_URL" + // GetServiceFeatureStoreYamlBase64 returns a base64 encoded feature_store.yaml config for the feast service func (feast *FeastServices) GetServiceFeatureStoreYamlBase64() (string, error) { fsYaml, err := feast.getServiceFeatureStoreYaml() @@ -44,14 +47,16 @@ func (feast *FeastServices) getServiceFeatureStoreYaml() ([]byte, error) { } func (feast *FeastServices) getServiceRepoConfig() (RepoConfig, error) { - return getServiceRepoConfig(feast.Handler.FeatureStore, feast.extractConfigFromSecret, feast.extractConfigFromConfigMap) + odhCaBundleExists := feast.GetCustomCertificatesBundle().IsDefined + return getServiceRepoConfig(feast.Handler.FeatureStore, feast.extractConfigFromSecret, feast.extractConfigFromConfigMap, odhCaBundleExists) } func getServiceRepoConfig( featureStore *feastdevv1.FeatureStore, secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), - configMapExtractionFunc func(configMapRef string, configMapKey string) (map[string]interface{}, error)) (RepoConfig, error) { - repoConfig, err := getBaseServiceRepoConfig(featureStore, secretExtractionFunc) + configMapExtractionFunc func(configMapRef string, configMapKey string) (map[string]interface{}, error), + odhCaBundleExists bool) (RepoConfig, error) { + repoConfig, err := getBaseServiceRepoConfig(featureStore, secretExtractionFunc, odhCaBundleExists) if err != nil { return repoConfig, err } @@ -80,50 +85,132 @@ func getServiceRepoConfig( } if appliedSpec.BatchEngine != nil { - err := setRepoConfigBatchEngine(appliedSpec.BatchEngine, configMapExtractionFunc, &repoConfig) + err := setRepoConfigBatchEngine(featureStore, appliedSpec.BatchEngine, configMapExtractionFunc, &repoConfig) if err != nil { return repoConfig, err } } + if appliedSpec.Services != nil && appliedSpec.Services.OnlineStore != nil && + appliedSpec.Services.OnlineStore.Serving != nil { + setRepoConfigFeatureServer(appliedSpec.Services.OnlineStore.Serving, &repoConfig) + } + + if appliedSpec.Materialization != nil { + setRepoConfigMaterialization(appliedSpec.Materialization, &repoConfig) + } + + if appliedSpec.OpenLineage != nil { + if err := setRepoConfigOpenLineage(appliedSpec.OpenLineage, secretExtractionFunc, &repoConfig); err != nil { + return repoConfig, err + } + } + + if appliedSpec.DataQualityMonitoring != nil { + setRepoConfigDataQualityMonitoring(appliedSpec.DataQualityMonitoring, &repoConfig) + } + return repoConfig, nil } func getBaseServiceRepoConfig( featureStore *feastdevv1.FeatureStore, - secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { + secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), + odhCaBundleExists bool) (RepoConfig, error) { repoConfig := defaultRepoConfig(featureStore) - clientRepoConfig, err := getClientRepoConfig(featureStore, secretExtractionFunc, nil) - if err != nil { - return repoConfig, err - } + clientRepoConfig := getClientRepoConfig(featureStore, nil) if isRemoteRegistry(featureStore) { repoConfig.Registry = clientRepoConfig.Registry } - repoConfig.AuthzConfig = clientRepoConfig.AuthzConfig - appliedSpec := featureStore.Status.Applied if appliedSpec.AuthzConfig != nil && appliedSpec.AuthzConfig.OidcAuthz != nil { - propertiesMap, authSecretErr := secretExtractionFunc("", appliedSpec.AuthzConfig.OidcAuthz.SecretRef.Name, "") - if authSecretErr != nil { - return repoConfig, authSecretErr - } - + repoConfig.AuthzConfig = AuthzConfig{Type: OidcAuthType} + oidcAuthz := appliedSpec.AuthzConfig.OidcAuthz oidcParameters := map[string]interface{}{} - for _, oidcProperty := range OidcProperties { - if val, exists := propertiesMap[string(oidcProperty)]; exists { - oidcParameters[string(oidcProperty)] = val - } else { - return repoConfig, missingOidcSecretProperty(oidcProperty) + + var secretProperties map[string]interface{} + if oidcAuthz.SecretRef != nil { + var err error + secretProperties, err = secretExtractionFunc("", oidcAuthz.SecretRef.Name, oidcAuthz.SecretKeyName) + if err != nil { + return repoConfig, err + } + for _, prop := range OidcOptionalSecretProperties { + if val, exists := secretProperties[string(prop)]; exists { + // Secret values are YAML-parsed on extraction, so an + // all-digits audience or issuer arrives as an int and + // would render unquoted, which the SDK's OidcAuthConfig + // rejects (Optional[str]). Coerce the claim keys back to + // strings; the five original keys keep their historical + // typing. + if prop == OidcAudience || prop == OidcIssuer { + if _, isString := val.(string); !isString { + val = fmt.Sprintf("%v", val) + } + } + oidcParameters[string(prop)] = val + } } } + + discoveryUrl, err := resolveAuthDiscoveryUrl(oidcAuthz, secretProperties) + if err != nil { + return repoConfig, err + } + oidcParameters[string(OidcAuthDiscoveryUrl)] = discoveryUrl + + if oidcAuthz.VerifySSL != nil { + oidcParameters[string(OidcVerifySsl)] = *oidcAuthz.VerifySSL + } + if caCertPath := resolveOidcCACertPath(oidcAuthz, odhCaBundleExists); caCertPath != "" { + oidcParameters[string(OidcCaCertPath)] = caCertPath + } repoConfig.AuthzConfig.OidcParameters = oidcParameters + } else { + repoConfig.AuthzConfig = clientRepoConfig.AuthzConfig } return repoConfig, nil } +// resolveAuthDiscoveryUrl determines the OIDC discovery URL from the first available source. +// Priority: CR issuerUrl > Secret auth_discovery_url > OIDC_ISSUER_URL env var. +func resolveAuthDiscoveryUrl(oidcAuthz *feastdevv1.OidcAuthz, secretProperties map[string]interface{}) (string, error) { + if oidcAuthz.IssuerUrl != "" { + return issuerToDiscoveryUrl(oidcAuthz.IssuerUrl), nil + } + + if val, ok := secretProperties[string(OidcAuthDiscoveryUrl)]; ok { + if s, ok := val.(string); ok && s != "" { + return s, nil + } + } + + if envIssuer := os.Getenv(oidcIssuerUrlEnvVar); envIssuer != "" { + return issuerToDiscoveryUrl(envIssuer), nil + } + + return "", fmt.Errorf("no OIDC discovery URL configured: set issuerUrl on the OidcAuthz CR, "+ + "include auth_discovery_url in the referenced Secret, or ensure the %s environment variable is set on the operator pod", oidcIssuerUrlEnvVar) +} + +func issuerToDiscoveryUrl(issuerUrl string) string { + return strings.TrimRight(issuerUrl, "/") + "/.well-known/openid-configuration" +} + +// resolveOidcCACertPath determines the CA cert file path for OIDC provider TLS verification. +// Priority: explicit CRD caCertConfigMap > ODH auto-detected bundle > empty (system CA fallback). +func resolveOidcCACertPath(oidcAuthz *feastdevv1.OidcAuthz, odhCaBundleExists bool) string { + if oidcAuthz.CACertConfigMap != nil { + return tlsPathOidcCA + } + if odhCaBundleExists { + return tlsPathOdhCABundle + } + return "" +} + func setRepoConfigRegistry(services *feastdevv1.FeatureStoreServices, secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { registryPersistence := services.Registry.Local.Persistence @@ -187,6 +274,15 @@ func setRepoConfigRegistry(services *feastdevv1.FeatureStoreServices, secretExtr repoConfig.Registry.DBParameters = parametersMap } } + + if services.Registry.Local.Server != nil && + services.Registry.Local.Server.Mcp != nil && + services.Registry.Local.Server.Mcp.Enabled { + repoConfig.Registry.Mcp = &RegistryMcpYamlConfig{ + Enabled: true, + } + } + return nil } @@ -257,6 +353,7 @@ func setRepoConfigOffline(services *feastdevv1.FeatureStoreServices, secretExtra } func setRepoConfigBatchEngine( + featureStore *feastdevv1.FeatureStore, batchEngineConfig *feastdevv1.BatchEngineConfig, configMapExtractionFunc func(configMapRef string, configMapKey string) (map[string]interface{}, error), repoConfig *RepoConfig) error { @@ -277,6 +374,12 @@ func setRepoConfigBatchEngine( return fmt.Errorf("batch engine config must contain 'type' field") } delete(config, "type") + // Inject service_account only for spark_application so baked feature_store.yaml + // matches the SA/RoleBinding created by reconcileBatchEngineRBAC. + // Other batch engines are left unchanged. + if engineType == "spark_application" { + config["service_account"] = resolveBatchDriverSAName(featureStore, config) + } repoConfig.BatchEngine = &ComputeEngineConfig{ Type: engineType, Parameters: config, @@ -284,24 +387,195 @@ func setRepoConfigBatchEngine( return nil } -func (feast *FeastServices) getClientFeatureStoreYaml(secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) ([]byte, error) { - clientRepo, err := getClientRepoConfig(feast.Handler.FeatureStore, secretExtractionFunc, feast) - if err != nil { - return []byte{}, err +// setRepoConfigFeatureServer maps the CRD ServingConfig into the feature_server YAML block. +// Type is set to "mcp" only when fs.Mcp is non-nil and fs.Mcp.Enabled is true; otherwise "local". +func setRepoConfigFeatureServer(fs *feastdevv1.ServingConfig, repoConfig *RepoConfig) { + serverType := "local" + if fs.Mcp != nil && fs.Mcp.Enabled { + serverType = "mcp" + } + + yamlCfg := &FeatureServerYamlConfig{ + Type: serverType, + } + + if fs.Metrics != nil { + m := &MetricsYamlConfig{ + Enabled: fs.Metrics.Enabled, + } + if len(fs.Metrics.Categories) > 0 { + m.Categories = make(map[string]interface{}, len(fs.Metrics.Categories)) + for k, v := range fs.Metrics.Categories { + m.Categories[k] = v + } + } + yamlCfg.Metrics = m + } + + if fs.OfflinePushBatching != nil { + enabled := fs.OfflinePushBatching.Enabled + yamlCfg.OfflinePushBatchingEnabled = &enabled + yamlCfg.OfflinePushBatchingBatchSize = fs.OfflinePushBatching.BatchSize + yamlCfg.OfflinePushBatchingBatchIntervalSeconds = fs.OfflinePushBatching.BatchIntervalSeconds } + + if fs.Mcp != nil && fs.Mcp.Enabled { + enabled := fs.Mcp.Enabled + yamlCfg.McpEnabled = &enabled + yamlCfg.McpServerName = fs.Mcp.ServerName + yamlCfg.McpServerVersion = fs.Mcp.ServerVersion + yamlCfg.McpTransport = fs.Mcp.Transport + } + + repoConfig.FeatureServer = yamlCfg +} + +// setRepoConfigMaterialization maps the CRD MaterializationConfig into the materialization YAML block. +func setRepoConfigMaterialization(mat *feastdevv1.MaterializationConfig, repoConfig *RepoConfig) { + yamlCfg := &MaterializationYamlConfig{ + OnlineWriteBatchSize: mat.OnlineWriteBatchSize, + } + if len(mat.ExtraConfig) > 0 { + ec := make(map[string]interface{}, len(mat.ExtraConfig)) + for k, v := range mat.ExtraConfig { + ec[k] = coerceStringToYamlType(v) + } + yamlCfg.ExtraConfig = ec + } + repoConfig.Materialization = yamlCfg +} + +// setRepoConfigOpenLineage maps the CRD OpenLineageConfig into the openlineage YAML block. +// When ApiKeySecretRef is set, the api_key value is resolved from the referenced Secret. +// ExtraConfig string values are coerced to native YAML types (bool/int) so that Feast's +// StrictBool/StrictInt Pydantic validators accept them correctly. +func setRepoConfigOpenLineage( + ol *feastdevv1.OpenLineageConfig, + secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), + repoConfig *RepoConfig) error { + + yamlCfg := &OpenLineageYamlConfig{ + Enabled: ol.Enabled, + TransportType: ol.TransportType, + TransportUrl: ol.TransportUrl, + TransportEndpoint: ol.TransportEndpoint, + } + if len(ol.ExtraConfig) > 0 { + ec := make(map[string]interface{}, len(ol.ExtraConfig)) + for k, v := range ol.ExtraConfig { + ec[k] = coerceStringToYamlType(v) + } + yamlCfg.ExtraConfig = ec + } + + if ol.ApiKeySecretRef != nil { + params, err := secretExtractionFunc("", ol.ApiKeySecretRef.Name, "") + if err != nil { + return fmt.Errorf("failed to read OpenLineage API key from secret %s: %w", ol.ApiKeySecretRef.Name, err) + } + apiKey, exists := params["api_key"] + if !exists { + return fmt.Errorf("secret %q does not contain the required key \"api_key\"", ol.ApiKeySecretRef.Name) + } + apiKeyStr, ok := apiKey.(string) + if !ok { + return fmt.Errorf("key \"api_key\" in secret %q must be a string, got %T", ol.ApiKeySecretRef.Name, apiKey) + } + yamlCfg.ApiKey = &apiKeyStr + } + + if ol.Consumer != nil { + consumerCfg := &OpenLineageConsumerYamlConfig{ + Enabled: ol.Consumer.Enabled, + StoreType: ol.Consumer.StoreType, + NamespaceMapping: ol.Consumer.NamespaceMapping, + } + + if ol.Consumer.ConnectionStringSecretRef != nil { + params, err := secretExtractionFunc("", ol.Consumer.ConnectionStringSecretRef.Name, "") + if err != nil { + return fmt.Errorf("failed to read consumer connection string from secret %s: %w", + ol.Consumer.ConnectionStringSecretRef.Name, err) + } + connStr, exists := params["connection_string"] + if !exists { + return fmt.Errorf("secret %q does not contain the required key \"connection_string\"", + ol.Consumer.ConnectionStringSecretRef.Name) + } + connStrStr, ok := connStr.(string) + if !ok { + return fmt.Errorf("key \"connection_string\" in secret %q must be a string, got %T", + ol.Consumer.ConnectionStringSecretRef.Name, connStr) + } + consumerCfg.ConnectionString = &connStrStr + } + + if ol.Consumer.ApiKeySecretRef != nil { + params, err := secretExtractionFunc("", ol.Consumer.ApiKeySecretRef.Name, "") + if err != nil { + return fmt.Errorf("failed to read consumer API key from secret %s: %w", + ol.Consumer.ApiKeySecretRef.Name, err) + } + apiKey, exists := params["api_key"] + if !exists { + return fmt.Errorf("secret %q does not contain the required key \"api_key\"", + ol.Consumer.ApiKeySecretRef.Name) + } + apiKeyStr, ok := apiKey.(string) + if !ok { + return fmt.Errorf("key \"api_key\" in secret %q must be a string, got %T", + ol.Consumer.ApiKeySecretRef.Name, apiKey) + } + consumerCfg.ApiKey = &apiKeyStr + } + + yamlCfg.Consumer = consumerCfg + } + + repoConfig.OpenLineage = yamlCfg + return nil +} + +// coerceStringToYamlType converts "true"/"false" strings to native Go booleans +// so the YAML marshaler emits an unquoted boolean rather than a quoted string. +// This is required because CRD map[string]string fields can only hold strings, +// but Feast Pydantic StrictBool fields reject string inputs. +// +// Integer coercion is intentionally omitted: some ExtraConfig target fields are +// typed as StrictStr (e.g. OpenLineageConfig.namespace, .producer), and coercing +// a numeric string like "123" to int64 would cause a Pydantic validation failure +// at runtime. Fields that genuinely require integers should be exposed as typed +// CRD fields rather than going through ExtraConfig. +func coerceStringToYamlType(v string) interface{} { + switch v { + case stringTrue: + return true + case stringFalse: + return false + } + return v +} + +func setRepoConfigDataQualityMonitoring(dqmConfig *feastdevv1.DataQualityMonitoringConfig, repoConfig *RepoConfig) { + if dqmConfig.AutoBaseline == nil { + return + } + repoConfig.DataQualityMonitoring = &DataQualityMonitoringYamlConfig{ + AutoBaseline: *dqmConfig.AutoBaseline, + } +} + +func (feast *FeastServices) getClientFeatureStoreYaml() ([]byte, error) { + clientRepo := getClientRepoConfig(feast.Handler.FeatureStore, feast) return yaml.Marshal(clientRepo) } func getClientRepoConfig( featureStore *feastdevv1.FeatureStore, - secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), - feast *FeastServices) (RepoConfig, error) { + feast *FeastServices) RepoConfig { status := featureStore.Status appliedServices := status.Applied.Services - clientRepoConfig, err := getRepoConfig(featureStore, secretExtractionFunc) - if err != nil { - return clientRepoConfig, err - } + clientRepoConfig := getRepoConfig(featureStore) if len(status.ServiceHostnames.OfflineStore) > 0 { clientRepoConfig.OfflineStore = OfflineStoreConfig{ Type: OfflineRemoteConfigType, @@ -339,12 +613,10 @@ func getClientRepoConfig( } } - return clientRepoConfig, nil + return clientRepoConfig } -func getRepoConfig( - featureStore *feastdevv1.FeatureStore, - secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { +func getRepoConfig(featureStore *feastdevv1.FeatureStore) RepoConfig { status := featureStore.Status repoConfig := initRepoConfig(status.Applied.FeastProject) if status.Applied.AuthzConfig != nil { @@ -356,24 +628,16 @@ func getRepoConfig( repoConfig.AuthzConfig = AuthzConfig{ Type: OidcAuthType, } - - propertiesMap, err := secretExtractionFunc("", status.Applied.AuthzConfig.OidcAuthz.SecretRef.Name, "") - if err != nil { - return repoConfig, err - } - oidcClientProperties := map[string]interface{}{} - for _, oidcProperty := range OidcProperties { - if val, exists := propertiesMap[string(oidcProperty)]; exists { - oidcClientProperties[string(oidcProperty)] = val - } else { - return repoConfig, missingOidcSecretProperty(oidcProperty) - } + if status.Applied.AuthzConfig.OidcAuthz.TokenEnvVar != nil { + oidcClientProperties[string(OidcTokenEnvVar)] = *status.Applied.AuthzConfig.OidcAuthz.TokenEnvVar + } + if len(oidcClientProperties) > 0 { + repoConfig.AuthzConfig.OidcParameters = oidcClientProperties } - repoConfig.AuthzConfig.OidcParameters = oidcClientProperties } } - return repoConfig, nil + return repoConfig } func getActualPath(filePath string, pvcConfig *feastdevv1.PvcConfig) string { diff --git a/infra/feast-operator/internal/controller/services/repo_config_test.go b/infra/feast-operator/internal/controller/services/repo_config_test.go index 70869568dea..e87efdf7dec 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -30,6 +30,8 @@ import ( var projectName = "test-project" +const marquezUrl = "http://marquez:5000" + var _ = Describe("Repo Config", func() { Context("When creating the RepoConfig of a FeatureStore", func() { It("should successfully create the repo configs", func() { @@ -46,7 +48,7 @@ var _ = Describe("Repo Config", func() { Path: EphemeralPath + "/" + DefaultOnlineStorePath, } - repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap) + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) @@ -74,7 +76,7 @@ var _ = Describe("Repo Config", func() { Path: testPath, } - repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) @@ -96,7 +98,7 @@ var _ = Describe("Repo Config", func() { Expect(appliedServices.OnlineStore).NotTo(BeNil()) Expect(appliedServices.Registry.Local).NotTo(BeNil()) - repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(defaultOfflineStoreConfig)) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) @@ -115,7 +117,7 @@ var _ = Describe("Repo Config", func() { }, } ApplyDefaultsToStatus(featureStore) - repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) @@ -135,7 +137,7 @@ var _ = Describe("Repo Config", func() { OnlineStore: &feastdevv1.OnlineStore{ Persistence: &feastdevv1.OnlineStorePersistence{ FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ - Path: "/data/online.db", + Path: dataOnlineDbPath, }, }, }, @@ -143,7 +145,7 @@ var _ = Describe("Repo Config", func() { Local: &feastdevv1.LocalRegistryConfig{ Persistence: &feastdevv1.RegistryPersistence{ FilePersistence: &feastdevv1.RegistryFilePersistence{ - Path: "/data/registry.db", + Path: dataRegistryDbPath, }, }, }, @@ -156,14 +158,14 @@ var _ = Describe("Repo Config", func() { } expectedRegistryConfig = RegistryConfig{ RegistryType: "file", - Path: "/data/registry.db", + Path: dataRegistryDbPath, } expectedOnlineConfig = OnlineStoreConfig{ Type: "sqlite", - Path: "/data/online.db", + Path: dataOnlineDbPath, } - repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) @@ -188,18 +190,18 @@ var _ = Describe("Repo Config", func() { Type: "dask", } - repoConfig, err = getServiceRepoConfig(featureStore, mockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap) + repoConfig, err = getServiceRepoConfig(featureStore, mockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) Expect(repoConfig.OnlineStore).To(Equal(defaultOnlineStoreConfig(featureStore))) Expect(repoConfig.Registry).To(Equal(defaultRegistryConfig(featureStore))) - By("Having oidc authorization") + By("Having oidc authorization with Secret") featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ OidcAuthz: &feastdevv1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ - Name: "oidc-secret", + SecretRef: &corev1.LocalObjectReference{ + Name: oidcSecretName, }, }, } @@ -207,32 +209,69 @@ var _ = Describe("Repo Config", func() { secretExtractionFunc := mockOidcConfigFromSecret(map[string]interface{}{ string(OidcAuthDiscoveryUrl): "discovery-url", - string(OidcClientId): "client-id", + string(OidcClientId): clientIDValue, string(OidcClientSecret): "client-secret", string(OidcUsername): "username", - string(OidcPassword): "password"}) - repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap) + string(OidcPassword): "password", + string(OidcAudience): "api://feast-feature-server", + string(OidcIssuer): "https://login.example.com/realms/master"}) + repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(5)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(7)) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientId))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcAuthDiscoveryUrl))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientSecret))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcUsername))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcPassword))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcAudience), "api://feast-feature-server")) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcIssuer), "https://login.example.com/realms/master")) Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) Expect(repoConfig.OnlineStore).To(Equal(defaultOnlineStoreConfig(featureStore))) Expect(repoConfig.Registry).To(Equal(defaultRegistryConfig(featureStore))) - repoConfig, err = getClientRepoConfig(featureStore, secretExtractionFunc, nil) + repoConfig = getClientRepoConfig(featureStore, nil) + Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) + + By("Coercing numeric audience and issuer Secret values to strings") + secretExtractionFunc = mockOidcConfigFromSecret(map[string]interface{}{ + string(OidcAuthDiscoveryUrl): "discovery-url", + string(OidcClientId): clientIDValue, + // Secret extraction YAML-parses values, so an all-digits + // audience/issuer reaches this code as an int. + string(OidcAudience): 1234567890, + string(OidcIssuer): 9876543210}) + repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcAudience), "1234567890")) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcIssuer), "9876543210")) + + By("Having oidc authorization with issuerUrl only (no Secret)") + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{ + IssuerUrl: "https://keycloak.example.com/realms/test", + }, + } + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(5)) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientId))) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcAuthDiscoveryUrl))) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientSecret))) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcUsername))) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcPassword))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(1)) + Expect(repoConfig.AuthzConfig.OidcParameters[string(OidcAuthDiscoveryUrl)]).To(Equal("https://keycloak.example.com/realms/test/.well-known/openid-configuration")) + + By("Having oidc with issuerUrl on CR and auth_discovery_url in Secret — CR wins") + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{ + IssuerUrl: "https://keycloak.example.com/realms/cr-wins", + SecretRef: &corev1.LocalObjectReference{ + Name: oidcSecretName, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.OidcParameters[string(OidcAuthDiscoveryUrl)]).To(Equal("https://keycloak.example.com/realms/cr-wins/.well-known/openid-configuration")) By("Having the all the db services") featureStore = minimalFeatureStore() @@ -275,7 +314,7 @@ var _ = Describe("Repo Config", func() { featureStore.Spec.Services.OfflineStore.Persistence.FilePersistence = nil featureStore.Spec.Services.OnlineStore.Persistence.FilePersistence = nil featureStore.Spec.Services.Registry.Local.Persistence.FilePersistence = nil - repoConfig, err = getServiceRepoConfig(featureStore, mockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap) + repoConfig, err = getServiceRepoConfig(featureStore, mockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) newMap := CopyMap(parameterMap) port := parameterMap["port"].(int) @@ -296,38 +335,465 @@ var _ = Describe("Repo Config", func() { Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) + + By("Having DQM config with auto_baseline disabled") + featureStore = minimalFeatureStore() + dqmAutoBaseline := false + featureStore.Spec.DataQualityMonitoring = &feastdevv1.DataQualityMonitoringConfig{ + AutoBaseline: &dqmAutoBaseline, + } + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.DataQualityMonitoring).NotTo(BeNil()) + Expect(repoConfig.DataQualityMonitoring.AutoBaseline).To(BeFalse()) + + fsYaml, marshalErr := yaml.Marshal(repoConfig) + Expect(marshalErr).NotTo(HaveOccurred()) + Expect(string(fsYaml)).To(ContainSubstring("data_quality_monitoring:")) + Expect(string(fsYaml)).To(ContainSubstring("auto_baseline: false")) + + By("Having no DataQualityMonitoring config — should be nil") + featureStore = minimalFeatureStore() + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.DataQualityMonitoring).To(BeNil()) + }) + + It("should set feature_server block with type local and all options", func() { + featureStore := minimalFeatureStore() + batchSize := int32(500) + batchInterval := int32(15) + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Serving: &feastdevv1.ServingConfig{ + Metrics: &feastdevv1.ServingMetricsConfig{ + Enabled: true, + Categories: map[string]bool{ + "resource": true, + "freshness": false, + "registry_sync": false, + }, + }, + OfflinePushBatching: &feastdevv1.OfflinePushBatchingConfig{ + Enabled: true, + BatchSize: &batchSize, + BatchIntervalSeconds: &batchInterval, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.FeatureServer).NotTo(BeNil()) + Expect(repoConfig.FeatureServer.Type).To(Equal("local")) + + Expect(repoConfig.FeatureServer.Metrics).NotTo(BeNil()) + Expect(repoConfig.FeatureServer.Metrics.Enabled).To(BeTrue()) + Expect(repoConfig.FeatureServer.Metrics.Categories).To(HaveKeyWithValue("resource", true)) + Expect(repoConfig.FeatureServer.Metrics.Categories).To(HaveKeyWithValue("freshness", false)) + Expect(repoConfig.FeatureServer.Metrics.Categories).To(HaveKeyWithValue("registry_sync", false)) + + Expect(repoConfig.FeatureServer.OfflinePushBatchingEnabled).NotTo(BeNil()) + Expect(*repoConfig.FeatureServer.OfflinePushBatchingEnabled).To(BeTrue()) + Expect(repoConfig.FeatureServer.OfflinePushBatchingBatchSize).To(Equal(&batchSize)) + Expect(repoConfig.FeatureServer.OfflinePushBatchingBatchIntervalSeconds).To(Equal(&batchInterval)) + + Expect(repoConfig.FeatureServer.McpEnabled).To(BeNil()) + }) + + It("should set feature_server block with type mcp", func() { + featureStore := minimalFeatureStore() + serverName := "my-mcp-server" + serverVersion := "2.0.0" + transport := HttpScheme + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Serving: &feastdevv1.ServingConfig{ + Mcp: &feastdevv1.McpConfig{ + Enabled: true, + ServerName: &serverName, + ServerVersion: &serverVersion, + Transport: &transport, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.FeatureServer).NotTo(BeNil()) + Expect(repoConfig.FeatureServer.Type).To(Equal("mcp")) + Expect(repoConfig.FeatureServer.McpEnabled).NotTo(BeNil()) + Expect(*repoConfig.FeatureServer.McpEnabled).To(BeTrue()) + Expect(repoConfig.FeatureServer.McpServerName).To(Equal(&serverName)) + Expect(repoConfig.FeatureServer.McpServerVersion).To(Equal(&serverVersion)) + Expect(repoConfig.FeatureServer.McpTransport).To(Equal(&transport)) + }) + + It("should use type local when Mcp is present but Enabled is false", func() { + featureStore := minimalFeatureStore() + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Serving: &feastdevv1.ServingConfig{ + Mcp: &feastdevv1.McpConfig{ + Enabled: false, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.FeatureServer).NotTo(BeNil()) + Expect(repoConfig.FeatureServer.Type).To(Equal("local")) + Expect(repoConfig.FeatureServer.McpEnabled).To(BeNil()) + }) + + It("should set registry mcp when enabled", func() { + featureStore := minimalFeatureStore() + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + Mcp: &feastdevv1.McpConfig{ + Enabled: true, + }, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Registry.Mcp).NotTo(BeNil()) + Expect(repoConfig.Registry.Mcp.Enabled).To(BeTrue()) + }) + + It("should not set registry mcp when disabled", func() { + featureStore := minimalFeatureStore() + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + Mcp: &feastdevv1.McpConfig{ + Enabled: false, + }, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Registry.Mcp).To(BeNil()) + }) + + It("should not set registry mcp when server has no mcp config", func() { + featureStore := minimalFeatureStore() + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{}, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Registry.Mcp).To(BeNil()) + }) + + It("should set materialization block", func() { + featureStore := minimalFeatureStore() + batchSize := int32(10000) + + featureStore.Spec.Materialization = &feastdevv1.MaterializationConfig{ + OnlineWriteBatchSize: &batchSize, + ExtraConfig: map[string]string{ + "pull_latest_features": stringFalse, + "max_workers": "4", + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Materialization).NotTo(BeNil()) + Expect(repoConfig.Materialization.OnlineWriteBatchSize).To(Equal(&batchSize)) + // "true"/"false" strings are coerced to native booleans; other strings pass through unchanged. + Expect(repoConfig.Materialization.ExtraConfig).To(HaveKeyWithValue("pull_latest_features", false)) + Expect(repoConfig.Materialization.ExtraConfig).To(HaveKeyWithValue("max_workers", "4")) + }) + + It("should set openlineage block without api_key secret", func() { + featureStore := minimalFeatureStore() + transportType := HttpScheme + transportUrl := marquezUrl + endpoint := "api/v1/lineage" + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + TransportUrl: &transportUrl, + TransportEndpoint: &endpoint, + ExtraConfig: map[string]string{ + "namespace": "my-feast", + "producer": "feast-operator", + "emit_on_apply": stringTrue, + "emit_on_materialize": "false", + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.OpenLineage).NotTo(BeNil()) + Expect(repoConfig.OpenLineage.Enabled).To(BeTrue()) + Expect(repoConfig.OpenLineage.TransportType).To(Equal(&transportType)) + Expect(repoConfig.OpenLineage.TransportUrl).To(Equal(&transportUrl)) + Expect(repoConfig.OpenLineage.TransportEndpoint).To(Equal(&endpoint)) + Expect(repoConfig.OpenLineage.ApiKey).To(BeNil()) + // ExtraConfig: "true"/"false" strings coerced to booleans; other strings unchanged. + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("namespace", "my-feast")) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("producer", "feast-operator")) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("emit_on_apply", true)) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("emit_on_materialize", false)) + }) + + It("should set openlineage block with kafka extraConfig", func() { + featureStore := minimalFeatureStore() + transportType := "kafka" + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + ExtraConfig: map[string]string{ + "bootstrap_servers": "kafka.svc:9092", + "topic": "openlineage", + "sasl_mechanism": "PLAIN", + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.OpenLineage).NotTo(BeNil()) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("bootstrap_servers", "kafka.svc:9092")) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("topic", "openlineage")) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("sasl_mechanism", "PLAIN")) + }) + + It("should resolve api_key from secret for openlineage", func() { + featureStore := minimalFeatureStore() + transportType := HttpScheme + transportUrl := marquezUrl + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + TransportUrl: &transportUrl, + ApiKeySecretRef: &corev1.LocalObjectReference{ + Name: lineageSecretName, + }, + } + ApplyDefaultsToStatus(featureStore) + + apiKeyMockExtract := func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error) { + return map[string]interface{}{ + "api_key": "my-secret-key", + }, nil + } + + repoConfig, err := getServiceRepoConfig(featureStore, apiKeyMockExtract, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.OpenLineage).NotTo(BeNil()) + Expect(repoConfig.OpenLineage.ApiKey).NotTo(BeNil()) + Expect(*repoConfig.OpenLineage.ApiKey).To(Equal("my-secret-key")) + }) + + It("should return error when apiKeySecretRef Secret is missing the api_key key", func() { + featureStore := minimalFeatureStore() + transportType := HttpScheme + transportUrl := marquezUrl + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + TransportUrl: &transportUrl, + ApiKeySecretRef: &corev1.LocalObjectReference{ + Name: lineageSecretName, + }, + } + ApplyDefaultsToStatus(featureStore) + + missingKeyMock := func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error) { + return map[string]interface{}{ + "wrong_key": "some-value", + }, nil + } + + _, err := getServiceRepoConfig(featureStore, missingKeyMock, emptyMockExtractConfigFromConfigMap, false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("api_key")) + Expect(err.Error()).To(ContainSubstring(lineageSecretName)) + }) + + It("should return error when apiKeySecretRef api_key value is not a string", func() { + featureStore := minimalFeatureStore() + transportType := HttpScheme + transportUrl := marquezUrl + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + TransportUrl: &transportUrl, + ApiKeySecretRef: &corev1.LocalObjectReference{ + Name: lineageSecretName, + }, + } + ApplyDefaultsToStatus(featureStore) + + nonStringMock := func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error) { + return map[string]interface{}{ + "api_key": 12345, // integer, not a string + }, nil + } + + _, err := getServiceRepoConfig(featureStore, nonStringMock, emptyMockExtractConfigFromConfigMap, false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("api_key")) + Expect(err.Error()).To(ContainSubstring(lineageSecretName)) + }) + + It("should not set feature_server block when serving is nil", func() { + featureStore := minimalFeatureStore() + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.FeatureServer).To(BeNil()) + Expect(repoConfig.Materialization).To(BeNil()) + Expect(repoConfig.OpenLineage).To(BeNil()) + }) + + It("should inject default batch_engine.service_account when ConfigMap omits it", func() { + featureStore := minimalFeatureStore() + featureStore.Name = "spark-pg-e2e" + featureStore.Spec.BatchEngine = &feastdevv1.BatchEngineConfig{ + ConfigMapRef: &corev1.LocalObjectReference{Name: "spark-pg-batch-engine"}, + } + ApplyDefaultsToStatus(featureStore) + + extractCM := func(configMapRef string, configMapKey string) (map[string]interface{}, error) { + return map[string]interface{}{ + "type": "spark_application", + "image": "quay.io/example/feast-spark-driver:v6", + // service_account intentionally omitted + }, nil + } + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, extractCM, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.BatchEngine).NotTo(BeNil()) + Expect(repoConfig.BatchEngine.Type).To(Equal("spark_application")) + Expect(repoConfig.BatchEngine.Parameters["service_account"]).To(Equal("feast-spark-pg-e2e-batch-driver")) + }) + + It("should preserve explicit batch_engine.service_account from ConfigMap", func() { + featureStore := minimalFeatureStore() + featureStore.Name = "spark-pg-e2e" + featureStore.Spec.BatchEngine = &feastdevv1.BatchEngineConfig{ + ConfigMapRef: &corev1.LocalObjectReference{Name: "spark-pg-batch-engine"}, + } + ApplyDefaultsToStatus(featureStore) + + extractCM := func(configMapRef string, configMapKey string) (map[string]interface{}, error) { + return map[string]interface{}{ + "type": "spark_application", + "service_account": "my-custom-driver", + }, nil + } + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, extractCM, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.BatchEngine.Parameters["service_account"]).To(Equal("my-custom-driver")) + }) + + It("should not inject service_account for non-spark_application batch engines", func() { + featureStore := minimalFeatureStore() + featureStore.Name = "spark-pg-e2e" + featureStore.Spec.BatchEngine = &feastdevv1.BatchEngineConfig{ + ConfigMapRef: &corev1.LocalObjectReference{Name: "other-batch-engine"}, + } + ApplyDefaultsToStatus(featureStore) + + extractCM := func(configMapRef string, configMapKey string) (map[string]interface{}, error) { + return map[string]interface{}{ + "type": "spark", + // no service_account — must stay omitted for non-spark_application + }, nil + } + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, extractCM, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.BatchEngine).NotTo(BeNil()) + Expect(repoConfig.BatchEngine.Type).To(Equal("spark")) + _, hasSA := repoConfig.BatchEngine.Parameters["service_account"] + Expect(hasSA).To(BeFalse()) }) }) It("should fail to create the repo configs", func() { featureStore := minimalFeatureStore() - By("Having invalid server oidc authorization") + By("Having oidc with no issuerUrl, no Secret, no env var — should fail") + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{}, + } + ApplyDefaultsToStatus(featureStore) + + _, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no OIDC discovery URL configured")) + + By("Having oidc with Secret missing auth_discovery_url and no issuerUrl — should fail") featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ OidcAuthz: &feastdevv1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ - Name: "oidc-secret", + SecretRef: &corev1.LocalObjectReference{ + Name: oidcSecretName, }, }, } ApplyDefaultsToStatus(featureStore) secretExtractionFunc := mockOidcConfigFromSecret(map[string]interface{}{ - string(OidcClientId): "client-id", + string(OidcClientId): clientIDValue, string(OidcClientSecret): "client-secret", string(OidcUsername): "username", string(OidcPassword): "password"}) - _, err := getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) - _, err = getClientRepoConfig(featureStore, secretExtractionFunc, nil) + _, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + Expect(err.Error()).To(ContainSubstring("no OIDC discovery URL configured")) By("Having invalid client oidc authorization") featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ OidcAuthz: &feastdevv1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ - Name: "oidc-secret", + SecretRef: &corev1.LocalObjectReference{ + Name: oidcSecretName, }, }, } @@ -335,15 +801,12 @@ var _ = Describe("Repo Config", func() { secretExtractionFunc = mockOidcConfigFromSecret(map[string]interface{}{ string(OidcAuthDiscoveryUrl): "discovery-url", - string(OidcClientId): "client-id", + string(OidcClientId): clientIDValue, string(OidcUsername): "username", string(OidcPassword): "password"}) - _, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) - _, err = getClientRepoConfig(featureStore, secretExtractionFunc, nil) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + _, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + getClientRepoConfig(featureStore, nil) }) }) @@ -476,7 +939,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "offline-tls"}, SecretKeyNames: feastdevv1.SecretKeyNames{ - TlsCrt: "tls.crt", + TlsCrt: tlsCertKey, }, }, }, @@ -486,7 +949,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "online-tls"}, SecretKeyNames: feastdevv1.SecretKeyNames{ - TlsCrt: "tls.crt", + TlsCrt: tlsCertKey, }, }, }, @@ -506,7 +969,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "registry-tls"}, SecretKeyNames: feastdevv1.SecretKeyNames{ - TlsCrt: "tls.crt", + TlsCrt: tlsCertKey, }, }, }, @@ -519,8 +982,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { } // Test with nil feast parameter (no custom CA bundle) - repoConfig, err := getClientRepoConfig(featureStore, emptyMockExtractConfigFromSecret, nil) - Expect(err).NotTo(HaveOccurred()) + repoConfig := getClientRepoConfig(featureStore, nil) // Verify individual service certificate paths are used Expect(repoConfig.OfflineStore.Cert).To(Equal("/tls/offline/tls.crt")) @@ -546,7 +1008,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "offline-tls"}, SecretKeyNames: feastdevv1.SecretKeyNames{ - TlsCrt: "tls.crt", + TlsCrt: tlsCertKey, }, }, }, @@ -556,7 +1018,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "online-tls"}, SecretKeyNames: feastdevv1.SecretKeyNames{ - TlsCrt: "tls.crt", + TlsCrt: tlsCertKey, }, }, }, @@ -576,7 +1038,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "registry-tls"}, SecretKeyNames: feastdevv1.SecretKeyNames{ - TlsCrt: "tls.crt", + TlsCrt: tlsCertKey, }, }, }, @@ -589,8 +1051,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { } // Test with nil feast parameter (no custom CA bundle available) - repoConfig, err := getClientRepoConfig(featureStore, emptyMockExtractConfigFromSecret, nil) - Expect(err).NotTo(HaveOccurred()) + repoConfig := getClientRepoConfig(featureStore, nil) Expect(repoConfig.OfflineStore.Cert).To(Equal("/tls/offline/tls.crt")) }) }) diff --git a/infra/feast-operator/internal/controller/services/scaling.go b/infra/feast-operator/internal/controller/services/scaling.go index b8555555498..b02dc1eee07 100644 --- a/infra/feast-operator/internal/controller/services/scaling.go +++ b/infra/feast-operator/internal/controller/services/scaling.go @@ -23,10 +23,12 @@ import ( appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" hpaac "k8s.io/client-go/applyconfigurations/autoscaling/v2" metaac "k8s.io/client-go/applyconfigurations/meta/v1" + pdbac "k8s.io/client-go/applyconfigurations/policy/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -179,13 +181,75 @@ func convertBehavior(behavior *autoscalingv2.HorizontalPodAutoscalerBehavior) *h return result } +// applyOrDeletePDB reconciles the PodDisruptionBudget for the FeatureStore +// deployment using Server-Side Apply. If PodDisruptionBudgets is not configured +// or scaling is not enabled, any existing PDB is deleted. +func (feast *FeastServices) applyOrDeletePDB() error { + cr := feast.Handler.FeatureStore + services := cr.Status.Applied.Services + + if services == nil || services.PodDisruptionBudgets == nil || !isScalingEnabled(cr) { + pdb := &policyv1.PodDisruptionBudget{ObjectMeta: feast.GetObjectMeta()} + pdb.SetGroupVersionKind(policyv1.SchemeGroupVersion.WithKind("PodDisruptionBudget")) + return feast.Handler.DeleteOwnedFeastObj(pdb) + } + + pdbAC := feast.buildPDBApplyConfig() + data, err := json.Marshal(pdbAC) + if err != nil { + return err + } + + pdb := &policyv1.PodDisruptionBudget{ObjectMeta: feast.GetObjectMeta()} + logger := log.FromContext(feast.Handler.Context) + if err := feast.Handler.Client.Patch(feast.Handler.Context, pdb, + client.RawPatch(types.ApplyPatchType, data), + client.FieldOwner(fieldManager), client.ForceOwnership); err != nil { + return err + } + logger.Info("Successfully applied", "PodDisruptionBudget", pdb.Name) + + return nil +} + +// buildPDBApplyConfig constructs the fully desired PDB state as a typed apply +// configuration for Server-Side Apply. +func (feast *FeastServices) buildPDBApplyConfig() *pdbac.PodDisruptionBudgetApplyConfiguration { + cr := feast.Handler.FeatureStore + pdbConfig := cr.Status.Applied.Services.PodDisruptionBudgets + objMeta := feast.GetObjectMeta() + + pdb := pdbac.PodDisruptionBudget(objMeta.Name, objMeta.Namespace). + WithLabels(feast.getLabels()). + WithOwnerReferences( + metaac.OwnerReference(). + WithAPIVersion(feastdevv1.GroupVersion.String()). + WithKind("FeatureStore"). + WithName(cr.Name). + WithUID(cr.UID). + WithController(true). + WithBlockOwnerDeletion(true), + ). + WithSpec(pdbac.PodDisruptionBudgetSpec(). + WithSelector(metaac.LabelSelector().WithMatchLabels(feast.getSelectorLabels())), + ) + + if pdbConfig.MinAvailable != nil { + pdb.Spec.WithMinAvailable(*pdbConfig.MinAvailable) + } + if pdbConfig.MaxUnavailable != nil { + pdb.Spec.WithMaxUnavailable(*pdbConfig.MaxUnavailable) + } + + return pdb +} + // updateScalingStatus updates the scaling status fields using the deployment func (feast *FeastServices) updateScalingStatus(deploy *appsv1.Deployment) { cr := feast.Handler.FeatureStore cr.Status.Replicas = deploy.Status.ReadyReplicas - labels := feast.getLabels() - cr.Status.Selector = metav1.FormatLabelSelector(metav1.SetAsLabelSelector(labels)) + cr.Status.Selector = metav1.FormatLabelSelector(metav1.SetAsLabelSelector(feast.getSelectorLabels())) if !isScalingEnabled(cr) { cr.Status.ScalingStatus = nil diff --git a/infra/feast-operator/internal/controller/services/scaling_test.go b/infra/feast-operator/internal/controller/services/scaling_test.go index db803757112..1f65a4835ce 100644 --- a/infra/feast-operator/internal/controller/services/scaling_test.go +++ b/infra/feast-operator/internal/controller/services/scaling_test.go @@ -29,6 +29,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -44,7 +46,7 @@ var _ = Describe("Horizontal Scaling", func() { ctx = context.Background() typeNamespacedName = types.NamespacedName{ Name: "scaling-test-fs", - Namespace: "default", + Namespace: DefaultNs, } featureStore = &feastdevv1.FeatureStore{ @@ -59,15 +61,15 @@ var _ = Describe("Horizontal Scaling", func() { Server: &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, }, }, Persistence: &feastdevv1.OnlineStorePersistence{ DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ - Type: "redis", + Type: redisType, SecretRef: corev1.LocalObjectReference{ - Name: "redis-secret", + Name: redisSecretName, }, }, }, @@ -78,17 +80,17 @@ var _ = Describe("Horizontal Scaling", func() { ServerConfigs: feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, }, }, - GRPC: ptr(true), + GRPC: ptr.To(true), }, Persistence: &feastdevv1.RegistryPersistence{ DBPersistence: &feastdevv1.RegistryDBStorePersistence{ Type: "sql", SecretRef: corev1.LocalObjectReference{ - Name: "registry-secret", + Name: registrySecretName, }, }, }, @@ -121,12 +123,12 @@ var _ = Describe("Horizontal Scaling", func() { }) It("should return false when replicas=1", func() { - featureStore.Status.Applied.Replicas = ptr(int32(1)) + featureStore.Status.Applied.Replicas = ptr.To(int32(1)) Expect(isScalingEnabled(featureStore)).To(BeFalse()) }) It("should return true when replicas > 1", func() { - featureStore.Status.Applied.Replicas = ptr(int32(3)) + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) Expect(isScalingEnabled(featureStore)).To(BeTrue()) }) @@ -144,8 +146,8 @@ var _ = Describe("Horizontal Scaling", func() { dbOnlineStore := &feastdevv1.OnlineStore{ Persistence: &feastdevv1.OnlineStorePersistence{ DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ - Type: "redis", - SecretRef: corev1.LocalObjectReference{Name: "redis-secret"}, + Type: redisType, + SecretRef: corev1.LocalObjectReference{Name: redisSecretName}, }, }, } @@ -155,7 +157,7 @@ var _ = Describe("Horizontal Scaling", func() { Persistence: &feastdevv1.RegistryPersistence{ DBPersistence: &feastdevv1.RegistryDBStorePersistence{ Type: "sql", - SecretRef: corev1.LocalObjectReference{Name: "registry-secret"}, + SecretRef: corev1.LocalObjectReference{Name: registrySecretName}, }, }, }, @@ -163,10 +165,10 @@ var _ = Describe("Horizontal Scaling", func() { It("should accept scaling with full DB persistence", func() { fs := &feastdevv1.FeatureStore{ - ObjectMeta: metav1.ObjectMeta{Name: "cel-valid-db", Namespace: "default"}, + ObjectMeta: metav1.ObjectMeta{Name: "cel-valid-db", Namespace: DefaultNs}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: dbOnlineStore, Registry: dbRegistry, @@ -179,10 +181,10 @@ var _ = Describe("Horizontal Scaling", func() { It("should reject scaling when online store is missing (implicit file default)", func() { fs := &feastdevv1.FeatureStore{ - ObjectMeta: metav1.ObjectMeta{Name: "cel-no-online", Namespace: "default"}, + ObjectMeta: metav1.ObjectMeta{Name: "cel-no-online", Namespace: DefaultNs}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ Registry: dbRegistry, }, @@ -197,13 +199,13 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-file-online", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: &feastdevv1.OnlineStore{ Persistence: &feastdevv1.OnlineStorePersistence{ FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ - Path: "/data/online.db", + Path: dataOnlineDbPath, }, }, }, @@ -220,8 +222,8 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-file-offline", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: dbOnlineStore, Registry: dbRegistry, @@ -244,8 +246,8 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-no-registry", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: dbOnlineStore, }, @@ -260,15 +262,15 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-file-registry", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: dbOnlineStore, Registry: &feastdevv1.Registry{ Local: &feastdevv1.LocalRegistryConfig{ Persistence: &feastdevv1.RegistryPersistence{ FilePersistence: &feastdevv1.RegistryFilePersistence{ - Path: "/data/registry.db", + Path: dataRegistryDbPath, }, }, }, @@ -285,8 +287,8 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-s3-registry", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: dbOnlineStore, Registry: &feastdevv1.Registry{ @@ -309,8 +311,8 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-gs-registry", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: dbOnlineStore, Registry: &feastdevv1.Registry{ @@ -333,13 +335,13 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-remote-reg", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: dbOnlineStore, Registry: &feastdevv1.Registry{ Remote: &feastdevv1.RemoteRegistryConfig{ - Hostname: ptr("registry.example.com:80"), + Hostname: ptr.To("registry.example.com:80"), }, }, }, @@ -353,8 +355,8 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-rep1-file", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(1)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(1)), }, } Expect(k8sClient.Create(ctx, fs)).To(Succeed()) @@ -365,7 +367,7 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-no-scaling", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", + FeastProject: celTestProject, }, } Expect(k8sClient.Create(ctx, fs)).To(Succeed()) @@ -376,7 +378,7 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-hpa-no-db", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", + FeastProject: celTestProject, Services: &feastdevv1.FeatureStoreServices{ Scaling: &feastdevv1.ScalingConfig{ Autoscaling: &feastdevv1.AutoscalingConfig{MaxReplicas: 5}, @@ -394,8 +396,8 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-online-nop", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ OnlineStore: &feastdevv1.OnlineStore{}, Registry: dbRegistry, @@ -411,8 +413,8 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "cel-mutual-excl", Namespace: "default"}, Spec: feastdevv1.FeatureStoreSpec{ - FeastProject: "celtest", - Replicas: ptr(int32(3)), + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), Services: &feastdevv1.FeatureStoreServices{ Scaling: &feastdevv1.ScalingConfig{ Autoscaling: &feastdevv1.AutoscalingConfig{MaxReplicas: 5}, @@ -436,7 +438,7 @@ var _ = Describe("Horizontal Scaling", func() { }) It("should return static replicas when configured", func() { - featureStore.Status.Applied.Replicas = ptr(int32(3)) + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) replicas := feast.getDesiredReplicas() Expect(replicas).NotTo(BeNil()) Expect(*replicas).To(Equal(int32(3))) @@ -460,13 +462,13 @@ var _ = Describe("Horizontal Scaling", func() { }) It("should default to RollingUpdate when scaling is enabled via replicas", func() { - featureStore.Status.Applied.Replicas = ptr(int32(3)) + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) strategy := feast.getDeploymentStrategy() Expect(strategy.Type).To(Equal(appsv1.RollingUpdateDeploymentStrategyType)) }) It("should respect user-defined strategy even with scaling", func() { - featureStore.Status.Applied.Replicas = ptr(int32(3)) + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) featureStore.Status.Applied.Services.DeploymentStrategy = &appsv1.DeploymentStrategy{ Type: appsv1.RecreateDeploymentStrategyType, } @@ -481,7 +483,7 @@ var _ = Describe("Horizontal Scaling", func() { Server: &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, }, }, @@ -497,11 +499,11 @@ var _ = Describe("Horizontal Scaling", func() { ServerConfigs: feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, }, }, - GRPC: ptr(true), + GRPC: ptr.To(true), }, Persistence: &feastdevv1.RegistryPersistence{ FilePersistence: &feastdevv1.RegistryFilePersistence{ @@ -514,7 +516,7 @@ var _ = Describe("Horizontal Scaling", func() { It("should set static replicas on the deployment", func() { setFilePersistence() - featureStore.Status.Applied.Replicas = ptr(int32(3)) + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) deployment := feast.initFeastDeploy() Expect(feast.setDeployment(deployment)).To(Succeed()) @@ -568,7 +570,7 @@ var _ = Describe("Horizontal Scaling", func() { It("should build an HPA apply config with custom min replicas", func() { featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ Autoscaling: &feastdevv1.AutoscalingConfig{ - MinReplicas: ptr(int32(2)), + MinReplicas: ptr.To(int32(2)), MaxReplicas: 10, }, } @@ -614,7 +616,7 @@ var _ = Describe("Horizontal Scaling", func() { Name: corev1.ResourceMemory, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: ptr(int32(75)), + AverageUtilization: ptr.To(int32(75)), }, }, }, @@ -633,12 +635,294 @@ var _ = Describe("Horizontal Scaling", func() { }) }) + Describe("PDB Configuration", func() { + It("should build a PDB apply config with maxUnavailable", func() { + maxUnavail := intstr.FromInt(1) + featureStore.Status.Applied.Services.PodDisruptionBudgets = &feastdevv1.PDBConfig{ + MaxUnavailable: &maxUnavail, + } + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + pdb := feast.buildPDBApplyConfig() + Expect(*pdb.Kind).To(Equal("PodDisruptionBudget")) + Expect(*pdb.APIVersion).To(Equal("policy/v1")) + Expect(pdb.Spec.MaxUnavailable).NotTo(BeNil()) + Expect(pdb.Spec.MaxUnavailable.IntValue()).To(Equal(1)) + Expect(pdb.Spec.MinAvailable).To(BeNil()) + Expect(pdb.Spec.Selector.MatchLabels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + }) + + It("should build a PDB apply config with minAvailable", func() { + minAvail := intstr.FromString("50%") + featureStore.Status.Applied.Services.PodDisruptionBudgets = &feastdevv1.PDBConfig{ + MinAvailable: &minAvail, + } + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + pdb := feast.buildPDBApplyConfig() + Expect(pdb.Spec.MinAvailable).NotTo(BeNil()) + Expect(pdb.Spec.MinAvailable.String()).To(Equal("50%")) + Expect(pdb.Spec.MaxUnavailable).To(BeNil()) + }) + + It("should set owner reference on PDB for SSA", func() { + maxUnavail := intstr.FromInt(1) + featureStore.Status.Applied.Services.PodDisruptionBudgets = &feastdevv1.PDBConfig{ + MaxUnavailable: &maxUnavail, + } + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + pdb := feast.buildPDBApplyConfig() + Expect(pdb.OwnerReferences).To(HaveLen(1)) + Expect(*pdb.OwnerReferences[0].Name).To(Equal(featureStore.Name)) + Expect(*pdb.OwnerReferences[0].Controller).To(BeTrue()) + }) + }) + + Describe("CEL admission validation rejects invalid PDB configurations", func() { + dbOnlineStore := &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ + Type: redisType, + SecretRef: corev1.LocalObjectReference{Name: redisSecretName}, + }, + }, + } + dbRegistry := &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + DBPersistence: &feastdevv1.RegistryDBStorePersistence{ + Type: "sql", + SecretRef: corev1.LocalObjectReference{Name: registrySecretName}, + }, + }, + }, + } + + It("should reject PDB with both minAvailable and maxUnavailable set", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-pdb-both", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + PodDisruptionBudgets: &feastdevv1.PDBConfig{ + MinAvailable: ptr.To(intstr.FromInt(1)), + MaxUnavailable: ptr.To(intstr.FromInt(1)), + }, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Exactly one of minAvailable or maxUnavailable")) + }) + + It("should reject PDB with neither minAvailable nor maxUnavailable set", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-pdb-none", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + PodDisruptionBudgets: &feastdevv1.PDBConfig{}, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Exactly one of minAvailable or maxUnavailable")) + }) + + It("should accept PDB with only maxUnavailable", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-pdb-maxu", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + PodDisruptionBudgets: &feastdevv1.PDBConfig{ + MaxUnavailable: ptr.To(intstr.FromInt(1)), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + + It("should accept PDB with only minAvailable", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-pdb-mina", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + PodDisruptionBudgets: &feastdevv1.PDBConfig{ + MinAvailable: ptr.To(intstr.FromString("50%")), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + }) + + Describe("Topology Spread", func() { + It("should auto-inject soft zone constraint when replicas > 1 and no explicit constraints", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(HaveLen(1)) + Expect(podSpec.TopologySpreadConstraints[0].TopologyKey).To(Equal("topology.kubernetes.io/zone")) + Expect(podSpec.TopologySpreadConstraints[0].WhenUnsatisfiable).To(Equal(corev1.ScheduleAnyway)) + Expect(podSpec.TopologySpreadConstraints[0].MaxSkew).To(Equal(int32(1))) + Expect(podSpec.TopologySpreadConstraints[0].LabelSelector.MatchLabels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + }) + + It("should auto-inject when autoscaling is configured", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{MaxReplicas: 5}, + } + + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(HaveLen(1)) + Expect(podSpec.TopologySpreadConstraints[0].WhenUnsatisfiable).To(Equal(corev1.ScheduleAnyway)) + }) + + It("should not inject when replicas is 1 and no autoscaling", func() { + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(BeEmpty()) + }) + + It("should use user-provided constraints instead of defaults", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{{ + MaxSkew: 2, + TopologyKey: kubernetesHostnameTopologyKey, + WhenUnsatisfiable: corev1.DoNotSchedule, + LabelSelector: metav1.SetAsLabelSelector(map[string]string{"custom": "label"}), + }} + + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(HaveLen(1)) + Expect(podSpec.TopologySpreadConstraints[0].TopologyKey).To(Equal("kubernetes.io/hostname")) + Expect(podSpec.TopologySpreadConstraints[0].WhenUnsatisfiable).To(Equal(corev1.DoNotSchedule)) + Expect(podSpec.TopologySpreadConstraints[0].MaxSkew).To(Equal(int32(2))) + }) + + It("should disable auto-injection when empty array is set", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{} + + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(BeEmpty()) + }) + }) + + Describe("Pod Anti-Affinity", func() { + It("should auto-inject soft node anti-affinity when replicas > 1", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity).NotTo(BeNil()) + Expect(podSpec.Affinity.PodAntiAffinity).NotTo(BeNil()) + terms := podSpec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution + Expect(terms).To(HaveLen(1)) + Expect(terms[0].Weight).To(Equal(int32(100))) + Expect(terms[0].PodAffinityTerm.TopologyKey).To(Equal("kubernetes.io/hostname")) + Expect(terms[0].PodAffinityTerm.LabelSelector.MatchLabels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + }) + + It("should auto-inject when autoscaling is configured", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{MaxReplicas: 5}, + } + + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity).NotTo(BeNil()) + Expect(podSpec.Affinity.PodAntiAffinity).NotTo(BeNil()) + }) + + It("should not inject when replicas is 1 and no autoscaling", func() { + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity).To(BeNil()) + }) + + It("should use user-provided affinity instead of defaults", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.Affinity = &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{{ + TopologyKey: "kubernetes.io/hostname", + LabelSelector: metav1.SetAsLabelSelector(map[string]string{"custom": "label"}), + }}, + }, + } + + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution).To(HaveLen(1)) + Expect(podSpec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution).To(BeEmpty()) + Expect(podSpec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution[0].TopologyKey).To(Equal("kubernetes.io/hostname")) + }) + + It("should allow user to set node affinity alongside anti-affinity", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.Affinity = &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{{ + MatchExpressions: []corev1.NodeSelectorRequirement{{ + Key: "gpu", + Operator: corev1.NodeSelectorOpIn, + Values: []string{stringTrue}, + }}, + }}, + }, + }, + } + + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity.NodeAffinity).NotTo(BeNil()) + Expect(podSpec.Affinity.PodAntiAffinity).To(BeNil()) + }) + }) + Describe("Scale sub-resource", func() { newDBFeatureStore := func(name string) *feastdevv1.FeatureStore { return &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: "default", + Namespace: DefaultNs, }, Spec: feastdevv1.FeatureStoreSpec{ FeastProject: "scaletest", @@ -656,7 +940,7 @@ var _ = Describe("Horizontal Scaling", func() { Persistence: &feastdevv1.RegistryPersistence{ DBPersistence: &feastdevv1.RegistryDBStorePersistence{ Type: "sql", - SecretRef: corev1.LocalObjectReference{Name: "registry-secret"}, + SecretRef: corev1.LocalObjectReference{Name: registrySecretName}, }, }, }, @@ -688,7 +972,7 @@ var _ = Describe("Horizontal Scaling", func() { fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: "scale-sub-reject", - Namespace: "default", + Namespace: DefaultNs, }, Spec: feastdevv1.FeatureStoreSpec{ FeastProject: "scaletest", @@ -708,7 +992,7 @@ var _ = Describe("Horizontal Scaling", func() { It("should read the status replicas from the scale sub-resource", func() { fs := newDBFeatureStore("scale-sub-status") - fs.Spec.Replicas = ptr(int32(2)) + fs.Spec.Replicas = ptr.To(int32(2)) Expect(k8sClient.Create(ctx, fs)).To(Succeed()) defer func() { Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) }() diff --git a/infra/feast-operator/internal/controller/services/service_monitor.go b/infra/feast-operator/internal/controller/services/service_monitor.go new file mode 100644 index 00000000000..8de4d131289 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/service_monitor.go @@ -0,0 +1,117 @@ +/* +Copyright 2026 Feast Community. + +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 + + http://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 services + +import ( + "encoding/json" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + monitoringv1apply "github.com/prometheus-operator/prometheus-operator/pkg/client/applyconfiguration/monitoring/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + metav1apply "k8s.io/client-go/applyconfigurations/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var serviceMonitorGVK = schema.GroupVersionKind{ + Group: "monitoring.coreos.com", + Version: "v1", + Kind: "ServiceMonitor", +} + +// createOrDeleteServiceMonitor reconciles the ServiceMonitor for the +// FeatureStore's online store metrics endpoint using Server-Side Apply. +// When the Prometheus Operator CRD is not present in the cluster, this is +// a no-op. When metrics are enabled on the online store, a ServiceMonitor +// is applied; otherwise any existing ServiceMonitor is deleted. +func (feast *FeastServices) createOrDeleteServiceMonitor() error { + if !hasServiceMonitorCRD { + return nil + } + + if feast.isOnlineStore() && feast.isMetricsEnabled(OnlineFeastType) { + return feast.applyServiceMonitor() + } + + return feast.deleteServiceMonitor() +} + +func (feast *FeastServices) applyServiceMonitor() error { + smApply := feast.buildServiceMonitorApplyConfig() + data, err := json.Marshal(smApply) + if err != nil { + return err + } + + sm := feast.initServiceMonitor() + logger := log.FromContext(feast.Handler.Context) + if err := feast.Handler.Client.Patch(feast.Handler.Context, sm, + client.RawPatch(types.ApplyPatchType, data), + client.FieldOwner(fieldManager), client.ForceOwnership); err != nil { + return err + } + logger.Info("Successfully applied", "ServiceMonitor", sm.GetName()) + + return nil +} + +func (feast *FeastServices) deleteServiceMonitor() error { + sm := feast.initServiceMonitor() + return feast.Handler.DeleteOwnedFeastObj(sm) +} + +func (feast *FeastServices) initServiceMonitor() *unstructured.Unstructured { + sm := &unstructured.Unstructured{} + sm.SetGroupVersionKind(serviceMonitorGVK) + sm.SetName(feast.GetFeastServiceName(OnlineFeastType)) + sm.SetNamespace(feast.Handler.FeatureStore.Namespace) + return sm +} + +// buildServiceMonitorApplyConfig constructs the fully desired ServiceMonitor +// state for Server-Side Apply. +func (feast *FeastServices) buildServiceMonitorApplyConfig() *monitoringv1apply.ServiceMonitorApplyConfiguration { + cr := feast.Handler.FeatureStore + objMeta := feast.GetObjectMetaType(OnlineFeastType) + + return monitoringv1apply.ServiceMonitor(objMeta.Name, objMeta.Namespace). + WithLabels(feast.getFeastTypeLabels(OnlineFeastType)). + WithOwnerReferences( + metav1apply.OwnerReference(). + WithAPIVersion(feastdevv1.GroupVersion.String()). + WithKind("FeatureStore"). + WithName(cr.Name). + WithUID(cr.UID). + WithController(true). + WithBlockOwnerDeletion(true), + ). + WithSpec(monitoringv1apply.ServiceMonitorSpec(). + WithEndpoints( + monitoringv1apply.Endpoint(). + WithPort("metrics"). + WithPath("/metrics"), + ). + WithSelector(metav1apply.LabelSelector(). + WithMatchLabels(map[string]string{ + NameLabelKey: cr.Name, + ServiceTypeLabelKey: string(OnlineFeastType), + }), + ), + ) +} diff --git a/infra/feast-operator/internal/controller/services/service_monitor_test.go b/infra/feast-operator/internal/controller/services/service_monitor_test.go new file mode 100644 index 00000000000..8d982e6953e --- /dev/null +++ b/infra/feast-operator/internal/controller/services/service_monitor_test.go @@ -0,0 +1,168 @@ +/* +Copyright 2026 Feast Community. + +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 + + http://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 services + +import ( + "context" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" +) + +var _ = Describe("ServiceMonitor", func() { + var ( + featureStore *feastdevv1.FeatureStore + feast *FeastServices + typeNamespacedName types.NamespacedName + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + typeNamespacedName = types.NamespacedName{ + Name: "sm-test-fs", + Namespace: DefaultNs, + } + + featureStore = &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: typeNamespacedName.Name, + Namespace: typeNamespacedName.Namespace, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "smtestproject", + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + }, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + GRPC: ptr.To(true), + }, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, featureStore)).To(Succeed()) + + feast = &FeastServices{ + Handler: handler.FeastHandler{ + Client: k8sClient, + Context: ctx, + Scheme: k8sClient.Scheme(), + FeatureStore: featureStore, + }, + } + + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + }) + + AfterEach(func() { + testSetHasServiceMonitorCRD(false) + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }) + + Describe("initServiceMonitor", func() { + It("should create an unstructured ServiceMonitor with correct GVK and name", func() { + sm := feast.initServiceMonitor() + Expect(sm).NotTo(BeNil()) + Expect(sm.GetKind()).To(Equal("ServiceMonitor")) + Expect(sm.GetAPIVersion()).To(Equal("monitoring.coreos.com/v1")) + Expect(sm.GetName()).To(Equal(feast.GetFeastServiceName(OnlineFeastType))) + Expect(sm.GetNamespace()).To(Equal(featureStore.Namespace)) + }) + }) + + Describe("buildServiceMonitorApplyConfig", func() { + It("should build the correct SSA payload with labels, endpoints, selector, and owner reference", func() { + sm := feast.buildServiceMonitorApplyConfig() + + Expect(*sm.APIVersion).To(Equal("monitoring.coreos.com/v1")) + Expect(*sm.Kind).To(Equal("ServiceMonitor")) + Expect(*sm.Name).To(Equal(feast.GetFeastServiceName(OnlineFeastType))) + Expect(*sm.Namespace).To(Equal(featureStore.Namespace)) + + Expect(sm.Labels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + Expect(sm.Labels).To(HaveKeyWithValue(ServiceTypeLabelKey, string(OnlineFeastType))) + + Expect(sm.OwnerReferences).To(HaveLen(1)) + ownerRef := sm.OwnerReferences[0] + Expect(*ownerRef.APIVersion).To(Equal(feastdevv1.GroupVersion.String())) + Expect(*ownerRef.Kind).To(Equal("FeatureStore")) + Expect(*ownerRef.Name).To(Equal(featureStore.Name)) + Expect(*ownerRef.Controller).To(BeTrue()) + Expect(*ownerRef.BlockOwnerDeletion).To(BeTrue()) + + Expect(sm.Spec).NotTo(BeNil()) + Expect(sm.Spec.Endpoints).To(HaveLen(1)) + Expect(*sm.Spec.Endpoints[0].Port).To(Equal("metrics")) + Expect(*sm.Spec.Endpoints[0].Path).To(Equal("/metrics")) + + Expect(sm.Spec.Selector).NotTo(BeNil()) + Expect(sm.Spec.Selector.MatchLabels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + Expect(sm.Spec.Selector.MatchLabels).To(HaveKeyWithValue(ServiceTypeLabelKey, string(OnlineFeastType))) + }) + }) + + Describe("createOrDeleteServiceMonitor", func() { + It("should be a no-op when ServiceMonitor CRD is not available", func() { + testSetHasServiceMonitorCRD(false) + Expect(feast.createOrDeleteServiceMonitor()).To(Succeed()) + }) + + It("should not error when metrics is not enabled and CRD is unavailable", func() { + testSetHasServiceMonitorCRD(false) + featureStore.Status.Applied.Services.OnlineStore.Server.Metrics = ptr.To(false) + Expect(feast.createOrDeleteServiceMonitor()).To(Succeed()) + }) + }) + + Describe("HasServiceMonitorCRD", func() { + It("should return false by default", func() { + testSetHasServiceMonitorCRD(false) + Expect(HasServiceMonitorCRD()).To(BeFalse()) + }) + + It("should return true when set", func() { + testSetHasServiceMonitorCRD(true) + Expect(HasServiceMonitorCRD()).To(BeTrue()) + }) + }) +}) diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index a76f21d18c8..6964938c093 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -18,6 +18,7 @@ package services import ( "errors" + "path" "strconv" "strings" @@ -32,6 +33,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" @@ -78,12 +80,18 @@ func (feast *FeastServices) Deploy() error { if err := feast.createServiceAccount(); err != nil { return err } + if err := feast.reconcileBatchEngineRBAC(); err != nil { + return err + } if err := feast.createDeployment(); err != nil { return err } if err := feast.createOrDeleteHPA(); err != nil { return err } + if err := feast.applyOrDeletePDB(); err != nil { + return err + } if err := feast.deployClient(); err != nil { return err } @@ -93,6 +101,9 @@ func (feast *FeastServices) Deploy() error { if err := feast.deployCronJob(); err != nil { return err } + if err := feast.createOrDeleteServiceMonitor(); err != nil { + return err + } return nil } @@ -378,10 +389,13 @@ func (feast *FeastServices) createPVC(pvcCreate *feastdevv1.PvcCreate, feastType } // PVCs are immutable, so we only create... we don't update an existing one. + // Treat AlreadyExists as success: a pre-existing PVC without the managed-by label + // won't appear in the filtered cache (Client.Get returns NotFound), but Create + // will hit AlreadyExists on the API server — both cases mean the PVC is present. err = feast.Handler.Client.Get(feast.Handler.Context, client.ObjectKeyFromObject(pvc), pvc) if err != nil && apierrors.IsNotFound(err) { err = feast.Handler.Client.Create(feast.Handler.Context, pvc) - if err != nil { + if err != nil && !apierrors.IsAlreadyExists(err) { return err } logger.Info("Successfully created", "PersistentVolumeClaim", pvc.Name) @@ -402,13 +416,15 @@ func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment) error { } deploy.Labels = feast.getLabels() + selectorLabels := feast.getSelectorLabels() deploy.Spec = appsv1.DeploymentSpec{ Replicas: replicas, - Selector: metav1.SetAsLabelSelector(deploy.GetLabels()), + Selector: metav1.SetAsLabelSelector(selectorLabels), Strategy: feast.getDeploymentStrategy(), Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: deploy.GetLabels(), + Labels: deploy.GetLabels(), + Annotations: cr.Status.Applied.Services.PodAnnotations, }, Spec: corev1.PodSpec{ ServiceAccountName: feast.initFeastSA().Name, @@ -431,11 +447,18 @@ func (feast *FeastServices) setPod(podSpec *corev1.PodSpec) error { feast.mountEmptyDirVolumes(podSpec) feast.mountUserDefinedVolumes(podSpec) feast.applyNodeSelector(podSpec) + feast.applyTopologySpread(podSpec) + feast.applyAffinity(podSpec) + feast.applyResourceClaims(podSpec) return nil } func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec) error { + if err := feast.validatePackagedFeatureRepoPath(); err != nil { + return err + } + fsYamlB64, err := feast.GetServiceFeatureStoreYamlBase64() if err != nil { return err @@ -454,6 +477,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 } @@ -490,7 +527,7 @@ func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastTy }) if feastType == OnlineFeastType && feast.isMetricsEnabled(feastType) { container.Ports = append(container.Ports, corev1.ContainerPort{ - Name: "metrics", + Name: metricsPortName, ContainerPort: MetricsPort, Protocol: corev1.ProtocolTCP, }) @@ -582,7 +619,7 @@ func (feast *FeastServices) setRoute(route *routev1.Route, feastType FeastServic } func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []string { - baseCommand := "feast" + baseCommand := feastCommand options := []string{} logLevel := feast.getLogLevelForType(feastType) if logLevel != nil { @@ -591,7 +628,10 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st deploySettings := FeastServiceConstants[feastType] deploySettings.Args = append([]string{}, deploySettings.Args...) - if feastType == OnlineFeastType && feast.isMetricsEnabled(feastType) { + // Only inject --metrics CLI flag for the server.metrics bool path. + // When serving.metrics.enabled is used, Python reads it from feature_store.yaml + // and starts the metrics server itself — no CLI flag needed. + if feastType == OnlineFeastType && feast.isMetricsEnabledViaCLI(feastType) { deploySettings.Args = append([]string{deploySettings.Args[0], "--metrics"}, deploySettings.Args[1:]...) } targetPort := deploySettings.TargetHttpPort @@ -669,9 +709,10 @@ func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 feastProjectDir := applied.FeastProjectDir workingDir := getOfflineMountPath(feast.Handler.FeatureStore) projectPath := workingDir + "/" + applied.FeastProject + initImage := getInitContainerImage(&applied) container := corev1.Container{ - Name: "feast-init", - Image: getFeatureServerImage(), + Name: feastInitContainerName, + Image: initImage, Env: []corev1.EnvVar{ { Name: TmpFeatureStoreYamlEnvVar, @@ -682,6 +723,7 @@ func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 WorkingDir: workingDir, } + featureRepoDir := feast.getFeatureRepoDir() var createCommand string if feastProjectDir.Init != nil { initSlice := []string{"feast", "init"} @@ -711,16 +753,79 @@ func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 if feastProjectDir.Git.EnvFrom != nil { container.EnvFrom = *feastProjectDir.Git.EnvFrom } + } else if feastProjectDir.Packaged != nil { + container.Env = append(container.Env, + corev1.EnvVar{ + Name: packagedFeatureRepoEnvVar, + Value: path.Clean(feastProjectDir.Packaged.FeatureRepoPath), + }, + corev1.EnvVar{ + Name: stagedFeatureRepoEnvVar, + Value: featureRepoDir, + }, + ) + container.Args = []string{ + "set -euo pipefail\n" + + "echo \"Staging packaged feast repository...\"\n" + + "if [[ ! -d \"${" + packagedFeatureRepoEnvVar + "}\" ]]; then " + + "echo \"Packaged feature repository not found: ${" + packagedFeatureRepoEnvVar + "}\" >&2; exit 1; fi\n" + + "rm -rf -- \"${" + stagedFeatureRepoEnvVar + "}\"\n" + + "mkdir -p -- \"${" + stagedFeatureRepoEnvVar + "}\"\n" + + "cp -a -- \"${" + packagedFeatureRepoEnvVar + "}/.\" \"${" + stagedFeatureRepoEnvVar + "}/\"\n" + + "printf '%s' \"${" + TmpFeatureStoreYamlEnvVar + "}\" | base64 -d > \"${" + stagedFeatureRepoEnvVar + "}/feature_store.yaml\"\n" + + "echo \"Packaged feast repository staging complete\"\n", + } } - featureRepoDir := feast.getFeatureRepoDir() - container.Args = []string{ - "echo \"Creating feast repository...\"\necho '" + createCommand + "'\n" + - "if [[ ! -d " + featureRepoDir + " ]]; then " + createCommand + "; fi;\n" + - "echo $" + TmpFeatureStoreYamlEnvVar + " | base64 -d \u003e " + featureRepoDir + "/feature_store.yaml;\necho \"Feast repo creation complete\";\n", + if feastProjectDir.Packaged == nil { + container.Args = []string{ + "echo \"Creating feast repository...\"\necho '" + createCommand + "'\n" + + "if [[ ! -d " + featureRepoDir + " ]]; then " + createCommand + "; fi;\n" + + "echo $" + TmpFeatureStoreYamlEnvVar + " | base64 -d \u003e " + featureRepoDir + "/feature_store.yaml;\necho \"Feast repo creation complete\";\n", + } } podSpec.InitContainers = append(podSpec.InitContainers, container) + + if applied.Services.RunFeastApplyOnInit != nil && *applied.Services.RunFeastApplyOnInit { + applyContainer := corev1.Container{ + Name: feastApplyContainerName, + Image: initImage, + Command: []string{feastCommand, "apply"}, + WorkingDir: featureRepoDir, + } + // feast apply needs DB/store connectivity, so inherit env, envFrom + // and volume mounts from all server container configs. + seen := map[string]bool{} + for _, feastType := range []FeastServiceType{RegistryFeastType, OnlineFeastType, OfflineFeastType} { + if serverConfigs := feast.getServerConfigs(feastType); serverConfigs != nil { + if serverConfigs.OptionalCtrConfigs.Env != nil { + applyContainer.Env = envOverride(applyContainer.Env, *serverConfigs.OptionalCtrConfigs.Env) + } + if serverConfigs.OptionalCtrConfigs.EnvFrom != nil { + applyContainer.EnvFrom = append(applyContainer.EnvFrom, *serverConfigs.OptionalCtrConfigs.EnvFrom...) + } + for _, vm := range feast.getVolumeMounts(feastType) { + if !seen[vm.MountPath] { + applyContainer.VolumeMounts = append(applyContainer.VolumeMounts, vm) + seen[vm.MountPath] = true + } + } + } + } + podSpec.InitContainers = append(podSpec.InitContainers, applyContainer) + } + } +} + +// getServiceAppProtocol returns the appProtocol for a Service port. +// The registry gRPC service uses the gRPC protocol, which requires HTTP/2. +// Setting appProtocol allows service meshes (e.g. Istio) and load balancers +// to correctly classify the traffic and avoid downgrading to HTTP/1.1. +func (feast *FeastServices) getServiceAppProtocol(feastType FeastServiceType, isRestService bool) *string { + if feastType == RegistryFeastType && !isRestService && feast.isRegistryGrpcEnabled() { + return ptr.To("grpc") } + return nil } func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServiceType, isRestService bool) error { @@ -741,26 +846,26 @@ func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServi // The certificate will include both hostnames as SANs if !isRestService { grpcSvcName := feast.initFeastSvc(RegistryFeastType).Name - svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = grpcSvcName + tlsNameSuffix + svc.Annotations[openshiftServingCertSecretAnnotation] = grpcSvcName + tlsNameSuffix // pragma: allowlist secret // Add Subject Alternative Names (SANs) for both services grpcHostname := grpcSvcName + "." + svc.Namespace + ".svc.cluster.local" restHostname := feast.GetFeastRestServiceName(RegistryFeastType) + "." + svc.Namespace + ".svc.cluster.local" - svc.Annotations["service.beta.openshift.io/serving-cert-sans"] = grpcHostname + "," + restHostname + svc.Annotations[openshiftServingCertSansAnnotation] = grpcHostname + "," + restHostname } // REST service should not have the annotation - it will use the same certificate // from the gRPC service secret (mounted in the pod) } else if grpcEnabled && !restEnabled { // Only gRPC enabled: Use gRPC service name grpcSvcName := feast.initFeastSvc(RegistryFeastType).Name - svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = grpcSvcName + tlsNameSuffix + svc.Annotations[openshiftServingCertSecretAnnotation] = grpcSvcName + tlsNameSuffix // pragma: allowlist secret } else if !grpcEnabled && restEnabled { // Only REST enabled: Use REST service name - svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = svc.Name + tlsNameSuffix + svc.Annotations[openshiftServingCertSecretAnnotation] = svc.Name + tlsNameSuffix // pragma: allowlist secret } } else { // Standard behavior for non-registry services - svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = svc.Name + tlsNameSuffix + svc.Annotations[openshiftServingCertSecretAnnotation] = svc.Name + tlsNameSuffix // pragma: allowlist secret } } @@ -780,21 +885,22 @@ func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServi } svc.Spec = corev1.ServiceSpec{ - Selector: feast.getLabels(), + Selector: feast.getSelectorLabels(), Type: corev1.ServiceTypeClusterIP, Ports: []corev1.ServicePort{ { - Name: scheme, - Port: port, - Protocol: corev1.ProtocolTCP, - TargetPort: intstr.FromInt(int(targetPort)), + Name: scheme, + Port: port, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(int(targetPort)), + AppProtocol: feast.getServiceAppProtocol(feastType, isRestService), }, }, } if feastType == OnlineFeastType && feast.isMetricsEnabled(feastType) { svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ - Name: "metrics", + Name: metricsPortName, Port: MetricsPort, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt(int(MetricsPort)), @@ -830,6 +936,7 @@ func (feast *FeastServices) setServiceAccount(sa *corev1.ServiceAccount) error { func (feast *FeastServices) createNewPVC(pvcCreate *feastdevv1.PvcCreate, feastType FeastServiceType) (*corev1.PersistentVolumeClaim, error) { pvc := feast.initPVC(feastType) + pvc.Labels = feast.getFeastTypeLabels(feastType) pvc.Spec = corev1.PersistentVolumeClaimSpec{ AccessModes: pvcCreate.AccessModes, @@ -876,14 +983,42 @@ func (feast *FeastServices) getWorkerConfigs(feastType FeastServiceType) *feastd return nil } -func (feast *FeastServices) isMetricsEnabled(feastType FeastServiceType) bool { +// isMetricsEnabledViaCLI returns true only when metrics are enabled via the +// server.metrics bool flag, which requires the --metrics CLI argument to be +// injected into the feast serve command. +func (feast *FeastServices) isMetricsEnabledViaCLI(feastType FeastServiceType) bool { if feastType != OnlineFeastType { return false } - if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil && serviceConfigs.Metrics != nil { return *serviceConfigs.Metrics } + return false +} + +func (feast *FeastServices) isMetricsEnabled(feastType FeastServiceType) bool { + if feastType != OnlineFeastType { + return false + } + + // CLI flag path: server.metrics: true → adds --metrics arg + exposes port 8000. + // Only return true immediately; an explicit false must not suppress the YAML path. + if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil && + serviceConfigs.Metrics != nil && *serviceConfigs.Metrics { + return true + } + + // YAML config path: serving.metrics.enabled: true → written into feature_store.yaml; + // Python reads it and starts the metrics server on port 8000 automatically. + // We still need to expose the port and Service so Prometheus can scrape it. + appliedSpec := feast.Handler.FeatureStore.Status.Applied + if appliedSpec.Services != nil && + appliedSpec.Services.OnlineStore != nil && + appliedSpec.Services.OnlineStore.Serving != nil && + appliedSpec.Services.OnlineStore.Serving.Metrics != nil && + appliedSpec.Services.OnlineStore.Serving.Metrics.Enabled { + return true + } return false } @@ -920,6 +1055,61 @@ func (feast *FeastServices) applyNodeSelector(podSpec *corev1.PodSpec) { podSpec.NodeSelector = finalNodeSelector } +func (feast *FeastServices) applyTopologySpread(podSpec *corev1.PodSpec) { + cr := feast.Handler.FeatureStore + services := cr.Status.Applied.Services + + // User-provided explicit constraints take precedence (including empty array to disable) + if services != nil && services.TopologySpreadConstraints != nil { + podSpec.TopologySpreadConstraints = services.TopologySpreadConstraints + return + } + + if !isScalingEnabled(cr) { + return + } + + podSpec.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{{ + MaxSkew: 1, + TopologyKey: "topology.kubernetes.io/zone", + WhenUnsatisfiable: corev1.ScheduleAnyway, + LabelSelector: metav1.SetAsLabelSelector(feast.getSelectorLabels()), + }} +} + +func (feast *FeastServices) applyAffinity(podSpec *corev1.PodSpec) { + cr := feast.Handler.FeatureStore + services := cr.Status.Applied.Services + + if services != nil && services.Affinity != nil { + podSpec.Affinity = services.Affinity + return + } + + if !isScalingEnabled(cr) { + return + } + + podSpec.Affinity = &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{{ + Weight: 100, + PodAffinityTerm: corev1.PodAffinityTerm{ + TopologyKey: "kubernetes.io/hostname", + LabelSelector: metav1.SetAsLabelSelector(feast.getSelectorLabels()), + }, + }}, + }, + } +} + +func (feast *FeastServices) applyResourceClaims(podSpec *corev1.PodSpec) { + services := feast.Handler.FeatureStore.Status.Applied.Services + if services != nil && len(services.ResourceClaims) > 0 { + podSpec.ResourceClaims = services.ResourceClaims + } +} + // mergeNodeSelectors merges existing and operator node selectors // Existing selectors are preserved, operator selectors can override existing keys func (feast *FeastServices) mergeNodeSelectors(existing, operator map[string]string) map[string]string { @@ -974,12 +1164,24 @@ func (feast *FeastServices) getFeastTypeLabels(feastType FeastServiceType) map[s return labels } -func (feast *FeastServices) getLabels() map[string]string { +// getSelectorLabels returns the minimal label set used for immutable selectors +// (Deployment spec.selector, Service spec.selector, TopologySpreadConstraints, PodAffinity). +// This must NOT change after initial resource creation. +func (feast *FeastServices) getSelectorLabels() map[string]string { return map[string]string{ NameLabelKey: feast.Handler.FeatureStore.Name, } } +// getLabels returns the full label set for mutable metadata (ObjectMeta.Labels). +// Includes the managed-by label used by the informer cache filter. +func (feast *FeastServices) getLabels() map[string]string { + return map[string]string{ + NameLabelKey: feast.Handler.FeatureStore.Name, + ManagedByLabelKey: ManagedByLabelValue, + } +} + func (feast *FeastServices) setServiceHostnames() error { feast.Handler.FeatureStore.Status.ServiceHostnames = feastdevv1.ServiceHostnames{} domain := svcDomain + ":" @@ -1019,13 +1221,12 @@ func (feast *FeastServices) setFeastServiceCondition(err error, feastType FeastS if err != nil { logger := log.FromContext(feast.Handler.Context) cond := conditionMap[metav1.ConditionFalse] - cond.Message = "Error: " + err.Error() + cond.Message = ErrorMessagePrefix + err.Error() apimeta.SetStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, cond) logger.Error(err, "Error deploying the FeatureStore "+string(ClientFeastType)+" service") return err - } else { - apimeta.SetStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, conditionMap[metav1.ConditionTrue]) } + apimeta.SetStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, conditionMap[metav1.ConditionTrue]) return nil } @@ -1069,9 +1270,6 @@ func (feast *FeastServices) getRemoteRegistryFeastHandler() (*FeastServices, err } return nil, err } - if feast.Handler.FeatureStore.Status.Applied.FeastProject != remoteFeastObj.Status.Applied.FeastProject { - return nil, errors.New("FeatureStore '" + remoteFeastObj.Name + "' is using a different feast project than '" + feast.Handler.FeatureStore.Status.Applied.FeastProject + "'. Project names must match.") - } return &FeastServices{ Handler: handler.FeastHandler{ Client: feast.Handler.Client, @@ -1123,7 +1321,7 @@ func (feast *FeastServices) isOnlineServer() bool { func (feast *FeastServices) isOnlineStore() bool { appliedServices := feast.Handler.FeatureStore.Status.Applied.Services - return appliedServices != nil && appliedServices.OnlineStore != nil + return appliedServices != nil && appliedServices.OnlineStore != nil && !appliedServices.OnlineStore.Disabled } func (feast *FeastServices) noLocalCoreServerConfigured() bool { @@ -1229,13 +1427,11 @@ func (feast *FeastServices) mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *f }, }, }) - if feastType == OfflineFeastType { - for i := range podSpec.InitContainers { - podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, corev1.VolumeMount{ - Name: volName, - MountPath: pvcConfig.MountPath, - }) - } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: pvcConfig.MountPath, + }) } for i := range podSpec.Containers { podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ @@ -1254,6 +1450,9 @@ func (feast *FeastServices) mountEmptyDirVolumes(podSpec *corev1.PodSpec) { func (feast *FeastServices) getFeatureRepoDir() string { applied := feast.Handler.FeatureStore.Status.Applied + if applied.FeastProjectDir != nil && applied.FeastProjectDir.Packaged != nil && applied.Services.DisableInitContainers { + return path.Clean(applied.FeastProjectDir.Packaged.FeatureRepoPath) + } feastProjectDir := getOfflineMountPath(feast.Handler.FeatureStore) + "/" + applied.FeastProject if applied.FeastProjectDir != nil && applied.FeastProjectDir.Git != nil && len(applied.FeastProjectDir.Git.FeatureRepoPath) > 0 { return feastProjectDir + "/" + applied.FeastProjectDir.Git.FeatureRepoPath @@ -1261,6 +1460,39 @@ func (feast *FeastServices) getFeatureRepoDir() string { return feastProjectDir + "/" + FeatureRepoDir } +func (feast *FeastServices) validatePackagedFeatureRepoPath() error { + applied := feast.Handler.FeatureStore.Status.Applied + if applied.FeastProjectDir == nil || applied.FeastProjectDir.Packaged == nil { + return nil + } + + featureRepoPath := applied.FeastProjectDir.Packaged.FeatureRepoPath + cleanFeatureRepoPath := path.Clean(featureRepoPath) + if !path.IsAbs(featureRepoPath) || cleanFeatureRepoPath == "/" || cleanFeatureRepoPath != featureRepoPath { + return errors.New("packaged feature repository path " + strconv.Quote(featureRepoPath) + " must be a canonical absolute, non-root path") + } + + if !applied.Services.DisableInitContainers { + stagedFeatureRepoPath := path.Clean(feast.getFeatureRepoDir()) + if pathsOverlap(cleanFeatureRepoPath, stagedFeatureRepoPath) { + return errors.New( + "packaged feature repository path " + strconv.Quote(cleanFeatureRepoPath) + + " overlaps staged repository path " + strconv.Quote(stagedFeatureRepoPath), + ) + } + } + + return nil +} + +func pathsOverlap(firstPath, secondPath string) bool { + firstPath = path.Clean(firstPath) + secondPath = path.Clean(secondPath) + return firstPath == secondPath || + strings.HasPrefix(firstPath, secondPath+"/") || + strings.HasPrefix(secondPath, firstPath+"/") +} + func mountEmptyDirVolume(podSpec *corev1.PodSpec) { if podSpec != nil { volName := strings.TrimPrefix(EphemeralPath, "/") @@ -1352,6 +1584,67 @@ func IsDeploymentAvailable(conditions []appsv1.DeploymentCondition) bool { return false } +// GetPodContainerFailureMessage inspects pods belonging to the given deployment +// and returns a human-readable message describing the first init or regular +// container that is in a failing state. Returns empty string if no failure found. +func (feast *FeastServices) GetPodContainerFailureMessage(deploy appsv1.Deployment) string { + podList := corev1.PodList{} + selectorLabels := feast.getSelectorLabels() + if err := feast.Handler.Client.List(feast.Handler.Context, &podList, + client.InNamespace(deploy.Namespace), + client.MatchingLabels(selectorLabels), + ); err != nil { + return "" + } + for i := range podList.Items { + pod := &podList.Items[i] + if msg := initContainerFailureMessage(pod); msg != "" { + return msg + } + if msg := containerFailureMessage(pod); msg != "" { + return msg + } + } + return "" +} + +func initContainerFailureMessage(pod *corev1.Pod) string { + for _, cs := range pod.Status.InitContainerStatuses { + if cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "PodInitializing" { + return "Init container '" + cs.Name + "' waiting: " + cs.State.Waiting.Reason + + messageIfPresent(cs.State.Waiting.Message) + } + if cs.State.Terminated != nil && cs.State.Terminated.ExitCode != 0 { + return "Init container '" + cs.Name + "' failed with exit code " + + strconv.Itoa(int(cs.State.Terminated.ExitCode)) + + messageIfPresent(cs.State.Terminated.Message) + } + } + return "" +} + +func containerFailureMessage(pod *corev1.Pod) string { + for _, cs := range pod.Status.ContainerStatuses { + if cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "ContainerCreating" { + return "Container '" + cs.Name + "' waiting: " + cs.State.Waiting.Reason + + messageIfPresent(cs.State.Waiting.Message) + } + if cs.State.Terminated != nil && cs.State.Terminated.ExitCode != 0 { + return "Container '" + cs.Name + "' failed with exit code " + + strconv.Itoa(int(cs.State.Terminated.ExitCode)) + + messageIfPresent(cs.State.Terminated.Message) + } + } + return "" +} + +func messageIfPresent(msg string) string { + if msg != "" { + return " - " + msg + } + return "" +} + // GetFeastRestServiceName returns the feast REST service object name based on service type func (feast *FeastServices) GetFeastRestServiceName(feastType FeastServiceType) string { return feast.GetFeastServiceName(feastType) + "-rest" diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index b8863e10a74..da3590674f1 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -27,12 +27,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ) -func ptr[T any](v T) *T { - return &v -} - func (feast *FeastServices) refreshFeatureStore(ctx context.Context, key types.NamespacedName) { fs := &feastdevv1.FeatureStore{} Expect(k8sClient.Get(ctx, key, fs)).To(Succeed()) @@ -54,8 +51,8 @@ var _ = Describe("Registry Service", func() { ) var setFeatureStoreServerConfig = func(grpcEnabled, restEnabled bool) { - featureStore.Spec.Services.Registry.Local.Server.GRPC = ptr(grpcEnabled) - featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr(restEnabled) + featureStore.Spec.Services.Registry.Local.Server.GRPC = ptr.To(grpcEnabled) + featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr.To(restEnabled) Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) Expect(feast.ApplyDefaults()).To(Succeed()) applySpecToStatus(featureStore) @@ -66,7 +63,7 @@ var _ = Describe("Registry Service", func() { ctx = context.Background() typeNamespacedName = types.NamespacedName{ Name: "testfeaturestore", - Namespace: "default", + Namespace: DefaultNs, } featureStore = &feastdevv1.FeatureStore{ @@ -83,12 +80,12 @@ var _ = Describe("Registry Service", func() { ServerConfigs: feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, }, }, - GRPC: ptr(true), - RestAPI: ptr(false), + GRPC: ptr.To(true), + RestAPI: ptr.To(false), }, }, }, @@ -205,12 +202,135 @@ var _ = Describe("Registry Service", func() { }) }) + Describe("PodAnnotations Configuration", func() { + It("should apply podAnnotations to deployment pod template", func() { + featureStore.Spec.Services.PodAnnotations = map[string]string{ + otelInjectPythonAnnotation: stringTrue, + "sidecar.istio.io/inject": stringTrue, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + Expect(deployment.Spec.Template.Annotations).To(Equal(map[string]string{ + otelInjectPythonAnnotation: stringTrue, + "sidecar.istio.io/inject": stringTrue, + })) + }) + + It("should have no pod template annotations when podAnnotations is not set", func() { + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + Expect(deployment.Spec.Template.Annotations).To(BeNil()) + }) + + It("should remove pod template annotations when podAnnotations is removed", func() { + featureStore.Spec.Services.PodAnnotations = map[string]string{ + otelInjectPythonAnnotation: stringTrue, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Template.Annotations).To(HaveKey("instrumentation.opentelemetry.io/inject-python")) + + featureStore.Spec.Services.PodAnnotations = nil + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Template.Annotations).To(BeNil()) + }) + }) + + Describe("ResourceClaims Configuration", func() { + It("should apply resourceClaims to deployment pod template", func() { + featureStore.Spec.Services.ResourceClaims = []corev1.PodResourceClaim{ + { + Name: "gpu-claim", + ResourceClaimName: ptr.To("my-gpu-claim"), + }, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + Expect(deployment.Spec.Template.Spec.ResourceClaims).To(Equal([]corev1.PodResourceClaim{ + { + Name: "gpu-claim", + ResourceClaimName: ptr.To("my-gpu-claim"), + }, + })) + }) + + It("should have no resourceClaims when field is not set", func() { + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + Expect(deployment.Spec.Template.Spec.ResourceClaims).To(BeNil()) + }) + + It("should remove resourceClaims when field is removed", func() { + featureStore.Spec.Services.ResourceClaims = []corev1.PodResourceClaim{ + { + Name: "gpu-claim", + ResourceClaimName: ptr.To("my-gpu-claim"), + }, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Template.Spec.ResourceClaims).To(HaveLen(1)) + + featureStore.Spec.Services.ResourceClaims = nil + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Template.Spec.ResourceClaims).To(BeNil()) + }) + }) + Describe("NodeSelector Configuration", func() { It("should apply NodeSelector to pod spec when configured", func() { // Set NodeSelector for registry service nodeSelector := map[string]string{ - "kubernetes.io/os": "linux", - "node-type": "compute", + kubernetesOsLabel: linuxOS, + nodeTypeLabel: computeNodeType, } featureStore.Spec.Services.Registry.Local.Server.ContainerConfigs.OptionalCtrConfigs.NodeSelector = &nodeSelector Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) @@ -225,8 +345,8 @@ var _ = Describe("Registry Service", func() { // Verify NodeSelector is applied to pod spec expectedNodeSelector := map[string]string{ - "kubernetes.io/os": "linux", - "node-type": "compute", + kubernetesOsLabel: linuxOS, + nodeTypeLabel: computeNodeType, } Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) @@ -234,21 +354,21 @@ var _ = Describe("Registry Service", func() { It("should merge NodeSelectors from multiple services", func() { // Set NodeSelector for registry service registryNodeSelector := map[string]string{ - "kubernetes.io/os": "linux", - "node-type": "compute", + kubernetesOsLabel: linuxOS, + nodeTypeLabel: computeNodeType, } featureStore.Spec.Services.Registry.Local.Server.ContainerConfigs.OptionalCtrConfigs.NodeSelector = ®istryNodeSelector // Set NodeSelector for online store service onlineNodeSelector := map[string]string{ - "node-type": "online", - "zone": "us-west-1a", + nodeTypeLabel: "online", + zoneLabel: "us-west-1a", } featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ Server: &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ NodeSelector: &onlineNodeSelector, @@ -269,9 +389,9 @@ var _ = Describe("Registry Service", func() { // Verify NodeSelector merges all service selectors (online overrides registry for node-type) expectedNodeSelector := map[string]string{ - "kubernetes.io/os": "linux", - "node-type": "online", - "zone": "us-west-1a", + kubernetesOsLabel: linuxOS, + "node-type": "online", + zoneLabel: "us-west-1a", } Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) @@ -284,7 +404,7 @@ var _ = Describe("Registry Service", func() { featureStore.Spec.Services.UI = &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ NodeSelector: &uiNodeSelector, @@ -326,13 +446,13 @@ var _ = Describe("Registry Service", func() { It("should apply UI service NodeSelector when UI has highest precedence", func() { // Set NodeSelector for online service onlineNodeSelector := map[string]string{ - "node-type": "online", + nodeTypeLabel: "online", } featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ Server: &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ NodeSelector: &onlineNodeSelector, @@ -349,7 +469,7 @@ var _ = Describe("Registry Service", func() { featureStore.Spec.Services.UI = &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ NodeSelector: &uiNodeSelector, @@ -377,7 +497,7 @@ var _ = Describe("Registry Service", func() { It("should enable metrics on the online service when configured", func() { featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ - Server: &feastdevv1.ServerConfigs{Metrics: ptr(true)}, + Server: &feastdevv1.ServerConfigs{Metrics: ptr.To(true)}, } Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) @@ -393,15 +513,15 @@ var _ = Describe("Registry Service", func() { onlineContainer := GetOnlineContainer(*deployment) Expect(onlineContainer).NotTo(BeNil()) - Expect(onlineContainer.Command).To(Equal([]string{"feast", "serve", "--metrics", "-h", "0.0.0.0", "-p", "6566"})) + Expect(onlineContainer.Command).To(Equal([]string{feastCommand, "serve", "--metrics", "-h", hostAllIPv4, "-p", "6566"})) Expect(onlineContainer.Ports).To(ContainElement(corev1.ContainerPort{ - Name: "metrics", + Name: metricsPortName, ContainerPort: MetricsPort, Protocol: corev1.ProtocolTCP, })) metricsPortCount := 0 for _, port := range onlineContainer.Ports { - if port.Name == "metrics" { + if port.Name == metricsPortName { metricsPortCount++ } } @@ -451,7 +571,7 @@ var _ = Describe("Registry Service", func() { Server: &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, }, WorkerConfigs: &feastdevv1.WorkerConfigs{ @@ -503,7 +623,7 @@ var _ = Describe("Registry Service", func() { Server: &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, }, WorkerConfigs: &feastdevv1.WorkerConfigs{ @@ -545,7 +665,7 @@ var _ = Describe("Registry Service", func() { Server: &feastdevv1.ServerConfigs{ ContainerConfigs: feastdevv1.ContainerConfigs{ DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Image: ptr.To("test-image"), }, }, // WorkerConfigs is not set (nil) @@ -576,3 +696,231 @@ var _ = Describe("Registry Service", func() { }) }) }) + +var _ = Describe("Service AppProtocol Configuration", func() { + var ( + featureStore *feastdevv1.FeatureStore + feast *FeastServices + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + featureStore = &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testfeaturestore-approtocol", + Namespace: DefaultNs, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "testproject", + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + GRPC: ptr.To(true), + RestAPI: ptr.To(false), + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, featureStore)).To(Succeed()) + applySpecToStatus(featureStore) + feast = &FeastServices{ + Handler: handler.FeastHandler{ + Client: k8sClient, + Context: ctx, + Scheme: k8sClient.Scheme(), + FeatureStore: featureStore, + }, + } + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + }) + + AfterEach(func() { + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }) + + It("should return grpc appProtocol for the registry gRPC service", func() { + Expect(feast.isRegistryGrpcEnabled()).To(BeTrue()) + Expect(feast.getServiceAppProtocol(RegistryFeastType, false)).To(Equal(ptr.To("grpc"))) + }) + + It("should return nil appProtocol for the registry REST service", func() { + featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr.To(true) + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + + Expect(feast.getServiceAppProtocol(RegistryFeastType, true)).To(BeNil()) + }) + + It("should return nil appProtocol for the online store service", func() { + Expect(feast.getServiceAppProtocol(OnlineFeastType, false)).To(BeNil()) + }) + + It("should return nil appProtocol for the offline store service", func() { + Expect(feast.getServiceAppProtocol(OfflineFeastType, false)).To(BeNil()) + }) + + It("should return nil appProtocol when registry gRPC is disabled", func() { + featureStore.Spec.Services.Registry.Local.Server.GRPC = ptr.To(false) + featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr.To(true) + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + + Expect(feast.isRegistryGrpcEnabled()).To(BeFalse()) + Expect(feast.getServiceAppProtocol(RegistryFeastType, false)).To(BeNil()) + }) + + It("should set grpc appProtocol on the registry gRPC Service port", func() { + Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) + svc := feast.initFeastSvc(RegistryFeastType) + Expect(svc).NotTo(BeNil()) + Expect(feast.setService(svc, RegistryFeastType, false)).To(Succeed()) + + Expect(svc.Spec.Ports).To(HaveLen(1)) + Expect(svc.Spec.Ports[0].AppProtocol).To(Equal(ptr.To("grpc"))) + }) + + It("should not set appProtocol on the registry REST Service port", func() { + featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr.To(true) + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + + Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) + restSvc := feast.initFeastRestSvc(RegistryFeastType) + Expect(restSvc).NotTo(BeNil()) + Expect(feast.setService(restSvc, RegistryFeastType, true)).To(Succeed()) + + Expect(restSvc.Spec.Ports).To(HaveLen(1)) + Expect(restSvc.Spec.Ports[0].AppProtocol).To(BeNil()) + }) +}) + +var _ = Describe("Pod Container Failure Messages", func() { + It("should detect init container in CrashLoopBackOff", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: feastInitContainerName, + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}, + }, + }, + { + Name: feastApplyContainerName, + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "CrashLoopBackOff", + Message: "back-off 5m0s restarting failed container", + }, + }, + }, + }, + }, + } + msg := initContainerFailureMessage(pod) + Expect(msg).To(ContainSubstring("feast-apply")) + Expect(msg).To(ContainSubstring("CrashLoopBackOff")) + Expect(msg).To(ContainSubstring("back-off 5m0s")) + }) + + It("should detect init container terminated with non-zero exit code", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: feastApplyContainerName, + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Message: "feast apply failed", + }, + }, + }, + }, + }, + } + msg := initContainerFailureMessage(pod) + Expect(msg).To(ContainSubstring("feast-apply")) + Expect(msg).To(ContainSubstring("exit code 1")) + Expect(msg).To(ContainSubstring("feast apply failed")) + }) + + It("should return empty for init containers still initializing", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: feastInitContainerName, + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "PodInitializing", + }, + }, + }, + }, + }, + } + Expect(initContainerFailureMessage(pod)).To(BeEmpty()) + }) + + It("should detect regular container failure", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: registryName, + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "ImagePullBackOff", + Message: "image not found", + }, + }, + }, + }, + }, + } + msg := containerFailureMessage(pod) + Expect(msg).To(ContainSubstring("registry")) + Expect(msg).To(ContainSubstring("ImagePullBackOff")) + }) + + It("should return empty for healthy pods", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: feastInitContainerName, + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}, + }, + }, + }, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: registryName, + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{}, + }, + }, + }, + }, + } + Expect(initContainerFailureMessage(pod)).To(BeEmpty()) + Expect(containerFailureMessage(pod)).To(BeEmpty()) + }) +}) diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index 10ac3538a99..090696eccaf 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -26,6 +26,8 @@ import ( const ( TmpFeatureStoreYamlEnvVar = "TMP_FEATURE_STORE_YAML_BASE64" + packagedFeatureRepoEnvVar = "FEAST_PACKAGED_FEATURE_REPO_PATH" + stagedFeatureRepoEnvVar = "FEAST_STAGED_FEATURE_REPO_PATH" feastServerImageVar = "RELATED_IMAGE_FEATURE_SERVER" cronJobImageVar = "RELATED_IMAGE_CRON_JOB" FeatureStoreYamlCmKey = "feature_store.yaml" @@ -40,6 +42,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" @@ -48,8 +58,17 @@ const ( tlsPathCustomCABundle = "/etc/pki/tls/custom-certs/ca-bundle.crt" tlsNameSuffix = "-tls" - caBundleAnnotation = "config.openshift.io/inject-trusted-cabundle" - caBundleName = "odh-trusted-ca-bundle" + caBundleAnnotation = "config.openshift.io/inject-trusted-cabundle" + caBundleName = "odh-trusted-ca-bundle" + odhCaBundleKey = "odh-ca-bundle.crt" + tlsPathOdhCABundle = "/etc/pki/tls/custom-certs/odh-ca-bundle.crt" + tlsPathOidcCA = "/etc/pki/tls/oidc-ca/ca.crt" + oidcCaVolumeName = "oidc-ca-cert" + defaultCACertKey = "ca-bundle.crt" + openshiftServingCertSecretAnnotation = "service.beta.openshift.io/serving-cert-secret-name" // pragma: allowlist secret + openshiftServingCertSansAnnotation = "service.beta.openshift.io/serving-cert-sans" + openshiftInjectCaBundleAnnotation = "service.beta.openshift.io/inject-cabundle" + ErrorMessagePrefix = "Error: " DefaultOfflineStorageRequest = "20Gi" DefaultOnlineStorageRequest = "5Gi" @@ -91,8 +110,59 @@ const ( OidcClientSecret OidcPropertyType = "client_secret" OidcUsername OidcPropertyType = "username" OidcPassword OidcPropertyType = "password" + OidcTokenEnvVar OidcPropertyType = "token_env_var" + OidcVerifySsl OidcPropertyType = "verify_ssl" + OidcCaCertPath OidcPropertyType = "ca_cert_path" + OidcAudience OidcPropertyType = "audience" + OidcIssuer OidcPropertyType = "issuer" OidcMissingSecretError string = "missing OIDC secret: %s" + + // Common string constants + stringTrue = "true" + stringFalse = "false" + hostAllIPv4 = "0.0.0.0" + tlsCertKey = "tls.crt" + DefaultNs = "default" + feastCommand = "feast" + metricsPortName = "metrics" + registryName = "registry" + feastInitContainerName = "feast-init" + feastApplyContainerName = "feast-apply" + + // Test-specific constants + dataOnlineDbPath = "/data/online.db" + dataRegistryDbPath = "/data/registry.db" + oidcSecretName = "oidc-secret" // pragma: allowlist secret + clientIDValue = "client-id" + lineageSecretName = "lineage-secret" // pragma: allowlist secret + redisType = "redis" + redisSecretName = "redis-secret" // pragma: allowlist secret + registrySecretName = "registry-secret" // pragma: allowlist secret + celTestProject = "celtest" + kubernetesHostnameTopologyKey = "kubernetes.io/hostname" + dailyMidnightCron = "0 0 * * *" + otelInjectPythonAnnotation = "instrumentation.opentelemetry.io/inject-python" + kubernetesOsLabel = "kubernetes.io/os" + computeNodeType = "compute" + nodeTypeLabel = "node-type" + zoneLabel = "zone" + linuxOS = "linux" + TestValue = "test" + OfflineStoreSecretName = "offline-store-secret" // pragma: allowlist secret + OnlineStoreSecretName = "online-store-secret" // pragma: allowlist secret + RegistryStoreSecretName = "registry-store-secret" // pragma: allowlist secret + FieldRefName = "fieldRefName" + ConfigOne = "config-1" + MetadataNameField = "metadata.name" + GrpcFlag = "--grpc" + ExampleConfigMapName = "example-configmap" + ExampleSecretName = "example-secret" // pragma: allowlist secret +) + +const ( + ManagedByLabelKey = "app.kubernetes.io/managed-by" + ManagedByLabelValue = "feast-operator" ) var ( @@ -104,12 +174,12 @@ var ( FeastServiceConstants = map[FeastServiceType]deploymentSettings{ OfflineFeastType: { - Args: []string{"serve_offline", "-h", "0.0.0.0"}, + Args: []string{"serve_offline", "-h", hostAllIPv4}, TargetHttpPort: 8815, TargetHttpsPort: 8816, }, OnlineFeastType: { - Args: []string{"serve", "-h", "0.0.0.0"}, + Args: []string{"serve", "-h", hostAllIPv4}, TargetHttpPort: 6566, TargetHttpsPort: 6567, }, @@ -208,9 +278,7 @@ var ( }, } - OidcServerProperties = []OidcPropertyType{OidcClientId, OidcAuthDiscoveryUrl} - OidcClientProperties = []OidcPropertyType{OidcClientSecret, OidcUsername, OidcPassword} - OidcProperties = []OidcPropertyType{OidcClientId, OidcAuthDiscoveryUrl, OidcClientSecret, OidcUsername, OidcPassword} + OidcOptionalSecretProperties = []OidcPropertyType{OidcAuthDiscoveryUrl, OidcClientId, OidcClientSecret, OidcUsername, OidcPassword, OidcAudience, OidcIssuer} ) // Feast server types: Reserved only for server types like Online, Offline, and Registry servers. Should not be used for client types like the UI, etc. @@ -249,14 +317,79 @@ type FeastServices struct { // RepoConfig is the Repo config. Typically loaded from feature_store.yaml. // https://rtd.feast.dev/en/stable/#feast.repo_config.RepoConfig type RepoConfig struct { - Project string `yaml:"project,omitempty"` - Provider FeastProviderType `yaml:"provider,omitempty"` - OfflineStore OfflineStoreConfig `yaml:"offline_store,omitempty"` - OnlineStore OnlineStoreConfig `yaml:"online_store,omitempty"` - Registry RegistryConfig `yaml:"registry,omitempty"` - AuthzConfig AuthzConfig `yaml:"auth,omitempty"` - EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` - BatchEngine *ComputeEngineConfig `yaml:"batch_engine,omitempty"` + Project string `yaml:"project,omitempty"` + Provider FeastProviderType `yaml:"provider,omitempty"` + OfflineStore OfflineStoreConfig `yaml:"offline_store,omitempty"` + OnlineStore OnlineStoreConfig `yaml:"online_store,omitempty"` + Registry RegistryConfig `yaml:"registry,omitempty"` + AuthzConfig AuthzConfig `yaml:"auth,omitempty"` + EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` + BatchEngine *ComputeEngineConfig `yaml:"batch_engine,omitempty"` + FeatureServer *FeatureServerYamlConfig `yaml:"feature_server,omitempty"` + Materialization *MaterializationYamlConfig `yaml:"materialization,omitempty"` + OpenLineage *OpenLineageYamlConfig `yaml:"openlineage,omitempty"` + DataQualityMonitoring *DataQualityMonitoringYamlConfig `yaml:"data_quality_monitoring,omitempty"` +} + +// FeatureServerYamlConfig maps to the feature_server section of feature_store.yaml. +// Field names match Feast's Python SDK YAML keys exactly. +type FeatureServerYamlConfig struct { + Type string `yaml:"type"` + Metrics *MetricsYamlConfig `yaml:"metrics,omitempty"` + OfflinePushBatchingEnabled *bool `yaml:"offline_push_batching_enabled,omitempty"` + OfflinePushBatchingBatchSize *int32 `yaml:"offline_push_batching_batch_size,omitempty"` + OfflinePushBatchingBatchIntervalSeconds *int32 `yaml:"offline_push_batching_batch_interval_seconds,omitempty"` + McpEnabled *bool `yaml:"mcp_enabled,omitempty"` + McpServerName *string `yaml:"mcp_server_name,omitempty"` + McpServerVersion *string `yaml:"mcp_server_version,omitempty"` + McpTransport *string `yaml:"mcp_transport,omitempty"` +} + +// MetricsYamlConfig maps to the feature_server.metrics section of feature_store.yaml. +// Category booleans are merged inline so they sit at the same YAML level as +// "enabled". Keys must be valid Feast MetricsConfig field names for the SDK +// version in use (e.g. resource, request, online_features, push, +// materialization, freshness). Note: Feast's MetricsConfig uses +// extra="forbid", so unknown keys will be rejected by SDK validation. +type MetricsYamlConfig struct { + Enabled bool `yaml:"enabled"` + Categories map[string]interface{} `yaml:",inline,omitempty"` +} + +// DataQualityMonitoringYamlConfig mirrors the Python DqmConfig in feature_store.yaml. +type DataQualityMonitoringYamlConfig struct { + AutoBaseline bool `yaml:"auto_baseline"` +} + +// MaterializationYamlConfig maps to the materialization section of feature_store.yaml. +// ExtraConfig is merged inline so future Feast MaterializationConfig fields appear +// at the same YAML level as the typed fields above. +type MaterializationYamlConfig struct { + OnlineWriteBatchSize *int32 `yaml:"online_write_batch_size,omitempty"` + ExtraConfig map[string]interface{} `yaml:",inline,omitempty"` +} + +// OpenLineageYamlConfig maps to the openlineage section of feature_store.yaml. +// ExtraConfig is merged inline so all extra key-value pairs (namespace, producer, +// emit_on_apply, emit_on_materialize, transport-specific options, etc.) appear at +// the same YAML level as the typed connection fields. +type OpenLineageYamlConfig struct { + Enabled bool `yaml:"enabled"` + TransportType *string `yaml:"transport_type,omitempty"` + TransportUrl *string `yaml:"transport_url,omitempty"` + TransportEndpoint *string `yaml:"transport_endpoint,omitempty"` + ApiKey *string `yaml:"api_key,omitempty"` + ExtraConfig map[string]interface{} `yaml:",inline,omitempty"` + Consumer *OpenLineageConsumerYamlConfig `yaml:"consumer,omitempty"` +} + +// OpenLineageConsumerYamlConfig maps to the openlineage.consumer section of feature_store.yaml. +type OpenLineageConsumerYamlConfig struct { + Enabled bool `yaml:"enabled"` + StoreType *string `yaml:"store_type,omitempty"` + ConnectionString *string `yaml:"connection_string,omitempty"` + ApiKey *string `yaml:"api_key,omitempty"` + NamespaceMapping map[string]string `yaml:"namespace_mapping,omitempty"` } // OfflineStoreConfig is the configuration that relates to reading from and writing to the Feast offline store. @@ -285,9 +418,15 @@ type RegistryConfig struct { S3AdditionalKwargs *map[string]string `yaml:"s3_additional_kwargs,omitempty"` CacheTTLSeconds *int32 `yaml:"cache_ttl_seconds,omitempty"` CacheMode *string `yaml:"cache_mode,omitempty"` + Mcp *RegistryMcpYamlConfig `yaml:"mcp,omitempty"` DBParameters map[string]interface{} `yaml:",inline,omitempty"` } +// RegistryMcpYamlConfig maps to the registry.mcp section of feature_store.yaml. +type RegistryMcpYamlConfig struct { + Enabled bool `yaml:"enabled"` +} + // AuthzConfig is the RBAC authorization configuration. type AuthzConfig struct { Type AuthzType `yaml:"type,omitempty"` diff --git a/infra/feast-operator/internal/controller/services/suite_test.go b/infra/feast-operator/internal/controller/services/suite_test.go index a3d5bb3dae8..de1b75817ef 100644 --- a/infra/feast-operator/internal/controller/services/suite_test.go +++ b/infra/feast-operator/internal/controller/services/suite_test.go @@ -88,3 +88,7 @@ var _ = AfterSuite(func() { func testSetIsOpenShift() { isOpenShift = true } + +func testSetHasServiceMonitorCRD(val bool) { + hasServiceMonitorCRD = val +} diff --git a/infra/feast-operator/internal/controller/services/tls.go b/infra/feast-operator/internal/controller/services/tls.go index 4a50697c5a1..a3a5493ba5b 100644 --- a/infra/feast-operator/internal/controller/services/tls.go +++ b/infra/feast-operator/internal/controller/services/tls.go @@ -210,6 +210,10 @@ func (feast *FeastServices) mountTlsConfigs(podSpec *corev1.PodSpec) { feast.mountTlsConfig(OnlineFeastType, podSpec) feast.mountTlsConfig(UIFeastType, podSpec) feast.mountCustomCABundle(podSpec) + appliedSpec := feast.Handler.FeatureStore.Status.Applied + if appliedSpec.AuthzConfig != nil && appliedSpec.AuthzConfig.OidcAuthz != nil { + feast.mountOidcCACert(podSpec, appliedSpec.AuthzConfig.OidcAuthz) + } } func (feast *FeastServices) mountTlsConfig(feastType FeastServiceType, podSpec *corev1.PodSpec) { @@ -224,12 +228,16 @@ func (feast *FeastServices) mountTlsConfig(feastType FeastServiceType, podSpec * }, }, }) + tlsMount := corev1.VolumeMount{ + Name: volName, + MountPath: GetTlsPath(feastType), + ReadOnly: true, + } if i, container := getContainerByType(feastType, *podSpec); container != nil { - podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ - Name: volName, - MountPath: GetTlsPath(feastType), - ReadOnly: true, - }) + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, tlsMount) + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, tlsMount) } } } @@ -245,12 +253,16 @@ func mountTlsRemoteRegistryConfig(podSpec *corev1.PodSpec, tls *feastdevv1.TlsRe }, }, }) + tlsMount := corev1.VolumeMount{ + Name: volName, + MountPath: GetTlsPath(RegistryFeastType), + ReadOnly: true, + } for i := range podSpec.Containers { - podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ - Name: volName, - MountPath: GetTlsPath(RegistryFeastType), - ReadOnly: true, - }) + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, tlsMount) + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, tlsMount) } } } @@ -267,24 +279,69 @@ func (feast *FeastServices) mountCustomCABundle(podSpec *corev1.PodSpec) { }, }) + caMount := corev1.VolumeMount{ + Name: customCaBundle.VolumeName, + MountPath: tlsPathCustomCABundle, + ReadOnly: true, + SubPath: "ca-bundle.crt", + } + odhCaMount := corev1.VolumeMount{ + Name: customCaBundle.VolumeName, + MountPath: tlsPathOdhCABundle, + ReadOnly: true, + SubPath: odhCaBundleKey, + } for i := range podSpec.Containers { - podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ - Name: customCaBundle.VolumeName, - MountPath: tlsPathCustomCABundle, - ReadOnly: true, - SubPath: "ca-bundle.crt", - }) + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, caMount, odhCaMount) + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, caMount, odhCaMount) } log.FromContext(feast.Handler.Context).Info("Mounted custom CA bundle ConfigMap to Feast pods.") } } +func (feast *FeastServices) mountOidcCACert(podSpec *corev1.PodSpec, oidcAuthz *feastdevv1.OidcAuthz) { + if oidcAuthz.CACertConfigMap == nil { + return + } + cmName := oidcAuthz.CACertConfigMap.Name + cmKey := oidcAuthz.CACertConfigMap.Key + if cmKey == "" { + cmKey = defaultCACertKey + } + + podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ + Name: oidcCaVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, + }, + }, + }) + + mount := corev1.VolumeMount{ + Name: oidcCaVolumeName, + MountPath: tlsPathOidcCA, + ReadOnly: true, + SubPath: cmKey, + } + for i := range podSpec.Containers { + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, mount) + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, mount) + } + + log.FromContext(feast.Handler.Context).Info("Mounted OIDC CA certificate ConfigMap to Feast pods.", "configMap", cmName, "key", cmKey) +} + // GetCustomCertificatesBundle retrieves the custom CA bundle ConfigMap if it exists when deployed with RHOAI or ODH func (feast *FeastServices) GetCustomCertificatesBundle() CustomCertificatesBundle { var customCertificatesBundle CustomCertificatesBundle configMapList := &corev1.ConfigMapList{} - labelSelector := client.MatchingLabels{caBundleAnnotation: "true"} + labelSelector := client.MatchingLabels{caBundleAnnotation: stringTrue} err := feast.Handler.Client.List( feast.Handler.Context, @@ -322,7 +379,7 @@ func getPortStr(tls *feastdevv1.TlsConfigs) string { func tlsDefaults(tls *feastdevv1.TlsConfigs) { if tls.IsTLS() { if len(tls.SecretKeyNames.TlsCrt) == 0 { - tls.SecretKeyNames.TlsCrt = "tls.crt" + tls.SecretKeyNames.TlsCrt = tlsCertKey } if len(tls.SecretKeyNames.TlsKey) == 0 { tls.SecretKeyNames.TlsKey = "tls.key" diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go index e5299d79119..7f1c94789bc 100644 --- a/infra/feast-operator/internal/controller/services/tls_test.go +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -37,7 +37,7 @@ var _ = Describe("TLS Config", func() { utilruntime.Must(feastdevv1.AddToScheme(scheme)) secretKeyNames := feastdevv1.SecretKeyNames{ - TlsCrt: "tls.crt", + TlsCrt: tlsCertKey, TlsKey: "tls.key", } @@ -128,8 +128,7 @@ var _ = Describe("TLS Config", func() { err = feast.ApplyDefaults() Expect(err).ToNot(HaveOccurred()) - repoConfig, err := getClientRepoConfig(feast.Handler.FeatureStore, emptyMockExtractConfigFromSecret, &feast) - Expect(err).NotTo(HaveOccurred()) + repoConfig := getClientRepoConfig(feast.Handler.FeatureStore, &feast) Expect(repoConfig.OfflineStore.Port).To(Equal(HttpsPort)) Expect(repoConfig.OfflineStore.Scheme).To(Equal(HttpsScheme)) Expect(repoConfig.OfflineStore.Cert).To(ContainSubstring(string(OfflineFeastType))) @@ -173,7 +172,7 @@ var _ = Describe("TLS Config", func() { feastDeploy := feast.initFeastDeploy() err = feast.setDeployment(feastDeploy) Expect(err).ToNot(HaveOccurred()) - Expect(feastDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(feastDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(feastDeploy.Spec.Template.Spec.Containers).To(HaveLen(4)) Expect(feastDeploy.Spec.Template.Spec.Containers[0].Command).To(ContainElements(ContainSubstring("--key"))) Expect(feastDeploy.Spec.Template.Spec.Containers[1].Command).To(ContainElements(ContainSubstring("--key"))) @@ -181,6 +180,19 @@ var _ = Describe("TLS Config", func() { Expect(feastDeploy.Spec.Template.Spec.Containers[3].Command).To(ContainElements(ContainSubstring("--key"))) Expect(feastDeploy.Spec.Template.Spec.Volumes).To(HaveLen(5)) + // verify init containers receive TLS volume mounts when all services have TLS + for _, initContainer := range feastDeploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement( + HaveField("MountPath", GetTlsPath(RegistryFeastType)), + ), "init container %s should have registry TLS mount", initContainer.Name) + Expect(initContainer.VolumeMounts).To(ContainElement( + HaveField("MountPath", GetTlsPath(OnlineFeastType)), + ), "init container %s should have online TLS mount", initContainer.Name) + Expect(initContainer.VolumeMounts).To(ContainElement( + HaveField("MountPath", GetTlsPath(OfflineFeastType)), + ), "init container %s should have offline TLS mount", initContainer.Name) + } + // registry service w/ tls and in an openshift cluster feast.Handler.FeatureStore = minimalFeatureStore() feast.Handler.FeatureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ @@ -262,8 +274,7 @@ var _ = Describe("TLS Config", func() { err = feast.ApplyDefaults() Expect(err).ToNot(HaveOccurred()) - repoConfig, err = getClientRepoConfig(feast.Handler.FeatureStore, emptyMockExtractConfigFromSecret, &feast) - Expect(err).NotTo(HaveOccurred()) + repoConfig = getClientRepoConfig(feast.Handler.FeatureStore, &feast) Expect(repoConfig.OfflineStore.Port).To(Equal(HttpsPort)) Expect(repoConfig.OfflineStore.Scheme).To(Equal(HttpsScheme)) Expect(repoConfig.OfflineStore.Cert).To(ContainSubstring(string(OfflineFeastType))) @@ -336,6 +347,19 @@ var _ = Describe("TLS Config", func() { Expect(GetUIContainer(*feastDeploy).Command).NotTo(ContainElements(ContainSubstring("--key"))) Expect(GetUIContainer(*feastDeploy).VolumeMounts).To(HaveLen(1)) + // verify init containers receive only the offline TLS mount when only offline has TLS + for _, initContainer := range feastDeploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement( + HaveField("MountPath", GetTlsPath(OfflineFeastType)), + ), "init container %s should have offline TLS mount", initContainer.Name) + Expect(initContainer.VolumeMounts).NotTo(ContainElement( + HaveField("MountPath", GetTlsPath(RegistryFeastType)), + ), "init container %s should not have registry TLS mount when registry TLS is disabled", initContainer.Name) + Expect(initContainer.VolumeMounts).NotTo(ContainElement( + HaveField("MountPath", GetTlsPath(OnlineFeastType)), + ), "init container %s should not have online TLS mount when online TLS is disabled", initContainer.Name) + } + // Test REST registry server TLS configuration feast.Handler.FeatureStore = minimalFeatureStore() restEnabled := true diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 9ce1ecd749a..84951f2077b 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -21,6 +21,7 @@ import ( ) var isOpenShift = false +var hasServiceMonitorCRD = false func IsRegistryServer(featureStore *feastdevv1.FeatureStore) bool { return IsLocalRegistry(featureStore) && featureStore.Status.Applied.Services.Registry.Local.Server != nil @@ -98,7 +99,11 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { if applied.Services == nil { applied.Services = &feastdevv1.FeatureStoreServices{} } + defaultFeatureServerImage := getFeatureServerImageForSpec(applied) services := applied.Services + if services.RunFeastApplyOnInit == nil { + services.RunFeastApplyOnInit = boolPtr(true) + } if services.Registry != nil { // if remote registry not set, proceed w/ local registry defaults @@ -124,7 +129,7 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { } if services.Registry.Local.Server != nil { - setDefaultCtrConfigs(&services.Registry.Local.Server.ContainerConfigs.DefaultCtrConfigs) + setDefaultCtrConfigs(&services.Registry.Local.Server.ContainerConfigs.DefaultCtrConfigs, defaultFeatureServerImage) // Set default for GRPC: true if nil if services.Registry.Local.Server.GRPC == nil { defaultGRPC := true @@ -155,37 +160,39 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { } if services.OfflineStore.Server != nil { - setDefaultCtrConfigs(&services.OfflineStore.Server.ContainerConfigs.DefaultCtrConfigs) + setDefaultCtrConfigs(&services.OfflineStore.Server.ContainerConfigs.DefaultCtrConfigs, defaultFeatureServerImage) } } - // default to onlineStore service deployment + // default to onlineStore service deployment unless it is explicitly disabled if services.OnlineStore == nil { services.OnlineStore = &feastdevv1.OnlineStore{} } - if services.OnlineStore.Persistence == nil { - services.OnlineStore.Persistence = &feastdevv1.OnlineStorePersistence{} - } - - if services.OnlineStore.Persistence.DBPersistence == nil { - if services.OnlineStore.Persistence.FilePersistence == nil { - services.OnlineStore.Persistence.FilePersistence = &feastdevv1.OnlineStoreFilePersistence{} + if !services.OnlineStore.Disabled { + if services.OnlineStore.Persistence == nil { + services.OnlineStore.Persistence = &feastdevv1.OnlineStorePersistence{} } - if len(services.OnlineStore.Persistence.FilePersistence.Path) == 0 { - services.OnlineStore.Persistence.FilePersistence.Path = defaultOnlineStorePath(cr) - } + if services.OnlineStore.Persistence.DBPersistence == nil { + if services.OnlineStore.Persistence.FilePersistence == nil { + services.OnlineStore.Persistence.FilePersistence = &feastdevv1.OnlineStoreFilePersistence{} + } - ensurePVCDefaults(services.OnlineStore.Persistence.FilePersistence.PvcConfig, OnlineFeastType) - } + if len(services.OnlineStore.Persistence.FilePersistence.Path) == 0 { + services.OnlineStore.Persistence.FilePersistence.Path = defaultOnlineStorePath(cr) + } + + ensurePVCDefaults(services.OnlineStore.Persistence.FilePersistence.PvcConfig, OnlineFeastType) + } - if services.OnlineStore.Server == nil { - services.OnlineStore.Server = &feastdevv1.ServerConfigs{} + if services.OnlineStore.Server == nil { + services.OnlineStore.Server = &feastdevv1.ServerConfigs{} + } + setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs, defaultFeatureServerImage) } - setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs) if services.UI != nil { - setDefaultCtrConfigs(&services.UI.ContainerConfigs.DefaultCtrConfigs) + setDefaultCtrConfigs(&services.UI.ContainerConfigs.DefaultCtrConfigs, defaultFeatureServerImage) } if applied.CronJob == nil { @@ -194,13 +201,20 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { setDefaultCronJobConfigs(applied.CronJob) } -func setDefaultCtrConfigs(defaultConfigs *feastdevv1.DefaultCtrConfigs) { +func setDefaultCtrConfigs(defaultConfigs *feastdevv1.DefaultCtrConfigs, defaultImage string) { if defaultConfigs.Image == nil { - img := getFeatureServerImage() + img := defaultImage defaultConfigs.Image = &img } } +func getFeatureServerImageForSpec(spec *feastdevv1.FeatureStoreSpec) string { + if spec != nil && spec.FeastProjectDir != nil && spec.FeastProjectDir.Packaged != nil && spec.FeastProjectDir.Packaged.Image != "" { + return spec.FeastProjectDir.Packaged.Image + } + return getFeatureServerImage() +} + func getFeatureServerImage() string { if img, exists := os.LookupEnv(feastServerImageVar); exists { return img @@ -208,6 +222,16 @@ func getFeatureServerImage() string { return DefaultImage } +// getInitContainerImage resolves the image for feast-init / feast-apply. +// Order: spec.services.initImage → spec.feastProjectDir.packaged.image → +// RELATED_IMAGE_FEATURE_SERVER → DefaultImage. +func getInitContainerImage(spec *feastdevv1.FeatureStoreSpec) string { + if spec != nil && spec.Services != nil && spec.Services.InitImage != nil && len(*spec.Services.InitImage) > 0 { + return *spec.Services.InitImage + } + return getFeatureServerImageForSpec(spec) +} + func checkOfflineStoreFilePersistenceType(value string) error { if slices.Contains(feastdevv1.ValidOfflineStoreFilePersistenceTypes, value) { return nil @@ -313,7 +337,7 @@ func hasAttrib(s interface{}, fieldName string, value interface{}) (bool, error) val := reflect.ValueOf(s) // Check that the object is a pointer so we can modify it - if val.Kind() != reflect.Ptr || val.IsNil() { + if val.Kind() != reflect.Pointer || val.IsNil() { return false, fmt.Errorf("expected a pointer to struct, got %v", val.Kind()) } @@ -371,6 +395,12 @@ func IsOpenShift() bool { return isOpenShift } +// HasServiceMonitorCRD returns whether the monitoring.coreos.com API group +// (Prometheus Operator) is available in the cluster. +func HasServiceMonitorCRD() bool { + return hasServiceMonitorCRD +} + // SetIsOpenShift sets the global flag isOpenShift by the controller manager. // We don't need to keep fetching the API every reconciliation cycle that we need to know about the platform. func SetIsOpenShift(cfg *rest.Config) { @@ -390,15 +420,13 @@ func SetIsOpenShift(cfg *rest.Config) { for _, v := range apiList.Groups { if v.Name == "route.openshift.io" { isOpenShift = true - break + } + if v.Name == "monitoring.coreos.com" { + hasServiceMonitorCRD = true } } } -func missingOidcSecretProperty(property OidcPropertyType) error { - return fmt.Errorf(OidcMissingSecretError, property) -} - // getEnvVar returns the position of the EnvVar found by name func getEnvVar(envName string, env []corev1.EnvVar) int { for pos, v := range env { diff --git a/infra/feast-operator/internal/controller/services/util_test.go b/infra/feast-operator/internal/controller/services/util_test.go new file mode 100644 index 00000000000..5a868d2d101 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/util_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2024 Feast Community. + +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 + + http://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 services + +import ( + "os" + "testing" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/utils/ptr" +) + +var _ = Describe("ApplyDefaultsToStatus", func() { + It("deploys the online store with defaults when it is not declared", func() { + cr := &feastdevv1.FeatureStore{ + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "test_project", + Services: &feastdevv1.FeatureStoreServices{}, + }, + } + + ApplyDefaultsToStatus(cr) + + online := cr.Status.Applied.Services.OnlineStore + Expect(online).ToNot(BeNil()) + Expect(online.Disabled).To(BeFalse()) + Expect(online.Persistence).ToNot(BeNil()) + Expect(online.Server).ToNot(BeNil()) + }) + + It("applies online store defaults when it is declared", func() { + cr := &feastdevv1.FeatureStore{ + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "test_project", + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{}, + }, + }, + } + + ApplyDefaultsToStatus(cr) + + online := cr.Status.Applied.Services.OnlineStore + Expect(online).ToNot(BeNil()) + Expect(online.Persistence).ToNot(BeNil()) + Expect(online.Server).ToNot(BeNil()) + }) + + // #6586: disabling the online store opts out of its persistence and serving + // pod, letting a registry-only or offline-only ViewerStore skip it while + // leaving the default-on behavior unchanged for everyone else. + It("does not apply persistence or server defaults when the online store is disabled", func() { + cr := &feastdevv1.FeatureStore{ + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "test_project", + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{Disabled: true}, + }, + }, + } + + ApplyDefaultsToStatus(cr) + + online := cr.Status.Applied.Services.OnlineStore + Expect(online).ToNot(BeNil()) + Expect(online.Disabled).To(BeTrue()) + Expect(online.Persistence).To(BeNil()) + Expect(online.Server).To(BeNil()) + }) +}) + +func TestGetInitContainerImage(t *testing.T) { + customInit := "quay.io/org/feast-init:custom" + packagedImage := "quay.io/org/feast-packaged:test" + envImage := "quay.io/org/feast-env:test" + + t.Run("uses initImage ahead of packaged and server images", func(t *testing.T) { + t.Setenv(feastServerImageVar, envImage) + got := getInitContainerImage(&feastdevv1.FeatureStoreSpec{ + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{Image: packagedImage}, + }, + Services: &feastdevv1.FeatureStoreServices{ + InitImage: ptr.To(customInit), + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("quay.io/org/offline:v1"), + }, + }, + }, + }, + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("quay.io/org/online:v1"), + }, + }, + }, + }, + }, + }) + if got != customInit { + t.Fatalf("got %q, want %q (must not inherit server images)", got, customInit) + } + }) + + t.Run("uses packaged image ahead of RELATED_IMAGE_FEATURE_SERVER", func(t *testing.T) { + t.Setenv(feastServerImageVar, envImage) + got := getInitContainerImage(&feastdevv1.FeatureStoreSpec{ + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{Image: packagedImage}, + }, + Services: &feastdevv1.FeatureStoreServices{}, + }) + if got != packagedImage { + t.Fatalf("got %q, want %q", got, packagedImage) + } + }) + + t.Run("falls back to RELATED_IMAGE_FEATURE_SERVER", func(t *testing.T) { + t.Setenv(feastServerImageVar, envImage) + got := getInitContainerImage(&feastdevv1.FeatureStoreSpec{ + Services: &feastdevv1.FeatureStoreServices{}, + }) + if got != envImage { + t.Fatalf("got %q, want %q", got, envImage) + } + }) + + t.Run("falls back to DefaultImage", func(t *testing.T) { + _ = os.Unsetenv(feastServerImageVar) + got := getInitContainerImage(nil) + if got != DefaultImage { + t.Fatalf("got %q, want %q", got, DefaultImage) + } + }) + + t.Run("ignores empty initImage", func(t *testing.T) { + t.Setenv(feastServerImageVar, envImage) + got := getInitContainerImage(&feastdevv1.FeatureStoreSpec{ + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{Image: packagedImage}, + }, + Services: &feastdevv1.FeatureStoreServices{ + InitImage: ptr.To(""), + }, + }) + if got != packagedImage { + t.Fatalf("got %q, want %q", got, packagedImage) + } + }) +} diff --git a/infra/feast-operator/test/api/featurestore_packaged_types_test.go b/infra/feast-operator/test/api/featurestore_packaged_types_test.go new file mode 100644 index 00000000000..97525ec1abe --- /dev/null +++ b/infra/feast-operator/test/api/featurestore_packaged_types_test.go @@ -0,0 +1,162 @@ +/* +Copyright 2026 Feast Community. + +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 + + http://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 api + +import ( + "context" + "strings" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type packagedFeatureStoreFactory func(name, featureRepoPath string) client.Object +type conflictingPackagedFeatureStoreFactory func(name, conflictingMode string) client.Object + +func newV1PackagedFeatureStore(name, featureRepoPath string) client.Object { + return &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespaceName}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "test_project", + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{FeatureRepoPath: featureRepoPath}, + }, + }, + } +} + +func newV1Alpha1PackagedFeatureStore(name, featureRepoPath string) client.Object { + return &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespaceName}, + Spec: feastdevv1alpha1.FeatureStoreSpec{ + FeastProject: "test_project", + FeastProjectDir: &feastdevv1alpha1.FeastProjectDir{ + Packaged: &feastdevv1alpha1.FeastPackagedOptions{FeatureRepoPath: featureRepoPath}, + }, + }, + } +} + +func newV1ConflictingPackagedFeatureStore(name, conflictingMode string) client.Object { + featureStore := newV1PackagedFeatureStore(name, "/opt/feast/feature_repo").(*feastdevv1.FeatureStore) + switch conflictingMode { + case "init": + featureStore.Spec.FeastProjectDir.Init = &feastdevv1.FeastInitOptions{} + case "git": + featureStore.Spec.FeastProjectDir.Git = &feastdevv1.GitCloneOptions{ + URL: "https://example.com/feature-repo.git", + } + } + return featureStore +} + +func newV1Alpha1ConflictingPackagedFeatureStore(name, conflictingMode string) client.Object { + featureStore := newV1Alpha1PackagedFeatureStore(name, "/opt/feast/feature_repo").(*feastdevv1alpha1.FeatureStore) + switch conflictingMode { + case "init": + featureStore.Spec.FeastProjectDir.Init = &feastdevv1alpha1.FeastInitOptions{} + case "git": + featureStore.Spec.FeastProjectDir.Git = &feastdevv1alpha1.GitCloneOptions{ + URL: "https://example.com/feature-repo.git", + } + } + return featureStore +} + +var _ = Describe("Packaged feature repository path validation", func() { + ctx := context.Background() + apiVersions := []struct { + name string + id string + factory packagedFeatureStoreFactory + conflictingFactory conflictingPackagedFeatureStoreFactory + }{ + { + name: "feast.dev/v1", + id: "v1", + factory: newV1PackagedFeatureStore, + conflictingFactory: newV1ConflictingPackagedFeatureStore, + }, + { + name: "feast.dev/v1alpha1", + id: "v1alpha1", + factory: newV1Alpha1PackagedFeatureStore, + conflictingFactory: newV1Alpha1ConflictingPackagedFeatureStore, + }, + } + + for _, apiVersion := range apiVersions { + apiVersion := apiVersion + Context(apiVersion.name, func() { + DescribeTable("accepts canonical absolute non-root paths", + func(nameSuffix, featureRepoPath string) { + featureStore := apiVersion.factory( + "packaged-"+apiVersion.id+"-"+nameSuffix, + featureRepoPath, + ) + Expect(k8sClient.Create(ctx, featureStore)).To(Succeed()) + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }, + Entry("standard", "standard", "/opt/feast/feature_repo"), + Entry("hidden component", "hidden", "/opt/.feast/feature_repo"), + Entry("dot in component", "dot-name", "/opt/feature_repo.v2"), + ) + + DescribeTable("rejects non-canonical, relative, or root paths", + func(nameSuffix, featureRepoPath string) { + featureStore := apiVersion.factory( + "packaged-"+apiVersion.id+"-"+nameSuffix, + featureRepoPath, + ) + err := k8sClient.Create(ctx, featureStore) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsInvalid(err)).To(BeTrue(), "expected invalid error, got %v", err) + Expect(strings.ToLower(err.Error())).To(ContainSubstring("canonical absolute, non-root path")) + }, + Entry("relative", "relative", "opt/feast/feature_repo"), + Entry("root", "root", "/"), + Entry("parent collapses to root", "parent-root", "/opt/.."), + Entry("leading parent traversal", "leading-parent", "/../x"), + Entry("repeated separator", "repeated-separator", "/opt//feature_repo"), + Entry("current-directory component", "current-dir", "/opt/./feature_repo"), + Entry("trailing separator", "trailing-separator", "/opt/feature_repo/"), + Entry("nested traversal", "nested-traversal", "/a/../../etc"), + Entry("repeated root separator", "repeated-root", "//"), + ) + + DescribeTable("rejects packaged together with another project directory mode", + func(nameSuffix, conflictingMode string) { + featureStore := apiVersion.conflictingFactory( + "packaged-"+apiVersion.id+"-"+nameSuffix, + conflictingMode, + ) + err := k8sClient.Create(ctx, featureStore) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsInvalid(err)).To(BeTrue(), "expected invalid error, got %v", err) + Expect(err.Error()).To(ContainSubstring("One selection required between init, git, or packaged")) + }, + Entry("init", "with-init", "init"), + Entry("git", "with-git", "git"), + ) + }) + } +}) diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index d426c8e0d7e..00312e0fabb 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -445,7 +445,7 @@ func cronJobWithAnnotations(featureStore *feastdevv1.FeatureStore) *feastdevv1.F "test-annotation": "test-value", "another-annotation": "another-value", }, - Schedule: "0 0 * * *", + Schedule: dailyMidnightCron, } return fsCopy } @@ -462,7 +462,7 @@ func cronJobWithEmptyAnnotations(featureStore *feastdevv1.FeatureStore) *feastde func cronJobWithoutAnnotations(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() fsCopy.Spec.CronJob = &feastdevv1.FeastCronJob{ - Schedule: "0 0 * * *", + Schedule: dailyMidnightCron, } return fsCopy } @@ -477,12 +477,16 @@ func quotedSlice(stringSlice []string) string { return strings.Join(quotedSlice, ", ") } -const resourceName = "test-resource" -const namespaceName = "default" +const ( + resourceName = "test-resource" + namespaceName = "default" + defaultNs = "default" + dailyMidnightCron = "0 0 * * *" +) var typeNamespacedName = types.NamespacedName{ Name: resourceName, - Namespace: "default", + Namespace: defaultNs, } func initContext() (context.Context, *feastdevv1.FeatureStore) { diff --git a/infra/feast-operator/test/api/suite_test.go b/infra/feast-operator/test/api/suite_test.go index 558068a7957..eef4718cf58 100644 --- a/infra/feast-operator/test/api/suite_test.go +++ b/infra/feast-operator/test/api/suite_test.go @@ -26,6 +26,7 @@ import ( . "github.com/onsi/gomega" feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -73,6 +74,8 @@ var _ = BeforeSuite(func() { err = feastdevv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = feastdevv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme diff --git a/infra/feast-operator/test/testdata/feast_integration_test_crs/feast.yaml b/infra/feast-operator/test/testdata/feast_integration_test_crs/feast.yaml index 9dec3831b8c..56ab0ffbd4b 100644 --- a/infra/feast-operator/test/testdata/feast_integration_test_crs/feast.yaml +++ b/infra/feast-operator/test/testdata/feast_integration_test_crs/feast.yaml @@ -7,7 +7,7 @@ stringData: redis: | connection_string: redis.test-ns-feast.svc.cluster.local:6379 sql: | - path: postgresql+psycopg://${POSTGRESQL_USER}:${POSTGRESQL_PASSWORD}@postgres.test-ns-feast.svc.cluster.local:5432/${POSTGRESQL_DATABASE} + path: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres.test-ns-feast.svc.cluster.local:5432/${POSTGRES_DB} cache_ttl_seconds: 60 sqlalchemy_config_kwargs: echo: false diff --git a/infra/feast-operator/test/testdata/feast_integration_test_crs/postgres.yaml b/infra/feast-operator/test/testdata/feast_integration_test_crs/postgres.yaml index 8fbb11cf1cd..ba4599cea25 100644 --- a/infra/feast-operator/test/testdata/feast_integration_test_crs/postgres.yaml +++ b/infra/feast-operator/test/testdata/feast_integration_test_crs/postgres.yaml @@ -4,9 +4,9 @@ metadata: name: postgres-secret namespace: test-ns-feast stringData: - POSTGRESQL_DATABASE: feast - POSTGRESQL_USER: feast - POSTGRESQL_PASSWORD: feast + POSTGRES_DB: feast + POSTGRES_USER: feast + POSTGRES_PASSWORD: feast --- apiVersion: apps/v1 kind: Deployment @@ -25,7 +25,7 @@ spec: spec: containers: - name: postgres - image: 'quay.io/sclorg/postgresql-16-c9s@sha256:5879226a0fd2ea295df6836cc30ab624d2a1c51b81b3406284885604e10ddefe' + image: 'quay.io/feastdev-ci/feast-test-images:postgres-17-alpine' ports: - containerPort: 5432 envFrom: diff --git a/infra/feast-operator/test/testdata/feast_integration_test_crs/redis.yaml b/infra/feast-operator/test/testdata/feast_integration_test_crs/redis.yaml index cd88fb88fe5..c81fdb46b67 100644 --- a/infra/feast-operator/test/testdata/feast_integration_test_crs/redis.yaml +++ b/infra/feast-operator/test/testdata/feast_integration_test_crs/redis.yaml @@ -15,7 +15,8 @@ spec: spec: containers: - name: redis - image: 'quay.io/sclorg/redis-7-c9s@sha256:ce07d358cea749e67bcc77f73b2c5244d771ac0781ed20d7ebb2ba271c169173' + image: 'quay.io/feastdev-ci/feast-test-images:redis-7-alpine' + command: ["redis-server", "--save", ""] ports: - containerPort: 6379 env: diff --git a/infra/feast-operator/test/utils/test_util.go b/infra/feast-operator/test/utils/test_util.go index 7b5f0f8d6a0..dfb5a9f31fd 100644 --- a/infra/feast-operator/test/utils/test_util.go +++ b/infra/feast-operator/test/utils/test_util.go @@ -27,6 +27,8 @@ const ( FeatureStoreName = "simple-feast-setup" FeastResourceName = FeastPrefix + FeatureStoreName FeatureStoreResourceName = "featurestores.feast.dev" + feastCommand = "feast" + listCommand = "list" ) // dynamically checks if all conditions of custom resource featurestore are in "Ready" state. @@ -409,6 +411,10 @@ func DeployOperatorFromCode(testDir string, skipBuilds bool) { _, err = Run(cmd, testDir) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("deleting existing controller-manager deployment to allow selector changes on upgrade") + cmd = exec.Command("kubectl", "delete", "deployment", ControllerDeploymentName, "-n", FeastControllerNamespace, "--ignore-not-found=true") + _, _ = Run(cmd, testDir) + By("deploying the controller-manager") cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage), fmt.Sprintf("FS_IMG=%s", feastLocalImage)) _, err = Run(cmd, testDir) @@ -544,27 +550,27 @@ func VerifyFeastMethods(namespace string, feastDeploymentName string, testDir st } checks := []feastCheck{ { - command: []string{"feast", "projects", "list"}, + command: []string{feastCommand, "projects", listCommand}, expected: []string{"credit_scoring_local"}, logPrefix: "Projects List", }, { - command: []string{"feast", "feature-views", "list"}, + command: []string{feastCommand, "feature-views", listCommand}, expected: []string{"credit_history", "zipcode_features", "total_debt_calc"}, logPrefix: "Feature Views List", }, { - command: []string{"feast", "entities", "list"}, + command: []string{feastCommand, "entities", listCommand}, expected: []string{"zipcode", "dob_ssn"}, logPrefix: "Entities List", }, { - command: []string{"feast", "data-sources", "list"}, + command: []string{feastCommand, "data-sources", listCommand}, expected: []string{"Zipcode source", "Credit history", "application_data"}, logPrefix: "Data Sources List", }, { - command: []string{"feast", "features", "list"}, + command: []string{feastCommand, "features", listCommand}, expected: []string{ "credit_card_due", "mortgage_due", "student_loan_due", "vehicle_loan_due", "hard_pulls", "missed_payments_2y", "missed_payments_1y", "missed_payments_6m", diff --git a/infra/scripts/feature_server_docker_smoke.py b/infra/scripts/feature_server_docker_smoke.py new file mode 100644 index 00000000000..801decac90c --- /dev/null +++ b/infra/scripts/feature_server_docker_smoke.py @@ -0,0 +1,45 @@ +from types import SimpleNamespace + +import uvicorn + +from feast.feature_server import get_app + + +class _FakeRegistry: + def proto(self): + return object() + + def list_projects(self, allow_cache=True, tags=None): + return [] + + +class _FakeStore: + def __init__(self): + self.config = SimpleNamespace() + self.project = "smoke_test" + self.registry = _FakeRegistry() + self._provider = SimpleNamespace( + async_supported=SimpleNamespace( + online=SimpleNamespace(read=False, write=False) + ) + ) + + def _get_provider(self): + return self._provider + + async def initialize(self): + return None + + def refresh_registry(self): + return None + + def list_feature_views(self): + return [] + + async def close(self): + return None + + +if __name__ == "__main__": + app = get_app(_FakeStore()) + uvicorn.run(app, host="0.0.0.0", port=6566, log_level="error") diff --git a/infra/scripts/pixi/pixi.toml b/infra/scripts/pixi/pixi.toml index afb6407042d..b5ce74b1791 100644 --- a/infra/scripts/pixi/pixi.toml +++ b/infra/scripts/pixi/pixi.toml @@ -1,4 +1,4 @@ -[project] +[workspace] name = "pixi-feast" channels = ["conda-forge"] platforms = ["linux-64", "osx-arm64", "osx-64"] diff --git a/infra/templates/README.md.jinja2 b/infra/templates/README.md.jinja2 index ccaadc29ff0..2c92401f83d 100644 --- a/infra/templates/README.md.jinja2 +++ b/infra/templates/README.md.jinja2 @@ -17,7 +17,7 @@ ## Join us on Slack! -👋👋👋 [Come say hi on Slack!](https://communityinviter.com/apps/feastopensource/feast-the-open-source-feature-store) +👋👋👋 [Come say hi on Slack!](https://slack.feast.dev/) [Check out our DeepWiki!](https://deepwiki.com/feast-dev/feast) diff --git a/infra/website/docs/blog/feast-agents-mcp.md b/infra/website/docs/blog/feast-agents-mcp.md new file mode 100644 index 00000000000..bfa46ee8b02 --- /dev/null +++ b/infra/website/docs/blog/feast-agents-mcp.md @@ -0,0 +1,363 @@ +--- +title: "Building AI Agents with Feast: Feature Stores as Context and Memory" +description: "How Feast's MCP integration turns your feature store into a governed context and memory layer for AI agents, bridging the gap between experimental agents and production-ready systems." +date: 2026-04-11 +authors: ["Nikhil Kathole"] +--- + +
+ AI Agents powered by Feast Feature Store +
+ +AI agents are moving from demos to production. They handle customer support, orchestrate complex workflows, and make real-time decisions that affect business outcomes. But there is a gap between a working prototype and a production system: agents need reliable, low-latency access to structured data, they need to remember what happened in prior interactions, and all of this access needs to be governed. + +This is where feature stores enter the picture. In this post, we show how **Feast** -- an open-source feature store -- can serve as both the **context provider** and the **persistent memory layer** for AI agents, using the **Model Context Protocol (MCP)**. + +## The Problem: Agents Need Context, Memory, and Governance + +A standalone LLM knows nothing about your customers, your products, or your internal processes. To make good decisions, agents need **tools** that give them access to real data: + +- **Who is this user?** Their plan tier, account age, purchase history, satisfaction score. +- **What do we know about this topic?** Relevant documentation, knowledge-base articles, FAQs. +- **What happened before?** What did this agent discuss with this customer last time? What was left unresolved? + +That last point is critical and often overlooked. Most agent demos are stateless -- every conversation starts from scratch. But real support interactions build on prior context: *"I called about this yesterday"*, *"you said you'd escalate"*, *"I prefer email over chat."* An agent without memory cannot handle these. + +Without a proper data layer, teams end up writing ad-hoc database queries, hardcoding API calls, stuffing memory into Redis with no governance, or giving agents raw database access. This creates fragile, ungoverned, and hard-to-audit agent systems. + +## Feature Stores Solve This -- Including Memory + +Feature stores were built to solve exactly this class of problem -- albeit originally for traditional ML. They provide: + +1. **Low-latency online serving** of pre-computed features. +2. **Versioned, governed access** to data with RBAC and audit trails. +3. **Consistency** between training/offline and serving/online environments. +4. **A single abstraction** over diverse data sources (databases, data warehouses, streaming systems, and vector stores -- including Milvus, Elasticsearch, Qdrant, PGVector, and FAISS). +5. **Entity-keyed read/write** -- the same mechanism that serves features can also store and retrieve agent memory, keyed by customer ID, session ID, or any entity. + +With Feast's MCP support, these capabilities are exposed as **tools that AI agents can discover and call dynamically** -- and critically, agents can **write back** to the feature store, turning it into a governed memory layer. + +## Feast + MCP: Turning a Feature Store into an Agent Tool + +The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that lets AI applications discover and interact with external tools through a unified interface. Feast's feature server can now expose its endpoints as MCP tools with a simple configuration change: + +```yaml +feature_server: + type: mcp + enabled: true + mcp_enabled: true + mcp_transport: http + mcp_server_name: "feast-feature-store" + mcp_server_version: "1.0.0" +``` + +Once enabled, any MCP-compatible agent -- whether built with LangChain, LlamaIndex, CrewAI, AutoGen, or a custom framework -- can connect to `http://your-feast-server/mcp` and discover available tools like `get-online-features` for entity-based retrieval, `search` for vector similarity search, `vector_store_search` for OpenAI-compatible text search, and `write-to-online-store` for persisting agent state. + +## A Concrete Example: Customer-Support Agent with Memory + +To make this tangible, let's walk through a customer-support agent that uses Feast for structured feature retrieval, document search, and persistent memory. + +> **Note on the implementation:** This example builds the agent loop from scratch using the OpenAI tool-calling API and the MCP Python SDK -- no framework required. All Feast interactions use the MCP protocol: the agent connects to Feast's MCP endpoint, discovers available tools via `session.list_tools()`, and invokes them via `session.call_tool()`. We chose this approach to keep dependencies minimal and make every Feast interaction visible. In production, you would typically use a framework like LangChain/LangGraph, LlamaIndex, CrewAI, or AutoGen. Because Feast exposes a standard MCP endpoint, any of these frameworks can auto-discover the tools with zero custom code (see [Connecting Your Agent Framework](#connecting-your-agent-framework) below). + +### The Setup + +We define three feature views in Feast: + +**Customer profiles** -- structured data served from the online store: + +```python +customer_profile = FeatureView( + name="customer_profile", + entities=[customer], + schema=[ + Field(name="name", dtype=String), + Field(name="email", dtype=String), + Field(name="plan_tier", dtype=String), + Field(name="account_age_days", dtype=Int64), + Field(name="total_spend", dtype=Float64), + Field(name="open_tickets", dtype=Int64), + Field(name="satisfaction_score", dtype=Float64), + ], + source=customer_profile_source, + ttl=timedelta(days=1), +) +``` + +**Knowledge base** -- support articles stored as vector embeddings (Feast supports multiple vector backends including Milvus, Elasticsearch, Qdrant, PGVector, and FAISS -- this example uses Milvus): + +```python +knowledge_base = FeatureView( + name="knowledge_base", + entities=[document], + schema=[ + Field( + name="vector", dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="title", dtype=String), + Field(name="content", dtype=String), + Field(name="category", dtype=String), + ], + source=knowledge_base_source, + ttl=timedelta(days=7), +) +``` + +**Agent memory** -- per-customer interaction state written back by the agent: + +```python +agent_memory = FeatureView( + name="agent_memory", + entities=[customer], + schema=[ + Field(name="last_topic", dtype=String), + Field(name="last_resolution", dtype=String), + Field(name="interaction_count", dtype=Int64), + Field(name="preferences", dtype=String), + Field(name="open_issue", dtype=String), + ], + ttl=timedelta(days=30), +) +``` + +This is the key insight: Feast is not just providing context to the agent -- it is also **storing the agent's memory**. The `agent_memory` feature view is entity-keyed by customer ID, TTL-managed (30-day expiration), schema-typed, and governed by the same RBAC as every other feature. The agent reads prior interactions via `recall_memory`, and memory is automatically checkpointed after each turn using the same online store infrastructure. + +### The Agent Loop + +The agent connects to Feast's MCP endpoint, discovers tools dynamically, and uses the MCP protocol for all Feast interactions. Each round, the LLM sees the conversation history plus the available read tools, and decides what to do. Memory is saved automatically after the turn -- framework-style, not as an LLM decision: + +```python +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +async with streamablehttp_client("http://localhost:6566/mcp") as (r, w, _): + async with ClientSession(r, w) as session: + await session.initialize() + tools = await session.list_tools() # discover Feast tools + + for round in range(MAX_ROUNDS): + response = call_llm(messages, tools=[...]) + + if response.finish_reason == "stop": + break + + for tool_call in response.tool_calls: + result = await session.call_tool(name, args) # MCP + messages.append(tool_result(result)) + + # Framework-style checkpoint: auto-save via MCP + await session.call_tool("write_to_online_store", {...}) +``` + +A typical multi-turn flow looks like: + +1. **Round 1**: Agent calls `recall_memory("C1001")` -- checks for prior interactions. Calls `lookup_customer("C1001")` -- gets profile data. Calls `search_knowledge_base("SSO setup")` -- finds the relevant article. +2. **Round 2**: Has enough context. Generates a personalised response and returns it. +3. **Checkpoint**: The framework auto-saves `topic="SSO setup"` and a resolution summary to Feast. + +When C1001 comes back later and says *"I'm following up on my SSO question"*, the agent calls `recall_memory` and immediately knows what was discussed -- no re-explanation needed. + +For a simpler question like *"What's my current plan?"*, the agent only calls `lookup_customer` -- skipping the knowledge base entirely. The LLM makes these routing decisions based on the question, not hardcoded logic. + +### Memory as Infrastructure + +Production agent frameworks treat persistence as infrastructure, not an LLM decision: + +- **LangGraph** uses checkpointers (`MemorySaver`, `PostgresSaver`) that auto-save state after every graph step, keyed by `thread_id`. +- **CrewAI** enables `memory=True` for automatic short-term, long-term, and entity memory. +- **AutoGen** uses post-conversation hooks to extract and store learnings. + +This demo follows the same pattern. The LLM has three read tools for reasoning; memory is checkpointed by the framework after each turn via Feast's `write-to-online-store` endpoint. This ensures consistent, reliable memory regardless of LLM behaviour -- no risk of the model forgetting to save or writing inconsistent state. Feast is a natural fit for this checkpoint layer because it provides entity-keyed storage, TTL-managed expiration, schema enforcement, RBAC governance, and offline queryability -- all out of the box. + +
+ Feast MCP Agent Workflow — agent loop with context retrieval, vector search, and memory persistence through Feast +
+ +The agent doesn't need to know *where* the data lives or *which* vector database is behind the scenes. It calls Feast through a standard protocol, and Feast handles the routing to the right store -- whether that's Milvus, Elasticsearch, Qdrant, PGVector, or FAISS. Swapping the vector backend is a configuration change, not a code change. The same protocol handles both reads and writes -- context retrieval and memory persistence use the same governed infrastructure. + +### The Response + +Instead of a generic answer, the agent produces something like: + +> *"Hi Alice! Since you're on our Enterprise plan, SSO is available for your team. Go to Settings > Security > SSO and enter your Identity Provider metadata URL. We support SAML 2.0 and OIDC. Once configured, all team members will authenticate through your IdP. As an Enterprise customer, you also have a dedicated Slack channel and account manager if you need hands-on help."* + +The personalisation (mentioning the Enterprise plan, dedicated Slack channel) comes directly from the Feast features. And if Alice calls back next week, the agent already knows what was discussed. + +## Why This Matters for Production + +### Unified Data Access: Structured Features + Vector Search + Memory + +Real-world agents need more than document retrieval. They need access to multiple data types through a single governed interface: + +| Data Type | Example | Feast Capability | +|---|---|---| +| Structured features | Account tier, spend history | `get_online_features` | +| Vector embeddings | Support article search | `retrieve_online_documents_v2` | +| Agent memory | Last interaction topic, open issues | `get_online_features` + `write_to_online_store` | +| Streaming features | Real-time click counts | Push sources with `write_to_online_store` | +| Pre-computed predictions | Churn probability | Served alongside other features | + +Feast unifies all of these behind a single API that agents can both read from and write to. + +### Context Memory: Why Feast Beats Ad-Hoc Solutions + +Many teams use Redis, in-memory dicts, or custom databases for agent memory. Feast provides a better foundation: + +| Concern | Ad-hoc Memory | Feast Memory | +|---|---|---| +| **Governance** | No RBAC, no audit trail | Same RBAC and permissions as all features | +| **TTL management** | Manual expiration logic | Declarative TTL on the feature view | +| **Entity-keying** | Custom key design | Native entity model (customer_id, session_id, etc.) | +| **Observability** | Custom logging | Integrated with OpenTelemetry and MLflow traces | +| **Offline analysis** | Separate export pipeline | Memory is just another feature -- queryable offline | +| **Schema evolution** | Unstructured blobs | Typed schema with versioning | + +Because agent memory is stored as a standard Feast feature view, it inherits all the infrastructure that already exists for serving ML features: monitoring, access control, TTL management, and offline queryability. There is no separate system to operate. + +### Governance: Who Can Access What + +In production, you do not want every agent to access every feature. Feast provides: + +- **RBAC**: Role-based access control with OIDC integration. +- **Feature-level permissions**: Control which feature views each service account can read or write. +- **Audit trails**: Track which agent accessed which features and when. + +This is especially important for memory: you want governance over what agents remember and who can read those memories. + +### Production Platform Architecture + +Deploying agents in production requires more than just the agent code. A well-architected platform wraps agents in enterprise infrastructure without requiring changes to the agent itself. Feast fits naturally into this layered approach: + +
+ Production platform architecture — Feast MCP Server behind MCP Gateway with observability, guardrails, and lifecycle management +
+ +- **MCP Gateway**: Feast sits behind an Envoy-based MCP Gateway as one of many tool servers. The gateway provides identity-based tool filtering -- an agent's JWT claims determine whether it can call Feast at all, and which features it can access. +- **Sandboxed Execution**: Feast runs as a standard Kubernetes service, benefiting from sandboxed container isolation that keeps agent workloads separated. +- **Observability**: Feast feature-retrieval and memory-write calls flow through the platform's OpenTelemetry pipeline, appearing in MLflow traces alongside LLM calls and tool invocations. +- **Agent Lifecycle Management (Kagenti)**: An operator like Kagenti can discover Feast as a tool server via AgentCard CRDs and inject tracing and governance without code changes. + +The principle is straightforward: the agent is yours, the platform provides the guardrails, and Feast provides the data and memory. + +## Connecting Your Agent Framework + +Since Feast exposes a standard MCP endpoint, integration is framework-agnostic: + +**LangChain / LangGraph:** +```python +from langchain_mcp_adapters.client import MultiServerMCPClient +from langgraph.prebuilt import create_react_agent + +async with MultiServerMCPClient( + {"feast": {"url": "http://feast-server:6566/mcp", "transport": "streamable_http"}} +) as client: + tools = client.get_tools() + agent = create_react_agent(llm, tools) + result = await agent.ainvoke({"messages": "How do I set up SSO?"}) +``` + +**LlamaIndex:** +```python +from llama_index.tools.mcp import aget_tools_from_mcp_url +from llama_index.core.agent.function_calling import FunctionCallingAgent +from llama_index.llms.openai import OpenAI + +tools = await aget_tools_from_mcp_url("http://feast-server:6566/mcp") +agent = FunctionCallingAgent.from_tools(tools, llm=OpenAI(model="gpt-4o-mini")) +response = await agent.achat("How do I set up SSO?") +``` + +**Claude Desktop / Cursor:** +```json +{ + "mcpServers": { + "feast": { + "url": "http://feast-server:6566/mcp", + "transport": "streamable_http" + } + } +} +``` + +**Direct REST API:** +```python +import requests + +features = requests.post("http://feast-server:6566/get-online-features", json={ + "features": ["customer_profile:plan_tier", "customer_profile:satisfaction_score"], + "entities": {"customer_id": ["C1001"]}, +}).json() +``` + +The Feast-specific integration is just connecting to the MCP endpoint and getting the tools. Once you have them, building the agent follows each framework's standard patterns -- the tool-calling loop, message threading, and state persistence are handled natively. Feast's MCP endpoint means zero custom wiring. + +### Customizing for Your Use Case + +This demo's system prompt, tool names, and feature views are all specific to the customer-support scenario. Here's what changes when you build your own agent: + +| What | This demo | Your agent | +|---|---|---| +| **Feature views** | `customer_profile`, `knowledge_base`, `agent_memory` | Define your own in `features.py` (e.g., `product_catalog`, `order_history`, `fraud_signals`) | +| **System prompt** | "Call recall_memory and lookup_customer first..." | Write instructions specific to your domain and workflow | +| **Tool wrappers** | `lookup_customer`, `search_knowledge_base`, `recall_memory` | Optional -- see below | + +**Do you need custom tool wrappers?** It depends on how you build your agent: + +- **With a framework (LangChain, LlamaIndex, etc.):** No. The framework discovers Feast's generic MCP tools (`get_online_features`, `retrieve_online_documents`, `write_to_online_store`) automatically. The LLM calls them directly with your feature view names and entities. No wrapper code needed. + +- **With a raw loop (like this demo):** Optional but recommended. This demo wraps `get_online_features` into `lookup_customer` and `recall_memory` to give the LLM friendlier, domain-specific tool names. You'd create similar wrappers for your use case (e.g., `check_inventory`, `get_order_status`). The wrappers are thin -- they just call the Feast MCP tool with the right feature names and entities. + +**What stays the same** regardless of use case: Feast's MCP server, the online/offline store infrastructure, RBAC, TTL management, and the auto-save memory pattern. You define feature views, `feast apply`, start the server, and connect -- the same three generic MCP tools serve any domain. + +## Try It Yourself + +We have published a complete working example in the Feast repository. A single script handles setup, server startup, and the demo: + +```bash +git clone https://github.com/feast-dev/feast.git +cd feast/examples/agent_feature_store + +./run_demo.sh # demo mode (no API key needed) +OPENAI_API_KEY=sk-... ./run_demo.sh # live LLM tool-calling +``` + +The script installs dependencies, generates sample data (customer profiles, knowledge-base articles, and the agent memory scaffold), starts the Feast MCP server, runs the agent, and tears everything down on exit. + +To run with a real LLM, set the API key and (optionally) the base URL and model: + +```bash +# OpenAI +export OPENAI_API_KEY="sk-..." # pragma: allowlist secret +./run_demo.sh + +# Ollama (free, local -- no API key needed) +ollama pull llama3.1:8b +export OPENAI_API_KEY="ollama" # pragma: allowlist secret +export OPENAI_BASE_URL="http://localhost:11434/v1" +export LLM_MODEL="llama3.1:8b" +./run_demo.sh + +# Any OpenAI-compatible provider (Azure, vLLM, etc.) +export OPENAI_API_KEY="your-key" # pragma: allowlist secret +export OPENAI_BASE_URL="https://your-endpoint/v1" +export LLM_MODEL="your-model" +./run_demo.sh +``` + +The agent demonstrates memory continuity: when the same customer returns, the agent recalls what was discussed previously. + +See the full example on [GitHub](https://github.com/feast-dev/feast/tree/master/examples/agent_feature_store). + +## What's Next + +The intersection of feature stores and agentic AI is just getting started. Here is what we are working on: + +- **Richer MCP tools**: Exposing `retrieve_online_documents_v2` as a first-class MCP tool for native vector search. +- **Memory patterns**: Expanding the memory model to support session-scoped memory, hierarchical summarisation, and cross-agent shared memory. +- **Platform integration**: First-class support in MCP Gateway tool catalogs and agent lifecycle operators. +- **Streaming features for agents**: Real-time feature updates from Kafka/Flink that agents can subscribe to. + +## Join the Conversation + +We would love to hear how you are using (or plan to use) Feast in your agent workflows. Reach out on [Slack](https://slack.feast.dev/) or [GitHub](https://github.com/feast-dev/feast) -- and give the example a try! diff --git a/infra/website/docs/blog/feast-data-quality-monitoring.md b/infra/website/docs/blog/feast-data-quality-monitoring.md new file mode 100644 index 00000000000..2c918c83a63 --- /dev/null +++ b/infra/website/docs/blog/feast-data-quality-monitoring.md @@ -0,0 +1,224 @@ +--- +title: Data Quality Monitoring in Feast 0.64 +description: Feast 0.64 adds native data quality monitoring with baseline metrics, batch and serving-log analysis, REST APIs, CLI workflows, and a built-in monitoring UI. +date: 2026-06-26 +authors: ["Jitendra Yejare", "Nikhil Kathole", "Francisco Javier Arceo"] +--- + +
+ Feast Data Quality Monitoring +
+ +# Data Quality Monitoring in Feast 0.64 + +Serving ML models in production is extremely hard. + +The reason is simple: production models depend on data from many different places. Every source system has some probability of operational error: a delayed pipeline, a schema change, a column that starts producing nulls, a categorical value that changes meaning, a late partition, a silent backfill, or a service that behaves differently under production traffic. + +The more data sources a model depends on, the more chances there are for one of those systems to drift, fail, or change underneath you. That creates a basic tension in ML systems. Models are data hungry and often benefit from orthogonal features from many upstream systems, but every additional upstream dependency increases operational risk. What ML wants for predictive power can conflict with what engineering wants for reliability. + +The only way to manage that tension is to monitor what is actually happening in production. Feature quality problems rarely arrive as neat exceptions. A model may keep serving predictions while one upstream table starts producing nulls, a batch pipeline shifts a numeric distribution, or production requests drift away from the training baseline. By the time these issues show up in model metrics, the debugging path usually crosses feature definitions, data sources, materialization jobs, and serving logs. + +Feast 0.64 adds a native data quality monitoring system that brings those signals directly into the feature store. Instead of relying on a separate validation framework, Feast can now compute, store, serve, and visualize feature-level statistics across batch data and logged serving data. + +The biggest change is that monitoring is now a first-class Feast workflow: + +- `feast apply` can compute baseline metrics for registered feature views +- `feast monitor run` can compute scheduled daily, weekly, biweekly, monthly, and quarterly metrics +- REST endpoints expose monitoring jobs, per-feature metrics, aggregate feature-view and feature-service metrics, baselines, and time series +- the Feast UI includes a Monitoring page with filters, summary tabs, feature drilldowns, histograms, and time-series charts +- compute is pushed into supported offline stores where possible, with a Python fallback for other backends + +## From validation to monitoring + +Feast previously supported an external-library-based validation path for historical retrievals. That integration was useful, but it lived outside the normal feature store workflow: users had to install extra dependencies, write profiler code, and run validation against saved datasets. + +That original integration proved the need for data quality inside Feast. It helped answer an important question: after generating a training dataset, does this dataset satisfy the expectations we care about? + +But production feature quality problems usually happen after the training dataset is generated. A pipeline may keep running while an upstream producer changes a column, shifts a distribution, starts sending nulls, or changes the meaning of a categorical value. In those cases, the feature code may be perfectly correct while the data feeding it has changed. + +Feast needed monitoring that was closer to the system that actually computes and serves features. By coupling DQM to Feast's compute engines and offline stores, Feast can compute quality metrics where the data already lives, reuse feature metadata, compare batch and serving-log distributions, and expose the results through the same CLI, REST API, and UI used to operate the feature store. + +This also helps when teams maintain multiple feature execution paths. For example, a feature may be generated one way for training and another way for low-latency serving or streaming. DQM is not a formal proof that two implementations are equivalent, but distribution metrics, baselines, and serving-log comparisons provide an early warning when those paths start producing meaningfully different values. + +The new system is broader and more operational. It automatically computes statistical profiles for registered features, stores them in monitoring tables, and makes them available to the CLI, REST API, and UI. This gives teams the kind of feature health view they need after features are already in production, not only during one historical retrieval. + +For each feature, Feast can track: + +| Metric family | Examples | +|---|---| +| completeness | row count, null count, null rate | +| numeric profile | mean, standard deviation, min, max | +| percentiles | p50, p75, p90, p95, p99 | +| distributions | numeric histograms or categorical top values | +| aggregate health | feature-view and feature-service summaries | + +## Baselines start at registration + +The simplest way to turn on monitoring is to enable DQM in `feature_store.yaml`: + +```yaml +data_quality_monitoring: + auto_baseline: true +``` + +When `auto_baseline` is enabled, `feast apply` computes baseline metrics for feature views that do not already have one. The baseline is marked as the reference distribution and can be compared with later scheduled metrics. + +That matters because the baseline lives next to the feature definitions. When a feature view is registered, Feast can also capture what "normal" looked like at registration time. Later monitoring runs can answer whether the current data still resembles that baseline. + +For Feast Operator deployments, the same setting is available on the `FeatureStore` custom resource: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +spec: + feastProject: my_project + dataQualityMonitoring: + autoBaseline: true +``` + +## Scheduled monitoring with the CLI + +For ongoing monitoring, schedule: + +```bash +feast monitor run +``` + +In auto mode, Feast detects the latest event timestamp in the source data and computes metrics across the supported granularities: daily, weekly, biweekly, monthly, and quarterly. + +You can also scope monitoring to a specific feature view: + +```bash +feast monitor run --feature-view driver_stats +``` + +Or compute a specific window and mark it as a baseline: + +```bash +feast monitor run \ + --feature-view driver_stats \ + --start-date 2025-01-01 \ + --end-date 2025-03-31 \ + --granularity daily \ + --set-baseline +``` + +This makes the CLI easy to wire into Airflow, Kubeflow Pipelines, cron, or any scheduler that already runs Feast materialization jobs. + +## Monitoring serving logs + +Batch data tells you whether source features look healthy. Serving logs tell you what your models actually received. + +If a `FeatureService` has logging configured, Feast can compute monitoring metrics from the logged online features: + +```bash +feast monitor run --source-type log +``` + +You can also run batch and log monitoring together: + +```bash +feast monitor run --source-type all +``` + +Log metrics are stored with `data_source_type="log"` alongside batch metrics. Feast normalizes logged feature names back to the feature view and feature name, which lets the UI and API compare batch and serving distributions without forcing users to maintain a separate mapping. + +## The new Monitoring UI + +The most visible 0.64 improvement is the Monitoring page in the Feast UI. It turns DQM from a background job into something feature owners can inspect without leaving Feast. + +The page includes three main tabs: + +| Tab | What it shows | +|---|---| +| Features | per-feature metrics such as null rate, row count, freshness, and health | +| Feature Views | aggregate quality summaries per feature view | +| Feature Services | aggregate quality summaries for model-facing feature services | + +At the top of the page, users can filter by feature view, granularity, source type, and date range. Baseline is treated as its own view because it represents all baseline data rather than a normal date window. The page also includes a Compute Metrics action that triggers DQM computation from the UI, plus Refresh for reloading already computed results. + +
+ Feast DQM Monitoring dashboard showing feature metrics, filters, histograms, and health status +
+ +Clicking a feature opens a detail page with: + +- a distribution chart for numeric histograms or categorical values +- a statistics panel with null rate, mean, standard deviation, min, max, and percentiles +- a granularity selector that can switch between computed windows and baseline +- time-series charts for metric drift, including aggregate statistics and null-rate trends + +
+ Feast DQM numeric feature detail page with distribution chart, statistics, and time-series analysis +
+ +
+ Feast DQM categorical feature detail page with category distribution and statistics +
+ +This is the workflow we wanted: feature owners can start from a table of health signals, filter down to the part of the feature store they care about, and then drill into the exact feature whose distribution changed. + +## How compute engines fit in + +DQM is intentionally tied to Feast's compute and offline-store architecture. The goal is to compute metrics where the data already lives whenever possible, then store the results in backend-specific monitoring tables. + +Supported backends push computation into the underlying system: + +| Backend | Compute path | Storage path | +|---|---|---| +| PostgreSQL | SQL push-down | `INSERT ON CONFLICT` | +| Snowflake | SQL push-down | `MERGE` with JSON metrics | +| BigQuery | SQL push-down | BigQuery `MERGE` | +| Redshift | SQL push-down | Data API-backed writes | +| Spark | SparkSQL push-down | Parquet-backed tables | +| Oracle | SQL through Ibis | `MERGE` | +| DuckDB | in-memory SQL | Parquet files | +| Dask | PyArrow compute | Parquet files | + +For backends without native monitoring support, Feast falls back to pulling data through the offline store and computing metrics with PyArrow and NumPy. That fallback keeps the API consistent while still allowing mature warehouse and distributed engines to do the heavy lifting. + +This design is especially important for larger feature stores. A null-rate or histogram job should not require exporting a warehouse table into a separate monitoring system. If the feature data already lives in Snowflake, BigQuery, Spark, Redshift, or another supported backend, Feast can push the computation closer to that data. + +Feast 0.64 also adds the Apache Flink compute engine, continuing the broader move toward a unified compute-engine model. DQM follows the same direction: feature quality checks should be part of the feature platform's execution model, not a sidecar that every team wires up differently. + +## REST APIs for automation + +The UI and CLI are built on top of monitoring APIs that can also be used by external systems: + +| Method | Endpoint | Use | +|---|---|---| +| `POST` | `/monitoring/compute` | submit a batch DQM job | +| `POST` | `/monitoring/auto_compute` | auto-detect dates and compute all granularities | +| `POST` | `/monitoring/compute/transient` | compute ad hoc metrics without storing them | +| `POST` | `/monitoring/compute/log` | compute metrics from serving logs | +| `POST` | `/monitoring/auto_compute/log` | auto-compute log metrics | +| `GET` | `/monitoring/jobs/{job_id}` | read DQM job status | +| `GET` | `/monitoring/metrics/features` | read per-feature metrics | +| `GET` | `/monitoring/metrics/feature_views` | read feature-view summaries | +| `GET` | `/monitoring/metrics/feature_services` | read feature-service summaries | +| `GET` | `/monitoring/metrics/baseline` | read baseline metrics | +| `GET` | `/monitoring/metrics/timeseries` | read trend data for charts and alerts | + +The transient compute endpoint is useful for exploration. If someone wants to inspect a very specific date range, Feast can compute fresh metrics and return them directly without storing them as part of the scheduled monitoring history. + +## Production shape + +A typical production setup now looks like this: + +1. Add `data_quality_monitoring.auto_baseline: true` to `feature_store.yaml` +2. Run `feast apply` to register features and compute baseline metrics +3. Schedule `feast monitor run` for batch metrics +4. Enable feature-service logging and schedule `feast monitor run --source-type log` for production serving metrics +5. Use the UI to investigate feature health and distribution changes +6. Use REST APIs to connect monitoring results to alerting, orchestration, or custom dashboards + +Monitoring also respects Feast's existing authorization model. Compute operations require update permissions, while reads and transient exploration require describe permissions. That keeps the new DQM surface aligned with the rest of the registry and feature-store API. + +## What's next + +Feast 0.64 makes DQM part of the feature store instead of an integration around it. The release adds the backend compute path, the CLI, the REST API, and the UI surface in one coherent workflow. + +The next step for users is simple: enable baselines, run monitoring jobs on the same cadence as your data pipelines, and use the UI to make feature quality visible to the teams that own production models. + +For setup details, see the [Feature Quality Monitoring guide](/docs/how-to-guides/feature-monitoring) and the [0.64.0 changelog](https://github.com/feast-dev/feast/blob/master/CHANGELOG.md#0640-2026-06-13). diff --git a/infra/website/docs/blog/feast-feature-server-monitoring.md b/infra/website/docs/blog/feast-feature-server-monitoring.md new file mode 100644 index 00000000000..c416e84e82b --- /dev/null +++ b/infra/website/docs/blog/feast-feature-server-monitoring.md @@ -0,0 +1,443 @@ +--- +title: "Monitoring Your Feast Feature Server with Prometheus and Grafana" +description: "Feast now ships built-in Prometheus metrics for the feature server — request latency, feature freshness, materialization health, ODFV transformation duration, and more. Enable with a single flag and get production-grade observability for your ML infrastructure." +date: 2026-03-26 +authors: ["Nikhil Kathole"] +--- + +
+ Feast Feature Server Monitoring — Feast exports metrics to Prometheus for monitoring and alerting, visualized in Grafana dashboards +
+ +# Monitoring Your Feast Feature Server with Prometheus and Grafana + +As feature stores become a critical part of production ML systems, the question shifts from *"Can I serve features?"* to *"Can I trust what I'm serving?"*. Are my features fresh? Is latency within SLA? Are materialization pipelines succeeding? How long are my on-demand transformations taking? + +Until now, answering these questions for Feast required ad-hoc monitoring — parsing logs, writing custom health checks, or bolting on external instrumentation. That changes today. + +**Feast now ships built-in Prometheus metrics for the feature server**, covering the full request lifecycle — from HTTP request handling through online store reads and on-demand feature transformations to materialization pipelines and feature freshness tracking. Enable it with a single flag, point Prometheus at the metrics endpoint, and get production-grade observability for your feature serving infrastructure. + +This post walks through the metrics available, what each one tells you, how to enable and configure them, and how to build a Grafana dashboard that gives you a complete operational picture of your feature server. + +## What's New + +Feast's feature server now exposes a comprehensive set of Prometheus metrics across seven categories, designed to give ML platform teams full visibility into their feature serving infrastructure: + +- **Request metrics** — Per-endpoint request counters and latency histograms with `feature_count` and `feature_view_count` labels, so you can correlate latency with request complexity. +- **Online store read duration** — A histogram capturing total time spent reading from the online store (Redis, DynamoDB, PostgreSQL, etc.), covering both synchronous and async paths across all backends. +- **ODFV transformation duration** — Per-ODFV histograms for both read-path (during `/get-online-features`) and write-path (during push/materialize) transformations, with `odfv_name` and `mode` labels to compare Pandas vs Python vs Substrait performance. +- **Online feature retrieval counters** — Request counts and entity-row-per-request histograms, revealing the shape of your traffic. +- **Push counters** — Tracked by push source and mode (online/offline/both), giving early warning when ingestion pipelines stop sending data. +- **Materialization tracking** — Success/failure counters and duration histograms per feature view, so you know immediately when a pipeline breaks. +- **Feature freshness gauges** — Per-feature-view staleness (seconds since last materialization), updated by a background thread every 30 seconds. The single most important metric for ML model quality. +- **CPU and memory gauges** — Per-worker resource usage for capacity planning and leak detection. +- **Kubernetes-native discovery** — The Feast Operator auto-generates a `ServiceMonitor` when metrics are enabled, so Prometheus Operator discovers the scrape target automatically. + +All metrics are fully opt-in with zero overhead when disabled. Per-category toggles let you enable exactly the metrics you need. + +## Enabling Metrics + +### CLI: One Flag + +The simplest way — one flag, everything enabled: + +```bash +feast serve --metrics +``` + +This starts the feature server on its default port (6566) and a Prometheus metrics endpoint on port 8000. + +### YAML: Fine-Grained Control + +For production deployments, configure metrics in `feature_store.yaml` with per-category toggles: + +```yaml +feature_server: + metrics: + enabled: true + resource: true # CPU and memory gauges + request: true # HTTP request counters and latency histograms + online_features: true # Entity count and retrieval tracking + push: true # Push/ingestion request counters + materialization: true # Pipeline success/failure and duration + freshness: true # Per-feature-view data staleness +``` + +### Kubernetes: Feast Operator + +If you're running Feast on Kubernetes with the [Feast Operator](/blog/scaling-feast-feature-server), set `metrics: true` on the online store server: + +```yaml +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +metadata: + name: production-feast +spec: + feastProject: my_project + services: + onlineStore: + server: + metrics: true +``` + +The operator automatically appends `--metrics` to the serve command and exposes port 8000 as a `metrics` port on the Service. It also auto-generates a `ServiceMonitor` resource for Prometheus Operator discovery. The operator detects the `monitoring.coreos.com` API group at startup; if the Prometheus Operator CRD is absent, ServiceMonitor creation is silently skipped, so vanilla Kubernetes clusters are unaffected. + +## The Metrics + +Feast exposes metrics across seven categories. Here's the full reference, organized by what each category helps you answer. + +### Request Metrics — "How is my API performing?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_server_request_total` | Counter | `endpoint`, `status` | +| `feast_feature_server_request_latency_seconds` | Histogram | `endpoint`, `feature_count`, `feature_view_count` | + +These are the core RED metrics (Rate, Errors, Duration) for your feature server. The latency histogram includes `feature_count` and `feature_view_count` labels so you can correlate latency with request complexity — a request fetching 200 features from 15 feature views will naturally be slower than one fetching 5 features from 2 views. + +The histogram uses bucket boundaries tuned for feature serving workloads: `5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s`. Most online feature requests should complete in the lower buckets. + +### Online Feature Retrieval — "What does my traffic look like?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_online_features_request_total` | Counter | — | +| `feast_online_features_entity_count` | Histogram | — | + +The entity count histogram (buckets: `1, 5, 10, 25, 50, 100, 250, 500, 1000`) tells you the shape of your traffic. Are callers sending single-entity lookups (real-time inference) or batch requests of hundreds (batch scoring)? A sudden spike in entity count per request means an upstream service changed its batching strategy — this directly impacts latency and memory. + +### Online Store Read Duration — "Where is my latency coming from?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_server_online_store_read_duration_seconds` | Histogram | — | + +This metric captures the total time spent reading from the online store (Redis, DynamoDB, PostgreSQL, etc.) during a `/get-online-features` request. It covers both the synchronous for-loop path and the async `asyncio.gather` path across all backends. + +By comparing this with the overall request latency, you can determine whether latency is dominated by the store read or by other processing (serialization, transformation, network overhead). + +### ODFV Transformation Duration — "How expensive are my transforms?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_server_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | +| `feast_feature_server_write_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | + +These metrics capture per-ODFV transformation time for both read-path (during `/get-online-features`) and write-path (during push/materialize with `write_to_online_store=True`) operations. The `mode` label distinguishes `pandas`, `python`, and `substrait` transformation modes, making it easy to compare their performance characteristics side by side. + +ODFV transformation metrics are **opt-in at the definition level** via `track_metrics=True`: + +```python +@on_demand_feature_view( + sources=[driver_stats_fv, input_request], + schema=[Field(name="conv_rate_plus_val1", dtype=Float64)], + mode="python", + track_metrics=True, # Enable Prometheus metrics for this ODFV +) +def transformed_conv_rate_python(inputs: Dict[str, Any]) -> Dict[str, Any]: + return {"conv_rate_plus_val1": inputs["conv_rate"] + inputs["val_to_add"]} +``` + +When `track_metrics=False` (the default), zero metrics code runs for that ODFV — no timing, no Prometheus recording. This lets you selectively instrument the transforms you care about without adding overhead to others. + +### Push Metrics — "Is my ingestion pipeline healthy?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_push_request_total` | Counter | `push_source`, `mode` | + +The `push_source` label identifies which source is pushing data. The `mode` label is one of `online`, `offline`, or `online_and_offline`. A push source that stops sending data is an early signal that an upstream pipeline is broken — long before feature staleness becomes visible. + +### Materialization Metrics — "Are my pipelines succeeding?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_materialization_total` | Counter | `feature_view`, `status` | +| `feast_materialization_duration_seconds` | Histogram | `feature_view` | + +The `status` label is `success` or `failure`. The duration histogram uses wide buckets (`1s, 5s, 10s, 30s, 60s, 2min, 5min, 10min, 30min, 1hr`) because materialization jobs can range from seconds to tens of minutes depending on the feature view size and offline store. + +### Feature Freshness — "How stale is my data?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_freshness_seconds` | Gauge | `feature_view`, `project` | + +**This is the single most important metric for ML teams.** It measures data staleness — the gap between "now" and the last successful materialization end time — per feature view. A background thread computes this every 30 seconds. + +If your model was trained on hourly features and the freshness gauge crosses 2 hours, your model is receiving data it has never seen patterns for. Before this metric existed, this was a silent failure. Now you can set an alert and catch it in minutes. + +The dashboard below shows these ML-specific metrics in action — latency correlated with feature count, online feature request rate, average entities per request, feature freshness per feature view, and materialization success counts with duration: + +
+ Grafana dashboard showing latency by feature count, feature freshness, average entities per request, and materialization metrics +
+ +### Resource Metrics — "Is my server healthy?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_server_cpu_usage` | Gauge | (per worker PID) | +| `feast_feature_server_memory_usage` | Gauge | (per worker PID) | + +Per-worker CPU and memory gauges, updated every 5 seconds by a background thread. In Gunicorn deployments, each worker reports independently, so you can spot an individual worker consuming excessive resources. + +## Latency Breakdown: Understanding Where Time Is Spent + +One of the most powerful uses of these metrics is **latency decomposition**. By overlaying the overall request latency with the online store read duration and ODFV transformation duration, you can pinpoint exactly where time is spent: + +``` +Total request latency = Store read + ODFV transforms + Serialization/overhead +``` + +The Grafana dashboard below shows this decomposition in action — online store read latency (p50/p95/p99), per-ODFV read-path and write-path transform latency, and a side-by-side Pandas vs Python ODFV comparison: + +
+ Grafana dashboard showing online store read latency, ODFV transformation latency by name, and Pandas vs Python ODFV comparison +
+ +If store reads dominate, the bottleneck is your online store (consider Redis instead of PostgreSQL, or tune your connection pool). If ODFV transforms dominate, consider switching from Pandas mode to Python mode — or re-evaluate whether the transformation should be precomputed during materialization instead of computed on the fly. + +### Pandas vs Python ODFV Comparison + +The `mode` label on transformation metrics makes it straightforward to compare Pandas and Python ODFV performance. The bottom-left panel in the dashboard above shows p50/p95 latencies for Pandas-mode and Python-mode ODFVs overlaid, making the comparison immediate. You can also query these directly: + +```promql +# Pandas p95 read-path latency +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket{mode="pandas"}[1m])) by (le)) + +# Python p95 read-path latency +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket{mode="python"}[1m])) by (le)) +``` + +## Building Alerts + +Here are the recommended alert rules, ordered by impact: + +### Feature Freshness SLO Breach + +```yaml +- alert: FeastFeatureViewStale + expr: feast_feature_freshness_seconds > 3600 + for: 5m + labels: + severity: critical + annotations: + summary: > + Feature view {{ $labels.feature_view }} in project {{ $labels.project }} + has not been materialized in {{ $value | humanizeDuration }}. + impact: Models consuming this feature view are receiving stale data. +``` + +### Materialization Failures + +```yaml +- alert: FeastMaterializationFailing + expr: rate(feast_materialization_total{status="failure"}[15m]) > 0 + for: 5m + labels: + severity: critical + annotations: + summary: > + Materialization is failing for feature view {{ $labels.feature_view }}. +``` + +### High p99 Latency + +```yaml +- alert: FeastHighLatency + expr: | + histogram_quantile(0.99, + rate(feast_feature_server_request_latency_seconds_bucket{ + endpoint="/get-online-features" + }[5m]) + ) > 1.0 + for: 5m + labels: + severity: warning + annotations: + summary: > + Feast p99 latency for online features is {{ $value }}s. +``` + +### High Error Rate + +```yaml +- alert: FeastHighErrorRate + expr: | + sum(rate(feast_feature_server_request_total{status="error"}[5m])) + / sum(rate(feast_feature_server_request_total[5m])) + > 0.01 + for: 5m + labels: + severity: warning + annotations: + summary: > + Feast feature server error rate is {{ $value | humanizePercentage }}. +``` + +## Building a Grafana Dashboard + +With these metrics exposed, you can build a Grafana dashboard that gives you a complete operational picture of your feature server. We've published a [ready-to-import Grafana dashboard JSON](https://github.com/ntkathole/feast-automated-setups/blob/main/feast-prometheus-metrics/grafana_dashboard.json) that covers all the panels described below — import it into your Grafana instance and point it at your Prometheus datasource to get started immediately. + +### Connecting Prometheus to Feast + +Add the Feast metrics endpoint to your Prometheus scrape configuration: + +```yaml +scrape_configs: + - job_name: feast + static_configs: + - targets: [":8000"] + scrape_interval: 15s +``` + +Once Prometheus is scraping, verify the raw metrics output: + +```bash +curl -s http://localhost:8000 | grep feast_ +``` + +### Key PromQL Queries + +Here are the most useful queries for building your own panels or running ad-hoc investigations in the Prometheus UI: + +**Throughput and errors:** + +```promql +# Request rate by endpoint +rate(feast_feature_server_request_total[5m]) + +# Error rate +sum(rate(feast_feature_server_request_total{status="error"}[5m])) + / sum(rate(feast_feature_server_request_total[5m])) +``` + +**Latency percentiles:** + +```promql +# p99 latency for online features +histogram_quantile(0.99, + rate(feast_feature_server_request_latency_seconds_bucket{endpoint="/get-online-features"}[5m])) + +# Online store read p95 +histogram_quantile(0.95, + sum(rate(feast_feature_server_online_store_read_duration_seconds_bucket[1m])) by (le)) + +# ODFV transform p95 by name and mode +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket[1m])) by (le, odfv_name)) + +# ODFV write-path transform p95 by name +histogram_quantile(0.95, + sum(rate(feast_feature_server_write_transformation_duration_seconds_bucket[1m])) by (le, odfv_name)) +``` + +**Latency decomposition:** + +```promql +# Average total request latency +rate(feast_feature_server_request_latency_seconds_sum{endpoint="/get-online-features"}[5m]) + / rate(feast_feature_server_request_latency_seconds_count{endpoint="/get-online-features"}[5m]) + +# Average store read time +rate(feast_feature_server_online_store_read_duration_seconds_sum[5m]) + / rate(feast_feature_server_online_store_read_duration_seconds_count[5m]) + +# Average ODFV transform time +rate(feast_feature_server_transformation_duration_seconds_sum[5m]) + / rate(feast_feature_server_transformation_duration_seconds_count[5m]) +``` + +**Pandas vs Python comparison:** + +```promql +# Pandas p95 +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket{mode="pandas"}[1m])) by (le)) + +# Python p95 +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket{mode="python"}[1m])) by (le)) +``` + +**ML-specific signals:** + +```promql +# Feature freshness — views stale beyond 1 hour +feast_feature_freshness_seconds > 3600 + +# Materialization failure rate +rate(feast_materialization_total{status="failure"}[1h]) + +# Average entities per request +rate(feast_online_features_entity_count_sum[5m]) + / rate(feast_online_features_entity_count_count[5m]) + +# Push rate by source +rate(feast_push_request_total[5m]) +``` + +## Try the Automated Demo + +Want to see all of this in action without manual setup? We've published an [automated demo](https://github.com/ntkathole/feast-automated-setups/tree/main/feast-prometheus-metrics) that deploys a Feast feature server with metrics, a Prometheus instance, and the pre-built Grafana dashboard — all with a single `./setup.sh` command. It includes a traffic generator that exercises every metric category (plain online features, Pandas and Python ODFVs, push, materialize, and write-path transforms), so the dashboard populates immediately. + +## Kubernetes: Automatic Prometheus Discovery + +For teams running Feast on Kubernetes, the Feast Operator now auto-generates a `ServiceMonitor` when `metrics: true` is set on the online store. The operator: + +1. Detects the `monitoring.coreos.com` API group at startup +2. If present, creates a `ServiceMonitor` owned by the `FeatureStore` CR, targeting the `metrics` port (8000) +3. If absent (vanilla Kubernetes without Prometheus Operator), skips silently — no errors, no CRD dependency + +This means on an OpenShift or Prometheus-Operator-enabled cluster, metrics discovery is fully automatic — no manual `ServiceMonitor` creation required. The `ServiceMonitor` is cleaned up automatically when the `FeatureStore` CR is deleted or `metrics` is set back to `false`. + +For teams using [KEDA for autoscaling](/blog/scaling-feast-feature-server), these Prometheus metrics also serve as scaling signals. For example, you can scale the feature server based on request rate: + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: feast-scaledobject +spec: + scaleTargetRef: + apiVersion: feast.dev/v1 + kind: FeatureStore + name: my-feast + triggers: + - type: prometheus + metadata: + serverAddress: http://prometheus.monitoring.svc:9090 + query: sum(rate(feast_feature_server_request_total[2m])) + threshold: "100" +``` + +## Metrics Summary + +| Category | Metric | What It Answers | +|---|---|---| +| Request | `feast_feature_server_request_total` | What is my throughput and error rate? | +| Request | `feast_feature_server_request_latency_seconds` | What are my p50/p99 latencies? | +| Online Features | `feast_online_features_entity_count` | What is my traffic shape? | +| Store Read | `feast_feature_server_online_store_read_duration_seconds` | Is my online store the bottleneck? | +| ODFV Transform | `feast_feature_server_transformation_duration_seconds` | How expensive are my read-path transforms? | +| ODFV Transform | `feast_feature_server_write_transformation_duration_seconds` | How expensive are my write-path transforms? | +| Push | `feast_push_request_total` | Is my ingestion pipeline sending data? | +| Materialization | `feast_materialization_total` | Are my pipelines succeeding? | +| Materialization | `feast_materialization_duration_seconds` | How long do my pipelines take? | +| Freshness | `feast_feature_freshness_seconds` | How stale is the data my models are using? | +| Resource | `feast_feature_server_cpu_usage` / `memory_usage` | Is my server healthy? | + +## How Can I Get Started? + +1. **Enable metrics**: `feast serve --metrics` or set `metrics.enabled: true` in your `feature_store.yaml` +2. **Verify the endpoint**: `curl http://localhost:8000` +3. **Import the dashboard**: Grab the [Grafana dashboard JSON](https://github.com/ntkathole/feast-automated-setups/blob/main/feast-prometheus-metrics/grafana_dashboard.json) and import it into your Grafana instance +4. **Set up alerts**: Start with freshness and materialization failures — those catch the problems that affect ML model quality first +5. **Try the automated demo**: Run the [feast-prometheus-metrics setup](https://github.com/ntkathole/feast-automated-setups/tree/main/feast-prometheus-metrics) for a one-command local experience +6. Check out the [Feast documentation](https://docs.feast.dev/) for the full configuration reference +7. Join the [Feast Slack](https://slack.feast.dev) to share feedback and ask questions + +We're excited to bring production-grade observability to Feast and welcome feedback from the community! diff --git a/infra/website/docs/blog/feast-mlflow-kubeflow.md b/infra/website/docs/blog/feast-mlflow-kubeflow.md new file mode 100644 index 00000000000..d0b89ac7138 --- /dev/null +++ b/infra/website/docs/blog/feast-mlflow-kubeflow.md @@ -0,0 +1,516 @@ +--- +title: "Feast + MLflow + Kubeflow: A Unified AI/ML Lifecycle" +description: Learn how to use Feast, MLflow, and Kubeflow to power your AI/ML Lifecycle +date: 2026-03-09 +authors: ["Francisco Javier Arceo", "Nikhil Kathole"] +--- + +
+ Feast, MLflow, and Kubeflow +
+ +# Feast + MLflow + Kubeflow: A Unified AI/ML Lifecycle + +## Overview + +Building production-ready machine learning systems requires more than a great model. It demands a clear separation of concerns between feature management, experiment tracking, and workflow orchestration. This post explores how [Feast](https://feast.dev/), [MLflow](https://mlflow.org/), and [Kubeflow](https://www.kubeflow.org/) work together as complementary open-source tools to cover the full AI/ML lifecycle — from raw data to serving predictions at scale. + +These tools are not competitors. Each one occupies a distinct role: + +* **Feast** manages feature data: defining, transforming, storing, and serving features consistently for both training and inference. It also tracks feature lineage and supports data quality monitoring. +* **MLflow** tracks experiments: logging runs, metrics, parameters, artifacts, and candidate models. +* **Kubeflow** orchestrates ML workflows: running distributed training, hyperparameter sweeps, and end-to-end pipelines on Kubernetes. + +Together they form a complete, open-source foundation for operationalizing ML. + +### How are Feast, MLflow, and Kubeflow different? + +If you are new to these tools, it is natural to wonder whether they overlap. The short answer is: they solve fundamentally different problems in the ML lifecycle. The table below makes this concrete. + +| Capability | Feast | MLflow | Kubeflow | +|---|---|---|---| +| Define and version feature schemas | Yes | No | No | +| Store and serve features (online + offline) | Yes | No | No | +| Point-in-time-correct feature retrieval | Yes | No | No | +| Feature transformations (training = serving) | Yes | No | No | +| Feature lineage and registry | Yes | No | No | +| Data quality validation on features | Yes | No | No | +| Log experiments, metrics, and parameters | No | Yes | No | +| Track and compare model versions | No | Yes | No | +| Model registry (promote / alias models) | No | Yes | No | +| Orchestrate multi-step ML pipelines | No | No | Yes (Pipelines) | +| Distributed training on Kubernetes | No | No | Yes (Training Operator) | +| Hyperparameter tuning | No | Yes (with Optuna, etc.) | Yes (Katib) | + +A few common misconceptions: + +* **"Can't MLflow track my features?"** — MLflow can *log* feature names as parameters, but it does not *define*, *store*, *transform*, or *serve* features. It has no concept of an offline store, an online store, or point-in-time joins. Feast fills that gap. +* **"Doesn't Kubeflow handle everything end-to-end?"** — Kubeflow orchestrates *workflows* — it tells your pipeline steps when to run and where. But it does not provide feature storage, experiment tracking, or model versioning. You still need Feast for the data layer and MLflow for the experiment layer. +* **"Why do I need Feast if I just read from a database?"** — Without Feast, teams typically duplicate feature logic between training scripts and serving endpoints, which leads to training–serving skew. Feast guarantees the same transformation and retrieval logic is used in both contexts. + +With that context, the rest of this post walks through each tool in detail and shows how they hand off to one another in practice. + +This topic has been explored by the community before — the post ["Feast with AI: Feed Your MLflow Models with Feature Store"](https://blog.qooba.net/2021/05/22/feast-with-ai-feed-your-mlflow-models-with-feature-store/) by [@qooba](https://github.com/qooba) is an excellent early look at combining Feast and MLflow. For a hands-on, end-to-end example of Feast and Kubeflow working together, see ["From Raw Data to Model Serving: A Blueprint for the AI/ML Lifecycle with Kubeflow and Feast"](/blog/kubeflow-fraud-detection-e2e) by Helber Belmiro. This post builds on that prior work and brings all three tools — Feast, MLflow, and Kubeflow — into a single narrative. + +--- + +## The AI/ML Lifecycle + +A typical production ML project passes through several stages: + +1. **Feature development** — raw data is transformed into meaningful signals. +2. **Model development** — data scientists experiment with algorithms, features, and hyperparameters. +3. **Model evaluation & selection** — the best experiment is chosen for promotion. +4. **Production deployment** — the selected model is deployed and features are served in real time. +5. **Monitoring & iteration** — model and feature health is observed; the cycle repeats. + +The diagram below maps each stage to its primary tool: + +``` +Raw Data ──► Feast (Feature Engineering & Storage) + │ + ▼ + MLflow + Kubeflow Pipelines (Experiment Tracking & Orchestration) + │ + ▼ + Kubeflow Training Operator (Distributed Training) + │ + ▼ + MLflow Model Registry (Candidate Models) + │ + ▼ + Feast Online Store + Feature Server (Production Serving) +``` + +--- + +## Feast: Feature Development, Iteration, and Serving + +Feast is the data layer of the ML stack. Its core job is to make the same feature logic available both at training time (via the offline store) and at inference time (via the online store), eliminating training–serving skew. Beyond storage and serving, Feast also handles **feature transformations**, **feature lineage tracking**, and **data quality monitoring** — capabilities that are essential when moving features from experimentation to production. + +### Defining features + +A Feast `FeatureView` declares how a feature is computed and where it is stored: + +```python +from datetime import timedelta +from feast import FeatureView, Field, FileSource +from feast.types import Float64, Int64 + +driver_stats = FeatureView( + name="driver_hourly_stats", + entities=["driver_id"], + ttl=timedelta(days=7), + schema=[ + Field(name="conv_rate", dtype=Float64), + Field(name="acc_rate", dtype=Float64), + Field(name="avg_daily_trips", dtype=Int64), + ], + source=FileSource(path="data/driver_stats.parquet", timestamp_field="event_timestamp"), +) +``` + +After running `feast apply`, these features are registered in the Feast registry and visible in the Feast UI: + +
+ Feast UI showing the Feature List for the Driver Ranking project with conv_rate, acc_rate, and avg_daily_trips features +

The Feast UI showing three registered features in the driver_hourly_stats feature view — conv_rate, acc_rate, and avg_daily_trips — each linked to the Driver Ranking project.

+
+ +### Retrieving historical features for training + +Point-in-time-correct historical features are retrieved from the offline store. This prevents future data from leaking into training examples: + +```python +from feast import FeatureStore +import pandas as pd + +store = FeatureStore(repo_path=".") + +entity_df = pd.DataFrame({ + "driver_id": [1001, 1002, 1003], + "event_timestamp": pd.to_datetime(["2025-01-01", "2025-01-02", "2025-01-03"]), +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], +).to_df() +``` + +### Materializing features for real-time serving + +When a model is promoted to production, features are materialized to the online store so they can be retrieved with single-digit millisecond latency: + +```python +from datetime import datetime + +store.materialize_incremental(end_date=datetime.utcnow()) +``` + +Serving then becomes a single call: + +```python +features = store.get_online_features( + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + entity_rows=[{"driver_id": 1001}], +).to_dict() +``` + +### Feature transformations + +Feast supports on-demand feature transformations, allowing you to define transformation logic that runs at retrieval time — both offline (for training) and online (for serving) — using the same Python function. This eliminates the need to duplicate transformation code across training and inference pipelines: + +```python +from feast.on_demand_feature_view import on_demand_feature_view +from feast import Field +from feast.types import Float64 + +@on_demand_feature_view( + sources=[driver_stats], + schema=[Field(name="conv_acc_ratio", dtype=Float64)], +) +def driver_ratios(inputs): + df = inputs.copy() + df["conv_acc_ratio"] = df["conv_rate"] / (df["acc_rate"] + 1e-6) + return df[["conv_acc_ratio"]] +``` + +Here `driver_stats` is the `FeatureView` object defined earlier. The `sources` parameter accepts `FeatureView`, `RequestSource`, or `FeatureViewProjection` objects. + +Using `on_demand_feature_view` ensures that the same transformation logic is applied whether features are retrieved from the offline store for training or from the online store at inference time, preventing transformation skew. + +### Feature lineage + +The Feast feature registry acts as the single source of truth for feature definitions. Every `FeatureView`, data source, entity, and transformation is registered and versioned in the registry. This gives you full lineage from raw data source through transformation logic to the feature values consumed by a model — a critical requirement for debugging, auditing, and regulatory compliance. + +You can inspect the lineage of any feature programmatically: + +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") +feature_view = store.get_feature_view("driver_hourly_stats") +print(feature_view.source) # upstream data source +print(feature_view.schema) # feature schema +``` + +For cross-system lineage that extends beyond Feast into upstream data pipelines and downstream model training, Feast also supports native [OpenLineage integration](/blog/feast-openlineage-integration). Enabling it in your `feature_store.yaml` automatically emits lineage events on `feast apply` and `feast materialize`, letting you visualize the full data flow in tools like [Marquez](https://marquezproject.ai/). + +### Data quality monitoring + +Feast's native data quality monitoring system automatically computes statistical metrics — null rates, distributions, percentiles, histograms — for every registered feature across both batch data and serving logs. It detects drift by comparing current metrics against baselines computed during `feast apply`. + +```yaml +# feature_store.yaml +data_quality_monitoring: + auto_baseline: true +``` + +```bash +# Compute metrics across all granularities (daily, weekly, monthly, quarterly) +feast monitor run + +# Monitor serving logs +feast monitor run --source-type log +``` + +The monitoring UI dashboard (accessible from the sidebar) provides per-feature health status, distribution histograms, time-series drift charts, and configurable filters. Metrics are also available via REST API endpoints for integration with external alerting systems. + +For details, see the [Feature Quality Monitoring guide](/docs/how-to-guides/feature-monitoring). + +### Feast Feature Registry vs. MLflow Model Registry + +A common question is how the **Feast feature registry** relates to the **MLflow model registry**. They are different things that serve complementary roles. + +| | Feast Feature Registry | MLflow Model Registry | +|---|---|---| +| **What it tracks** | Feature definitions, schemas, data sources, entity relationships | Model artifacts, versions, model aliases (e.g., "production", "staging") | +| **Primary users** | Feature engineers, data scientists, ML platform teams | Data scientists, ML engineers | +| **Relationship to production** | Defines what data is available for training *and* serving | Tracks which model version is promoted to production | +| **Scope** | All features ever defined — a superset of what any one model uses | All model versions, including candidates that never ship | + +This distinction is important: the **Feast registry is a superset of the MLflow model registry** from a feature perspective. During experimentation, a data scientist may train models using dozens of features. Once a model is selected for production, only a *subset* of those features will be needed for online serving. Feast's registry records all available features; the specific features required by the production model are a narrower slice that corresponds to what MLflow logged as model inputs. + +--- + +## MLflow: Experiment Tracking, Hyperparameter Optimization, and Feature Selection + +MLflow is the experimentation layer. It answers the question: *"Which combination of features, model architecture, and hyperparameters produced the best result?"* + +### Logging a training run with Feast features + +Because Feast provides a consistent `get_historical_features` API, it is straightforward to combine it with MLflow tracking: + +```python +import mlflow +import mlflow.sklearn +from feast import FeatureStore +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split +from sklearn.metrics import roc_auc_score +import pandas as pd + +store = FeatureStore(repo_path=".") + +entity_df = pd.read_parquet("data/driver_labels.parquet") +feature_df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate", "driver_hourly_stats:avg_daily_trips"], +).to_df() + +X = feature_df[["conv_rate", "acc_rate", "avg_daily_trips"]] +y = feature_df["label"] +X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + +with mlflow.start_run(): + mlflow.log_param("features", ["conv_rate", "acc_rate", "avg_daily_trips"]) + mlflow.log_param("model_type", "LogisticRegression") + + model = LogisticRegression() + model.fit(X_train, y_train) + + auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) + mlflow.log_metric("auc", auc) + + # Log the feature store snapshot alongside the model + mlflow.sklearn.log_model(model, artifact_path="model") + mlflow.log_artifact("feature_store.yaml", artifact_path="feast_config") +``` + +Logging `feature_store.yaml` together with the model artifact ensures that, at any future point, the exact set of Feast feature definitions used for that run can be reproduced. + +### Feature selection with MLflow + +One of the most powerful uses of Feast + MLflow together is systematic **feature selection**: training models with different subsets of Feast features and using MLflow's comparison UI to identify which combination produces the best results. This is far more rigorous than manually trying feature sets in a notebook, and the results are often counterintuitive. + +The pattern is to loop over candidate feature subsets, retrieve each one from Feast, train a model, and log the metrics and feature names as a separate MLflow run: + +```python +import mlflow +import mlflow.sklearn +from feast import FeatureStore +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split +from sklearn.metrics import roc_auc_score +import pandas as pd + +store = FeatureStore(repo_path=".") + +entity_df = pd.read_parquet("data/driver_labels.parquet") + +# Define candidate feature subsets to compare +feature_subsets = { + "acc_rate_only": ["driver_hourly_stats:acc_rate"], + "acc_rate_trips": ["driver_hourly_stats:acc_rate", "driver_hourly_stats:avg_daily_trips"], + "all_features": ["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips"], +} + +with mlflow.start_run(run_name="feast_feature_selection"): + for subset_name, feature_refs in feature_subsets.items(): + feature_df = store.get_historical_features( + entity_df=entity_df, + features=feature_refs, + ).to_df() + + feature_cols = [ref.split(":")[1] for ref in feature_refs] + X = feature_df[feature_cols] + y = feature_df["label"] + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) + + with mlflow.start_run(run_name=subset_name, nested=True): + mlflow.log_param("features", feature_cols) + model = LogisticRegression() + model.fit(X_train, y_train) + auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) + mlflow.log_metric("auc", auc) + mlflow.sklearn.log_model(model, artifact_path="model") +``` + +After running, the MLflow UI lets you sort all nested runs by AUC and immediately see which feature subset wins. The results can be surprising — for example, with synthetic driver data a single feature may outperform the full feature set: + +| Features | Model | AUC | +|---|---|---| +| `acc_rate` only | LogisticRegression | 0.645 | +| `acc_rate` + `avg_daily_trips` | LogisticRegression | 0.613 | +| All 3 features | LogisticRegression | 0.570 | + +
+ MLflow UI showing a LogisticRegression run with all three Feast features, metrics, parameters, and feature tags +

The MLflow UI showing a LogisticRegression run trained with all three Feast features (conv_rate, acc_rate, avg_daily_trips). The run logs five metrics (accuracy, AUC, precision, recall, F1), the feature list as a parameter, and the demo tags each included feature (e.g., feature_conv_rate: included) for easy filtering.

+
+ +This is exactly the kind of insight MLflow's comparison interface is built for. You can sort runs by AUC, filter by which features were included, and visualize performance across experiments. Note that with synthetic data these numbers won't carry real meaning — the point is that the tooling makes it trivial to *observe* these differences systematically and let data drive the feature selection decision. + +
+ MLflow comparison view showing three experiment runs side by side with different feature combinations +

MLflow's comparison view showing three runs side by side with different feature subsets. The "Show diff only" toggle highlights how the features parameter varies across runs, making it easy to identify which combination of Feast features produces the best results.

+
+ +
+ MLflow metric charts showing accuracy, AUC, F1, precision, and recall grouped by num_features across three feature subsets +

MLflow's metric charts view visualizing accuracy, AUC, F1, precision, and recall across all feature selection runs, grouped by num_features. This chart makes it easy to spot how model performance changes as more Feast features are included.

+
+ +Once you have identified the winning subset, the Feast registry ensures that only those features need to be materialized into the online store for production serving. + +### Hyperparameter sweeps + +MLflow integrates natively with hyperparameter optimization libraries. For example, using MLflow with [Optuna](https://optuna.org/): + +```python +import optuna +import mlflow + +def objective(trial): + C = trial.suggest_float("C", 1e-3, 10.0, log=True) + max_iter = trial.suggest_int("max_iter", 100, 1000) + + with mlflow.start_run(nested=True): + mlflow.log_params({"C": C, "max_iter": max_iter}) + model = LogisticRegression(C=C, max_iter=max_iter) + model.fit(X_train, y_train) + auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) + mlflow.log_metric("auc", auc) + return auc + +with mlflow.start_run(run_name="optuna_sweep"): + study = optuna.create_study(direction="maximize") + study.optimize(objective, n_trials=20) +``` + +All trials, their parameters, and their metrics are automatically captured in the MLflow tracking server, making it easy to compare runs and select the best candidate for promotion. + +--- + +## Kubeflow: Orchestrating the ML Workflow + +[Kubeflow](https://www.kubeflow.org/) brings Kubernetes-native orchestration to the ML lifecycle. Its two most relevant components here are: + +* **Kubeflow Pipelines** — a platform for building and deploying repeatable ML workflows as DAGs. +* **Kubeflow Training Operator** — manages distributed training jobs (PyTorchJob, TFJob, etc.) on Kubernetes. + +### Kubeflow Pipelines integrating Feast and MLflow + +Kubeflow Pipelines lets you compose the entire workflow — feature retrieval, training, evaluation, and registration — as a single, reproducible pipeline: + +```python +from kfp import dsl + +@dsl.component(base_image="python:3.10-slim", packages_to_install=["feast", "mlflow", "scikit-learn", "pandas", "pyarrow"]) +def retrieve_features(entity_df_path: str, feature_store_repo: str, output_path: dsl.Output[dsl.Dataset]): + from feast import FeatureStore + import pandas as pd + + store = FeatureStore(repo_path=feature_store_repo) + entity_df = pd.read_parquet(entity_df_path) + df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + ).to_df() + df.to_parquet(output_path.path) + + +@dsl.component(base_image="python:3.10-slim", packages_to_install=["feast", "mlflow", "scikit-learn", "pandas"]) +def train_and_log(features_path: dsl.Input[dsl.Dataset], mlflow_tracking_uri: str, model_name: str): + import mlflow, mlflow.sklearn + import pandas as pd + from sklearn.linear_model import LogisticRegression + from sklearn.model_selection import train_test_split + from sklearn.metrics import roc_auc_score + + mlflow.set_tracking_uri(mlflow_tracking_uri) + df = pd.read_parquet(features_path.path) + X = df[["conv_rate", "acc_rate"]] + y = df["label"] + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + + with mlflow.start_run(): + model = LogisticRegression() + model.fit(X_train, y_train) + auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) + mlflow.log_metric("auc", auc) + mlflow.sklearn.log_model(model, artifact_path="model", registered_model_name=model_name) + + +@dsl.pipeline(name="feast-mlflow-training-pipeline") +def training_pipeline(entity_df_path: str, feature_store_repo: str, mlflow_tracking_uri: str, model_name: str): + fetch_step = retrieve_features(entity_df_path=entity_df_path, feature_store_repo=feature_store_repo) + train_and_log( + features_path=fetch_step.outputs["output_path"], + mlflow_tracking_uri=mlflow_tracking_uri, + model_name=model_name, + ) +``` + +Each step runs in its own container, making the pipeline portable and reproducible across environments. + +### Distributed training with the Kubeflow Training Operator + +For large-scale models, the [Kubeflow Training Operator](https://www.kubeflow.org/docs/components/training/) schedules distributed training jobs. Feast integrates naturally because it provides a consistent Python API for retrieving feature data — whether training is running on a single machine or across a cluster of workers. Each worker calls `get_historical_features` for its shard of the entity dataframe, and the resulting features are passed directly into the training loop. + +--- + +## Bringing It All Together: Feast → MLflow → Production + +The following end-to-end workflow shows how the three tools hand off to one another: + +### Step 1: Register and materialize features with Feast + +```bash +feast apply # Register feature definitions in the registry +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +### Step 2: Run experiments and select the best model with MLflow + +Feature engineers iterate on feature definitions in Feast while data scientists run experiments in MLflow, logging which features were used for each run. The best run is registered in the MLflow Model Registry: + +```python +mlflow.register_model(f"runs:/{best_run_id}/model", "driver_conversion_model") +``` + +### Step 3: Promote to production + +Promoting the model in MLflow signals that it is ready for deployment. At this point, you also know the exact subset of Feast features required by that model — these are the features to materialize and serve. + +```python +client = mlflow.tracking.MlflowClient() +client.set_registered_model_alias( + name="driver_conversion_model", alias="production", version="3" +) +``` + +### Step 4: Serve features and predictions + +The deployed model reads its inputs from the Feast online store at inference time: + +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +def predict(driver_id: int) -> float: + features = store.get_online_features( + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + entity_rows=[{"driver_id": driver_id}], + ).to_dict() + return model.predict_proba([[features["conv_rate"][0], features["acc_rate"][0]]])[0][1] +``` + +--- + +## Summary + +| Concern | Tool | +|---|---| +| Feature definition, storage, and serving | **Feast** | +| Experiment tracking, metric logging, and model versioning | **MLflow** | +| Workflow orchestration and distributed training | **Kubeflow Pipelines + Training Operator** | +| Hyperparameter optimization | **MLflow + Katib (Kubeflow)** | +| Production feature serving | **Feast Online Store / Feature Server** | + +Feast, MLflow, and Kubeflow are each best-in-class at what they do, and they are designed to work alongside one another rather than replace each other. By combining them you get a fully open-source, end-to-end ML platform that handles everything from raw data to live predictions — without lock-in. + +If you are new to Feast, check out the [Feast documentation](https://docs.feast.dev/) and [GitHub](https://github.com/feast-dev/feast) to get started. Join the community on [Slack](http://slack.feastsite.wpenginepowered.com/) and let us know how you are using Feast in your ML stack! diff --git a/infra/website/docs/blog/feast-mlflow-native-integration.md b/infra/website/docs/blog/feast-mlflow-native-integration.md new file mode 100644 index 00000000000..882fd7ef6f6 --- /dev/null +++ b/infra/website/docs/blog/feast-mlflow-native-integration.md @@ -0,0 +1,270 @@ +--- +title: "Native MLflow Integration for Feast: Automatic Feature Lineage for Every Experiment" +description: "Feast now ships native MLflow integration : enable it in feature_store.yaml and every feature retrieval is automatically linked to the MLflow run that consumed it. No glue code, no manual tagging, full model-to-feature traceability." +date: 2026-06-01 +authors: ["Vanshika"] +--- + +
+ Feast Native MLflow Integration +
+ +# Native MLflow Integration for Feast + +## The Problem: Features and Experiments Live in Separate Worlds + +Feast manages your features. MLflow tracks your experiments. But between the two, there has always been a manual gap. + +When a data scientist trains a model, the features that shaped it are retrieved from Feast, but MLflow has no idea which features were used, which feature service they belong to, or what entity DataFrame produced the training set. The result is a familiar set of problems: + +- **"Which features did model v3 use?"** — dig through notebooks and hope the comments are accurate. +- **"Can I reproduce the training data for last month's experiment?"** — re-derive the entity DataFrame from memory. +- **"Which models break if I change `driver_hourly_stats`?"** — grep through repos and ask around. +- **"I promoted a model — which features do I need to serve?"** — read the training script, cross-reference with the feature registry. + +Teams have tried to close this gap with manual `mlflow.log_param("features", ...)` calls, custom wrappers, or convention-based tagging. These approaches are fragile, inconsistent, and the first thing to break when someone new joins the team. + +## The Solution: One Config Line, Automatic Lineage + +Starting with Feast v0.62, the Feast–MLflow integration is **native and zero-code**. Add an `mlflow:` block to your `feature_store.yaml`, and every feature retrieval inside an active MLflow run is automatically tagged with the features, feature views, feature service, entity count, and retrieval duration. + +```yaml +project: driver_ranking +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db +mlflow: + enabled: true + tracking_uri: http://127.0.0.1:5000 +``` + +That's it. No decorators, no wrappers, no `import mlflow` scattered through your training code. + +## How It Works + +### Auto-Logging: Zero Code, Full Lineage + +When `mlflow.enabled: true` and an active MLflow run exists, Feast hooks into `get_historical_features()` and `get_online_features()` at the end of each call and writes structured metadata to the run: + +| Tag | Example | +|-----|---------| +| `feast.project` | `driver_ranking` | +| `feast.retrieval_type` | `historical` | +| `feast.feature_service` | `driver_activity_v1` | +| `feast.feature_views` | `driver_hourly_stats` | +| `feast.feature_refs` | `driver_hourly_stats:conv_rate, driver_hourly_stats:acc_rate` | +| `feast.entity_count` | `200` | +| `feast.feature_count` | `5` | +| `feast.job_submission_sec` | `0.43` (metric) | + +Even if features are passed as a list of refs rather than a `FeatureService` object, Feast auto resolves the matching feature service from the registry. The resolution is cached with a 5-minute TTL, so there is no registry overhead on every call. + +
+ Model metadata with Feast tags in MLflow + Feature lineage from data source to model +
+ +### The `store.mlflow` API + +The integration surfaces through a single property on `FeatureStore`: + +```python +from feast import FeatureStore +store = FeatureStore(".") + +with store.mlflow.start_run(run_name="v1_training"): + # Auto-logged: feature refs, feature views, entity count, duration + training_df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + model = train(training_df) + + # Saves feast_features.json alongside the model artifact + store.mlflow.log_model(model, "model") + + train_run_id = store.mlflow.active_run_id + +# Propagates feast.feature_service to the model version +store.mlflow.register_model(f"runs:/{train_run_id}/model", "driver_model") + +# Prediction: links back to the training run +with store.mlflow.start_run(run_name="batch_prediction"): + model = store.mlflow.load_model("models:/driver_model/1") + features = store.get_online_features( + features=store.get_feature_service("driver_activity_v1"), + entity_rows=[{"driver_id": 1001}], + ) + predictions = model.predict(...) +``` + +`store.mlflow` is lazy-initialized on first access. When MLflow is not installed or `enabled` is `false`, it returns `None` — so existing code that doesn't use MLflow is unaffected. + +### Model-to-Feature Resolution + +This is the capability that closes the loop between experiment tracking and production serving. Given any registered model URI, Feast can tell you exactly which feature service it needs: + +```python +fs_name = store.mlflow.resolve_features("models:/driver_model/1") +# Returns: "driver_activity_v1" +``` + +Resolution follows a precise chain: + +1. Check the model version tag `feast.feature_service` (set by `register_model`) +2. Fall back to the training run tag `feast.feature_service` (set by auto-logging) +3. Validate against the `feast_features.json` artifact to ensure the feature service projections match the features the model was actually trained on + +If there is a mismatch : say someone renamed a feature in the service after training — `resolve_features()` raises `FeastMlflowModelResolutionError` with a clear diff. No silent serving skew. + +This enables a powerful production pattern: your serving pipeline doesn't hardcode feature names. It resolves them from the model: + +```python +fs_name = store.mlflow.resolve_features(f"models:/driver_model/production") +features = store.get_online_features( + features=store.get_feature_service(fs_name), + entity_rows=request_entities, +) +``` + +Promote a new model version that uses different features, and the serving pipeline auto-adapts. + +
+ Registered model with feast.feature_service tag +
+ +### Training Reproducibility + +When `auto_log_entity_df: true`, the integration saves the entity DataFrame as a Parquet artifact on every historical retrieval. Later, you can reconstruct the exact training inputs: + +```python +entity_df = store.mlflow.get_training_entity_df(run_id="abc123") + +with store.mlflow.start_run(run_name="retrain_v2"): + new_df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() +``` + +Even without entity DataFrame archival, Feast always logs metadata : row count, column names, date range, or the SQL query — so you have an audit trail of what went into the model. + +
+ Entity DataFrame saved as artifact in MLflow +
+ +### Operations Audit Trail + +When `log_operations: true`, `feast apply` and `feast materialize` are logged to a dedicated MLflow experiment (`{project}-feast-ops`). These are self-contained runs : they don't require a user-initiated active run: + +```yaml +mlflow: + enabled: true + log_operations: true + ops_experiment_suffix: "-feast-ops" +``` + +Apply runs record which feature views, feature services, and entities were created, updated, or deleted. Materialize runs record the feature views, date range, and duration. This gives platform teams a time-series audit trail of every registry and materialization change. + +
+ Operations audit trail in MLflow +
+ +### Dataset Tracking + +For teams that use MLflow's dataset tracking, the integration provides an explicit API: + +```python +store.mlflow.log_training_dataset( + df=training_df, + dataset_name="driver_training_v1", + source="feast.get_historical_features", +) +``` + +This uses `mlflow.data.from_pandas` and `mlflow.log_input` to register the DataFrame as a dataset input on the active run. + +## Two Access Patterns + +The integration provides two ways to access MLflow, depending on your preference: + +### 1. `store.mlflow` — explicit, multi-store safe + +```python +store = FeatureStore(".") +store.mlflow.start_run(run_name="training") +store.mlflow.log_model(model, "model") +``` + +`store.mlflow` only exposes Feast-enhanced methods. For raw MLflow access, use the escape hatches: + +```python +store.mlflow.client # MlflowClient instance +store.mlflow.mlflow # raw mlflow module +``` + +### 2. `feast.mlflow` — drop-in module replacement + +```python +import feast.mlflow + +feast.mlflow.start_run(run_name="training") # Feast-enhanced +feast.mlflow.log_params({"lr": "0.01"}) # passthrough to mlflow +feast.mlflow.log_model(model, "model") # Feast-enhanced +``` + +`feast.mlflow` auto-discovers the most recently created `FeatureStore`. For Feast-specific methods (like `log_model`, `register_model`, `resolve_features`), it uses the enhanced version. For everything else (`log_params`, `set_tag`, `MlflowClient`, ...), it delegates to raw `mlflow`. One import, both worlds. + +## Feast UI Integration + +The Feast UI automatically surfaces MLflow data when the integration is enabled. Three new API endpoints power the UI: + +| Endpoint | What it shows | +|----------|---------------| +| `/api/mlflow-runs` | All Feast-tagged runs with linked registered models | +| `/api/mlflow-feature-usage` | Per-feature-view: run count, last used, associated models | +| `/api/mlflow-feature-models` | Reverse index: feature ref to registered models | + +The feature view detail page shows MLflow training run count, last-used date, and a table of registered models that depend on the view. The registry graph visualization draws edges from feature services through MLflow runs to registered models. + +When MLflow is not enabled, these endpoints return empty responses and the UI components are hidden — no visual noise for users who don't use MLflow. + +
+ Feast UI Feature List with MLflow model associations + Feast UI Feature View detail with MLflow usage +
+ +## Configuration Reference + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | `bool` | `false` | Master switch | +| `tracking_uri` | `string` | (env/default) | MLflow tracking URI | +| `auto_log` | `bool` | `true` | Auto-tag runs on retrieval | +| `auto_log_entity_df` | `bool` | `false` | Save entity DataFrame as artifact | +| `entity_df_max_rows` | `int` | `100000` | Skip artifact for large DataFrames | +| `log_operations` | `bool` | `false` | Log apply/materialize to ops experiment | +| `ops_experiment_suffix` | `string` | `"-feast-ops"` | Ops experiment name suffix | + +## Getting Started + +Install Feast with MLflow support: + +```bash +pip install feast[mlflow] +``` + +Add the `mlflow:` block to your `feature_store.yaml`, start an MLflow tracking server, and run your training code. Features are automatically linked to experiments from the first retrieval. + +
+ End-to-end lineage from data source to registered model +
+ +## Join the Conversation + +We'd love to hear how you're using (or plan to use) the Feast–MLflow integration. Reach out on [Slack](https://slack.feast.dev/) or [GitHub](https://github.com/feast-dev/feast) — issues and PRs welcome! + + diff --git a/infra/website/docs/blog/feast-offline-store-sox-metrics.md b/infra/website/docs/blog/feast-offline-store-sox-metrics.md new file mode 100644 index 00000000000..a41f951f604 --- /dev/null +++ b/infra/website/docs/blog/feast-offline-store-sox-metrics.md @@ -0,0 +1,386 @@ +--- +title: "Extending Feast Observability: Offline Store Metrics and SOX Audit Logging" +description: "Feast now captures RED metrics for offline store retrievals and emits structured SOX audit logs for both online and offline feature access — closing the observability gap between serving and training paths." +date: 2026-06-09 +authors: ["Jitendra Yejare"] +--- + +
+ Feast Offline Store Metrics and SOX Audit Logging — Prometheus metrics for offline retrievals and structured audit logs for compliance +
+ +# Extending Feast Observability: Offline Store Metrics and SOX Audit Logging + +In [our previous post](/blog/feast-feature-server-monitoring), we introduced built-in Prometheus metrics for the Feast feature server — covering the full online serving lifecycle from HTTP request handling through online store reads, on-demand feature transformations, materialization pipelines, and feature freshness tracking. + +That covered the **online** path. But production ML systems don't just serve features in real time — they also build training datasets through offline store retrievals. And for teams operating in regulated environments (financial services, healthcare, government), observability isn't enough. You need an **auditable record** of who accessed what data, when, and how much. + +This post covers two new capabilities added to Feast: + +1. **Offline Store RED Metrics** — Prometheus counters and histograms for offline store retrieval operations (request rate, error rate, latency, row counts) +2. **SOX Audit Logging** — Structured JSON audit log entries for both online and offline feature retrieval paths, routed to a dedicated `feast.audit` logger + +Together, these close the observability gap between online and offline operations and give compliance teams the structured audit trail they need. + +## Offline Store Metrics: Closing the Observability Gap + +The online feature server already had comprehensive metrics, but the offline store — where `get_historical_features` queries execute against your data warehouse to build training datasets — had zero instrumentation. This matters because training-serving skew, stalled pipelines, and data volume anomalies all originate in the offline path. + +### The Problem + +Without offline store metrics, teams faced three blind spots: + +- **Silent training failures** — An offline retrieval that returns incomplete data (or errors out) produces a corrupted training dataset. Models trained on bad data degrade in production, and without metrics, there's no signal until prediction quality drops. +- **Invisible pipeline stalls** — A `get_historical_features` call that normally takes 30 seconds but suddenly takes 10 minutes looks like a "hang" from the orchestrator's perspective. No latency metrics means no alerting until the pipeline times out. +- **Data volume anomalies** — If a typical training query returns 500K rows but suddenly returns 50K, something changed upstream. Without row count tracking, this silently propagates into model training. + +### How Feast Solves It + +Feast now automatically captures RED metrics (Rate, Errors, Duration) for every offline store retrieval — regardless of the backend. Whether you're running against BigQuery, Redshift, Snowflake, DuckDB, or local files, you get the same three Prometheus metrics out of the box: + +- **`feast_offline_store_request_total`** — Counts every retrieval, labeled by success/error. Set an alert and know immediately when training pipelines start failing. +- **`feast_offline_store_request_latency_seconds`** — Latency histogram with buckets tuned for offline workloads (`0.1s` to `10min`). Set SLOs and catch slow queries before pipelines time out. +- **`feast_offline_store_row_count`** — Row count histogram covering `100` to `5M` rows. Detect data volume anomalies before they reach model training. + +Metrics collection never interferes with your queries — if the metrics path fails for any reason, your offline retrieval completes normally. + +``` +# Alert when offline retrievals start failing +- alert: FeastOfflineStoreErrors + expr: rate(feast_offline_store_request_total{status="error"}[15m]) > 0 + for: 5m + labels: + severity: critical + annotations: + summary: > + Offline store retrievals are failing ({{ $value }} errors/sec). + Training pipelines may be producing incomplete datasets. +``` + +## Why SOX Audit Logging Matters + +For organizations subject to SOX (Sarbanes-Oxley), GDPR, HIPAA, or other regulatory frameworks, you need to answer questions like: + +- *Who accessed customer features at 3:47 PM on March 15th?* +- *Which feature views were involved in the training dataset built yesterday?* +- *How many rows of PII-adjacent data were retrieved by the batch scoring pipeline?* + +Before this change, answering these questions required parsing unstructured application logs and correlating timestamps across services. Feature stores sit at the intersection of data access and ML model behavior — yet most have no structured audit trail. + +Feast now emits **structured JSON audit entries** for both online and offline retrieval paths, routed to a dedicated `feast.audit` logger that can be independently sent to your SIEM, log aggregator, or compliance sink — without touching your operational log pipeline. + +What makes this production-ready: + +- **PII-minimized by design.** Entity key *names* are logged, not *values*. A compliance auditor sees "the ML pipeline accessed `user_id` features from `transaction_features` at 3:47 PM" without the log itself containing PII. +- **Dedicated logger.** Audit entries go to `feast.audit`, separate from the application logger. Route them to a SOX-compliant sink (Splunk, ELK with retention policies, S3 with WORM locks) independently. +- **Never breaks your serving path.** Audit logging is best-effort — a broken audit sink never affects feature serving latency or availability. +- **Zero overhead when disabled.** `audit_logging` defaults to `false`. Enable it only when you need it. + +## The New Metrics + +### Offline Store RED Metrics + +| Metric | Type | Labels | What It Answers | +|--------|------|--------|-----------------| +| `feast_offline_store_request_total` | Counter | `method`, `status` | What is my offline retrieval throughput and error rate? | +| `feast_offline_store_request_latency_seconds` | Histogram | `method` | How long are my training data queries taking? | +| `feast_offline_store_row_count` | Histogram | `method` | How much data are my offline retrievals returning? | + +The `method` label captures the retrieval type (`to_arrow`), and `status` is `success` or `error`. The latency histogram uses wide buckets tuned for offline workloads: `0.1s, 0.5s, 1s, 5s, 10s, 30s, 60s, 2min, 5min, 10min` — because offline queries can range from sub-second (small entity sets against local files) to minutes (large point-in-time joins against BigQuery or Redshift). + +The row count histogram uses exponential buckets: `100, 1K, 10K, 100K, 500K, 1M, 5M` — covering the range from small test retrievals to production training datasets. + +### SOX Audit Log Entries + +**Online feature request audit entry:** + +```json +{ + "event": "online_feature_request", + "timestamp": "2026-06-07T14:42:29.739Z", + "requestor_id": "service-account:ml-pipeline", + "entity_keys": ["driver_id"], + "entity_count": 5, + "feature_views": ["driver_hourly_stats"], + "feature_count": 3, + "status": "success", + "latency_ms": 12.45 +} +``` + +**Offline feature retrieval audit entry:** + +```json +{ + "event": "offline_feature_retrieval", + "timestamp": "2026-06-07T14:42:29.739Z", + "method": "to_arrow", + "start_time": "2026-06-07T14:42:29.697Z", + "end_time": "2026-06-07T14:42:29.739Z", + "feature_views": ["driver_hourly_stats"], + "feature_count": 3, + "row_count": 150000, + "status": "success", + "duration_ms": 42.39 +} +``` + +Each entry is a single JSON line, making it trivial to parse with `jq`, ingest into Elasticsearch, or stream to a Kafka topic for compliance processing. + +**Note on accessor identity:** Online audit entries include `requestor_id`, extracted from the Feast authentication layer (SecurityManager). Offline retrievals run as direct SDK calls in the user's own process (a notebook, Airflow task, or training script) — there is no server in the middle to extract auth context. In production SOX environments, offline accessor identity is typically established at the infrastructure level: the Kubernetes service account running the job, the IAM role accessing the data warehouse, or the CI/CD pipeline identity. A future enhancement could optionally capture identity from `os.getenv("USER")` or an explicit SDK parameter. + +## Enabling the New Metrics + +### YAML Configuration + +Add `offline_features` and `audit_logging` to your `feature_store.yaml`: + +```yaml +feature_server: + metrics: + enabled: true + resource: true + request: true + online_features: true + push: true + materialization: true + freshness: true + offline_features: true # NEW: Offline store RED metrics + audit_logging: true # NEW: SOX audit log entries +``` + +`offline_features` defaults to `true` when metrics are enabled (consistent with other categories). `audit_logging` defaults to `false` — it's opt-in because audit entries have a non-trivial cost (JSON serialization + I/O per request) and are only needed in regulated environments. + +### CLI + +When using `feast serve --metrics`, offline store metrics are enabled by default. Audit logging still requires the YAML toggle since it's opt-in. + +### Routing Audit Logs + +The `feast.audit` logger is a standard Python logger. Configure it like any other: + +```python +import logging + +audit_logger = logging.getLogger("feast.audit") +audit_logger.setLevel(logging.INFO) +audit_logger.propagate = False + +handler = logging.FileHandler("/var/log/feast/audit.log") +handler.setFormatter(logging.Formatter("%(message)s")) +audit_logger.addHandler(handler) +``` + +Or route to a JSON-aware sink in production: + +```yaml +# logging.yaml for production +loggers: + feast.audit: + level: INFO + propagate: false + handlers: [audit_file, splunk_forwarder] +``` + +## Key PromQL Queries for Offline Store + +**Throughput and errors:** + +```promql +# Offline retrieval rate +rate(feast_offline_store_request_total[5m]) + +# Offline error rate +sum(rate(feast_offline_store_request_total{status="error"}[5m])) + / sum(rate(feast_offline_store_request_total[5m])) +``` + +**Latency percentiles:** + +```promql +# Offline retrieval p95 latency +histogram_quantile(0.95, + sum(rate(feast_offline_store_request_latency_seconds_bucket[5m])) by (le)) + +# Average offline retrieval duration +rate(feast_offline_store_request_latency_seconds_sum[5m]) + / rate(feast_offline_store_request_latency_seconds_count[5m]) +``` + +**Row count analysis:** + +```promql +# Average rows per retrieval +feast_offline_store_row_count_sum / feast_offline_store_row_count_count + +# p95 row count (detect large retrievals) +histogram_quantile(0.95, + sum(rate(feast_offline_store_row_count_bucket[5m])) by (le)) +``` + +## Building Alerts for Offline Store + +### Offline Retrieval Failures + +```yaml +- alert: FeastOfflineStoreErrors + expr: rate(feast_offline_store_request_total{status="error"}[15m]) > 0 + for: 5m + labels: + severity: critical + annotations: + summary: > + Offline store retrievals are failing. + Training pipelines may be producing incomplete datasets. +``` + +### Slow Offline Queries + +```yaml +- alert: FeastOfflineStoreSlowQuery + expr: | + histogram_quantile(0.95, + sum(rate(feast_offline_store_request_latency_seconds_bucket[5m])) by (le) + ) > 300 + for: 5m + labels: + severity: warning + annotations: + summary: > + Offline store p95 latency is {{ $value | humanizeDuration }}. + Training pipelines may be stalling. +``` + +### Row Count Anomaly + +```yaml +- alert: FeastOfflineStoreRowCountDrop + expr: | + feast_offline_store_row_count_sum / feast_offline_store_row_count_count + < 0.5 * avg_over_time( + (feast_offline_store_row_count_sum / feast_offline_store_row_count_count)[1d:1h]) + for: 10m + labels: + severity: warning + annotations: + summary: > + Average rows per offline retrieval dropped by >50%. + Possible upstream data issue. +``` + +## The Extended Grafana Dashboard + +We've extended the existing Feast Grafana dashboard with a dedicated **Offline Store** section containing six new panels: + +- **Offline Store Request Rate** — Rate of offline retrievals by method and status +- **Offline Store Total Requests** — Cumulative request counts (stat panel) +- **Offline Store Retrieval Latency (p50/p95/p99)** — Latency percentile time series +- **Offline Store Row Count Distribution** — Row count percentiles over time +- **Avg Offline Retrieval Duration** — Average duration per method +- **Offline Store Error Rate** — Gauge showing current error percentage with threshold coloring + +
+ Grafana dashboard showing dedicated offline store containing six new panels +
+ +These panels sit alongside the existing online store panels, giving you a single dashboard that covers both serving paths. + +For SOX compliance, a separate **Audit Trail** dashboard powered by Loki visualizes: + +- **Total Audited Events** — Count of all audited access events +- **Online vs Offline Access Timeline** — Stacked time series showing access patterns +- **Offline Data Volume** — Total rows retrieved over time, flagging bulk data exports +- **Anomaly Detection** — Large row counts and slow queries that may need compliance review + +
+ Grafana dashboard showing SOX compliance and access containing five new panels +
+ +- **Live Audit Log Stream** — Raw structured audit entries, expandable for investigation + +
+ Grafana dashboard showing audit logs for offline store +
+ + +## Updated Metrics Summary + +| Category | Metric | What It Answers | +|----------|--------|-----------------| +| **Online** Request | `feast_feature_server_request_total` | What is my online throughput and error rate? | +| **Online** Request | `feast_feature_server_request_latency_seconds` | What are my online p50/p99 latencies? | +| **Online** Features | `feast_online_features_entity_count` | What is my online traffic shape? | +| **Online** Store Read | `feast_feature_server_online_store_read_duration_seconds` | Is my online store the bottleneck? | +| ODFV Transform | `feast_feature_server_transformation_duration_seconds` | How expensive are my read-path transforms? | +| ODFV Transform | `feast_feature_server_write_transformation_duration_seconds` | How expensive are my write-path transforms? | +| Push | `feast_push_request_total` | Is my ingestion pipeline sending data? | +| Materialization | `feast_materialization_total` | Are my pipelines succeeding? | +| Materialization | `feast_materialization_duration_seconds` | How long do my pipelines take? | +| Freshness | `feast_feature_freshness_seconds` | How stale is the data my models are using? | +| Resource | `feast_feature_server_cpu_usage / memory_usage` | Is my server healthy? | +| **Offline** Request | `feast_offline_store_request_total` | What is my offline retrieval throughput? | +| **Offline** Latency | `feast_offline_store_request_latency_seconds` | How long are my training queries taking? | +| **Offline** Row Count | `feast_offline_store_row_count` | How much data are retrievals returning? | +| **Audit** | `feast.audit` logger (online) | Who requested which features, when? | +| **Audit** | `feast.audit` logger (offline) | Which training datasets were built, with how much data? | + +## How to Try It + +### Automated Demo + +We've extended the [feast-prometheus-metrics](https://github.com/ntkathole/feast-automated-setups/tree/main/feast-prometheus-metrics) automated demo to include offline store metrics and SOX audit logging. The extended traffic generator exercises both online and offline paths: + +```bash +# Clone and run +git clone https://github.com/ntkathole/feast-automated-setups.git +cd feast-automated-setups/feast-prometheus-metrics + +# Run setup (uses feast from your environment) +./setup.sh + +# Generate extended traffic including offline retrievals +python3 generate_traffic_extended.py \ + --url http://localhost:6566 \ + --duration 120 \ + --repo-path workspace/feast_demo/feature_repo \ + --log-dir workspace/logs +``` + +After traffic generation, check the audit log: + +```bash +# View structured audit entries +cat workspace/logs/feast_audit.log | python3 -m json.tool + +# Count by event type +cat workspace/logs/feast_audit.log | \ + python3 -c "import sys,json; events=[json.loads(l)['event'] for l in sys.stdin]; print({e:events.count(e) for e in set(events)})" +``` + +### Manual Verification + +Verify offline store metrics are being emitted: + +```bash +# Check the Prometheus metrics endpoint for offline store metrics +curl -s http://localhost:8000 | grep feast_offline + +# Query Prometheus directly +curl -s 'http://localhost:9090/api/v1/query?query=feast_offline_store_request_total' +``` + +### Enable in Your Deployment + +1. **Update `feature_store.yaml`** — Add `offline_features: true` and `audit_logging: true` to the metrics block +2. **Configure audit log routing** — Set up a handler for the `feast.audit` logger in your logging config +3. **Import the updated Grafana dashboard** — Add the offline store panels to your existing dashboard +4. **Set up alerts** — Start with offline retrieval failures and row count anomalies + + +We're excited to bring full-lifecycle observability to Feast — covering both the real-time serving path and the batch training path — and welcome feedback from the community! + +--- + +*References:* +- *[Existing blog: Monitoring Your Feast Feature Server with Prometheus and Grafana](https://feast.dev/blog/feast-feature-server-monitoring/)* +- *[Feast Prometheus Metrics Demo](https://github.com/ntkathole/feast-automated-setups/tree/main/feast-prometheus-metrics)* diff --git a/infra/website/docs/blog/feast-online-server-performance-tuning.md b/infra/website/docs/blog/feast-online-server-performance-tuning.md new file mode 100644 index 00000000000..ffe29910b9e --- /dev/null +++ b/infra/website/docs/blog/feast-online-server-performance-tuning.md @@ -0,0 +1,338 @@ +--- +title: "Tuning the Feast Feature Server for Sub-2ms Online Serving" +description: "A practical guide to achieving low-latency, high-throughput feature serving with Feast on Kubernetes — from default configuration to production-grade performance with pre-computed feature vectors and benchmarks at every step." +date: 2026-06-02 +authors: ["Nikhil Kathole"] +--- + +**Feast supports production-grade worker configuration, connection pooling, async reads, batched pipelines, serialization optimizations, and pre-computed feature vectors for the Python feature server.** This post walks through a real-world performance tuning exercise in two stages: first, server and client tuning that brings p99 latency down to **sub-5ms** for single-row requests; then, **pre-computed feature vectors** that push it further to **sub-2ms p99** — regardless of how many feature views your FeatureService spans. We share the benchmarking methodology, the exact configuration changes, and the measured impact of each step so you can apply the same approach to your own deployments. + +--- + +## The Problem + +When you deploy Feast on Kubernetes using the [Feast Operator](https://docs.feast.dev/how-to-guides/production-deployment-topologies), the default configuration is designed for simplicity — a single Gunicorn worker, short keep-alive timeouts, and frequent registry refreshes. This is fine for development but leaves significant performance on the table for production workloads where every millisecond matters. + +We set out to answer a practical question: **how low can we push the Feast online server's p99 latency, and what does it take to get there?** + +--- + +## Test Environment + +| Component | Configuration | +|-----------|--------------| +| **Online Store** | Redis 7.0.12 (standalone, in-cluster) | +| **Registry** | PostgreSQL 16 (SQL registry) | +| **Platform** | Kubernetes | +| **Deployment** | Feast Operator with `FeatureStore` CR | + +We used a banking feature store project with multiple feature views spanning customer demographics, transactions, and behavioral profiles. + +All benchmarks run 200 iterations (after 30–50 warmup) for each scenario, measuring p50, p95, p99, and mean latency. Throughput is measured with 10 concurrent workers over 15 seconds. + +--- + +## Three Access Modes + +Feast supports three ways to retrieve online features. Understanding how each one works is key to knowing where latency comes from — and where to optimize. + +### REST API + +``` +Client → HTTP POST (JSON) → Gunicorn/FastAPI Server → Redis mget() → JSON response +``` + +The simplest and most common pattern. Your application sends a JSON request to the feature server's `/get-online-features` endpoint. The server holds persistent Redis connections and a pre-loaded registry, so each request is just a Redis read plus JSON serialization. HTTP keep-alive reuses TCP/TLS connections across requests. + +### Direct SDK + +``` +Client (Python) → FeatureStore SDK → Redis mget() directly +``` + +The Python SDK connects to Redis directly — no HTTP hop, no JSON overhead. However, it pays for in-process registry lookups and entity key serialization on every call, and reads each FeatureView sequentially. + +### Remote SDK + +``` +Client (Python SDK) → HTTP POST → Feature Server → Redis → JSON → Client +``` + +The SDK delegates feature retrieval to a remote feature server over HTTP. This combines the worst of both worlds: SDK-side overhead *plus* an HTTP round-trip. Without connection pooling, each call creates a new TCP connection and TLS handshake. + +--- + +## Baseline: Default Configuration + +With no tuning applied — a single Gunicorn worker, default timeouts, and no connection pooling: + +| Mode | p99 (1 row) | p99 (5 rows) | Throughput | +|------|----------------|-------------------|------------| +| **REST API** | 6.92 ms | 4.94 ms | 480 RPS | +| **Direct SDK** | 5.83 ms | 5.59 ms | — | +| **Remote SDK** | 11.71 ms | **74.31 ms** | ~2 RPS | + +The REST API and Direct SDK are already in the 5–7ms range out of the box, but the Remote SDK fails badly — p99 spiking to **74ms** at just 5 rows due to per-request TCP/TLS setup overhead. This is our starting point. + +--- + +## Server-Side Configuration + +These are changes you apply to the **feature server deployment** — no code changes needed, just configuration via the `FeatureStore` CR and Redis runtime settings. + +### Worker Tuning via the Feast Operator + +The Feast Operator exposes `workerConfigs` in the `FeatureStore` CR, letting you tune the Gunicorn server without rebuilding images: + +```yaml +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +spec: + services: + onlineStore: + server: + workerConfigs: + workers: -1 # Auto: 2 × CPU cores + 1 + keepAliveTimeout: 120 # Reuse connections longer + maxRequests: 5000 # Recycle workers to prevent memory leaks + maxRequestsJitter: 200 # Stagger recycling + registryTTLSeconds: 300 # Reduce registry refresh overhead + workerConnections: 2000 # High-concurrency support +``` + +Setting `workers: -1` on a 4-core pod gives 9 Gunicorn workers, each with its own event loop and Redis connection. This is the **single most impactful change** — it transforms the server from single-threaded to multi-process, dropping 5-row p99 from ~10ms to ~8ms and putting us on the path to sub-5ms. + +### Redis Runtime Tuning + +Three Redis settings made a measurable difference: + +- **`hz 100`** (default 10) — Redis processes expired keys and timeouts 10x faster, reducing tail latency spikes. +- **`tcp-keepalive 60`** (default 300) — Detects dead connections 5x faster, freeing resources sooner. +- **`save ""`** (disable RDB persistence) — Eliminates periodic snapshot I/O that causes 10–50ms p99 spikes. Since features are materialized from the offline store and reconstructible at any time, persistence is unnecessary. + +### High Availability and Auto-Scaling + +For production, we added horizontal scaling and availability guarantees using the Feast Operator's [built-in HA support](https://docs.feast.dev/how-to-guides/feast-snowflake-gcp-aws/scaling-feast): + +```yaml +spec: + replicas: 2 + services: + onlineStore: + server: + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + scaling: + autoscaling: + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + pdb: + minAvailable: 1 +``` + +When scaling is enabled, the operator auto-injects pod anti-affinity and zone topology spread constraints, ensuring replicas land on different nodes for resilience. With HPA, the cluster auto-scales based on CPU utilization — we observed it scaling from 2 to 3 pods in response to load during benchmarks. At 10 pods with 9 workers each, theoretical throughput reaches ~7,180 RPS (~25.8M RPH). + +### Server-side quick wins summary + +1. **Set `workers: -1`** — single most impactful change +2. **Disable Redis persistence** — `CONFIG SET save ""` +3. **Set `registryTTLSeconds: 300`** — reduce registry refresh overhead +4. **Use `replicas: 2`** minimum with HPA for burst capacity +5. **Set resource limits** — defaults are far too low for production + +--- + +## Client-Side Configuration + +These are changes you apply on the **client** — how the SDK connects to the feature server and which access mode you choose. + +### Connection Pooling for the Remote SDK + +The biggest problem with the Remote SDK was that every call created a brand-new `requests.Session`, established a fresh TCP connection, negotiated TLS, and then threw it all away — adding 2–4ms per call for HTTPS endpoints. + +Feast now includes `HttpSessionManager` — a thread-safe, singleton session manager that reuses HTTP connections across requests with configurable pooling and retry: + +```yaml +online_store: + type: remote + path: https://feast-server:443 + connection_pool_size: 50 + connection_idle_timeout: 300 + connection_retries: 3 +``` + +This dropped Remote SDK 5-row p99 from **74ms to 21ms** — a 72% reduction — by eliminating the per-request TLS handshake. + +### Choosing the right access mode + +| Use Case | Recommended Mode | Why | +|----------|-----------------|-----| +| **Application serving** | REST API | Sub-5ms single-row p99, simplest integration, 718 RPS per pod | +| **Python ML pipeline** | Direct SDK | No HTTP hop, sub-5ms p99, native protobuf | +| **Async Python applications** | Async Direct SDK | Non-blocking, batched pipeline, sub-5ms p99 | +| **Cross-cluster serving** | Remote SDK + pooling | When the client can't reach Redis directly; 760 RPS with pooling | + +--- + +## Code Enhancements in Feast + +Beyond configuration, several code-level improvements in Feast itself contributed to reaching sub-5ms p99. These require no user configuration — just upgrading to the latest Feast version. + +### Serialization Optimization + +The feature server used `google.protobuf.json_format.MessageToDict` to convert protobuf responses to JSON — a generic, reflection-based serializer that was a meaningful fraction of server-side latency. Replacing it with an optimized custom dict builder delivered a **66% throughput increase** (432 to 718 RPS) and **72% reduction in tail latency under load** (132ms to 37ms p99). + +### Async Redis Reads with Batched Pipeline + +The `RedisOnlineStore` had async support (`online_read_async` with `redis_asyncio`), but the `async_supported` property was not overridden, so the feature server never used it. Enabling it unlocks non-blocking I/O on the server side — the FastAPI handler calls `get_online_features_async` directly instead of wrapping the sync path in `run_in_threadpool`. + +Additionally, the base class async path issued O(N_feature_views) separate round trips to Redis via `asyncio.gather`. We added a `get_online_features_async` override to `RedisOnlineStore` that batches all HMGET commands across all feature views into a **single async pipeline execution** (O(1) round trips), matching the existing sync batched pipeline. This cut async 5-row p99 from ~11ms to **5.6ms** — a 49% improvement. + +### Cached Per-Request Checks + +`_check_versioned_read_support()` performed up to 7 lazy module imports on **every request** to determine if the current online store supports versioned reads. We cache the result per store instance, resolving imports once and eliminating ~0.5–1ms of overhead per request. + +### Skip Duplicate Feature Resolution + +When auth is `no_auth` (the common case), the feature server was resolving feature views solely to check permissions (which are no-ops), then resolving them again inside `get_online_features`. We skip the first resolution entirely, avoiding a redundant registry lookup. + +### Session Wrapping Fix + +The `rest_error_handling_decorator` re-wrapped cached `requests.Session` HTTP methods on every call. After ~1000 requests, this caused progressive performance degradation and eventually a `RecursionError`. We now wrap each method exactly once per session lifetime, fixing Remote SDK stability and enabling it to sustain **760 RPS**. + +--- + +## Final Results + +After applying all server-side configuration, client-side configuration, and code enhancements: + +### Stage 1: Tuning only (sub-5ms target) + +| Mode | p50 (1 row) | p99 (1 row) | p50 (5 rows) | p99 (5 rows) | Throughput | +|------|----------|----------|----------|----------|------------| +| **REST API** | 3.34 ms | **4.61 ms** | 7.88 ms | 11.32 ms | 718 RPS | +| **REST API (FeatureService)** | 4.21 ms | **6.15 ms** | 9.15 ms | 17.43 ms | — | +| **Direct SDK** | 3.12 ms | **4.21 ms** | 3.29 ms | **4.60 ms** | 402 RPS | +| **Direct SDK (FeatureService)** | 3.48 ms | **5.70 ms** | 3.44 ms | **5.10 ms** | — | +| **Async Direct SDK** | 3.25 ms | **6.25 ms** | 3.47 ms | **8.72 ms** | — | +| **Async Direct SDK (FeatureService)** | 3.60 ms | **4.84 ms** | 3.76 ms | **5.13 ms** | — | +| **Remote SDK** | 3.34 ms | **5.30 ms** | 8.15 ms | 11.63 ms | 760 RPS | +| **Remote SDK (FeatureService)** | 3.78 ms | **5.17 ms** | 9.86 ms | 16.06 ms | — | + +### Stage 2: Pre-computed vectors (sub-2ms target) + +| Batch Size | p50 Regular | p99 Regular | p50 Precomputed | p99 Precomputed | Speedup (p50) | +|---|---|---|---|---|---| +| 1 | 5.95 ms | 10.74 ms | **0.98 ms** | **1.70 ms** | 6.1x | +| 5 | 9.66 ms | 44.91 ms | **1.37 ms** | **3.00 ms** | 7.1x | +| 10 | 16.60 ms | 60.37 ms | **1.81 ms** | **2.07 ms** | 9.2x | +| 50 | 60.07 ms | 120.12 ms | **5.27 ms** | **7.49 ms** | 11.4x | +| 100 | 85.58 ms | 208.18 ms | **9.48 ms** | **114.38 ms** | 9.0x | +| 500 | 218.79 ms | 424.91 ms | **40.25 ms** | **198.13 ms** | 5.4x | + +**Key takeaways:** + +- **Stage 1 (tuning)** gets all SDK modes to **sub-5ms p99** for single-row requests — REST API at 4.61ms, Direct SDK at 4.21ms, Async SDK at 4.84ms. +- **Stage 2 (pre-computed vectors)** pushes latency to **sub-2ms p99** for single-row requests — a 6x improvement over the tuned regular path. +- **REST API** delivers the best throughput at **718 RPS** (2.6M RPH); **Remote SDK** sustains **760 RPS** after the session wrapping fix. +- For FeatureServices spanning multiple feature views, **`precompute_online=True` is the single most impactful optimization** — it changes the read complexity from O(N feature views) to O(1). +- At large batch sizes, the bottleneck shifts from store I/O to Python CPU overhead (protobuf deserialization). For these workloads, split large requests into smaller batches on the client side. + +--- + +## A Note on Online Store Selection + +All benchmarks in this post used a **standalone Redis pod** running in the same Kubernetes cluster as the feature server. Production deployments often use managed services — here's how that changes the picture. + +**Managed Redis** (ElastiCache, Memorystore, Azure Cache for Redis) provides dedicated compute, optimized networking, cluster mode for sharding, and automatic failover. In our benchmarks, Redis RTT was ~0.5ms (in-cluster). A managed instance in the **same availability zone** would deliver comparable latency with more consistent tail behavior. Cross-AZ hops add 1–2ms per request. + +**DynamoDB** offers zero operational overhead and automatic scaling. When the feature server runs in the **same AWS region and VPC**, single-digit millisecond reads are typical (1–5ms for eventually consistent reads). With [DAX](https://aws.amazon.com/dynamodb/dax/), read latency drops to microseconds for cached items. A same-region setup could deliver comparable sub-5ms p99 for single-row reads. + +Feast also supports PostgreSQL, SQLite, Snowflake, Bigtable, and more. The general rule is: **the online store is the single largest factor in `get_online_features()` latency** — choose based on your latency budget, throughput needs, and operational requirements. The tuning steps in this post (worker configuration, registry caching, connection pooling, serialization optimization) apply equally to all stores — they optimize the layers above. + +--- + +## Pre-computed Feature Vectors + +The tuning steps above achieve our first target: **sub-5ms p99** for single-row requests. But for FeatureServices spanning multiple feature views, per-FV read fan-out becomes the dominant bottleneck — each request issues N separate store reads, N protobuf deserializations, and N response assemblies. To reach our final target of **sub-2ms**, we need to eliminate this fan-out entirely. + +**Pre-computed feature vectors** do exactly that: at materialize time, all features for a FeatureService are assembled into a single serialized blob per entity. At read time, one key lookup replaces N feature-view reads — reducing the operation from O(N feature views) to O(1) and delivering **sub-2ms p99 latency**. + +### How it works + +1. **Define** a FeatureService with `precompute_online=True`: + +```python +scoring_service = FeatureService( + name="realtime_scoring", + features=[user_profile_fv, transaction_fv, risk_fv], + precompute_online=True, +) +``` + +2. **Apply** and **materialize** as usual — vectors are built automatically: + +```bash +feast apply +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +Feast detects which FeatureServices have `precompute_online=True` and rebuilds their pre-computed vectors after the per-feature-view writes complete. Vectors are also refreshed automatically on `feast push`. + +3. **Read** features as usual — the server automatically uses the pre-computed path: + +```python +features = store.get_online_features( + features=store.get_feature_service("realtime_scoring"), + entity_rows=[{"user_id": "U12345"}], + full_feature_names=True, +) +``` + +### Design decisions + +- **Store-agnostic**: The pre-computed logic lives in the base `OnlineStore` class and works with all backends (Redis, DynamoDB, PostgreSQL, etc.). No store-specific code is needed. +- **Opt-in**: `precompute_online` defaults to `False`. Existing deployments are completely unaffected. +- **Strict error handling**: When `precompute_online=True`, there is no silent fallback to per-FV reads. If vectors are missing or stale, the server raises a `RuntimeError`, making problems visible immediately. +- **Schema-aware**: A fingerprint of feature names detects schema changes and rejects stale vectors, with column-order-independent comparison. +- **Per-FV TTL enforcement**: Individual feature view TTLs are checked within the pre-computed blob. +- **Materialized view pattern**: Conceptually similar to a database materialized view — trades storage for read speed with explicit refresh. + +### Benchmark: precomputed vs regular path + +We benchmarked a FeatureService spanning multiple feature views against the same features read via the regular per-feature-view path. All numbers from the same pod, same run, 200 iterations with 30 warmup. + +| Batch Size (rows/request) | p50 Regular | p50 Precomputed | p99 Regular | p99 Precomputed | Speedup (p50) | +|---|---|---|---|---|---| +| 1 | 5.95 ms | **0.98 ms** | 10.74 ms | **1.70 ms** | 6.1x | +| 5 | 9.66 ms | **1.37 ms** | 44.91 ms | **3.00 ms** | 7.1x | +| 10 | 16.60 ms | **1.81 ms** | 60.37 ms | **2.07 ms** | 9.2x | +| 50 | 60.07 ms | **5.27 ms** | 120.12 ms | **7.49 ms** | 11.4x | + +For the typical production use case of 1–10 rows per inference request, pre-computed vectors deliver **sub-2ms p99** — well under any reasonable SLA target. The speedup ranges from **6x to 9x** depending on batch size, with p50 consistently under 2ms for up to 10 rows. + +--- + +## Try It Yourself + +To deploy the same setup: + +1. Deploy Feast with the Feast Operator using a `FeatureStore` CR with `workerConfigs` +2. Use Redis as the online store and PostgreSQL for the registry +3. Apply the [production tuning guide](https://docs.feast.dev/how-to-guides/online-server-performance-tuning) for worker configuration, registry caching, and scaling +4. For FeatureServices spanning multiple feature views, enable `precompute_online=True` and materialize — see the [feature service docs](https://docs.feast.dev/getting-started/concepts/feature-retrieval#pre-computed-feature-vectors-precompute_online) +5. Monitor with [built-in Prometheus metrics](https://docs.feast.dev/reference/feature-servers/python-feature-server) — `feast_feature_server_request_latency_seconds` is your primary SLI + +We'd love to hear about your production performance results. Join the conversation on [Feast Slack](https://slack.feast.dev) or open an issue on [GitHub](https://github.com/feast-dev/feast). diff --git a/infra/website/docs/blog/feast-openai-compatible-api.md b/infra/website/docs/blog/feast-openai-compatible-api.md new file mode 100644 index 00000000000..f228836afd6 --- /dev/null +++ b/infra/website/docs/blog/feast-openai-compatible-api.md @@ -0,0 +1,364 @@ +--- +title: "Using Feast's OpenAI Compatible Search API" +description: "Feast now exposes an OpenAI-compatible vector store search endpoint. Send a plain text query, get results back in the standard OpenAI format. No client-side embeddings required." +date: 2026-07-07 +authors: ["Chaitanya Patel", "Nikhil Kathole"] +--- + +
+ Sequence diagram showing a client sending a text query to Feast, which embeds and searches server-side +
+ +If you've tried to connect an AI agent to Feast's vector search, you've probably hit this wall: the agent needs to search your feature store, but Feast expects a raw embedding vector. The agent doesn't have one. It has a question in English. + +Until now, the workaround was ugly. You'd call an embedding provider (OpenAI, Ollama, whatever) to turn the text into a float array, then pass that array to Feast's vector search endpoint (`POST /search`, formerly `retrieve-online-documents`). Every client had to know both APIs, carry both sets of credentials, and run glue code whose only job was bridging the gap. + +Feast now has a new endpoint: `POST /v1/vector_stores/{vector_store_id}/search`. It follows the [OpenAI Vector Store Search API](https://platform.openai.com/docs/api-reference/vector-stores-search) format, including proper `vs_{hash}` identifiers for vector stores. You send text, Feast handles the embedding internally, and you get results back in the same JSON shape that OpenAI returns. No float arrays, no extra SDK. + +Each feature view with vector search enabled gets a deterministic `vs_` identifier (e.g. `vs_a1b2c3d4e5f6...`). Discover them via `GET /v1/vector_stores`. + +## The two-API tax + +Here's what searching Feast looked like before: + +```python +import openai +import requests + +# Step 1: Call the embedding provider yourself +embed_response = openai.embeddings.create( + model="text-embedding-3-small", + input="wireless noise-cancelling headphones" +) +query_vector = embed_response.data[0].embedding # 1536 floats + +# Step 2: Call Feast's proprietary API with the raw vector +result = requests.post("http://feast-server:6566/search", json={ + "features": [ + "product_catalog:vector", + "product_catalog:name", + "product_catalog:description", + "product_catalog:price", + ], + "query": query_vector, + "top_k": 5, + "api_version": 2, +}) +``` + +This works fine. But it has costs that add up: + +- Every service calling Feast needs an embedding SDK, an API key, and logic to handle the embedding call. Five microservices means five places managing embedding credentials. +- LLM agents can't use it. They discover tools through MCP or function calling, and they know how to call OpenAI-shaped endpoints. They don't know how to compute embeddings and pass raw float arrays to a custom API. +- The embedding model becomes a client-side decision. Different clients might use different models or versions, which means inconsistent search results against the same vector store. +- Feast's filter syntax is its own format. Not something an agent framework knows out of the box. + +## One endpoint, standard format + +With the new endpoint, that same search looks like this: + +```python +import requests + +# First, discover your vector store IDs +stores = requests.get("http://feast-server:6566/v1/vector_stores").json() +vs_id = stores["data"][0]["id"] # e.g. "vs_a1b2c3d4e5f6..." + +# Then search using the vs_ identifier +result = requests.post( + f"http://feast-server:6566/v1/vector_stores/{vs_id}/search", + json={ + "query": "wireless noise-cancelling headphones", + "max_num_results": 5, + }, +) +``` + +No embedding SDK. No raw vectors. The request and response match OpenAI's format, so anything that already talks to OpenAI can talk to Feast. + +### What happens under the hood + +When Feast receives this request, it: + +1. Embeds the query server-side using the model configured in `feature_store.yaml` (via [Sentence Transformers](https://www.sbert.net/) for local inference — no external API key required). +2. Runs vector similarity search against the feature view's online store (Postgres/pgvector, Milvus, Elasticsearch, SQLite, or whatever backend you've configured). +3. Applies filters if you provided any, using string equality, numeric comparisons, or compound AND/OR conditions in the OpenAI filter format. +4. Returns results in OpenAI's `vector_store.search_results.page` format. + +Because the embedding model is a server-side configuration, every client gets consistent results. No more worrying about whether service A is using `text-embedding-3-small` while service B accidentally stuck with `ada-002`. + +## Setting it up + +### Step 1: Configure the embedding model + +Add an `embedding_model` section to your `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local + +online_store: + type: postgres + host: localhost + port: 5432 + database: feast + user: feast + password: ${DB_PASSWORD} + pgvector_enabled: true + vector_len: 384 + enable_openai_compatible_store: true + +embedding_model: + provider: sentence_transformers # default; can be omitted + model: all-MiniLM-L6-v2 +``` + +Feast uses [Sentence Transformers](https://www.sbert.net/) for embedding, so everything runs locally — no external API key required. You can use any HuggingFace model compatible with `SentenceTransformer`: + +```yaml +# Default — lightweight, fast +embedding_model: + model: all-MiniLM-L6-v2 + +# Higher quality, larger model +embedding_model: + model: BAAI/bge-small-en-v1.5 +``` + +### Step 2: Define a feature view with vector search + +```python +from feast import Entity, FeatureView, Field +from feast.types import Array, Float32, String, Float64, Int64 +from datetime import timedelta + +product = Entity(name="product_id", join_keys=["product_id"]) + +product_catalog = FeatureView( + name="product_catalog", + entities=[product], + schema=[ + Field( + name="vector", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="name", dtype=String), + Field(name="description", dtype=String), + Field(name="category", dtype=String), + Field(name="price", dtype=Float64), + Field(name="rating", dtype=Float64), + ], + source=product_source, + ttl=timedelta(days=7), +) +``` + +### Step 3: Apply, load data, and serve + +```bash +feast apply +feast serve +``` + +### Step 4: Discover your vector store ID + +```bash +curl http://localhost:6566/v1/vector_stores +``` + +```json +{ + "object": "list", + "data": [ + { + "id": "vs_a1b2c3d4e5f6a1b2c3d4e5f6", + "object": "vector_store", + "name": "product_catalog", + "status": "completed", + "created_at": 1717200000 + } + ] +} +``` + +### Step 5: Search + +```bash +curl -X POST http://localhost:6566/v1/vector_stores/vs_a1b2c3d4e5f6a1b2c3d4e5f6/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "wireless noise-cancelling headphones", + "max_num_results": 3 + }' +``` + +Response: + +```json +{ + "object": "vector_store.search_results.page", + "search_query": ["wireless noise-cancelling headphones"], + "data": [ + { + "file_id": "vs_a1b2c3d4e5f6a1b2c3d4e5f6_42", + "filename": "vs_a1b2c3d4e5f6a1b2c3d4e5f6", + "score": 0.92, + "attributes": { + "name": "Sony WH-1000XM5", + "description": "Premium wireless noise-cancelling headphones", + "category": "Electronics", + "price": 349.99, + "rating": 4.8 + }, + "content": [ + {"type": "text", "text": "Sony WH-1000XM5"}, + {"type": "text", "text": "Premium wireless noise-cancelling headphones"}, + {"type": "text", "text": "Electronics"} + ] + } + ], + "has_more": false, + "next_page": null +} +``` + +The response follows OpenAI's `vector_store.search_results.page` schema. Any client that already parses OpenAI search results can parse this without changes. + +## Filtering + +The endpoint supports OpenAI-style filters for narrowing results beyond vector similarity. Filters work on the metadata stored alongside your vectors. + +### String filters + +```json +{ + "query": "running shoes", + "max_num_results": 5, + "filters": { + "type": "eq", + "key": "category", + "value": "Footwear" + } +} +``` + +### Numeric filters + +```json +{ + "query": "budget laptop", + "max_num_results": 5, + "filters": { + "type": "lt", + "key": "price", + "value": 500.0 + } +} +``` + +### Compound filters (AND / OR) + +```json +{ + "query": "wireless earbuds", + "max_num_results": 5, + "filters": { + "type": "and", + "filters": [ + {"type": "eq", "key": "category", "value": "Electronics"}, + {"type": "gte", "key": "rating", "value": 4.5}, + {"type": "lt", "key": "price", "value": 200.0} + ] + } +} +``` + +Comparison operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`. Compound operators: `and`, `or`. These nest to arbitrary depth. + +Numeric and boolean filters require the `enable_openai_compatible_store` flag in your online store config, plus a `feast apply` to add the `value_num` column to existing tables. String filters work on all existing schemas without migration. + +## What this means for AI agents + +We built this with agents in mind. When Feast added [MCP support](./feast-agents-mcp) earlier this year, agents could discover and call Feast tools dynamically. But vector search still had this gap where the agent needed to produce a float array. LLMs can't do that. + +Now the search tool is just text in, structured results out. An agent calls it the same way it calls any other OpenAI-compatible service. The feature server currently exposes these tools: + +| Capability | Endpoint | What it does | +|---|---|---| +| Structured feature lookup | `get-online-features` | Get customer profiles, account data, etc. | +| Vector search | `search` | Search with a pre-computed embedding vector (or text via `api_version: 2`) | +| List vector stores | `GET /v1/vector_stores` | Discover available vector stores and their `vs_` IDs | +| Get vector store | `GET /v1/vector_stores/{id}` | Get metadata for a specific vector store | +| Vector search (OpenAI format) | `POST /v1/vector_stores/{id}/search` | Search with plain text, embedding handled server-side | +| Write features / memory | `write-to-online-store` | Persist agent state, update features | + +`POST /retrieve-online-documents` remains available as a deprecated alias for `POST /search`. + +That last row is what this post is about. Before it existed, agents could read structured features and write state back, but they couldn't search vectors without help from glue code. + +## What this is, and what it isn't + +This makes Feast's vector search speak OpenAI's protocol. It doesn't turn Feast into a general purpose OpenAI-compatible vector database. + +| Works today | Not yet | +|---|---| +| `GET /v1/vector_stores` (list) | Creating vector stores via the API | +| `GET /v1/vector_stores/{id}` (get) | | +| `POST /v1/vector_stores/{id}/search` | | +| Plain text queries with server-side embedding | Client-provided embedding vectors on this endpoint | +| OpenAI-format filters (string, numeric, compound) | `ranking_options.score_threshold`, `ranking_options.ranker`, `rewrite_query: true` (rejected with 422) | +| All Feast online store backends | Standalone `/v1/embeddings` endpoint | + +Feature views are still defined in Python and managed through `feast apply`. Data is still ingested through Feast's existing write paths. The OpenAI-compatible layer is a read API that gives standard access to what's already in your feature store. + +## Deploying on Kubernetes + +Below is an example Kubernetes setup that deploys the feature server with Sentence Transformers for local embedding: + +```yaml +# configmap.yaml (embedding model section) +embedding_model: + provider: sentence_transformers + model: all-MiniLM-L6-v2 +``` + +```yaml +# deployment.yaml +containers: + - name: feast-server + command: ["feast", "serve", "-h", "0.0.0.0", "-p", "6566"] + ports: + - containerPort: 6566 +``` + +With this setup, embedding happens in-cluster. Nothing leaves your network. + +## Try it yourself + +```bash +# Install Feast with Sentence Transformers support +pip install feast sentence-transformers +``` + +Configure your `feature_store.yaml` with an `embedding_model` section, define a feature view with vector search enabled, run `feast apply`, load your data, start the server with `feast serve`, and search: + +```bash +# Discover your vector store IDs +curl -s http://localhost:6566/v1/vector_stores | python -m json.tool + +# Search using the vs_ identifier from the list response +curl -s http://localhost:6566/v1/vector_stores/YOUR_VS_ID/search \ + -H "Content-Type: application/json" \ + -d '{"query": "your search query", "max_num_results": 5}' | python -m json.tool +``` + +## What's next + +Next on the list: wiring up `ranking_options` and `rewrite_query` so they actually do something (right now they're accepted but ignored). We also want a standalone `/v1/embeddings` endpoint for clients that just need embeddings, and eventually the ability to create feature views through the OpenAI vector store API instead of requiring Python + `feast apply`. + +## Join the conversation + +If you're using this or have thoughts on what the OpenAI-compatible layer should support next, come find us on [Slack](https://slack.feast.dev/) or [GitHub](https://github.com/feast-dev/feast). diff --git a/infra/website/docs/blog/feast-oracle-offline-store.md b/infra/website/docs/blog/feast-oracle-offline-store.md new file mode 100644 index 00000000000..e8ce13d81ae --- /dev/null +++ b/infra/website/docs/blog/feast-oracle-offline-store.md @@ -0,0 +1,274 @@ +--- +title: "Feast Meets Oracle: Unlocking Feature Store for Oracle Database Users" +description: Oracle Database is now a fully featured Feast offline store, integrated with Kubernetes-native operators. This enables teams to leverage their existing Oracle infrastructure for scalable, production ML feature engineering. +date: 2026-03-16 +authors: ["Aniket Paluskar", "Srihari Venkataramaiah"] +--- + +
+ Feast and Oracle Database +
+ +# Feast Meets Oracle: Unlocking Feature Store for Oracle Database Users + +## The Problem: Your Data Is Already in Oracle — Why Move It? + +If you work in a Fortune 500 company, chances are your most valuable data lives in Oracle Database. It is the one of the most widely used enterprise database for a reason — decades of battle-tested reliability, performance, and governance have made it the backbone of mission-critical systems across finance, healthcare, telecommunications, government, and retail. + +But here's the friction: when ML teams want to build features for their models, they typically export data *out* of Oracle into some other system — a data lake, a warehouse, a CSV on someone's laptop. That data movement introduces latency, staleness bugs, security blind spots, and an entire class of silent failures that only surface when a model starts degrading in production. + +**What if you didn't have to move your data at all?** + +With Feast's new Oracle offline store support — now fully integrated into the Feast Kubernetes operator — you can define, compute, and serve ML features directly from your existing Oracle infrastructure. No data migration. No pipeline duct tape. No compromises. + +--- + +## What's New + +Oracle Database is now a first-class offline store in Feast, supported across the full stack: + +| Layer | What Changed | +|---|---| +| **Python SDK** | `OracleOfflineStore` and `OracleSource` — a complete offline store implementation built on `ibis-framework[oracle]` | +| **Feast Operator (v1 API)** | `oracle` is a validated persistence type in the `FeatureStore` CRD, with Secret-backed credential management | +| **CRD & Validation** | Kubernetes validates `oracle` at admission time — bad configs are rejected before they ever reach the operator | +| **Type System** | Full Oracle-to-Feast type mapping covering `NUMBER`, `VARCHAR2`, `CLOB`, `BLOB`, `BINARY_FLOAT`, `TIMESTAMP`, and more | +| **Documentation** | Reference docs for the [Oracle offline store](https://docs.feast.dev/reference/offline-stores/oracle) and [Oracle data source](https://docs.feast.dev/reference/data-sources/oracle) | + +This isn't a thin wrapper or a partial integration. The Oracle offline store supports the complete Feast offline store interface: + +- **`get_historical_features`** — point-in-time correct feature retrieval for training datasets, preventing future data leakage +- **`pull_latest_from_table_or_query`** — fetch the most recent feature values +- **`pull_all_from_table_or_query`** — full table scans for batch processing +- **`offline_write_batch`** — write feature data back to Oracle +- **`write_logged_features`** — persist logged features for monitoring and debugging + +--- + +## Why This Matters: Oracle Is Where the Enterprise Lives + +Oracle Database isn't just another backend option. It is the database that runs the world's banks, hospitals, supply chains, and telecom networks. When we say "number one enterprise database," we mean it in terms of: + +- **Installed base** — More Fortune 100 companies run Oracle than any other database +- **Data gravity** — Petabytes of the world's most regulated, most valuable data already sits in Oracle +- **Operational maturity** — Decades of enterprise features: partitioning, RAC, Data Guard, Advanced Security, Audit Vault + +For ML teams in these organizations, the path to production has always involved a painful detour: extract data from Oracle, load it somewhere else, build features there, then figure out how to serve them. Every step in that chain is a potential point of failure, a security review, and a compliance headache. + +Feast's Oracle integration eliminates the detour entirely. Your features are computed where your data already has governance, backup, encryption, and access controls in place. + +--- + +## How It Works: From Oracle Table to Production Features + +### Step 1: Configure your feature store + +Point Feast at your Oracle database in `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: oracle + host: oracle-db.example.com + port: 1521 + user: feast_user + password: ${DB_PASSWORD} + service_name: ORCL +online_store: + path: data/online_store.db +``` + +Feast supports three Oracle connection modes — `service_name`, `sid`, or `dsn` — so it fits however your DBA has set things up: + +```yaml +# Using SID +offline_store: + type: oracle + host: oracle-db.example.com + port: 1521 + user: feast_user + password: ${DB_PASSWORD} + sid: ORCL + +# Using full DSN +offline_store: + type: oracle + host: oracle-db.example.com + port: 1521 + user: feast_user + password: ${DB_PASSWORD} + dsn: "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=oracle-db.example.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))" +``` + +### Step 2: Define features backed by Oracle tables + +```python +from feast import FeatureView, Field, Entity +from feast.types import Float64, Int64 +from feast.infra.offline_stores.contrib.oracle_offline_store.oracle_source import OracleSource +from datetime import timedelta + +customer = Entity(name="customer_id", join_keys=["customer_id"]) + +customer_transactions = OracleSource( + name="customer_txn_source", + table_ref="ANALYTICS.CUSTOMER_TRANSACTIONS", + event_timestamp_column="TXN_TIMESTAMP", +) + +customer_features = FeatureView( + name="customer_transaction_features", + entities=[customer], + ttl=timedelta(days=30), + schema=[ + Field(name="avg_txn_amount_30d", dtype=Float64), + Field(name="txn_count_7d", dtype=Int64), + Field(name="max_txn_amount_90d", dtype=Float64), + ], + source=customer_transactions, +) +``` + +### Step 3: Retrieve features for training + +```python +from feast import FeatureStore +import pandas as pd + +store = FeatureStore(repo_path=".") + +entity_df = pd.DataFrame({ + "customer_id": [101, 102, 103, 104], + "event_timestamp": pd.to_datetime(["2026-01-15", "2026-01-16", "2026-02-01", "2026-02-15"]), +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "customer_transaction_features:avg_txn_amount_30d", + "customer_transaction_features:txn_count_7d", + "customer_transaction_features:max_txn_amount_90d", + ], +).to_df() +``` + +Feast performs point-in-time correct joins against Oracle — no future data leaks into your training set, and the query runs *inside* Oracle, not in some external compute engine. + +### Step 4: Serve features in production + +```python +store.materialize_incremental(end_date=datetime.utcnow()) + +features = store.get_online_features( + features=[ + "customer_transaction_features:avg_txn_amount_30d", + "customer_transaction_features:txn_count_7d", + ], + entity_rows=[{"customer_id": 101}], +).to_dict() +``` + +--- + +## Kubernetes-Native: The Feast Operator and Oracle + +For teams running Feast on Kubernetes, the Feast operator now natively manages Oracle-backed feature stores through the `FeatureStore` custom resource. + +### Create a Secret with your Oracle credentials + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: oracle-offline-store +type: Opaque +stringData: + oracle: | + host: oracle-db.example.com + port: "1521" + user: feast_user + password: changeme + service_name: ORCL +``` + +### Define a FeatureStore custom resource + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: production-feature-store +spec: + services: + offlineStore: + persistence: + store: + type: oracle + secretRef: + name: oracle-offline-store +``` + +### Apply and let the operator do the rest + +```bash +kubectl apply -f feature-store.yaml +``` + +The operator validates the configuration against the CRD schema (rejecting invalid types at admission), reads the Secret, merges the Oracle connection parameters into the generated Feast config, and deploys the offline store service. Credential rotation, version upgrades, and config changes are all handled through Kubernetes-native reconciliation — the same operational model your platform team already knows. + +Because credentials live in Kubernetes Secrets, they integrate naturally with external secret managers like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault through standard Kubernetes mechanisms. Oracle credentials never appear in plain text in your manifests or CI/CD logs. + +--- + +## Real-World Use Cases + +### Financial Services: Fraud Detection Without Data Movement + +A global bank running Oracle for core banking can now build fraud detection features — transaction velocity, merchant category patterns, geographic anomaly scores — directly from their existing Oracle tables. The features stay within the same security perimeter, audit trail, and encryption boundary as the source data. No ETL pipeline to a secondary warehouse means no replication lag and no additional attack surface. + +### Healthcare: Predictive Models on Regulated Data + +Hospitals and insurers with patient data in Oracle can compute ML features (readmission risk scores, treatment outcome signals, resource utilization patterns) without copying PHI into a less governed system. Feast's feature definitions become the documented lineage trail that compliance teams need. + +### Telecommunications: Network Optimization at Scale + +Telcos managing billions of CDRs and network metrics in Oracle can build churn prediction, capacity forecasting, and service quality features on top of the data they already have — avoiding the cost and latency of replicating to a separate analytical platform. + +### Retail: Demand Forecasting from Point-of-Sale Data + +Retailers with Oracle-backed inventory and transaction systems can build demand forecasting and recommendation features without standing up a parallel data infrastructure. Features computed in Oracle can be materialized to the online store for real-time serving at the edge. + +--- + +## Under the Hood: Built on ibis + +The Oracle offline store is built on the [ibis framework](https://ibis-project.org/), a portable Python dataframe API that compiles to native SQL for each backend. This means: + +- **Queries execute inside Oracle** — ibis translates Feast's retrieval operations into Oracle SQL, pushing computation to where the data lives +- **No intermediate data movement** — results are streamed back as Arrow tables without staging in a temporary system +- **Full Oracle type fidelity** — the type mapping covers the complete spectrum of Oracle data types, including `NUMBER`, `VARCHAR2`, `NVARCHAR2`, `CHAR`, `CLOB`, `NCLOB`, `BLOB`, `RAW`, `BINARY_FLOAT`, `BINARY_DOUBLE`, `DATE`, `TIMESTAMP`, `INTEGER`, `SMALLINT`, and `FLOAT` +- **Automatic DATE-to-TIMESTAMP casting** — Oracle's `DATE` type (which includes time components, unlike SQL standard) is properly handled + +--- + +## Getting Started + +Install Feast with Oracle support: + +```bash +pip install 'feast[oracle]' +``` + +For Kubernetes deployments, ensure you're running Feast operator v0.61.0+ with the v1 API. + +The full configuration reference, functionality matrix, and data source documentation are available in the Feast docs: + +- [Oracle Offline Store Reference](https://docs.feast.dev/reference/offline-stores/oracle) +- [Oracle Data Source Reference](https://docs.feast.dev/reference/data-sources/oracle) +- [Feast Operator Documentation](https://docs.feast.dev/) + +--- + +*Get started with the [Feast documentation](https://docs.feast.dev/) and join the community on [GitHub](https://github.com/feast-dev/feast) and [Slack](https://feastopensource.slack.com/). We'd love to hear how you're using Feast with Oracle.* diff --git a/infra/website/docs/blog/feast-ray-llm-posttrain.md b/infra/website/docs/blog/feast-ray-llm-posttrain.md new file mode 100644 index 00000000000..4b5eac30214 --- /dev/null +++ b/infra/website/docs/blog/feast-ray-llm-posttrain.md @@ -0,0 +1,304 @@ +--- +title: "How to Use Feast for SLM/LLM Post-Training with Ray" +description: "Keep conversation features in Feast, retrieve them for training, then stream into your trainer with Ray." +date: 2026-07-14 +authors: ["Chaitanya Patel"] +--- + +# How to Use Feast for SLM/LLM Post-Training with Ray + +Your support bot answers a lot of tickets. It’s fine—but it sounds generic. The team wants a smaller model that talks more like *your* agents: your refund wording, your product names, your tone. + +So someone says: **fine-tune on our real chats.** + +That part sounds easy. The messy part is the data—exports, notebook cleaning, and prompt formatting scattered across training scripts. + +This post walks through the [ray-llm-posttrain example](https://github.com/feast-dev/feast/tree/master/examples/ray-llm-posttrain): + +1. Put conversation features in Feast +2. Retrieve them with `get_historical_features` (entity-less date range) +3. Get rows into your trainer — stream with Ray **or** materialize with `.to_df()` + +You bring your own trainer. GPT-2 in the script is optional smoke only. + +## What’s in the example + +| Name | Type | What it holds | +|---|---|---| +| `web_documents` | [FeatureView](https://docs.feast.dev/getting-started/concepts/feature-view) | `human`, `bot`, `human_repeat_ratio`, `bot_repeat_ratio` | +| `train_example` | [OnDemandFeatureView](https://docs.feast.dev/reference/beta-on-demand-feature-view) | `cleaned_human`, `cleaned_bot`, `char_count`, `is_trainable`, `sft_text` | +| `llm_posttrain` | FeatureService | Bundles `web_documents` + `train_example` | + +Full definitions live in [feature_definitions.py](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/feature_repo/feature_definitions.py). Ray is the [offline store](https://docs.feast.dev/reference/offline-stores/ray) and one way to stream rows out—not a separate feature catalog. + +This example stays on **supported Feast APIs only** (no core patches). Conversation rows already include `document_id` and `event_timestamp` before Feast reads them. + +## Step 1: Point Feast at conversation data + +### Ray offline store (local) + +From the example [feature_store.yaml](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/feature_repo/feature_store.yaml). Cap Ray resources on a laptop—see [Ray offline store: resource management](https://docs.feast.dev/reference/offline-stores/ray#important-resource-management): + +```yaml +project: ray_llm_posttrain +registry: data/registry.db +provider: local + +offline_store: + type: ray + storage_path: data/ray_storage + enable_ray_logging: false + ray_conf: + num_cpus: 2 + object_store_memory: 104857600 + _memory: 524288000 + +batch_engine: + type: ray.engine + max_workers: 2 + +online_store: + type: sqlite + path: data/online_store.db + +entity_key_serialization_version: 3 +auth: + type: no_auth +``` + +You can also start from the built-in template: + +```bash +feast init -t ray my_ray_project +``` + +See the [Ray template / offline store docs](https://docs.feast.dev/reference/offline-stores/ray#quick-start-with-ray-template) and the related blog [Scaling ML with Feast and Ray](/blog/feast-ray-distributed-processing). + +### Demo seed: prepare parquet, then `RaySource` + +[RaySource](https://docs.feast.dev/reference/data-sources/ray) tells Feast how to load data through Ray. Hugging Face is only used in a **prepare script**—not as a live Feast source that invents timestamps at retrieval time. + +`nampdn-ai/tiny-webtext` has no `document_id` / `event_timestamp`. Entity-less retrieval needs those columns on the source. We add them **outside Feast**, write parquet, then point Feast at that file (supported path): + +```bash +PYTHONPATH=../../sdk/python python scripts/prepare_data.py +# → feature_repo/data/tiny_webtext.parquet +``` + +```python +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +tiny_web = RaySource( + name="tiny_webtext", + reader_type="parquet", + path="data/tiny_webtext.parquet", + timestamp_field="event_timestamp", +) +``` + +In production you’d skip the HF prepare step and register your real conversation store (warehouse / lake / parquet) that already has join keys and timestamps. + +More reader types are in the [Ray data source reference](https://docs.feast.dev/reference/data-sources/ray#supported-reader_type-values). + +### Feature view + +```python +web_documents = FeatureView( + name="web_documents", + entities=[document], + ttl=timedelta(days=365), + schema=[ + Field(name="human", dtype=String), + Field(name="bot", dtype=String), + Field(name="human_repeat_ratio", dtype=Float64), + Field(name="bot_repeat_ratio", dtype=Float64), + ], + source=tiny_web, + online=False, +) +``` + +### Optional: OnDemandFeatureView for derived training features + +If you want Feast to own `sft_text` / quality gates (same idea as in the [ODFV docs](https://docs.feast.dev/reference/beta-on-demand-feature-view)): + +```python +@on_demand_feature_view( + sources=[web_documents], + schema=[ + Field(name="cleaned_human", dtype=String), + Field(name="cleaned_bot", dtype=String), + Field(name="char_count", dtype=Int64), + Field(name="is_trainable", dtype=Bool), + Field(name="sft_text", dtype=String), + ], + mode="pandas", +) +def train_example(inputs): + cleaned_human = inputs["human"].fillna("").astype(str).str.strip() + cleaned_bot = inputs["bot"].fillna("").astype(str).str.strip() + # ... length + repeat-ratio gate ... + sft_text = ( + "<|im_start|>user\n" + cleaned_human + "<|im_end|>\n" + "<|im_start|>assistant\n" + cleaned_bot + "<|im_end|>" + ) + return pd.DataFrame({...}) +``` + +```python +llm_posttrain = FeatureService( + name="llm_posttrain", + features=[web_documents, train_example], +) +``` + +Apply: + +```bash +cd examples/ray-llm-posttrain/feature_repo +feast apply +``` + +## Step 2: Retrieve for training (entity-less) + +No `entity_df`—just a date window. That pattern is covered in [Historical Features Without Entity IDs](/blog/entity-less-historical-features-retrieval) and the [FAQ](https://docs.feast.dev/getting-started/faq#how-do-i-run-get_historical_features-without-providing-an-entity-dataframe): + +```python +from datetime import datetime, timezone +from feast import FeatureStore + +store = FeatureStore(repo_path="feature_repo") + +job = store.get_historical_features( + features=[ + "web_documents:human", + "web_documents:bot", + "web_documents:human_repeat_ratio", + "web_documents:bot_repeat_ratio", + ], + start_date=datetime(2024, 6, 1, tzinfo=timezone.utc), + end_date=datetime(2024, 7, 1, tzinfo=timezone.utc), +) +``` + +Then choose how you turn that job into training rows. + +## Step 3: Two ways into the trainer + +| Path | ODFV runs? | When to use | +|---|---|---| +| `job.to_ray_dataset()` then preprocess | **No** | Stream FeatureView columns; shape `sft_text` yourself | +| `job.to_df()` / `to_arrow()` | **Yes** | Want `train_example` outputs from Feast | + +Pick **Option A** when you want full control over text formatting or need custom preprocessing (e.g., multi-turn chat templates, tokenization-aware truncation). Pick **Option B** when you want Feast to enforce quality gates consistently across training and serving. + +### Option A — Stream with Ray, preprocess yourself + +`to_ray_dataset()` returns a Ray Dataset of retrieved FeatureView columns. It does **not** apply OnDemandFeatureViews. Build training text with Ray `map_batches` (as in [train_sft.py](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/scripts/train_sft.py)): + +```python +ds = job.to_ray_dataset() + +def preprocess_sft(batch): + import pandas as pd + + if not isinstance(batch, pd.DataFrame): + batch = pd.DataFrame(batch) + human = batch["human"].fillna("").astype(str).str.strip() + bot = batch["bot"].fillna("").astype(str).str.strip() + ok = bot.str.len() >= 64 + sft_text = ( + "<|im_start|>user\n" + human + "<|im_end|>\n" + "<|im_start|>assistant\n" + bot + "<|im_end|>" + ) + return pd.DataFrame({"sft_text": sft_text}).loc[ok].reset_index(drop=True) + +train_ds = ds.map_batches(preprocess_sft, batch_format="pandas") +# → hand train_ds to your SLM/LLM trainer +``` + +Run the example default path: + +```bash +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run +``` + +### Option B — Use the ODFV, then train + +Materialize with `.to_df()` so `train_example` runs (same retrieval/serving idea as in the [ODFV overview](https://docs.feast.dev/reference/beta-on-demand-feature-view#why-use-on-demand-feature-views)): + +```python +df = store.get_historical_features( + features=store.get_feature_service("llm_posttrain"), + start_date=datetime(2024, 6, 1, tzinfo=timezone.utc), + end_date=datetime(2024, 7, 1, tzinfo=timezone.utc), +).to_df() + +trainable = df[df["is_trainable"] & df["sft_text"].astype(str).str.len().gt(0)] +# trainable["sft_text"] → your trainer +# or: import ray; ray.data.from_pandas(trainable[["sft_text"]]) +``` + +```bash +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df +``` + +### The same ODFV at serving time + +The `train_example` ODFV runs identically during online serving — the quality gate and formatting logic stay in one place: + +```python +# At inference time, the same ODFV runs on the fly +features = store.get_online_features( + features=["train_example:sft_text", "train_example:is_trainable"], + entity_rows=[{"document_id": "doc_42"}], +).to_dict() +# features["sft_text"], features["is_trainable"] — same logic as training +``` + +## Try the full example + +```bash +cd examples/ray-llm-posttrain +uv pip install -e "../../sdk/python[ray]" -r requirements.txt +PYTHONPATH=../../sdk/python python scripts/prepare_data.py +cd feature_repo && feast apply && cd .. + +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df + +# optional GPT-2 smoke +PYTHONPATH=../../sdk/python python scripts/train_sft.py --max-steps 20 +``` + +Details: [ray-llm-posttrain README](https://github.com/feast-dev/feast/tree/master/examples/ray-llm-posttrain). + +## Takeaways + +1. **Keep conversation features in Feast** — this example’s `web_documents`. +2. **Stream with Ray** — `to_ray_dataset()`, then preprocess training text yourself. +3. **Want ODFVs** — `.to_df()` / `.to_arrow()` to materialize, then train. +4. **Bring your own trainer** — GPT-2 in the example is optional. + +## References + +**This example** + +- [ray-llm-posttrain example](https://github.com/feast-dev/feast/tree/master/examples/ray-llm-posttrain) +- [feature_definitions.py](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/feature_repo/feature_definitions.py) +- [train_sft.py](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/scripts/train_sft.py) + +**Docs** + +- [Ray offline store](https://docs.feast.dev/reference/offline-stores/ray) +- [Ray data source](https://docs.feast.dev/reference/data-sources/ray) +- [Ray compute engine](https://docs.feast.dev/reference/compute-engine/ray) +- [On demand feature views](https://docs.feast.dev/reference/beta-on-demand-feature-view) +- [Feature retrieval](https://docs.feast.dev/getting-started/concepts/feature-retrieval) +- [FAQ: historical features without entity dataframe](https://docs.feast.dev/getting-started/faq#how-do-i-run-get_historical_features-without-providing-an-entity-dataframe) + +**Related blogs & tutorials** + +- [Historical Features Without Entity IDs](/blog/entity-less-historical-features-retrieval) +- [Scaling ML with Feast and Ray](/blog/feast-ray-distributed-processing) +- [Validating historical features](https://docs.feast.dev/tutorials/validating-historical-features) diff --git a/infra/website/docs/blog/feast-unity-catalog-integration.md b/infra/website/docs/blog/feast-unity-catalog-integration.md new file mode 100644 index 00000000000..1ee977a71eb --- /dev/null +++ b/infra/website/docs/blog/feast-unity-catalog-integration.md @@ -0,0 +1,342 @@ +--- +title: "Feast Gets Native Apache Iceberg Support" +description: "Feast now reads features from any Iceberg catalog — REST, SQL, Hive, Glue, DynamoDB. Connect to Unity Catalog, Apache Polaris, Nessie, or your own PyIceberg catalog. Full support for get_historical_features, materialize, and online serving." +date: 2026-07-18 +authors: ["Nikhil Kathole"] +--- + +# Feast Gets Native Apache Iceberg Support + +Apache Iceberg has become the open table format. Your data lake is probably already on it — whether through Databricks, Snowflake, AWS, or self-managed infrastructure. But until now, connecting Feast to Iceberg tables meant either going through Spark (heavyweight, slow to start) or copying data into Feast-managed Parquet files (data duplication, governance gap). + +Feast now ships a native `IcebergSource` that reads directly from any Iceberg catalog. No data copies. Your feature tables live where they already live — in your Iceberg catalog — and Feast reads from them via PyIceberg. With the DuckDB offline store, you don't even need a Spark cluster — reads happen entirely in-process. + +## Why This Matters + +Before this, the path from "data in Iceberg" to "features in Feast" looked like this: + +1. Data engineers build Iceberg tables in their catalog (UC, Glue, Hive) +2. ML engineers copy data to Feast-managed Parquet files, or configure a SparkSource that couples them to a specific compute engine +3. Two copies of the data. Two metadata systems. No connection between them. + +Now the path is: + +1. Data engineers build Iceberg tables in their catalog +2. ML engineers point `IcebergSource` at the table +3. Done. Feast reads directly from the catalog via PyIceberg. One copy. One source of truth. Choose DuckDB for lightweight local reads or Spark when you need distributed compute — the data source definition stays the same either way. + +## What You Get + +### Any Iceberg Catalog + +`IcebergSource` supports every catalog backend that PyIceberg supports: + +| `catalog_type` | Backend | Example Use Case | +|---|---|---| +| `"rest"` | Iceberg REST Catalog | Databricks Unity Catalog, Apache Polaris, Project Nessie, Snowflake Open Catalog | +| `"sql"` | SQL-backed catalog | Local dev with SQLite, CI/CD, PostgreSQL-backed catalogs | +| `"hive"` | Hive Metastore | On-premise Hadoop, EMR | +| `"glue"` | AWS Glue Data Catalog | AWS-native lakehouse | +| `"dynamodb"` | DynamoDB catalog | Serverless AWS | + +### Both Offline Stores + +| Operation | DuckDB | Spark | +|---|---|---| +| `feast apply` | Yes | Yes | +| `get_historical_features` | Yes | Yes | +| `materialize` / `materialize-incremental` | Yes | Yes | +| `get_online_features` | Yes | Yes | + +Both offline stores use PyIceberg for the actual Iceberg table scan — the difference is what happens after. DuckDB processes the Arrow table in-process (no JVM, no cluster), making it ideal for local development and moderate-scale workloads. Spark is there when you need distributed compute over large datasets. The same `IcebergSource` definition works with either offline store — just change `offline_store.type` in your YAML. + +### Full Iceberg Semantics + +Every read goes through PyIceberg's `table.scan().to_arrow()`. This means you get proper Iceberg semantics: schema evolution, partition pruning, and snapshot isolation — not just raw Parquet file reads. + +## Quick Start + +### Install + +```bash +pip install "feast[iceberg]" +``` + +### Define a Source + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import IcebergSource + +driver_stats = IcebergSource( + warehouse="my_catalog", + namespace="ml_features", + table="driver_hourly_stats", + catalog_type="rest", + endpoint="https://my-iceberg-catalog.example.com", + token_env_var="CATALOG_TOKEN", + timestamp_field="event_timestamp", +) +``` + +### Use It + +```python +from datetime import timedelta +from feast import Entity, FeatureView, Field +from feast.types import Float64, Int64 + +driver = Entity(name="driver", join_keys=["driver_id"]) + +driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=365), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + Field(name="acc_rate", dtype=Float64), + Field(name="avg_daily_trips", dtype=Int64), + ], + source=driver_stats, + online=True, +) +``` + +```yaml +# feature_store.yaml +project: my_project +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db +offline_store: + type: duckdb +``` + +```bash +feast apply +``` + +Then use it like any other Feast source: + +```python +from feast import FeatureStore +import pandas as pd +from datetime import datetime, timezone + +store = FeatureStore(repo_path=".") + +# Training data +training_df = store.get_historical_features( + entity_df=pd.DataFrame({ + "driver_id": [1001, 1002, 1003], + "event_timestamp": [datetime(2026, 7, 1, tzinfo=timezone.utc)] * 3, + }), + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], +).to_df() + +# Materialize to online store +store.materialize_incremental(end_date=datetime.now(tz=timezone.utc)) + +# Online serving +online = store.get_online_features( + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + entity_rows=[{"driver_id": 1001}], +).to_dict() +``` + +## Catalog Examples + +### AWS Glue + +```python +glue_source = IcebergSource( + warehouse="my_glue_database", + namespace="ml_features", + table="driver_stats", + catalog_type="glue", + catalog_properties={"region_name": "us-east-1"}, + timestamp_field="event_timestamp", +) +``` + +### Hive Metastore + +```python +hive_source = IcebergSource( + endpoint="thrift://hive-metastore:9083", + warehouse="warehouse", + namespace="features", + table="driver_stats", + catalog_type="hive", + timestamp_field="event_timestamp", +) +``` + +### Apache Polaris / Nessie + +```python +polaris_source = IcebergSource( + endpoint="https://polaris.example.com", + warehouse="my_catalog", + namespace="ml", + table="features", + catalog_type="rest", + token_env_var="POLARIS_TOKEN", + timestamp_field="event_timestamp", +) +``` + +### Local Development (SQLite-backed) + +For development and CI/CD, use a local PyIceberg SQL catalog — no external service required: + +```python +local_source = IcebergSource( + warehouse="dev_warehouse", + namespace="default", + table="driver_stats", + catalog_type="sql", + catalog_name="dev_catalog", + catalog_properties={ + "uri": "sqlite:////tmp/iceberg_catalog.db", + "warehouse": "file:///tmp/iceberg_warehouse", + }, + timestamp_field="event_timestamp", +) +``` + +## Use Case: Unity Catalog Integration + +Unity Catalog users get everything above with simpler configuration. `UnityCatalogSource` extends `IcebergSource` with UC-specific defaults: + +- Default connection via `DATABRICKS_HOST` and `DATABRICKS_TOKEN` environment variables — no manual endpoint or token setup +- Three-level naming (`warehouse.namespace.table`) maps directly to UC's catalog/schema/table hierarchy + +### Databricks Setup + +```bash +export DATABRICKS_HOST="https://your-workspace.cloud.databricks.com" +export DATABRICKS_TOKEN="dapi_your_token_here" +``` + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import UnityCatalogSource + +driver_stats_source = UnityCatalogSource( + warehouse="ml_catalog", + namespace="driver_features", + table="driver_hourly_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created", + description="Hourly aggregated driver statistics", +) +``` + +That's it. No endpoint or token parameters needed — they come from the environment variables. + +### Optional: Governance Metadata Sync + +If you want `feast apply` to annotate your UC tables with `feast.*` properties (project name, feature view, primary keys, owner), you can use the `UnityCatalogProvider`: + +```yaml +# feature_store.yaml +provider: unity_catalog +``` + +This is optional. For most users, `provider: local` is sufficient. Reads, materialization, and online serving work the same regardless of which provider you use. + +### OSS Unity Catalog + +The integration also works with the open-source Unity Catalog, with some differences: + +| Capability | Databricks UC | OSS UC | +|---|---|---| +| Read via Iceberg REST | Built-in | Requires `uniform_iceberg_metadata_location` in H2 DB | +| Read via SQL catalog | Yes | Yes | +| Credential vending | Yes | Not available | + +For OSS UC, set `credential_vending=False` and `token_env_var=None`: + +```python +source = UnityCatalogSource( + warehouse="unity", + namespace="default", + table="driver_hourly_stats", + endpoint="http://localhost:8080/api/2.1/unity-catalog/iceberg", + token_env_var=None, + credential_vending=False, + catalog_type="sql", # recommended for OSS UC + catalog_name="my_catalog", + catalog_properties={ + "uri": "sqlite:////tmp/pyiceberg_catalog.db", + "warehouse": "file:///tmp/warehouse", + }, + timestamp_field="event_timestamp", + register_as_feature_table=False, +) +``` + +## How It Works + +### Read Path + +All data reads go through PyIceberg, regardless of catalog type: + +``` +IcebergSource.get_catalog_client() + → PyIceberg catalog (REST / SQL / Hive / Glue / DynamoDB) + → table.scan().to_arrow() + → Arrow table + → DuckDB ibis memtable or Spark DataFrame +``` + +If the catalog is misconfigured, you get a clear error — consistent with how every other Feast data source works. + +### Catalog Name Isolation + +Each source has a `catalog_name` parameter (default: `"feast_iceberg"`). This is the instance name PyIceberg uses when loading the catalog. If you have multiple sources pointing at different catalogs, use different names to avoid collisions: + +```python +production = IcebergSource(catalog_name="prod_catalog", ...) +staging = IcebergSource(catalog_name="staging_catalog", ...) +``` + +## What's Not Supported + +- **Write-back to Iceberg/UC tables.** Feast reads from existing tables; it doesn't write feature data back. The `write_to_offline_store` API only supports `FileSource` (DuckDB) and `SparkSource` (Spark). Your data engineering pipelines create and populate the tables. +- **Table creation.** Tables must exist before Feast can read from them. `feast apply` registers the feature view in Feast's registry, not the table in the catalog. + +## Configuration Reference + +### IcebergSource + +| Parameter | Default | Description | +|---|---|---| +| `warehouse` | *required* | Catalog or warehouse name | +| `namespace` | *required* | Schema or namespace | +| `table` | *required* | Table name | +| `catalog_type` | `"rest"` | Backend: `"rest"`, `"sql"`, `"hive"`, `"glue"`, `"dynamodb"` | +| `catalog_name` | `"feast_iceberg"` | PyIceberg instance name (unique per catalog to avoid collisions) | +| `endpoint` | `None` | Catalog endpoint URL | +| `catalog_properties` | `{}` | Additional catalog config (e.g., `{"uri": "sqlite:///..."}`) | +| `token_env_var` | `None` | Env var containing auth token | +| `credential_vending` | `True` | Request scoped storage credentials | +| `timestamp_field` | `None` | Event timestamp column | +| `created_timestamp_column` | `None` | Creation timestamp for deduplication | + +### UnityCatalogSource (extends IcebergSource) + +All `IcebergSource` parameters plus: + +| Parameter | Default | Description | +|---|---|---| +| `endpoint` | From `DATABRICKS_HOST` | Defaults to `{DATABRICKS_HOST}/api/2.1/unity-catalog/iceberg` | +| `token_env_var` | `"DATABRICKS_TOKEN"` | Defaults to Databricks token env var | +| `register_as_feature_table` | `True` | Sync `feast.*` properties to UC on `feast apply` | +| `sync_lineage` | `True` | Record lineage in UC (Databricks only) | + +--- + +*Native Iceberg support is available in Feast 0.64+. Install with `pip install "feast[iceberg]"` and check the [Iceberg data source documentation](/reference/data-sources/iceberg) for the full API reference.* diff --git a/infra/website/docs/blog/feature-view-versioning.md b/infra/website/docs/blog/feature-view-versioning.md new file mode 100644 index 00000000000..574dac54cc1 --- /dev/null +++ b/infra/website/docs/blog/feature-view-versioning.md @@ -0,0 +1,244 @@ +--- +title: Feast Introduces Experimental Feature View Versioning +description: Feast now supports experimental feature view versioning — bringing automatic version tracking, safe rollback, and multi-version online serving to your feature store. Only supported for SQLite today; we're inviting the community to test and give feedback. +date: 2026-03-31 +authors: ["Francisco Javier Arceo"] +--- + +
+ Feast Feature Versioning +
+ +# Feast Introduces Experimental Feature View Versioning 🚀 + +We are excited to announce the experimental release of **Feature View Versioning** in Feast — a [long-requested capability](https://github.com/feast-dev/feast/issues/2728) that brings automatic version tracking, safe rollback, and multi-version online serving to your feature store. + +This feature is still **experimental** and we would love to hear your feedback. Try it out and let us know what works, what doesn't, and which online stores you'd like to see supported next. + +## Why Feature Versioning Matters + +Serving data in production AI applications is one of the hardest problems in ML engineering. The Feast community is built on practitioners who run these high-stakes pipelines every day — where **point-in-time correctness** is not just a nice-to-have but a hard requirement for model integrity. A feature value seen by the wrong model at the wrong time can silently corrupt predictions and, in high stakes scenarios, impact critical business applications. This is the most important motivating factor behind our versioning work. + +Feature versioning solves two distinct but equally real problems: + +### Case 1: Fixing Forward in Production (the critical path) + +A live feature view powering a production model needs to be updated — perhaps a critical bug in a transformation logic, a renamed column, or a changed data source. In an ideal world you'd cut over to a brand-new feature view and update every downstream consumer atomically. In practice that's rarely viable: models are already deployed, consumers are already reading that name, and a rename causes an immediate outage. + +With feature versioning you can overwrite the existing feature view definition while retaining the complete history as a recoverable snapshot. If the change turns out to break something, you can roll back to the previous version. You always have an audit trail: who changed what, when, and what the feature looked like at every prior point in time. + +This case is the hardest to get right. When your feature is live in production, correctness is non-negotiable. The materialized data that feeds real-time predictions must stay consistent with the schema that was active when it was written — otherwise you risk serving stale rows with the wrong columns to models that expect the new schema, or vice versa. Our per-version table design (see below) exists precisely to prevent that class of failure. + +### Case 2: Offline Experimentation Before Production + +During active feature development, multiple data scientists may be building and evaluating competing versions of the same feature simultaneously — different transformations, different data sources, different schemas. Today there is no first-class way to manage this in Feast without creating separate feature views with different names and hoping teams don't collide. + +With versioning, teams can stage a new feature version using `--no-promote`, test it in isolation, and only promote it to the active (default) definition once it has been validated. The rest of the system sees no change until the promotion happens. + +### The Persistent Pain Points + +Both cases expose the same underlying gaps that exist today: + +1. **No audit trail.** Teams struggle to answer "what did this feature view look like last week?" or "when was this schema changed and by whom?" +2. **No safe rollback.** If a schema change breaks a downstream model, the only recourse is to manually reconstruct the old definition — often from memory or version control history. +3. **No multi-version serving.** During a migration, Model A might rely on the old feature schema while Model B needs the new one. Without versioning, serving both simultaneously requires duplicating the entire feature view under a different name. + +## How It Works + +Version tracking is fully automatic. You don't need to change anything about how you write or apply feature views: + +```bash +feast apply # First apply → saved as v0 +# ... edit schema ... +feast apply # Detects change → saved as v1 +feast apply # No change detected → still v1 (idempotent) +# ... edit source or UDF ... +feast apply # Detects change → saved as v2 +``` + +Every time `feast apply` detects a real change to a feature view's schema or transformation logic, it automatically saves a versioned snapshot to the registry. Metadata-only changes (description, tags, TTL) are updated in place without creating a new version. + +### What Gets Versioned + +- Changes to the feature schema (adding, removing, or renaming fields) +- Changes to batch or stream data sources +- Changes to on-demand feature view UDFs +- Any other structural change that affects data layout or derivation + +### What Does Not Trigger a New Version + +- Tag or description updates +- TTL changes +- Re-applying an identical definition (idempotent behavior) + +## Exploring Version History + +You can list all recorded versions of a feature view from the CLI: + +```bash +feast feature-views list-versions driver_stats +``` + +``` +VERSION TYPE CREATED VERSION_ID +v0 feature_view 2026-01-15 10:30:00 a1b2c3d4-... +v1 feature_view 2026-01-16 14:22:00 e5f6g7h8-... +v2 feature_view 2026-01-20 09:15:00 i9j0k1l2-... +``` + +Or programmatically: + +```python +store = FeatureStore(repo_path=".") +versions = store.list_feature_view_versions("driver_stats") +for v in versions: + print(f"{v['version']} created at {v['created_timestamp']}") +``` + +## Multi-Version Online Serving + +When `enable_online_feature_view_versioning: true` is set in your `feature_store.yaml`, you can read features from a specific version using the `@v` syntax: + +```yaml +registry: + path: data/registry.db + enable_online_feature_view_versioning: true +``` + +```python +online_features = store.get_online_features( + features=[ + "driver_stats:trips_today", # latest version (default) + "driver_stats@v1:trips_today", # read from v1 + "driver_stats@v2:avg_rating", # read from v2 + ], + entity_rows=[{"driver_id": 1001}], +) +``` + +Multiple versions can be queried in a single call, making gradual migrations straightforward: keep serving the old version to existing consumers while routing new consumers to the latest. **By default, unversioned requests always resolve to the latest promoted version** — opting into a specific version requires the explicit `@v` syntax. + +### Online Store Table Naming + +Each version owns its own isolated online table (see [The Challenges of Correctness](#the-challenges-of-correctness-in-feature-versioning) below for why this design is necessary for point-in-time correctness): + +- v0 continues to use the existing, unversioned table (e.g., `project_driver_stats`) — fully backward compatible +- v1 and later use suffixed tables (e.g., `project_driver_stats_v1`, `project_driver_stats_v2`) + +Each version requires its own materialization: + +```bash +feast materialize --views driver_stats --version v2 +``` + +## Safe Rollback + +You can pin a feature view to a specific historical version by setting the `version` parameter. `feast apply` will replace the active definition with the stored snapshot: + +```python +# Revert to v1 — restores schema, source, and transformations from the v1 snapshot +driver_stats = FeatureView( + name="driver_stats", + entities=[driver], + schema=[...], + source=my_source, + version="v1", +) +``` + +After running `feast apply`, the active feature view will match the v1 snapshot exactly. Remove the `version` parameter (or set it to `"latest"`) to resume auto-incrementing behavior. + +## Staged Publishing with `--no-promote` + +For breaking schema changes, you may want to publish a new version without immediately making it the default for unversioned consumers. Use the `--no-promote` flag: + +```bash +feast apply --no-promote +``` + +This saves the version snapshot without updating the active definition. Unversioned consumers (`driver_stats:trips_today`) continue reading from the previous version, while opted-in consumers can start using `driver_stats@v2:trips_today` right away. When you're ready to make the new schema the default, run `feast apply` without the flag. + +## The Challenges of Correctness in Feature Versioning + +Feature versioning might sound simple in principle, but **getting it right is surprisingly hard**, especially for materialization. This is particularly acute for Case 1 above — production systems where the consistency of data matters and the cost of a mistake is high. + +### Why We Can't Share a Single Online Table + +The naive approach to versioning would be to keep a single online table and tag rows with a version column. We explicitly rejected this design. + +Each version of a feature view may have a completely different schema — different columns, different types, different derivation logic. A single shared table would require the union of all column schemas across all versions, leading to sparse rows, broken type contracts, and materialization jobs that cannot safely run in parallel. More fundamentally, it makes **point-in-time correctness impossible**: a model trained against v1 of a feature must retrieve v1 rows, not v2 rows that happen to occupy the same table. + +Instead, we give each version its own online table: + +- v0 continues to use the existing, unversioned table (e.g., `project_driver_stats`) — fully backward compatible +- v1 and later use suffixed tables (e.g., `project_driver_stats_v1`, `project_driver_stats_v2`) + +This is more operationally complex — more tables to manage, more materialization jobs to schedule — but it is the **only design that guarantees correctness**. Each version's materialized data is isolated, independently freshed, and independently queryable. A buggy v2 materialization cannot corrupt v1 data. + +### The Core Tension + +Each version of a feature view may have a completely different schema, source, or transformation. This means: + +- Each version needs its own online store table with the right columns +- Materializing one version must not corrupt data in another version's table +- Version-qualified online reads must resolve the snapshot at the right point in time before looking up the online table +- The active (promoted) version and historical snapshots must stay consistent + +### Materialization Complexity + +Today, running `feast materialize` without specifying a version fills the **active** version's table. To populate a historical version's table, you must explicitly pass `--version v` so Feast can reconstruct the schema from the saved snapshot and target the correct online table. + +This matters for correctness: if you apply v2 of a feature view (which drops a column) and then run an unversioned `feast materialize`, the v1 online table is not automatically backfilled or maintained. Teams need to think carefully about which versions they want to keep materialized and for how long. + +### What This Means for Clients + +The multi-table design does introduce a responsibility shift toward the client. By default, unversioned feature requests (`driver_stats:trips_today`) resolve to the **latest promoted version** — no change to existing consumers. But clients that need to pin to a specific version must opt in explicitly using the `@v` syntax (`driver_stats@v1:trips_today`). + +This is an intentional design choice. Automatic version following would hide schema changes from consumers that may not be ready for them. Explicit version pinning keeps the contract between producers and consumers clear and auditable — each consumer controls exactly which version of a feature it reads. + +### Tradeoffs + +| Concern | Tradeoff | +|---|---| +| Storage cost | Each active version requires its own online table — storage scales with the number of versions kept live | +| Operational complexity | Teams must manage materialization schedules per version | +| Consistency windows | Because each version has its own materialization job, two versions of the same feature for the same entity may have different freshness | +| Concurrency | Two simultaneous `feast apply` calls can race on version number assignment — the registry backends use optimistic locking to handle this, but teams should be aware | + +These tradeoffs are real and we're still refining the model based on community experience. We've tried to make the defaults safe (versioning is opt-in for online reads, backward compatible for unversioned access) while giving teams the controls they need. + +## Current Limitations + +This is an experimental feature and there are known gaps: + +- **Online store support** — Version-qualified reads (`@v`) are **SQLite-only** today. We plan to add Redis, DynamoDB, Bigtable, Postgres, and others based on community demand. If you need a specific store, [comment or upvote on the appropriate the child GitHub issue of the main GitHub issue](https://github.com/feast-dev/feast/issues/2728) and let us know. +- **Offline store versioning** — Versioned historical retrieval is not yet supported. +- **Version deletion** — There is no mechanism today to prune old versions from the registry. +- **Feature services** — Feature services always resolve to the active (promoted) version. `--no-promote` versions are not accessible through feature services until promoted. + +## Supported Feature View Types + +Versioning works across all three feature view types: + +- `FeatureView` (and `BatchFeatureView`) +- `StreamFeatureView` +- `OnDemandFeatureView` + +## Getting Started + +1. Upgrade to the latest version of Feast. +2. Add `enable_online_feature_view_versioning: true` to your registry config in `feature_store.yaml` (only needed for versioned online reads). +3. Run `feast apply` as usual — version history tracking starts automatically. +4. Explore your version history with `feast feature-views list-versions `. + +For full details, see the [Feature View Versioning documentation](https://docs.feast.dev/reference/alpha-feature-view-versioning). + +## Share Your Feedback + +We want to hear from you! Try out feature view versioning and tell us: + +- Which online stores you need supported next +- Which workflows feel awkward or incomplete +- How the materialization model fits your real-world pipelines + +Join the conversation on [GitHub](https://github.com/feast-dev/feast/issues) or in the [Feast Slack community](https://slack.feast.dev/). Your feedback directly shapes what we build next. diff --git a/infra/website/docs/blog/mongodb-feast-integration.md b/infra/website/docs/blog/mongodb-feast-integration.md new file mode 100644 index 00000000000..8a9ea4c255b --- /dev/null +++ b/infra/website/docs/blog/mongodb-feast-integration.md @@ -0,0 +1,210 @@ +--- +title: "Native MongoDB Support in Feast: One Database for Operational Data, Features, and Vectors" +description: Feast now ships first-class support for MongoDB as both an online and an offline store, plus native Vector Search for embedding-based retrieval. Machine Learning teams running on MongoDB can serve features at low latency, generate point-in-time-correct training datasets, and power RAG or recommender workloads, all from a single MongoDB Atlas cluster, with no separate cache, no separate warehouse, and no parallel vector database to keep in sync. +date: 2026-05-07 +authors: ["Rishabh Bisht"] +--- + + +
+MongoDB Feast Stores +
+ + +## The three-database problem in production ML + +A typical Feast deployment runs three different databases: + +1. The **application's primary database** where the operational data that features are derived from actually lives. +2. A dedicated **online store** used to serve features at low latency to live models. +3. A **separate warehouse** used as the offline store for training-set generation and historical retrieval. + +That's three sets of credentials, three security postures, three monitoring stacks, and a constant feedback loop of "the feature is in the warehouse but stale in the online store" or "we materialized last night but the model is reading yesterday's values." + +For teams whose operational data already lives in MongoDB, this was especially painful. Until now, Feast had no native MongoDB option so teams either stood up parallel infrastructure they didn't want, or settled for community plugins of varying maturity. + +With this release, both types of the feature store run on MongoDB - same connection string, same auth, same backups, same observability. The features sit next to the operational data they were derived from. + +## What's in the integration + +Three components ship together as generally available: + +### 1. MongoDBOnlineStore - low-latency feature serving + +Available in Feast `v0.61.0` and above. Built on the official PyMongo driver, with both sync and native async paths (the async implementation uses PyMongo's `AsyncMongoClient`). It supports `online_write_batch`, `online_read`, and their async equivalents. + +Features from multiple feature views for the same entity are colocated in a single MongoDB collection keyed by the serialized entity key, so a read for an entity is a single primary-key lookup, not a fan-out across collections. + +### 2. MongoDBOfflineStore - historical retrieval and training-set generation + +Available in `v0.63.0` and above. Uses the MongoDB aggregation framework for retrieval, with `pandas.merge_asof` for the point-in-time join when entities repeat across timestamps. Ships with `MongoDBSource` (the `DataSource` class), `offline_write_batch` for ingest, and `persist` to write joined results to Parquet for downstream training pipelines. + +### 3. MongoDB Vector Search - embeddings as first-class features + +When you set `vector_enabled: true` on the online store, Feast automatically creates and manages MongoDB vector search indexes on any `FeatureView` field marked with `vector_index=True`. The `retrieve_online_documents_v2()` method runs a `$vectorSearch` aggregation under the hood and returns nearest-neighbor results as `(event_ts, entity_key, feature_dict)` tuples with a similarity score - with `top_k` limiting and configurable distance metrics (`cosine`, `dot product`, `euclidean`). + +The result: a team running RAG, recommenders, or agent workloads can store, serve, and similarity-search feature embeddings in the same Atlas cluster as their other features — with no separate vector database to bolt on. + +## **Quick start** + +### Install + +```shell +pip install 'feast[mongodb]' +``` + +### Configure your `feature_store.yaml` + +Point both the online and offline store at the same Atlas cluster. No separate Atlas feature flag or opt-in required. + +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local + +online_store: + type: mongodb + connection_string: "mongodb+srv://:@.mongodb.net" + database: "feast_online" + +offline_store: + type: mongodb + connection_string: "mongodb+srv://:@.mongodb.net" + database: "feast_offline" + +entity_key_serialization_version: 3 +``` + +### Define a feature view backed by `MongoDBSource` + +```py +from datetime import timedelta +from feast import Entity, FeatureView, Field +from feast.types import Float32, Int64 +from feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb_source import ( + MongoDBSource, +) + +driver = Entity(name="driver", join_keys=["driver_id"]) + +driver_stats_source = MongoDBSource( + name="driver_stats_source", + database="feast_offline", + collection="driver_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=7), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, +) +``` + +### Apply, materialize, and serve + +```shell +feast apply +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +```py +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +features = store.get_online_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}], +).to_dict() +``` + +That's it. Same connection string, same auth model, same cluster - features in, features out. + +## RAG and embeddings: vector search in the same cluster + +If you're building a RAG pipeline, a recommender, or an agent that needs nearest-neighbor lookup over feature embeddings, the online store doubles as a vector store when `vector_enabled` is set: + +```yaml +online_store: + type: mongodb + connection_string: "mongodb+srv://:@.mongodb.net" + database: "feast_online" + vector_enabled: true + vector_index_wait_timeout: 60 + vector_index_wait_poll_interval: 2 +``` + +Mark the embedding field on your `FeatureView`: + +```py +from feast import FeatureView, Field +from feast.types import Array, Float32, Int64, String, UnixTimestamp + +document_embeddings = FeatureView( + name="embedded_documents", + entities=[item], + schema=[ + Field( + name="vector", + dtype=Array(Float32), + vector_index=True, # ← enable vector index + vector_search_metric="COSINE", # cosine | dot product | euclidean + ), + Field(name="item_id", dtype=Int64), + Field(name="sentence_chunks", dtype=String), + Field(name="event_timestamp", dtype=UnixTimestamp), + ], + source=rag_documents_source, +) +``` + +When you run `feast apply`, Feast creates the corresponding Atlas vector search index. When the feature view is removed, the index is dropped. The `vector_index_wait_timeout` and `vector_index_wait_poll_interval` settings control how long Feast waits for newly created Atlas Search indexes to become queryable before returning. + +Querying nearest neighbors is then one call: + +```py +results = store.retrieve_online_documents_v2( + features=[ + "embedded_documents:vector", + "embedded_documents:item_id", + "embedded_documents:sentence_chunks", + ], + query=query_embedding, # list[float] of the same dim + top_k=5, + distance_metric="COSINE", +).to_df() +``` + +Under the hood, this becomes a `$vectorSearch` aggregation against your Atlas cluster - no second system to provision, no vector data to keep in sync with the rest of your features. + +## Why this matters + +A few reasons we think this lands in the right place for ML teams already on MongoDB: + +* **One database for training and inference.** The same Atlas cluster powers historical retrieval, materialization, and online serving. No ETL pipelines pushing features from a warehouse. Update a feature once, see it everywhere. +* **One security and compliance posture.** Atlas networking, IAM, encryption, and audit logging cover both halves of the feature store. Architects don't have to add a new database vendor and a new threat model to say yes to ML. +* **Vector and operational data colocated.** For RAG, recommenders, and agents, the embeddings live next to the entity data they describe. Filter your vector search on operational fields with the same query language you already use. +* **Flexible schema where it helps.** Feature engineering is iterative. MongoDB's document model means adding a field to a feature view doesn't require a schema migration on day one. +* **Async serving when you need it.** The online store ships a native async path on `AsyncMongoClient`, so feature lookups don't block the rest of your serving stack. + +## Where to next + +* **Online store reference:** [Feast docs - MongoDB online store](https://docs.feast.dev/master/reference/online-stores/mongodb) +* **Offline store reference:** [Feast docs - MongoDB offline store](https://docs.feast.dev/master/reference/offline-stores/mongodb) +* **Vector search:** [Feast docs - Vector Search](https://docs.feast.dev/master/reference/data-sources/mongodb#vector-search) +* **Tutorial:** [Integrate MongoDB with Feast](https://www.mongodb.com/docs/atlas/ai-integrations/feast/) + +If you're already on MongoDB and want to standardize your ML stack on a single backend, this is the time to try it. Spin up a feature repo, point both stores at your cluster, and let us know how it goes - issues and PRs welcome on GitHub. \ No newline at end of file diff --git a/infra/website/docs/blog/scaling-feast-feature-server.md b/infra/website/docs/blog/scaling-feast-feature-server.md index 4406d280c81..994811ea0ac 100644 --- a/infra/website/docs/blog/scaling-feast-feature-server.md +++ b/infra/website/docs/blog/scaling-feast-feature-server.md @@ -1,11 +1,11 @@ --- -title: Scaling the Feast Feature Server on Kubernetes -description: The Feast Operator now supports horizontal scaling with static replicas, HPA autoscaling, and external autoscalers like KEDA — enabling production-grade, high-availability feature serving. -date: 2026-02-21 -authors: ["Nikhil Kathole"] +title: Feature Server High-Availability and Auto-Scaling on Kubernetes +description: The Feast Operator now supports horizontal scaling with static replicas, HPA autoscaling, KEDA, and high-availability features including PodDisruptionBudgets and topology spread constraints. +date: 2026-03-02 +authors: ["Nikhil Kathole", "Antonin Stefanutti"] --- -# Scaling the Feast Feature Server on Kubernetes +# Feature Server High-Availability and Auto-Scaling on Kubernetes As ML systems move from experimentation to production, the feature server often becomes a critical bottleneck. A single-replica deployment might handle development traffic, but production workloads — real-time inference, batch scoring, multiple consuming services — demand the ability to scale horizontally. @@ -79,6 +79,8 @@ spec: target: type: Utilization averageUtilization: 70 + podDisruptionBudgets: + maxUnavailable: 1 onlineStore: persistence: store: @@ -102,7 +104,7 @@ spec: name: feast-data-stores ``` -The operator creates the HPA as an owned resource — it's automatically cleaned up if you remove the autoscaling configuration or delete the FeatureStore CR. If no custom metrics are specified, the operator defaults to **80% CPU utilization**. +The operator creates the HPA as an owned resource — it's automatically cleaned up if you remove the autoscaling configuration or delete the FeatureStore CR. If no custom metrics are specified, the operator defaults to **80% CPU utilization**. The operator also auto-injects soft pod anti-affinity (node-level) and topology spread constraints (zone-level) to improve resilience — see the [High Availability](#high-availability) section for details. ## 3. External Autoscalers (KEDA, Custom HPAs) @@ -154,6 +156,62 @@ spec: When KEDA scales up `spec.replicas` via the scale sub-resource, the CRD's CEL validation rules automatically ensure DB-backed persistence is configured. The operator also automatically switches the deployment strategy to `RollingUpdate` when `replicas > 1`. This gives you the full power of KEDA's 50+ event-driven triggers with built-in safety checks. +# High Availability + +Scaling to multiple replicas is only half the story — you also need to ensure pods are spread across failure domains and protected during disruptions. The operator includes two HA features that activate when scaling is enabled: + +## Pod Anti-Affinity + +When scaling is enabled, the operator **automatically injects** a soft pod anti-affinity rule that prefers spreading pods across different nodes: + +```yaml +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + feast.dev/name: my-feast +``` + +This means the scheduler will *try* to place each replica on a separate node, but won't prevent scheduling if nodes are constrained. You can override this with your own `affinity` configuration in the CR, or set it to an explicit value to customize the behavior (e.g. `requiredDuringSchedulingIgnoredDuringExecution` for strict anti-affinity). + +## Topology Spread Constraints + +When `replicas > 1` or autoscaling is configured, the operator **automatically injects** a soft zone-spread constraint: + +```yaml +topologySpreadConstraints: +- maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + feast.dev/name: my-feast +``` + +This distributes pods across availability zones on a best-effort basis. If your cluster has 3 zones and 3 replicas, each zone gets one pod. If zones are unavailable, pods still get scheduled rather than staying pending. + +You can override this with explicit constraints (e.g. strict `DoNotSchedule`) or disable it entirely by setting `topologySpreadConstraints: []`. + +## PodDisruptionBudgets + +For protection during voluntary disruptions (node drains, cluster upgrades), you can configure a PDB: + +```yaml +spec: + replicas: 3 + services: + podDisruptionBudgets: + maxUnavailable: 1 + onlineStore: + # ... +``` + +The PDB requires explicit configuration — it's not auto-injected because a misconfigured PDB can block node drains. The operator enforces that exactly one of `minAvailable` or `maxUnavailable` is set via CEL validation. The PDB is only created when scaling is enabled and is automatically cleaned up when scaling is disabled. + # Safety First: Persistence Validation Not all persistence backends are safe for multi-replica deployments. File-based stores like SQLite, DuckDB, and local `registry.db` use single-writer file locks that don't work across pods. @@ -188,6 +246,8 @@ The implementation adds three key behaviors to the operator's reconciliation loo **3. HPA lifecycle** — The operator creates, updates, and deletes the HPA as an owned resource tied to the FeatureStore CR. Removing the `autoscaling` configuration automatically cleans up the HPA. +**4. HA features** — The operator auto-injects soft topology spread constraints across zones when scaling is enabled, and manages PodDisruptionBudgets as owned resources when explicitly configured. + The scaling status is reported back on the FeatureStore status: ```yaml @@ -209,17 +269,22 @@ Scaling is designed to work seamlessly with existing operator features: **1. Ensure DB-backed persistence** for all enabled services (online store, offline store, registry). -**2. Configure scaling** in your FeatureStore CR — use either static replicas or HPA (mutually exclusive): +**2. Configure scaling** in your FeatureStore CR — use either static replicas or HPA (mutually exclusive). Optionally add a PDB for disruption protection: ```yaml spec: replicas: 3 # static replicas (top-level) + services: + podDisruptionBudgets: # optional: protect against disruptions + maxUnavailable: 1 # -- OR -- # services: # scaling: # autoscaling: # HPA # minReplicas: 2 # maxReplicas: 10 + # podDisruptionBudgets: + # maxUnavailable: 1 ``` **3. Apply** the updated CR: diff --git a/infra/website/public/images/blog/end_to_end_lineage.png b/infra/website/public/images/blog/end_to_end_lineage.png new file mode 100644 index 00000000000..8d49cd2ec74 Binary files /dev/null and b/infra/website/public/images/blog/end_to_end_lineage.png differ diff --git a/infra/website/public/images/blog/entity_dataframe.png b/infra/website/public/images/blog/entity_dataframe.png new file mode 100644 index 00000000000..5588c9260d8 Binary files /dev/null and b/infra/website/public/images/blog/entity_dataframe.png differ diff --git a/infra/website/public/images/blog/feast-dqm-monitoring-hero.png b/infra/website/public/images/blog/feast-dqm-monitoring-hero.png new file mode 100644 index 00000000000..86d7125a3c3 Binary files /dev/null and b/infra/website/public/images/blog/feast-dqm-monitoring-hero.png differ diff --git a/infra/website/public/images/blog/feast-dqm-ui-all-features.png b/infra/website/public/images/blog/feast-dqm-ui-all-features.png new file mode 100644 index 00000000000..4e728a2e86e Binary files /dev/null and b/infra/website/public/images/blog/feast-dqm-ui-all-features.png differ diff --git a/infra/website/public/images/blog/feast-dqm-ui-categorical-feature.png b/infra/website/public/images/blog/feast-dqm-ui-categorical-feature.png new file mode 100644 index 00000000000..a3e6f22b74c Binary files /dev/null and b/infra/website/public/images/blog/feast-dqm-ui-categorical-feature.png differ diff --git a/infra/website/public/images/blog/feast-dqm-ui-numeric-feature.png b/infra/website/public/images/blog/feast-dqm-ui-numeric-feature.png new file mode 100644 index 00000000000..0b5e7f0d23d Binary files /dev/null and b/infra/website/public/images/blog/feast-dqm-ui-numeric-feature.png differ diff --git a/infra/website/public/images/blog/feast-feature-versioning-hero.png b/infra/website/public/images/blog/feast-feature-versioning-hero.png new file mode 100644 index 00000000000..d40d71a16dd Binary files /dev/null and b/infra/website/public/images/blog/feast-feature-versioning-hero.png differ diff --git a/infra/website/public/images/blog/feast-features-ui.png b/infra/website/public/images/blog/feast-features-ui.png new file mode 100644 index 00000000000..2b728e4c43a Binary files /dev/null and b/infra/website/public/images/blog/feast-features-ui.png differ diff --git a/infra/website/public/images/blog/feast-mcp-agent-workflow-prod.png b/infra/website/public/images/blog/feast-mcp-agent-workflow-prod.png new file mode 100644 index 00000000000..5cf94df580b Binary files /dev/null and b/infra/website/public/images/blog/feast-mcp-agent-workflow-prod.png differ diff --git a/infra/website/public/images/blog/feast-mcp-agent-workflow.png b/infra/website/public/images/blog/feast-mcp-agent-workflow.png new file mode 100644 index 00000000000..87504642e39 Binary files /dev/null and b/infra/website/public/images/blog/feast-mcp-agent-workflow.png differ diff --git a/infra/website/public/images/blog/feast-mcp-agent.png b/infra/website/public/images/blog/feast-mcp-agent.png new file mode 100644 index 00000000000..191e97517fe Binary files /dev/null and b/infra/website/public/images/blog/feast-mcp-agent.png differ diff --git a/infra/website/public/images/blog/feast-metrics-dashboard-overview.png b/infra/website/public/images/blog/feast-metrics-dashboard-overview.png new file mode 100644 index 00000000000..21293e8499e Binary files /dev/null and b/infra/website/public/images/blog/feast-metrics-dashboard-overview.png differ diff --git a/infra/website/public/images/blog/feast-metrics-hero.png b/infra/website/public/images/blog/feast-metrics-hero.png new file mode 100644 index 00000000000..b7c11e13e8d Binary files /dev/null and b/infra/website/public/images/blog/feast-metrics-hero.png differ diff --git a/infra/website/public/images/blog/feast-metrics-odfv-latency.png b/infra/website/public/images/blog/feast-metrics-odfv-latency.png new file mode 100644 index 00000000000..d6d33c0e5db Binary files /dev/null and b/infra/website/public/images/blog/feast-metrics-odfv-latency.png differ diff --git a/infra/website/public/images/blog/feast-mlflow-kubeflow.png b/infra/website/public/images/blog/feast-mlflow-kubeflow.png new file mode 100644 index 00000000000..c4e92228189 Binary files /dev/null and b/infra/website/public/images/blog/feast-mlflow-kubeflow.png differ diff --git a/infra/website/public/images/blog/feast-mlflow-native-integration.png b/infra/website/public/images/blog/feast-mlflow-native-integration.png new file mode 100644 index 00000000000..83df6a2c990 Binary files /dev/null and b/infra/website/public/images/blog/feast-mlflow-native-integration.png differ diff --git a/infra/website/public/images/blog/feast-openai-compat-flow.png b/infra/website/public/images/blog/feast-openai-compat-flow.png new file mode 100644 index 00000000000..c7dcac8a5ed Binary files /dev/null and b/infra/website/public/images/blog/feast-openai-compat-flow.png differ diff --git a/infra/website/public/images/blog/feast-oracle-offline-store.png b/infra/website/public/images/blog/feast-oracle-offline-store.png new file mode 100644 index 00000000000..425c7fda0cc Binary files /dev/null and b/infra/website/public/images/blog/feast-oracle-offline-store.png differ diff --git a/infra/website/public/images/blog/lineage_till_training.png b/infra/website/public/images/blog/lineage_till_training.png new file mode 100644 index 00000000000..a91adf8bf0e Binary files /dev/null and b/infra/website/public/images/blog/lineage_till_training.png differ diff --git a/infra/website/public/images/blog/mlflow-feast-feature-selection-metrics.png b/infra/website/public/images/blog/mlflow-feast-feature-selection-metrics.png new file mode 100644 index 00000000000..b6ad16c3346 Binary files /dev/null and b/infra/website/public/images/blog/mlflow-feast-feature-selection-metrics.png differ diff --git a/infra/website/public/images/blog/mlflow-feature-selection-comparison.png b/infra/website/public/images/blog/mlflow-feature-selection-comparison.png new file mode 100644 index 00000000000..4cad26065c6 Binary files /dev/null and b/infra/website/public/images/blog/mlflow-feature-selection-comparison.png differ diff --git a/infra/website/public/images/blog/mlflow-feature-selection-run.png b/infra/website/public/images/blog/mlflow-feature-selection-run.png new file mode 100644 index 00000000000..aabc6055480 Binary files /dev/null and b/infra/website/public/images/blog/mlflow-feature-selection-run.png differ diff --git a/infra/website/public/images/blog/mlflow-feature-selection-ui.png b/infra/website/public/images/blog/mlflow-feature-selection-ui.png new file mode 100644 index 00000000000..a598bbf4151 Binary files /dev/null and b/infra/website/public/images/blog/mlflow-feature-selection-ui.png differ diff --git a/infra/website/public/images/blog/mlflow_dashboard.png b/infra/website/public/images/blog/mlflow_dashboard.png new file mode 100644 index 00000000000..1d9896a0880 Binary files /dev/null and b/infra/website/public/images/blog/mlflow_dashboard.png differ diff --git a/infra/website/public/images/blog/mlflow_featurelist.png b/infra/website/public/images/blog/mlflow_featurelist.png new file mode 100644 index 00000000000..23920c179ff Binary files /dev/null and b/infra/website/public/images/blog/mlflow_featurelist.png differ diff --git a/infra/website/public/images/blog/model_metadata.png b/infra/website/public/images/blog/model_metadata.png new file mode 100644 index 00000000000..7ef300d9b2b Binary files /dev/null and b/infra/website/public/images/blog/model_metadata.png differ diff --git a/infra/website/public/images/blog/mongodb-feature-stores.png b/infra/website/public/images/blog/mongodb-feature-stores.png new file mode 100644 index 00000000000..c0705834dc9 Binary files /dev/null and b/infra/website/public/images/blog/mongodb-feature-stores.png differ diff --git a/infra/website/public/images/blog/offline_store_operational_metrics.png b/infra/website/public/images/blog/offline_store_operational_metrics.png new file mode 100644 index 00000000000..12006128d81 Binary files /dev/null and b/infra/website/public/images/blog/offline_store_operational_metrics.png differ diff --git a/infra/website/public/images/blog/operation.png b/infra/website/public/images/blog/operation.png new file mode 100644 index 00000000000..43b6500faee Binary files /dev/null and b/infra/website/public/images/blog/operation.png differ diff --git a/infra/website/public/images/blog/registered_model_with_feature_service.png b/infra/website/public/images/blog/registered_model_with_feature_service.png new file mode 100644 index 00000000000..1195d4c3cbb Binary files /dev/null and b/infra/website/public/images/blog/registered_model_with_feature_service.png differ diff --git a/infra/website/public/images/blog/sox_compliance_and_access.png b/infra/website/public/images/blog/sox_compliance_and_access.png new file mode 100644 index 00000000000..0236e5b5cdc Binary files /dev/null and b/infra/website/public/images/blog/sox_compliance_and_access.png differ diff --git a/infra/website/public/images/blog/sox_offline_store_audit_logs.png b/infra/website/public/images/blog/sox_offline_store_audit_logs.png new file mode 100644 index 00000000000..fa7be01ebcc Binary files /dev/null and b/infra/website/public/images/blog/sox_offline_store_audit_logs.png differ diff --git a/infra/website/src/components/Navigation.astro b/infra/website/src/components/Navigation.astro index a5987bf6348..143fcbc047f 100644 --- a/infra/website/src/components/Navigation.astro +++ b/infra/website/src/components/Navigation.astro @@ -14,11 +14,29 @@ COMMUNITY - +
@@ -38,7 +56,7 @@ top: 0; left: 0; right: 0; - background-color: white; + background-color: var(--color-nav-bg); z-index: 1000; } @@ -80,6 +98,35 @@ margin-left: 32px; } + .nav-right { + display: flex; + align-items: center; + gap: 4px; + padding-right: var(--content-padding); + } + + .theme-toggle { + background: none; + border: none; + padding: 8px; + cursor: pointer; + color: var(--color-text); + display: flex; + align-items: center; + justify-content: center; + opacity: 0.7; + transition: opacity 0.2s ease; + } + + .theme-toggle:hover { + opacity: 1; + } + + .icon-sun { display: none; } + .icon-moon { display: block; } + :global([data-theme="dark"]) .icon-sun { display: block; } + :global([data-theme="dark"]) .icon-moon { display: none; } + .mobile-menu-button { display: block; background: none; @@ -87,7 +134,6 @@ padding: 8px; cursor: pointer; color: var(--color-text); - margin-right: var(--content-padding); } @media (min-width: 1024px) { @@ -95,7 +141,7 @@ display: flex; align-items: center; } - + .mobile-menu-button { display: none; } @@ -104,9 +150,9 @@ .mobile-menu { display: none; width: 100%; - background: white; + background: var(--color-nav-bg); padding: 16px 0; - border-top: 1px solid #eee; + border-top: 1px solid var(--color-border); position: absolute; top: 52px; left: 0; @@ -122,17 +168,27 @@ } .mobile-menu a:hover { - background-color: #f5f5f5; + background-color: var(--color-nav-hover); } \ No newline at end of file diff --git a/infra/website/src/layouts/BaseLayout.astro b/infra/website/src/layouts/BaseLayout.astro index d73c7b0ebde..96e30a6aee2 100644 --- a/infra/website/src/layouts/BaseLayout.astro +++ b/infra/website/src/layouts/BaseLayout.astro @@ -4,9 +4,17 @@ import '../styles/global.css'; interface Props { title: string; description?: string; + socialImage?: string; + ogUrl?: string; } -const { title, description = "Feast is an end-to-end open source feature store for machine learning. It allows teams to define, manage, discover, and serve features." } = Astro.props; +const defaultSocialImage = "https://feast.dev/wp-content/uploads/2023/01/feast-og@2x.png"; +const { + title, + description = "Feast is an end-to-end open source feature store for machine learning. It allows teams to define, manage, discover, and serve features.", + socialImage = defaultSocialImage, + ogUrl = "https://feast.dev/", +} = Astro.props; --- @@ -25,16 +33,16 @@ const { title, description = "Feast is an end-to-end open source feature store f - + - + - + @@ -44,11 +52,21 @@ const { title, description = "Feast is an end-to-end open source feature store f - + +
- \ No newline at end of file + diff --git a/infra/website/src/layouts/BlogLayout.astro b/infra/website/src/layouts/BlogLayout.astro index c5f8379fc80..9fe68a9daa7 100644 --- a/infra/website/src/layouts/BlogLayout.astro +++ b/infra/website/src/layouts/BlogLayout.astro @@ -14,7 +14,7 @@ const { frontmatter } = Astro.props;

{frontmatter.title}

{frontmatter.date && ( -
)} @@ -207,6 +256,40 @@ const CustomNode = ({ data }: { data: NodeData }) => { )} + {/* Version indicator */} + {hasVersion && ( + +
Version: {data.versionNumber}
+ {data.versionInfo && ( + <> +
Total versions: {data.versionInfo.totalVersions}
+ {data.versionInfo.latestDescription && ( +
Latest: {data.versionInfo.latestDescription}
+ )} + + )} +
+ Click node for full history +
+ + } + > +
+ v{data.versionNumber} +
+
+ )} + { @@ -414,8 +502,11 @@ const Legend = () => { const types = [ { type: FEAST_FCO_TYPES.featureService, label: "Feature Service" }, { type: FEAST_FCO_TYPES.featureView, label: "Feature View" }, + { type: FEAST_FCO_TYPES.labelView, label: "Label View" }, { type: FEAST_FCO_TYPES.entity, label: "Entity" }, { type: FEAST_FCO_TYPES.dataSource, label: "Data Source" }, + { type: FEAST_FCO_TYPES.mlflowRun, label: "MLflow Run" }, + { type: FEAST_FCO_TYPES.mlflowModel, label: "Registered Model" }, ]; const isDarkMode = colorMode === "dark"; @@ -474,6 +565,20 @@ const Legend = () => {
{item.label}
))} +
+ + vN + +
Version Changed
+
); }; @@ -482,10 +587,43 @@ const registryToFlow = ( objects: feast.core.Registry, relationships: EntityRelation[], permissions?: any[], + versionHistory?: feast.core.IFeatureViewVersionRecord[], + mlflowRuns?: MlflowRunData[], ) => { const nodes: Node[] = []; const edges: Edge[] = []; + // Build a lookup of version info by feature view name + const versionInfoMap = new Map< + string, + { totalVersions: number; latestDescription?: string } + >(); + if (versionHistory) { + const grouped: Record = {}; + for (let i = 0; i < versionHistory.length; i++) { + const record = versionHistory[i]; + const name = record.featureViewName; + if (!name) continue; + if (!grouped[name]) grouped[name] = []; + grouped[name].push(record); + } + const groupedNames = Object.keys(grouped); + for (let i = 0; i < groupedNames.length; i++) { + const name = groupedNames[i]; + const records = grouped[name]; + records.sort( + ( + a: feast.core.IFeatureViewVersionRecord, + b: feast.core.IFeatureViewVersionRecord, + ) => (b.versionNumber ?? 0) - (a.versionNumber ?? 0), + ); + versionInfoMap.set(name, { + totalVersions: records.length, + latestDescription: records[0]?.description ?? undefined, + }); + } + } + objects.featureServices?.forEach((fs) => { nodes.push({ id: `fs-${fs.spec?.name}`, @@ -507,60 +645,69 @@ const registryToFlow = ( }); objects.featureViews?.forEach((fv) => { + const fvName = fv.spec?.name; nodes.push({ - id: `fv-${fv.spec?.name}`, + id: `fv-${fvName}`, type: "custom", data: { - label: fv.spec?.name, + label: fvName, type: FEAST_FCO_TYPES.featureView, metadata: fv, permissions: permissions ? getEntityPermissions( permissions, FEAST_FCO_TYPES.featureView, - fv.spec?.name, + fvName, ) : [], + versionNumber: fv.meta?.currentVersionNumber ?? undefined, + versionInfo: fvName ? versionInfoMap.get(fvName) : undefined, }, position: { x: 0, y: 0 }, }); }); objects.onDemandFeatureViews?.forEach((odfv) => { + const odfvName = odfv.spec?.name; nodes.push({ - id: `odfv-${odfv.spec?.name}`, + id: `odfv-${odfvName}`, type: "custom", data: { - label: odfv.spec?.name, + label: odfvName, type: FEAST_FCO_TYPES.featureView, metadata: odfv, permissions: permissions ? getEntityPermissions( permissions, FEAST_FCO_TYPES.featureView, - odfv.spec?.name, + odfvName, ) : [], + versionNumber: odfv.meta?.currentVersionNumber ?? undefined, + versionInfo: odfvName ? versionInfoMap.get(odfvName) : undefined, }, position: { x: 0, y: 0 }, }); }); objects.streamFeatureViews?.forEach((sfv) => { + const sfvName = sfv.spec?.name; nodes.push({ - id: `sfv-${sfv.spec?.name}`, + id: `sfv-${sfvName}`, type: "custom", data: { - label: sfv.spec?.name, + label: sfvName, type: FEAST_FCO_TYPES.featureView, metadata: sfv, permissions: permissions ? getEntityPermissions( permissions, FEAST_FCO_TYPES.featureView, - sfv.spec?.name, + sfvName, ) : [], + versionNumber: sfv.meta?.currentVersionNumber ?? undefined, + versionInfo: sfvName ? versionInfoMap.get(sfvName) : undefined, }, position: { x: 0, y: 0 }, }); @@ -586,6 +733,25 @@ const registryToFlow = ( }); }); + objects.labelViews?.forEach((lv: any) => { + const lvName = lv.spec?.name; + nodes.push({ + id: `lv-${lvName}`, + type: "custom", + data: { + label: lvName, + type: FEAST_FCO_TYPES.labelView, + metadata: lv, + permissions: permissions + ? getEntityPermissions(permissions, FEAST_FCO_TYPES.labelView, lvName) + : [], + versionNumber: lv.meta?.currentVersionNumber ?? undefined, + versionInfo: lvName ? versionInfoMap.get(lvName) : undefined, + }, + position: { x: 0, y: 0 }, + }); + }); + const dataSources = new Set(); objects.featureViews?.forEach((fv) => { @@ -603,6 +769,18 @@ const registryToFlow = ( } }); + (objects as any).labelViews?.forEach((lv: any) => { + if (lv.spec?.source?.name) { + dataSources.add(lv.spec.source.name); + } + if (lv.spec?.source?.batchSource?.name) { + dataSources.add(lv.spec.source.batchSource.name); + } + if (lv.spec?.batchSource?.name) { + dataSources.add(lv.spec.batchSource.name); + } + }); + Array.from(dataSources).forEach((dsName) => { nodes.push({ id: `ds-${dsName}`, @@ -650,6 +828,101 @@ const registryToFlow = ( }); }); + if (mlflowRuns && mlflowRuns.length > 0) { + mlflowRuns.forEach((run) => { + const runLabel = run.run_name || run.run_id.substring(0, 8); + nodes.push({ + id: `mlflow-${run.run_id}`, + type: "custom", + data: { + label: runLabel, + type: FEAST_FCO_TYPES.mlflowRun, + metadata: { + mlflow_url: run.mlflow_url, + retrieval_type: run.retrieval_type, + status: run.status, + run_id: run.run_id, + }, + }, + position: { x: 0, y: 0 }, + }); + + if (run.feature_service) { + const fsNodeId = `fs-${run.feature_service}`; + const fsNodeExists = nodes.some((n) => n.id === fsNodeId); + if (fsNodeExists) { + edges.push({ + id: `edge-mlflow-${run.run_id}`, + source: fsNodeId, + sourceHandle: "source", + target: `mlflow-${run.run_id}`, + targetHandle: "target", + animated: true, + style: { + strokeWidth: 3, + stroke: "#0194e2", + strokeDasharray: "10 5", + animation: "dataflow 2s linear infinite", + }, + type: "smoothstep", + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + color: "#0194e2", + }, + }); + } + } + + if (run.registered_models && run.registered_models.length > 0) { + run.registered_models.forEach((model) => { + const modelNodeId = `model-${model.model_name}-v${model.version}`; + const modelExists = nodes.some((n) => n.id === modelNodeId); + if (!modelExists) { + nodes.push({ + id: modelNodeId, + type: "custom", + data: { + label: `${model.model_name} v${model.version}`, + type: FEAST_FCO_TYPES.mlflowModel, + metadata: { + mlflow_url: model.mlflow_url, + model_name: model.model_name, + version: model.version, + stage: model.stage, + }, + }, + position: { x: 0, y: 0 }, + }); + } + + edges.push({ + id: `edge-model-${run.run_id}-${model.model_name}-v${model.version}`, + source: `mlflow-${run.run_id}`, + sourceHandle: "source", + target: modelNodeId, + targetHandle: "target", + animated: true, + style: { + strokeWidth: 3, + stroke: "#7b2d8e", + strokeDasharray: "10 5", + animation: "dataflow 2s linear infinite", + }, + type: "smoothstep", + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + color: "#7b2d8e", + }, + }); + }); + } + }); + } + return { nodes, edges }; }; @@ -663,6 +936,16 @@ const getNodePrefix = (type: FEAST_FCO_TYPES) => { return "entity"; case FEAST_FCO_TYPES.dataSource: return "ds"; + case FEAST_FCO_TYPES.labelView: + return "lv"; + case FEAST_FCO_TYPES.mlflowRun: + return "mlflow"; + case FEAST_FCO_TYPES.mlflowModel: + return "model"; + case FEAST_FCO_TYPES.openlineageJob: + return "ol-job"; + case FEAST_FCO_TYPES.openlineageDataset: + return "ol-ds"; default: return "unknown"; } @@ -673,7 +956,10 @@ interface RegistryVisualizationProps { relationships: EntityRelation[]; indirectRelationships: EntityRelation[]; filterNode?: { type: FEAST_FCO_TYPES; name: string }; - permissions?: any[]; // Add permissions field + permissions?: any[]; + mlflowRuns?: MlflowRunData[]; + extraCheckboxes?: React.ReactNode; + filterControls?: React.ReactNode; } const RegistryVisualization: React.FC = ({ @@ -682,6 +968,9 @@ const RegistryVisualization: React.FC = ({ indirectRelationships, filterNode, permissions, + mlflowRuns, + extraCheckboxes, + filterControls, }) => { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -750,10 +1039,15 @@ const RegistryVisualization: React.FC = ({ return rel.source && rel.target && rel.source.name && rel.target.name; }); + const versionRecords = + registryData.featureViewVersionHistory?.records ?? undefined; + const { nodes: initialNodes, edges: initialEdges } = registryToFlow( registryData, validRelationships, permissions, + versionRecords as feast.core.IFeatureViewVersionRecord[] | undefined, + mlflowRuns, ); const { nodes: layoutedNodes, edges: layoutedEdges } = @@ -775,6 +1069,8 @@ const RegistryVisualization: React.FC = ({ showIndirectRelationships, showIsolatedNodes, filterNode, + permissions, + mlflowRuns, setNodes, setEdges, ]); @@ -792,7 +1088,15 @@ const RegistryVisualization: React.FC = ({

Lineage

-
+
+ {extraCheckboxes}
+ {filterControls} {loading ? (
diff --git a/ui/src/components/RegistryVisualizationTab.tsx b/ui/src/components/RegistryVisualizationTab.tsx index ebc77604322..4fed3c4f856 100644 --- a/ui/src/components/RegistryVisualizationTab.tsx +++ b/ui/src/components/RegistryVisualizationTab.tsx @@ -10,18 +10,26 @@ import { EuiFlexItem, } from "@elastic/eui"; import useLoadRegistry from "../queries/useLoadRegistry"; +import useLoadMlflowRuns from "../queries/useLoadMlflowRuns"; import RegistryPathContext from "../contexts/RegistryPathContext"; import RegistryVisualization from "./RegistryVisualization"; import { FEAST_FCO_TYPES } from "../parsers/types"; import { filterPermissionsByAction } from "../utils/permissionUtils"; -const RegistryVisualizationTab = () => { +interface RegistryVisualizationTabProps { + feastOnlyCheckbox?: React.ReactNode; +} + +const RegistryVisualizationTab: React.FC = ({ + feastOnlyCheckbox, +}) => { const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); const { isLoading, isSuccess, isError, data } = useLoadRegistry( registryUrl, projectName, ); + const { data: mlflowData } = useLoadMlflowRuns(); const [selectedObjectType, setSelectedObjectType] = useState(""); const [selectedObjectName, setSelectedObjectName] = useState(""); const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); @@ -40,6 +48,13 @@ const RegistryVisualizationTab = () => { if (sfv.spec?.streamSource?.name) dataSources.add(sfv.spec.streamSource.name); }); + objects.labelViews?.forEach((lv: any) => { + if (lv.spec?.source?.name) dataSources.add(lv.spec.source.name); + if (lv.spec?.source?.batchSource?.name) + dataSources.add(lv.spec.source.batchSource.name); + if (lv.spec?.batchSource?.name) + dataSources.add(lv.spec.batchSource.name); + }); return Array.from(dataSources); case "entity": return objects.entities?.map((entity: any) => entity.spec?.name) || []; @@ -52,6 +67,8 @@ const RegistryVisualizationTab = () => { ...(objects.streamFeatureViews?.map((sfv: any) => sfv.spec?.name) || []), ]; + case "labelView": + return objects.labelViews?.map((lv: any) => lv.spec?.name) || []; case "featureService": return objects.featureServices?.map((fs: any) => fs.spec?.name) || []; default: @@ -82,66 +99,6 @@ const RegistryVisualizationTab = () => { {isSuccess && data && ( <> - - - - { - setSelectedObjectType(e.target.value); - setSelectedObjectName(""); // Reset name when type changes - }} - aria-label="Select object type" - /> - - - - - ({ - value: name, - text: name, - }), - ), - ]} - value={selectedObjectName} - onChange={(e) => setSelectedObjectName(e.target.value)} - aria-label="Select object" - disabled={selectedObjectType === ""} - /> - - - - - setSelectedPermissionAction(e.target.value)} - aria-label="Filter by permissions" - /> - - - { } : undefined } + mlflowRuns={mlflowData?.runs?.length ? mlflowData.runs : undefined} + extraCheckboxes={feastOnlyCheckbox} + filterControls={ + + + + { + setSelectedObjectType(e.target.value); + setSelectedObjectName(""); + }} + aria-label="Select object type" + /> + + + + + ({ + value: name, + text: name, + })), + ]} + value={selectedObjectName} + onChange={(e) => setSelectedObjectName(e.target.value)} + aria-label="Select object" + disabled={selectedObjectType === ""} + /> + + + + + + setSelectedPermissionAction(e.target.value) + } + aria-label="Filter by permissions" + /> + + + + } /> )} diff --git a/ui/src/components/forms/FeatureFieldEditor.tsx b/ui/src/components/forms/FeatureFieldEditor.tsx new file mode 100644 index 00000000000..be8b4f9f210 --- /dev/null +++ b/ui/src/components/forms/FeatureFieldEditor.tsx @@ -0,0 +1,163 @@ +import React from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiSelect, + EuiButtonEmpty, + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiSpacer, + EuiCallOut, +} from "@elastic/eui"; +import { VALUE_TYPE_OPTIONS } from "./ValueTypeSelect"; +import { feast } from "../../protos"; + +interface FeatureFieldEntry { + name: string; + valueType: string; + description: string; +} + +interface FeatureFieldEditorProps { + features: FeatureFieldEntry[]; + onChange: (features: FeatureFieldEntry[]) => void; + error?: string; +} + +const EMPTY_FEATURE: FeatureFieldEntry = { + name: "", + valueType: String(feast.types.ValueType.Enum.INT64), + description: "", +}; + +const FeatureFieldEditor: React.FC = ({ + features, + onChange, + error, +}) => { + const addFeature = () => { + onChange([...features, { ...EMPTY_FEATURE }]); + }; + + const removeFeature = (index: number) => { + onChange(features.filter((_, i) => i !== index)); + }; + + const updateFeature = ( + index: number, + field: keyof FeatureFieldEntry, + val: string, + ) => { + const updated = [...features]; + updated[index] = { ...updated[index], [field]: val }; + onChange(updated); + }; + + return ( + <> + + + + +

Features

+
+
+ + + Add feature + + +
+ + {error && ( + <> + + + + )} + + {features.length > 0 && ( + + + + Name + + + + + Type + + + + + Description + + + + + )} + + {features.map((feature, index) => ( + + + updateFeature(index, "name", e.target.value)} + compressed + /> + + + + updateFeature(index, "valueType", e.target.value) + } + compressed + /> + + + + updateFeature(index, "description", e.target.value) + } + compressed + /> + + + removeFeature(index)} + /> + + + ))} + + {features.length === 0 && ( + + No features added yet. Click "Add feature" above. + + )} + + ); +}; + +export default FeatureFieldEditor; +export type { FeatureFieldEntry }; diff --git a/ui/src/components/forms/FormModal.tsx b/ui/src/components/forms/FormModal.tsx new file mode 100644 index 00000000000..b205f9f35a2 --- /dev/null +++ b/ui/src/components/forms/FormModal.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, + EuiForm, +} from "@elastic/eui"; + +interface FormModalProps { + title: string; + submitLabel: string; + onClose: () => void; + onSubmit: () => void; + children: React.ReactNode; + width?: number; + isSubmitting?: boolean; +} + +const FormModal: React.FC = ({ + title, + submitLabel, + onClose, + onSubmit, + children, + width = 600, + isSubmitting = false, +}) => { + return ( + + + {title} + + + + {children} + + + + + Cancel + + + {submitLabel} + + + + ); +}; + +export default FormModal; diff --git a/ui/src/components/forms/NameDescriptionOwnerFields.tsx b/ui/src/components/forms/NameDescriptionOwnerFields.tsx new file mode 100644 index 00000000000..46d21c369ba --- /dev/null +++ b/ui/src/components/forms/NameDescriptionOwnerFields.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import { EuiFormRow, EuiFieldText, EuiTextArea } from "@elastic/eui"; + +interface NameDescriptionOwnerFieldsProps { + name: string; + description: string; + owner?: string; + onChangeName: (value: string) => void; + onChangeDescription: (value: string) => void; + onChangeOwner?: (value: string) => void; + nameDisabled?: boolean; + nameError?: string; + nameHelpText?: string; + namePlaceholder?: string; + descriptionPlaceholder?: string; +} + +const NameDescriptionOwnerFields: React.FC = ({ + name, + description, + owner, + onChangeName, + onChangeDescription, + onChangeOwner, + nameDisabled = false, + nameError, + nameHelpText, + namePlaceholder = "e.g. my_resource", + descriptionPlaceholder = "Describe this resource...", +}) => { + return ( + <> + + onChangeName(e.target.value)} + isInvalid={!!nameError} + disabled={nameDisabled} + placeholder={namePlaceholder} + /> + + + + onChangeDescription(e.target.value)} + placeholder={descriptionPlaceholder} + rows={2} + /> + + + {onChangeOwner !== undefined && ( + + onChangeOwner(e.target.value)} + placeholder="e.g. team-ml-platform" + /> + + )} + + ); +}; + +export default NameDescriptionOwnerFields; diff --git a/ui/src/components/forms/TagsEditor.tsx b/ui/src/components/forms/TagsEditor.tsx new file mode 100644 index 00000000000..73ea5df9c82 --- /dev/null +++ b/ui/src/components/forms/TagsEditor.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiButtonEmpty, + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiSpacer, + EuiCallOut, +} from "@elastic/eui"; + +interface TagEntry { + key: string; + value: string; +} + +interface TagsEditorProps { + tags: TagEntry[]; + onChange: (tags: TagEntry[]) => void; + error?: string; +} + +const TagsEditor: React.FC = ({ tags, onChange, error }) => { + const addTag = () => { + onChange([...tags, { key: "", value: "" }]); + }; + + const removeTag = (index: number) => { + onChange(tags.filter((_, i) => i !== index)); + }; + + const updateTag = (index: number, field: "key" | "value", val: string) => { + const updated = [...tags]; + updated[index] = { ...updated[index], [field]: val }; + onChange(updated); + }; + + return ( + <> + + + + +

Labels

+
+
+ + + Add label + + +
+ + {error && ( + <> + + + + )} + + {tags.map((tag, index) => ( + + + updateTag(index, "key", e.target.value)} + compressed + /> + + + updateTag(index, "value", e.target.value)} + compressed + /> + + + removeTag(index)} + /> + + + ))} + + {tags.length === 0 && ( + + No labels added yet. + + )} + + ); +}; + +export default TagsEditor; +export type { TagEntry }; diff --git a/ui/src/components/forms/ValueTypeSelect.tsx b/ui/src/components/forms/ValueTypeSelect.tsx new file mode 100644 index 00000000000..2718b7c027e --- /dev/null +++ b/ui/src/components/forms/ValueTypeSelect.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { EuiFormRow, EuiSelect } from "@elastic/eui"; +import { feast } from "../../protos"; + +const VALUE_TYPE_OPTIONS = [ + { value: String(feast.types.ValueType.Enum.STRING), text: "STRING" }, + { value: String(feast.types.ValueType.Enum.INT32), text: "INT32" }, + { value: String(feast.types.ValueType.Enum.INT64), text: "INT64" }, + { value: String(feast.types.ValueType.Enum.FLOAT), text: "FLOAT" }, + { value: String(feast.types.ValueType.Enum.DOUBLE), text: "DOUBLE" }, + { value: String(feast.types.ValueType.Enum.BOOL), text: "BOOL" }, + { value: String(feast.types.ValueType.Enum.BYTES), text: "BYTES" }, + { + value: String(feast.types.ValueType.Enum.UNIX_TIMESTAMP), + text: "UNIX_TIMESTAMP", + }, +]; + +interface ValueTypeSelectProps { + value: string; + onChange: (value: string) => void; + label?: string; + helpText?: string; + compressed?: boolean; +} + +const ValueTypeSelect: React.FC = ({ + value, + onChange, + label = "Value Type", + helpText, + compressed = false, +}) => { + return ( + + onChange(e.target.value)} + compressed={compressed} + /> + + ); +}; + +export default ValueTypeSelect; +export { VALUE_TYPE_OPTIONS }; diff --git a/ui/src/contexts/AuthContext.tsx b/ui/src/contexts/AuthContext.tsx new file mode 100644 index 00000000000..6003ff761d0 --- /dev/null +++ b/ui/src/contexts/AuthContext.tsx @@ -0,0 +1,245 @@ +import React, { + createContext, + useContext, + useState, + useCallback, + useEffect, + useRef, +} from "react"; +// @ts-ignore -- keycloak-js types use "exports" field; bundler resolves the JS fine +import Keycloak from "keycloak-js"; + +interface AuthUser { + username: string; + roles: string[]; + groups: string[]; + email?: string; +} + +interface AuthContextValue { + user: AuthUser | null; + isAuthenticated: boolean; + isAuthEnabled: boolean; + isInitializing: boolean; + logout: () => void; + keycloak: Keycloak | null; +} + +const AuthContext = createContext({ + user: null, + isAuthenticated: false, + isAuthEnabled: false, + isInitializing: true, + logout: () => {}, + keycloak: null, +}); + +interface ServerAuthConfig { + auth_type: string; + url?: string; + realm?: string; + client_id?: string; + auth_discovery_url?: string; +} + +function readServerAuthConfig(): ServerAuthConfig | null { + // Injected by the Feast UI server (feast ui) into index.html from feature_store.yaml + const el = document.getElementById("feast-auth-config"); + if (el) { + try { + return JSON.parse(el.textContent || "{}"); + } catch { + /* fall through */ + } + } + + // Dev mode fallback: fetch from /api/auth-config (served by setupProxy or REST server) + return null; +} + +function extractUser(kc: Keycloak): AuthUser { + const parsed = kc.tokenParsed as any; + const clientRoles: string[] = + parsed?.resource_access?.[kc.clientId!]?.roles || []; + const realmRoles: string[] = parsed?.realm_access?.roles || []; + const combined = [...clientRoles, ...realmRoles]; + + return { + username: parsed?.preferred_username || "unknown", + roles: combined.filter((v, i) => combined.indexOf(v) === i), + groups: parsed?.groups || [], + email: parsed?.email, + }; +} + +const TOKEN_REFRESH_INTERVAL = 30_000; +const MIN_VALIDITY_SECS = 60; + +const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [kc, setKc] = useState(null); + const [user, setUser] = useState(null); + const [isInitializing, setIsInitializing] = useState(true); + const [isAuthEnabled, setIsAuthEnabled] = useState(false); + const initStarted = useRef(false); + + const syncUser = useCallback((keycloak: Keycloak) => { + if (keycloak.authenticated) { + setUser(extractUser(keycloak)); + } + }, []); + + const doRefresh = useCallback( + (keycloak: Keycloak) => { + keycloak + .updateToken(MIN_VALIDITY_SECS) + .then((refreshed: boolean) => { + if (refreshed) { + syncUser(keycloak); + } + }) + .catch(() => { + console.warn("Token refresh failed — redirecting to login"); + keycloak.login(); + }); + }, + [syncUser], + ); + + useEffect(() => { + if (initStarted.current) return; + initStarted.current = true; + + const initAuth = async () => { + const config = readServerAuthConfig(); + + if (!config || config.auth_type !== "oidc") { + // Auth is disabled — skip Keycloak, render the app immediately + setIsAuthEnabled(false); + setIsInitializing(false); + return; + } + + setIsAuthEnabled(true); + + const keycloak = new Keycloak({ + url: config.url || "http://localhost:8080", + realm: config.realm || "feast", + clientId: config.client_id || "feast-ui", + }); + + keycloak.onTokenExpired = () => { + console.info("Access token expired — attempting refresh"); + doRefresh(keycloak); + }; + + keycloak.onAuthRefreshSuccess = () => { + syncUser(keycloak); + }; + + keycloak.onAuthRefreshError = () => { + console.warn("Auth refresh error — redirecting to login"); + keycloak.login(); + }; + + try { + const authenticated = await keycloak.init({ + onLoad: "login-required", + checkLoginIframe: false, + pkceMethod: "S256", + }); + + setKc(keycloak); + if (authenticated) { + syncUser(keycloak); + } + } catch (err) { + console.error("Keycloak init failed:", err); + } + + setIsInitializing(false); + }; + + initAuth(); + }, [doRefresh, syncUser]); + + // Proactive token refresh + useEffect(() => { + if (!kc?.authenticated) return; + const id = setInterval(() => doRefresh(kc), TOKEN_REFRESH_INTERVAL); + return () => clearInterval(id); + }, [kc, doRefresh]); + + // Global fetch interceptor: inject auth header on /api/ calls, handle 401 + useEffect(() => { + if (!kc) return; + + const originalFetch = window.fetch; + window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const url = + typeof input === "string" + ? input + : input instanceof Request + ? input.url + : input.toString(); + + const isApiCall = url.includes("/api/"); + + if (isApiCall && kc.authenticated && kc.token) { + const mergedHeaders = new Headers(init?.headers); + if (!mergedHeaders.has("Authorization")) { + mergedHeaders.set("Authorization", `Bearer ${kc.token}`); + } + init = { ...init, headers: mergedHeaders }; + } + + let response = await originalFetch(input, init); + + if (response.status === 401 && isApiCall) { + console.warn("API returned 401 — attempting token refresh and retry"); + try { + await kc.updateToken(5); + syncUser(kc); + const retryHeaders = new Headers(init?.headers); + retryHeaders.set("Authorization", `Bearer ${kc.token}`); + response = await originalFetch(input, { + ...init, + headers: retryHeaders, + }); + } catch { + kc.login(); + } + } + + return response; + }; + + return () => { + window.fetch = originalFetch; + }; + }, [kc, syncUser]); + + const logout = useCallback(() => { + if (kc) { + kc.logout({ redirectUri: window.location.origin }); + } + }, [kc]); + + const value: AuthContextValue = { + user, + isAuthenticated: !!kc?.authenticated, + isAuthEnabled, + isInitializing, + logout, + keycloak: kc, + }; + + return {children}; +}; + +const useAuth = () => useContext(AuthContext); + +export default AuthContext; +export { AuthProvider, useAuth }; +export type { AuthUser }; diff --git a/ui/src/contexts/DataModeContext.tsx b/ui/src/contexts/DataModeContext.tsx new file mode 100644 index 00000000000..c8ef4ea0bab --- /dev/null +++ b/ui/src/contexts/DataModeContext.tsx @@ -0,0 +1,20 @@ +import React, { useContext } from "react"; + +interface FetchOptions { + headers?: Record; + credentials?: RequestCredentials; +} + +interface DataModeConfig { + fetchOptions?: FetchOptions; +} + +const defaultConfig: DataModeConfig = {}; + +const DataModeContext = React.createContext(defaultConfig); + +const useDataMode = () => useContext(DataModeContext); + +export default DataModeContext; +export { useDataMode }; +export type { DataModeConfig, FetchOptions }; diff --git a/ui/src/contexts/MonitoringContext.ts b/ui/src/contexts/MonitoringContext.ts new file mode 100644 index 00000000000..f701cbcd5bf --- /dev/null +++ b/ui/src/contexts/MonitoringContext.ts @@ -0,0 +1,14 @@ +import React from "react"; + +interface MonitoringConfig { + apiBaseUrl: string; + enabled: boolean; +} + +const MonitoringContext = React.createContext({ + apiBaseUrl: "/api/v1", + enabled: false, +}); + +export default MonitoringContext; +export type { MonitoringConfig }; diff --git a/ui/src/contexts/ProjectListContext.ts b/ui/src/contexts/ProjectListContext.ts index c42b22f6611..c0c24840efb 100644 --- a/ui/src/contexts/ProjectListContext.ts +++ b/ui/src/contexts/ProjectListContext.ts @@ -13,12 +13,14 @@ const ProjectEntrySchema = z.object({ const ProjectsListSchema = z.object({ default: z.string().optional(), projects: z.array(ProjectEntrySchema), + mode: z.string().optional(), }); type ProjectsListType = z.infer; interface ProjectsListContextInterface { projectsListPromise: Promise; isCustom: boolean; + basename?: string; } const ProjectListContext = React.createContext< diff --git a/ui/src/contexts/RegistryRefreshContext.ts b/ui/src/contexts/RegistryRefreshContext.ts new file mode 100644 index 00000000000..12be5977d88 --- /dev/null +++ b/ui/src/contexts/RegistryRefreshContext.ts @@ -0,0 +1,22 @@ +import React, { useContext } from "react"; + +interface RegistryRefreshContextInterface { + refreshing: boolean; + handleRefresh: () => Promise; +} + +const RegistryRefreshContext = React.createContext< + RegistryRefreshContextInterface | undefined +>(undefined); + +const useRegistryRefreshContext = () => { + const ctx = useContext(RegistryRefreshContext); + if (!ctx) { + throw new Error( + "useRegistryRefreshContext must be used within RegistryRefreshContext.Provider", + ); + } + return ctx; +}; + +export { RegistryRefreshContext, useRegistryRefreshContext }; diff --git a/ui/src/custom-tabs/TabsRegistryContext.tsx b/ui/src/custom-tabs/TabsRegistryContext.tsx index 38c9ccea486..4152edd832d 100644 --- a/ui/src/custom-tabs/TabsRegistryContext.tsx +++ b/ui/src/custom-tabs/TabsRegistryContext.tsx @@ -16,7 +16,6 @@ import FeatureCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureCustomTa import DataSourceCustomTabLoadingWrapper from "../utils/custom-tabs/DataSourceCustomTabLoadingWrapper"; import EntityCustomTabLoadingWrapper from "../utils/custom-tabs/EntityCustomTabLoadingWrapper"; import DatasetCustomTabLoadingWrapper from "../utils/custom-tabs/DatasetCustomTabLoadingWrapper"; -import CurlGeneratorTab from "../pages/feature-views/CurlGeneratorTab"; import { RegularFeatureViewCustomTabRegistrationInterface, diff --git a/ui/src/graphics/ComputeEngineIcon.tsx b/ui/src/graphics/ComputeEngineIcon.tsx new file mode 100644 index 00000000000..82873776ccf --- /dev/null +++ b/ui/src/graphics/ComputeEngineIcon.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +const ComputeEngineIcon = (props: React.SVGProps) => { + return ( + + + + + + + + + + + ); +}; + +export { ComputeEngineIcon }; diff --git a/ui/src/graphics/JobsIcon.tsx b/ui/src/graphics/JobsIcon.tsx new file mode 100644 index 00000000000..35f4c80af7c --- /dev/null +++ b/ui/src/graphics/JobsIcon.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +const JobsIcon = (props: React.SVGProps) => { + return ( + + + + + ); +}; + +export { JobsIcon }; diff --git a/ui/src/graphics/LabelViewIcon.tsx b/ui/src/graphics/LabelViewIcon.tsx new file mode 100644 index 00000000000..d154e8731a1 --- /dev/null +++ b/ui/src/graphics/LabelViewIcon.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +const LabelViewIcon = (props: React.SVGProps) => { + return ( + + + + + + ); +}; + +export { LabelViewIcon }; diff --git a/ui/src/graphics/data-source-icons.tsx b/ui/src/graphics/data-source-icons.tsx new file mode 100644 index 00000000000..813c714a560 --- /dev/null +++ b/ui/src/graphics/data-source-icons.tsx @@ -0,0 +1,377 @@ +import React from "react"; + +export const BigQueryIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const SnowflakeIcon = (props: React.SVGProps) => ( + + + + +); + +export const RedshiftIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const KafkaIcon = (props: React.SVGProps) => ( + + + + + + + + + + + + + + + +); + +export const SparkIcon = (props: React.SVGProps) => ( + + + + + +); + +export const FileIcon = (props: React.SVGProps) => ( + + + + + + + + +); + +export const RequestSourceIcon = (props: React.SVGProps) => ( + + + + + + + +); + +export const PushSourceIcon = (props: React.SVGProps) => ( + + + + + +); + +export const KinesisIcon = (props: React.SVGProps) => ( + + + + + +); + +export const TrinoIcon = (props: React.SVGProps) => ( + + + + + + + +); + +export const AthenaIcon = (props: React.SVGProps) => ( + + + + +); + +export const CustomSourceIcon = (props: React.SVGProps) => ( + + + + +); + +export const RayIcon = (props: React.SVGProps) => ( + + + + + +); + +export const PostgresIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const MongoDBIcon = (props: React.SVGProps) => ( + + + + + +); + +export const SqlServerIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const OracleIcon = (props: React.SVGProps) => ( + + + + + ORA + + +); + +export const CouchbaseIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const ClickHouseIcon = (props: React.SVGProps) => ( + + + + + + + +); diff --git a/ui/src/hooks/useFCOExploreSuggestions.ts b/ui/src/hooks/useFCOExploreSuggestions.ts index 43a0e1bea3f..e9ab456f72b 100644 --- a/ui/src/hooks/useFCOExploreSuggestions.ts +++ b/ui/src/hooks/useFCOExploreSuggestions.ts @@ -22,6 +22,11 @@ const FCO_TO_URL_NAME_MAP: Record = { entity: "/entity", featureView: "/feature-view", featureService: "/feature-service", + labelView: "/label-view", + mlflowRun: "/mlflow-run", + mlflowModel: "/mlflow-model", + openlineageJob: "/lineage", + openlineageDataset: "/lineage", }; const createSearchLink = ( diff --git a/ui/src/hooks/useRegistryRefresh.ts b/ui/src/hooks/useRegistryRefresh.ts new file mode 100644 index 00000000000..cb684cf5636 --- /dev/null +++ b/ui/src/hooks/useRegistryRefresh.ts @@ -0,0 +1,80 @@ +import { useCallback, useContext, useState } from "react"; +import { useQueryClient } from "react-query"; +import { + ProjectListContext, + ProjectsListSchema, +} from "../contexts/ProjectListContext"; +import { useDataMode } from "../contexts/DataModeContext"; + +interface Toast { + id: string; + title: string; + color: "success" | "danger"; + iconType: string; +} + +const useRegistryRefresh = () => { + const [refreshing, setRefreshing] = useState(false); + const [toasts, setToasts] = useState([]); + const queryClient = useQueryClient(); + const projectListCtx = useContext(ProjectListContext); + const basename = projectListCtx?.basename || ""; + const { fetchOptions } = useDataMode(); + + const removeToast = useCallback((removedToast: { id: string }) => { + setToasts((prev) => prev.filter((t) => t.id !== removedToast.id)); + }, []); + + const handleRefresh = useCallback(async () => { + setRefreshing(true); + try { + const refreshRes = await fetch(`${basename}/api/v1/registry/refresh`, { + method: "POST", + headers: { ...fetchOptions?.headers }, + credentials: fetchOptions?.credentials, + }); + if (!refreshRes.ok) { + throw new Error(`Registry refresh failed (${refreshRes.status})`); + } + const res = await fetch(`${basename}/projects-list.json`, { + headers: { + "Content-Type": "application/json", + ...fetchOptions?.headers, + }, + credentials: fetchOptions?.credentials, + }); + if (!res.ok) { + throw new Error(`Failed to fetch project list (${res.status})`); + } + const json = await res.json(); + const parsed = ProjectsListSchema.parse(json); + queryClient.setQueryData("feast-projects-list", parsed); + await queryClient.invalidateQueries("registry-rest-bulk"); + setToasts((prev) => [ + ...prev, + { + id: String(Date.now()), + title: "Refresh successful", + color: "success" as const, + iconType: "check", + }, + ]); + } catch { + setToasts((prev) => [ + ...prev, + { + id: String(Date.now()), + title: "Refresh failed", + color: "danger" as const, + iconType: "alert", + }, + ]); + } finally { + setRefreshing(false); + } + }, [basename, queryClient, fetchOptions]); + + return { refreshing, toasts, handleRefresh, removeToast }; +}; + +export default useRegistryRefresh; diff --git a/ui/src/hooks/useTagsAggregation.ts b/ui/src/hooks/useTagsAggregation.ts index 5d36fd54285..9ad0d78d6f5 100644 --- a/ui/src/hooks/useTagsAggregation.ts +++ b/ui/src/hooks/useTagsAggregation.ts @@ -1,13 +1,13 @@ -import { useContext, useMemo } from "react"; -import RegistryPathContext from "../contexts/RegistryPathContext"; -import useLoadRegistry from "../queries/useLoadRegistry"; -import { feast } from "../protos"; +import { useMemo } from "react"; +import { useParams } from "react-router-dom"; +import useResourceQuery, { + featureViewListPath, + featureServiceListPath, +} from "../queries/useResourceQuery"; -// Usage of generic type parameter T -// https://stackoverflow.com/questions/53203409/how-to-tell-typescript-that-im-returning-an-array-of-arrays-of-the-input-type const buildTagCollection = ( array: T[], - recordExtractor: (unknownFCO: T) => Record | undefined, // Assumes that tags are always a Record + recordExtractor: (unknownFCO: T) => Record | undefined, ): Record> => { const tagCollection = array.reduce( (memo: Record>, fco: T) => { @@ -38,17 +38,17 @@ const buildTagCollection = ( }; const useFeatureViewTagsAggregation = () => { - const registryUrl = useContext(RegistryPathContext); - const query = useLoadRegistry(registryUrl); + const { projectName } = useParams(); + const query = useResourceQuery({ + resourceType: "tags-fvs", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: (d) => d.featureViews, + }); const data = useMemo(() => { - return query.data && query.data.objects && query.data.objects.featureViews - ? buildTagCollection( - query.data.objects.featureViews!, - (fv) => { - return fv.spec?.tags!; - }, - ) + return query.data + ? buildTagCollection(query.data, (fv) => fv.spec?.tags) : undefined; }, [query.data]); @@ -59,19 +59,17 @@ const useFeatureViewTagsAggregation = () => { }; const useFeatureServiceTagsAggregation = () => { - const registryUrl = useContext(RegistryPathContext); - const query = useLoadRegistry(registryUrl); + const { projectName } = useParams(); + const query = useResourceQuery({ + resourceType: "tags-fss", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices, + }); const data = useMemo(() => { - return query.data && - query.data.objects && - query.data.objects.featureServices - ? buildTagCollection( - query.data.objects.featureServices, - (fs) => { - return fs.spec?.tags!; - }, - ) + return query.data + ? buildTagCollection(query.data, (fs) => fs.spec?.tags) : undefined; }, [query.data]); diff --git a/ui/src/mocks/handlers.ts b/ui/src/mocks/handlers.ts index 1c32bb2cf87..d9fb8a031b7 100644 --- a/ui/src/mocks/handlers.ts +++ b/ui/src/mocks/handlers.ts @@ -1,10 +1,48 @@ import { http, HttpResponse } from "msw"; import { readFileSync } from "fs"; import path from "path"; +import { feast } from "../protos"; -const registry = readFileSync( +const registryBuf = readFileSync( path.resolve(__dirname, "../../public/registry.db"), ); +const parsedRegistry = feast.core.Registry.decode(registryBuf); + +const toJSON = (obj: any) => (obj && obj.toJSON ? obj.toJSON() : obj); + +const entitiesJSON = (parsedRegistry.entities || []).map(toJSON); +const featureViewsJSON = (parsedRegistry.featureViews || []).map((fv) => ({ + ...toJSON(fv), + type: "featureView", +})); +const onDemandFVsJSON = (parsedRegistry.onDemandFeatureViews || []).map( + (fv) => ({ + ...toJSON(fv), + type: "onDemandFeatureView", + }), +); +const streamFVsJSON = (parsedRegistry.streamFeatureViews || []).map((fv) => ({ + ...toJSON(fv), + type: "streamFeatureView", +})); +const allFeatureViewsJSON = [ + ...featureViewsJSON, + ...onDemandFVsJSON, + ...streamFVsJSON, +]; +const featureServicesJSON = (parsedRegistry.featureServices || []).map(toJSON); +const dataSourcesJSON = (parsedRegistry.dataSources || []).map(toJSON); +const savedDatasetsJSON = (parsedRegistry.savedDatasets || []).map(toJSON); +const projectsJSON = (parsedRegistry.projects || []).map(toJSON); + +const allFeatures = featureViewsJSON.flatMap((fv: any) => + (fv?.spec?.features || []).map((f: any) => ({ + name: f.name, + featureViewName: fv.spec?.name, + valueType: f.valueType, + project: fv.spec?.project, + })), +); const projectsListWithDefaultProject = http.get("/projects-list.json", () => HttpResponse.json({ @@ -14,22 +52,266 @@ const projectsListWithDefaultProject = http.get("/projects-list.json", () => name: "Credit Score Project", description: "Project for credit scoring team and associated models.", id: "credit_scoring_aws", - registryPath: "/registry.db", // Changed to match what the test expects + registryPath: "/api/v1", }, ], }), ); -const creditHistoryRegistryPB = http.get("/registry.pb", () => { - return HttpResponse.arrayBuffer(registry.buffer); -}); +// REST API list endpoints +const restEntities = http.get("/api/v1/entities", () => + HttpResponse.json({ + entities: entitiesJSON, + pagination: {}, + relationships: {}, + }), +); + +const restFeatureViews = http.get("/api/v1/feature_views", () => + HttpResponse.json({ + featureViews: allFeatureViewsJSON, + pagination: {}, + relationships: {}, + }), +); + +const restFeatureServices = http.get("/api/v1/feature_services", () => + HttpResponse.json({ + featureServices: featureServicesJSON, + pagination: {}, + relationships: {}, + }), +); + +const restDataSources = http.get("/api/v1/data_sources", () => + HttpResponse.json({ + dataSources: dataSourcesJSON, + pagination: {}, + relationships: {}, + }), +); + +const restSavedDatasets = http.get("/api/v1/saved_datasets", () => + HttpResponse.json({ + savedDatasets: savedDatasetsJSON, + pagination: {}, + }), +); + +const restProjects = http.get("/api/v1/projects", () => + HttpResponse.json({ + projects: projectsJSON, + pagination: {}, + }), +); + +const restFeatures = http.get("/api/v1/features", () => + HttpResponse.json({ + features: allFeatures, + pagination: {}, + }), +); -const creditHistoryRegistryDB = http.get("/registry.db", () => { - return HttpResponse.arrayBuffer(registry.buffer); +const restPermissions = http.get("/api/v1/permissions", () => + HttpResponse.json({ + permissions: [], + pagination: {}, + }), +); + +// Detail endpoints +const restFeatureViewDetail = http.get( + "/api/v1/feature_views/:name", + ({ params }) => { + const name = params.name as string; + const fv = allFeatureViewsJSON.find((f: any) => f.spec?.name === name); + if (!fv) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(fv); + }, +); + +const restEntityDetail = http.get("/api/v1/entities/:name", ({ params }) => { + const name = params.name as string; + const entity = entitiesJSON.find((e: any) => e.spec?.name === name); + if (!entity) + return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(entity); }); -export { +const restFeatureServiceDetail = http.get( + "/api/v1/feature_services/:name", + ({ params }) => { + const name = params.name as string; + const fs = featureServicesJSON.find((f: any) => f.spec?.name === name); + if (!fs) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(fs); + }, +); + +const restDataSourceDetail = http.get( + "/api/v1/data_sources/:name", + ({ params }) => { + const name = params.name as string; + const ds = dataSourcesJSON.find((d: any) => d.name === name); + if (!ds) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(ds); + }, +); + +const restFeatureDetail = http.get( + "/api/v1/features/:fvName/:featureName", + ({ params }) => { + const fvName = params.fvName as string; + const featureName = params.featureName as string; + const fv = allFeatureViewsJSON.find((f: any) => f.spec?.name === fvName); + if (!fv) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + const feature = (fv as any).spec?.features?.find( + (f: any) => f.name === featureName, + ); + if (!feature) + return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json({ + featureViewName: fvName, + featureName, + feature, + featureView: fv, + }); + }, +); + +// "all" endpoints (for global search / all-projects view) +const restEntitiesAll = http.get("/api/v1/entities/all", () => + HttpResponse.json({ + entities: entitiesJSON.map((e: any) => ({ + ...e, + project: e.spec?.project, + })), + pagination: {}, + relationships: {}, + }), +); + +const restFeatureViewsAll = http.get("/api/v1/feature_views/all", () => + HttpResponse.json({ + featureViews: allFeatureViewsJSON.map((fv: any) => ({ + ...fv, + project: fv.spec?.project, + })), + pagination: {}, + relationships: {}, + }), +); + +const restFeatureServicesAll = http.get("/api/v1/feature_services/all", () => + HttpResponse.json({ + featureServices: featureServicesJSON.map((fs: any) => ({ + ...fs, + project: fs.spec?.project, + })), + pagination: {}, + relationships: {}, + }), +); + +const restDataSourcesAll = http.get("/api/v1/data_sources/all", () => + HttpResponse.json({ + dataSources: dataSourcesJSON.map((ds: any) => ({ + ...ds, + project: ds.project, + })), + pagination: {}, + relationships: {}, + }), +); + +const restSavedDatasetsAll = http.get("/api/v1/saved_datasets/all", () => + HttpResponse.json({ + savedDatasets: savedDatasetsJSON, + pagination: {}, + }), +); + +const restFeaturesAll = http.get("/api/v1/features/all", () => + HttpResponse.json({ + features: allFeatures, + pagination: {}, + }), +); + +const restSavedDatasetDetail = http.get( + "/api/v1/saved_datasets/:name", + ({ params }) => { + const name = params.name as string; + const sd = savedDatasetsJSON.find((d: any) => d.spec?.name === name); + if (!sd) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(sd); + }, +); + +const restLabelViews = http.get("/api/v1/label_views", () => + HttpResponse.json({ + featureViews: [], + pagination: {}, + relationships: {}, + }), +); + +const restLabelViewsAll = http.get("/api/v1/label_views/all", () => + HttpResponse.json({ + featureViews: [], + pagination: {}, + relationships: {}, + }), +); + +const restLabels = http.get("/api/v1/labels", () => + HttpResponse.json({ + labels: [], + pagination: {}, + }), +); + +const restLabelsAll = http.get("/api/v1/labels/all", () => + HttpResponse.json({ + labels: [], + pagination: {}, + }), +); + +const restMetrics = http.get("/api/v1/metrics/:type", () => + HttpResponse.json({}), +); + +const allRestHandlers = [ projectsListWithDefaultProject, - creditHistoryRegistryPB as creditHistoryRegistry, - creditHistoryRegistryDB, -}; + // "all" endpoints must come before parameterized detail routes + restEntitiesAll, + restFeatureViewsAll, + restFeatureServicesAll, + restDataSourcesAll, + restSavedDatasetsAll, + restFeaturesAll, + restLabelViewsAll, + restLabelsAll, + // List endpoints + restEntities, + restFeatureViews, + restFeatureServices, + restDataSources, + restSavedDatasets, + restProjects, + restFeatures, + restLabelViews, + restLabels, + restPermissions, + // Detail endpoints + restFeatureViewDetail, + restEntityDetail, + restFeatureServiceDetail, + restDataSourceDetail, + restSavedDatasetDetail, + restFeatureDetail, + restMetrics, +]; + +export { projectsListWithDefaultProject, allRestHandlers }; diff --git a/ui/src/pages/Layout.tsx b/ui/src/pages/Layout.tsx index 0e3341b8820..a951b9a2649 100644 --- a/ui/src/pages/Layout.tsx +++ b/ui/src/pages/Layout.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect } from "react"; import { + EuiGlobalToastList, EuiPage, EuiPageSidebar, EuiPageBody, @@ -9,6 +10,13 @@ import { EuiSpacer, EuiFlexGroup, EuiFlexItem, + EuiAvatar, + EuiText, + EuiBadge, + EuiToolTip, + EuiPopover, + EuiButtonEmpty, + EuiIcon, } from "@elastic/eui"; import { Outlet } from "react-router-dom"; @@ -26,14 +34,18 @@ import RegistrySearch, { } from "../components/RegistrySearch"; import GlobalSearchShortcut from "../components/GlobalSearchShortcut"; import CommandPalette from "../components/CommandPalette"; +import { useAuth } from "../contexts/AuthContext"; +import { RegistryRefreshContext } from "../contexts/RegistryRefreshContext"; +import useRegistryRefresh from "../hooks/useRegistryRefresh"; const Layout = () => { - // Registry Path Context has to be inside Layout - // because it has to be under routes - // in order to use useParams let { projectName } = useParams(); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const searchRef = useRef(null); + const { user, logout, isAuthEnabled } = useAuth(); + const { refreshing, toasts, handleRefresh, removeToast } = + useRegistryRefresh(); const { data: projectsData } = useLoadProjectsList(); @@ -54,47 +66,6 @@ const Layout = () => { // Load unfiltered data for global search (across all projects) const { data: globalData } = useLoadRegistry(globalRegistryPath); - // Categories for page-level search (filtered to current project) - const categories = data - ? [ - { - name: "Data Sources", - data: data.objects.dataSources || [], - getLink: (item: any) => `/p/${projectName}/data-source/${item.name}`, - }, - { - name: "Entities", - data: data.objects.entities || [], - getLink: (item: any) => `/p/${projectName}/entity/${item.name}`, - }, - { - name: "Features", - data: data.allFeatures || [], - getLink: (item: any) => { - const featureView = item?.featureView; - return featureView - ? `/p/${projectName}/feature-view/${featureView}/feature/${item.name}` - : "#"; - }, - }, - { - name: "Feature Views", - data: data.mergedFVList || [], - getLink: (item: any) => `/p/${projectName}/feature-view/${item.name}`, - }, - { - name: "Feature Services", - data: data.objects.featureServices || [], - getLink: (item: any) => { - const serviceName = item?.name || item?.spec?.name; - return serviceName - ? `/p/${projectName}/feature-service/${serviceName}` - : "#"; - }, - }, - ] - : []; - // Helper function to extract project ID from an item const getProjectId = (item: any): string => { // Try different possible locations for the project field @@ -151,6 +122,18 @@ const Layout = () => { return `/p/${project}/feature-view/${item.name}`; }, }, + { + name: "Label Views", + data: (globalData.objects.labelViews || []).map((item: any) => ({ + ...item, + projectId: getProjectId(item), + })), + getLink: (item: any) => { + const lvName = item?.name || item?.spec?.name; + const project = item?.projectId || getProjectId(item); + return `/p/${project}/label-view/${lvName}`; + }, + }, { name: "Feature Services", data: (globalData.objects.featureServices || []).map((item: any) => ({ @@ -188,52 +171,52 @@ const Layout = () => { }, []); return ( - - - setIsCommandPaletteOpen(false)} - categories={globalCategories} - /> - - - - - - {registryPath && ( - - - - - + + + + setIsCommandPaletteOpen(false)} + categories={globalCategories} + /> + + + + + + {registryPath && ( + + + + + +
+ +
+
+ )} +
+ + +
- -
-
- )} -
- - - -
- {data && (
{ backgroundColor: "var(--euiPageBackgroundColor)", borderBottom: "1px solid #D3DAE6", boxShadow: "0px 1px 5px rgba(0, 0, 0, 0.05)", - padding: "16px", + padding: "12px 16px", width: "100%", }} > - - - - + + {data && ( + +
+ +
+
+ )} + {!data && } + + {projectName && ( + + + Refresh + + + )} + + {isAuthEnabled && user && ( + + setIsUserMenuOpen((v) => !v)} + style={{ + display: "flex", + alignItems: "center", + gap: 8, + background: "none", + border: "none", + cursor: "pointer", + padding: "4px 8px", + borderRadius: 6, + }} + aria-label="User menu" + > + + + {user.username} + + + + } + isOpen={isUserMenuOpen} + closePopover={() => setIsUserMenuOpen(false)} + anchorPosition="downRight" + panelPaddingSize="m" + > +
+ + + + + + + {user.username} + + {user.email && ( + + {user.email} + + )} + + + + {user.roles.length > 0 && ( + <> + + + Roles + + +
+ {user.roles + .filter( + (r) => + ![ + "default-roles-feast", + "offline_access", + "uma_authorization", + ].includes(r), + ) + .map((role) => ( + + + {role} + + + ))} +
+ + )} + + {user.groups.length > 0 && ( + <> + + + Groups + + +
+ {user.groups.map((group) => ( + + {group} + + ))} +
+ + )} + + + + Sign out + +
+
+
+ )}
- )} -
- +
+ +
-
-
-
-
-
+ + + + + + ); }; diff --git a/ui/src/pages/ProjectOverviewPage.tsx b/ui/src/pages/ProjectOverviewPage.tsx index 839fbcc5d89..017c32d56e7 100644 --- a/ui/src/pages/ProjectOverviewPage.tsx +++ b/ui/src/pages/ProjectOverviewPage.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React from "react"; import { EuiPageTemplate, EuiText, @@ -7,8 +7,6 @@ import { EuiTitle, EuiSpacer, EuiSkeletonText, - EuiEmptyPrompt, - EuiFieldSearch, EuiPanel, EuiStat, EuiCard, @@ -17,54 +15,107 @@ import { import { useDocumentTitle } from "../hooks/useDocumentTitle"; import ObjectsCountStats from "../components/ObjectsCountStats"; import ExplorePanel from "../components/ExplorePanel"; -import useLoadRegistry from "../queries/useLoadRegistry"; -import RegistryPathContext from "../contexts/RegistryPathContext"; -import RegistryVisualizationTab from "../components/RegistryVisualizationTab"; -import RegistrySearch from "../components/RegistrySearch"; +import useResourceQuery, { + restFeatureViewsToMergedList, + restLabelViewsFromResponse, +} from "../queries/useResourceQuery"; import { useParams, useNavigate } from "react-router-dom"; import { useLoadProjectsList } from "../contexts/ProjectListContext"; +import type { genericFVType } from "../parsers/mergedFVTypes"; + +const getItemProject = (item: any): string => + item?.project || item?.spec?.project || ""; // Component for "All Projects" view const AllProjectsDashboard = () => { - const registryUrl = useContext(RegistryPathContext); const navigate = useNavigate(); const { data: projectsData } = useLoadProjectsList(); - const { data: registryData } = useLoadRegistry(registryUrl); - if (!registryData) { + const fvQuery = useResourceQuery({ + resourceType: "all-proj-fvs", + restPath: "/feature_views/all?limit=100&include_relationships=true", + restSelect: restFeatureViewsToMergedList, + }); + + const entQuery = useResourceQuery({ + resourceType: "all-proj-entities", + restPath: "/entities/all?limit=100", + restSelect: (d) => d.entities, + }); + + const dsQuery = useResourceQuery({ + resourceType: "all-proj-ds", + restPath: "/data_sources/all?limit=100", + restSelect: (d) => d.dataSources, + }); + + const fsQuery = useResourceQuery({ + resourceType: "all-proj-fs", + restPath: "/feature_services/all?limit=100", + restSelect: (d) => d.featureServices, + }); + + const featQuery = useResourceQuery({ + resourceType: "all-proj-features", + restPath: "/features/all?limit=100", + restSelect: (d) => d.features, + }); + + const lvQuery = useResourceQuery({ + resourceType: "all-proj-lvs", + restPath: "/label_views/all?limit=100&include_relationships=true", + restSelect: restLabelViewsFromResponse, + }); + + const settled = (q: { isSuccess: boolean; isError: boolean }) => + q.isSuccess || q.isError; + const allSettled = + settled(fvQuery) && + settled(entQuery) && + settled(dsQuery) && + settled(fsQuery) && + settled(featQuery) && + settled(lvQuery); + + if (!allSettled) { return ; } - // Calculate total counts across all projects + const allFVs = fvQuery.data || []; + const allEntities = entQuery.data || []; + const allDS = dsQuery.data || []; + const allFS = fsQuery.data || []; + const allFeatures = featQuery.data || []; + const allLabelViews = lvQuery.data || []; + const totalCounts = { - featureViews: registryData.objects.featureViews?.length || 0, - entities: registryData.objects.entities?.length || 0, - dataSources: registryData.objects.dataSources?.length || 0, - featureServices: registryData.objects.featureServices?.length || 0, - features: registryData.allFeatures?.length || 0, + featureViews: fvQuery.isPermissionDenied ? null : allFVs.length, + entities: entQuery.isPermissionDenied ? null : allEntities.length, + dataSources: dsQuery.isPermissionDenied ? null : allDS.length, + featureServices: fsQuery.isPermissionDenied ? null : allFS.length, + features: featQuery.isPermissionDenied ? null : allFeatures.length, + labelViews: lvQuery.isPermissionDenied ? null : allLabelViews.length, }; - // Get projects from registry and count their objects const projects = projectsData?.projects.filter((p) => p.id !== "all") || []; const projectStats = projects.map((project) => { - const projectFVs = - registryData.objects.featureViews?.filter( - (fv: any) => fv?.spec?.project === project.id, - ) || []; - const projectEntities = - registryData.objects.entities?.filter( - (e: any) => e?.spec?.project === project.id, - ) || []; - const projectFeatures = - registryData.allFeatures?.filter((f: any) => f?.project === project.id) || - []; + const matchesProject = (item: any) => getItemProject(item) === project.id; return { ...project, counts: { - featureViews: projectFVs.length, - entities: projectEntities.length, - features: projectFeatures.length, + featureViews: fvQuery.isPermissionDenied + ? null + : allFVs.filter((fv: any) => matchesProject(fv.object || fv)).length, + entities: entQuery.isPermissionDenied + ? null + : allEntities.filter(matchesProject).length, + features: featQuery.isPermissionDenied + ? null + : allFeatures.filter(matchesProject).length, + labelViews: lvQuery.isPermissionDenied + ? null + : allLabelViews.filter(matchesProject).length, }, }; }); @@ -92,47 +143,76 @@ const AllProjectsDashboard = () => { - - - - - - - - - - - - - - - + {totalCounts.featureViews != null && ( + + + + )} + {totalCounts.entities != null && ( + + + + )} + {totalCounts.features != null && ( + + + + )} + {totalCounts.featureServices != null && ( + + + + )} + {totalCounts.dataSources != null && ( + + + + )} + {totalCounts.labelViews != null && totalCounts.labelViews > 0 && ( + + + + + + + + + + + + + )} @@ -156,33 +236,53 @@ const AllProjectsDashboard = () => { > - - - {project.counts.featureViews} -
- - Feature Views - -
-
- - - {project.counts.entities} -
- - Entities - -
-
- - - {project.counts.features} -
- - Features - -
-
+ {project.counts.featureViews != null && ( + + + {project.counts.featureViews} +
+ + Feature Views + +
+
+ )} + {project.counts.entities != null && ( + + + {project.counts.entities} +
+ + Entities + +
+
+ )} + {project.counts.features != null && ( + + + {project.counts.features} +
+ + Features + +
+
+ )} + {project.counts.labelViews != null && + project.counts.labelViews > 0 && ( + + + {project.counts.labelViews} +
+ + Label Views + +
+
+ )}
@@ -195,112 +295,59 @@ const AllProjectsDashboard = () => { const ProjectOverviewPage = () => { useDocumentTitle("Feast Home"); - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams<{ projectName: string }>(); - const { isLoading, isSuccess, isError, data } = useLoadRegistry( - registryUrl, - projectName, - ); + const { data: projectsData } = useLoadProjectsList(); // Show aggregated dashboard for "All Projects" view if (projectName === "all") { return ; } - const categories = [ - { - name: "Data Sources", - data: data?.objects.dataSources || [], - getLink: (item: any) => `/p/${projectName}/data-source/${item.name}`, - }, - { - name: "Entities", - data: data?.objects.entities || [], - getLink: (item: any) => `/p/${projectName}/entity/${item.name}`, - }, - { - name: "Features", - data: data?.allFeatures || [], - getLink: (item: any) => { - const featureView = item?.featureView; - return featureView - ? `/p/${projectName}/feature-view/${featureView}/feature/${item.name}` - : "#"; - }, - }, - { - name: "Feature Views", - data: data?.mergedFVList || [], - getLink: (item: any) => `/p/${projectName}/feature-view/${item.name}`, - }, - { - name: "Feature Services", - data: data?.objects.featureServices || [], - getLink: (item: any) => { - const serviceName = item?.name || item?.spec?.name; - return serviceName - ? `/p/${projectName}/feature-service/${serviceName}` - : "#"; - }, - }, - ]; + const currentProject = projectsData?.projects.find( + (p) => p.id === projectName, + ); return (

- {isLoading && } - {isSuccess && data?.project && `Project: ${data.project}`} + {currentProject + ? `Project: ${currentProject.name}` + : projectName + ? `Project: ${projectName}` + : ""}

- {isLoading && } - {isError && ( - Error Loading Project Configs} - body={ -

- There was an error loading the Project Configurations. - Please check that feature_store.yaml file is - available and well-formed. -

- } - /> + {currentProject?.description ? ( + +
{currentProject.description}
+
+ ) : ( + +

+ Welcome to your new Feast project. In this UI, you can see + Data Sources, Entities, Features, Feature Views, and Feature + Services registered in Feast. +

+

+ It looks like this project already has some objects + registered. If you are new to this project, we suggest + starting by exploring the Feature Services, as they represent + the collection of Feature Views serving a particular model. +

+

+ Note: We encourage you to replace this + welcome message with more suitable content for your team. You + can do so by specifying a project_description in + your feature_store.yaml file. +

+
)} - {isSuccess && - (data?.description ? ( - -
{data.description}
-
- ) : ( - -

- Welcome to your new Feast project. In this UI, you can see - Data Sources, Entities, Features, Feature Views, and Feature - Services registered in Feast. -

-

- It looks like this project already has some objects - registered. If you are new to this project, we suggest - starting by exploring the Feature Services, as they - represent the collection of Feature Views serving a - particular model. -

-

- Note: We encourage you to replace this - welcome message with more suitable content for your team. - You can do so by specifying a{" "} - project_description in your{" "} - feature_store.yaml file. -

-
- ))}
diff --git a/ui/src/pages/RootProjectSelectionPage.tsx b/ui/src/pages/RootProjectSelectionPage.tsx index fb488e714bc..6740e266f2a 100644 --- a/ui/src/pages/RootProjectSelectionPage.tsx +++ b/ui/src/pages/RootProjectSelectionPage.tsx @@ -1,7 +1,9 @@ import React, { useEffect } from "react"; import { + EuiButtonEmpty, EuiCard, EuiFlexGrid, + EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSkeletonText, @@ -13,10 +15,12 @@ import { import { useLoadProjectsList } from "../contexts/ProjectListContext"; import { useNavigate } from "react-router-dom"; import FeastIconBlue from "../graphics/FeastIconBlue"; +import { useRegistryRefreshContext } from "../contexts/RegistryRefreshContext"; const RootProjectSelectionPage = () => { const { isLoading, isSuccess, data } = useLoadProjectsList(); const navigate = useNavigate(); + const { refreshing, handleRefresh } = useRegistryRefreshContext(); useEffect(() => { if (data && data.default) { @@ -48,12 +52,27 @@ const RootProjectSelectionPage = () => { return ( - -

Welcome to Feast

-
- -

Select one of the projects.

-
+ + + +

Welcome to Feast

+
+ +

Select one of the projects.

+
+
+ + + Refresh + + +
{isLoading && } {isSuccess && data?.projects && ( diff --git a/ui/src/pages/Sidebar.tsx b/ui/src/pages/Sidebar.tsx index 55c8ec805c9..1054b3d8e3c 100644 --- a/ui/src/pages/Sidebar.tsx +++ b/ui/src/pages/Sidebar.tsx @@ -1,10 +1,19 @@ -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; import { EuiIcon, EuiSideNav, htmlIdGenerator } from "@elastic/eui"; import { Link, useParams } from "react-router-dom"; import { useMatchSubpath } from "../hooks/useMatchSubpath"; -import useLoadRegistry from "../queries/useLoadRegistry"; -import RegistryPathContext from "../contexts/RegistryPathContext"; +import useResourceQuery, { + entityListPath, + featureViewListPath, + featureServiceListPath, + dataSourceListPath, + savedDatasetListPath, + featuresListPath, + labelViewListPath, + restFeatureViewsToMergedList, + restLabelViewsFromResponse, +} from "../queries/useResourceQuery"; import { DataSourceIcon } from "../graphics/DataSourceIcon"; import { EntityIcon } from "../graphics/EntityIcon"; @@ -14,11 +23,67 @@ import { DatasetIcon } from "../graphics/DatasetIcon"; import { FeatureIcon } from "../graphics/FeatureIcon"; import { HomeIcon } from "../graphics/HomeIcon"; import { PermissionsIcon } from "../graphics/PermissionsIcon"; +import { LabelViewIcon } from "../graphics/LabelViewIcon"; +import { ComputeEngineIcon } from "../graphics/ComputeEngineIcon"; +import type { genericFVType } from "../parsers/mergedFVTypes"; const SideNav = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const { isSuccess, data } = useLoadRegistry(registryUrl, projectName); + + const { isSuccess: dsSuccess, data: dataSources } = useResourceQuery({ + resourceType: "sidebar-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources, + }); + + const { isSuccess: entSuccess, data: entities } = useResourceQuery({ + resourceType: "sidebar-entities", + project: projectName, + restPath: entityListPath(projectName), + restSelect: (d) => d.entities, + }); + + const { isSuccess: fvSuccess, data: featureViews } = useResourceQuery< + genericFVType[] + >({ + resourceType: "sidebar-fvs", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: restFeatureViewsToMergedList, + }); + + const { isSuccess: featSuccess, data: features } = useResourceQuery({ + resourceType: "sidebar-features", + project: projectName, + restPath: featuresListPath(projectName), + restSelect: (d) => d.features, + }); + + const { isSuccess: fsSuccess, data: featureServices } = useResourceQuery< + any[] + >({ + resourceType: "sidebar-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices, + }); + + const { isSuccess: sdSuccess, data: savedDatasets } = useResourceQuery( + { + resourceType: "sidebar-sd", + project: projectName, + restPath: savedDatasetListPath(projectName), + restSelect: (d) => d.savedDatasets, + }, + ); + + const { isSuccess: lvSuccess, data: labelViews } = useResourceQuery({ + resourceType: "sidebar-lvs", + project: projectName, + restPath: labelViewListPath(projectName), + restSelect: restLabelViewsFromResponse, + }); const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); @@ -26,43 +91,16 @@ const SideNav = () => { setisSideNavOpenOnMobile(!isSideNavOpenOnMobile); }; - const dataSourcesLabel = `Data Sources ${ - isSuccess && data?.objects.dataSources - ? `(${data?.objects.dataSources?.length})` - : "" - }`; - - const entitiesLabel = `Entities ${ - isSuccess && data?.objects.entities - ? `(${data?.objects.entities?.length})` - : "" - }`; - - const featureViewsLabel = `Feature Views ${ - isSuccess && data?.mergedFVList && data?.mergedFVList.length > 0 - ? `(${data?.mergedFVList.length})` - : "" - }`; - - const featureListLabel = `Features ${ - isSuccess && data?.allFeatures && data?.allFeatures.length > 0 - ? `(${data?.allFeatures.length})` - : "" - }`; - - const featureServicesLabel = `Feature Services ${ - isSuccess && data?.objects.featureServices - ? `(${data?.objects.featureServices?.length})` - : "" - }`; - - const savedDatasetsLabel = `Datasets ${ - isSuccess && data?.objects.savedDatasets - ? `(${data?.objects.savedDatasets?.length})` - : "" - }`; + const dataSourcesLabel = `Data Sources ${dsSuccess && dataSources ? `(${dataSources.length})` : ""}`; + const entitiesLabel = `Entities ${entSuccess && entities ? `(${entities.length})` : ""}`; + const featureViewsLabel = `Feature Views ${fvSuccess && featureViews && featureViews.length > 0 ? `(${featureViews.length})` : ""}`; + const featureListLabel = `Features ${featSuccess && features && features.length > 0 ? `(${features.length})` : ""}`; + const featureServicesLabel = `Feature Services ${fsSuccess && featureServices ? `(${featureServices.length})` : ""}`; + const savedDatasetsLabel = `Datasets ${sdSuccess && savedDatasets ? `(${savedDatasets.length})` : ""}`; + const labelViewsLabel = `Label Views ${lvSuccess && labelViews && labelViews.length > 0 ? `(${labelViews.length})` : ""}`; const baseUrl = `/p/${projectName}`; + const monitoringSelected = useMatchSubpath(`${baseUrl}/monitoring`); const sideNav: React.ComponentProps["items"] = [ { @@ -124,6 +162,15 @@ const SideNav = () => { ), isSelected: useMatchSubpath(`${baseUrl}/feature-service`), }, + { + name: labelViewsLabel, + id: htmlIdGenerator("labelViews")(), + icon: , + renderItem: (props) => ( + + ), + isSelected: useMatchSubpath(`${baseUrl}/label-view`), + }, { name: savedDatasetsLabel, id: htmlIdGenerator("savedDatasets")(), @@ -131,15 +178,6 @@ const SideNav = () => { renderItem: (props) => , isSelected: useMatchSubpath(`${baseUrl}/data-set`), }, - { - name: "Data Labeling", - id: htmlIdGenerator("dataLabeling")(), - icon: , - renderItem: (props) => ( - - ), - isSelected: useMatchSubpath(`${baseUrl}/data-labeling`), - }, { name: "Permissions", id: htmlIdGenerator("permissions")(), @@ -149,6 +187,24 @@ const SideNav = () => { ), isSelected: useMatchSubpath(`${baseUrl}/permissions`), }, + { + name: "Monitoring", + id: htmlIdGenerator("monitoring")(), + icon: , + renderItem: (props: any) => ( + + ), + isSelected: monitoringSelected, + }, + { + name: "Compute & Jobs", + id: htmlIdGenerator("computeEngine")(), + icon: , + renderItem: (props: any) => ( + + ), + isSelected: useMatchSubpath(`${baseUrl}/compute-engine`), + }, ], }, ]; diff --git a/ui/src/pages/compute-engines/Index.tsx b/ui/src/pages/compute-engines/Index.tsx new file mode 100644 index 00000000000..f03d4e2269f --- /dev/null +++ b/ui/src/pages/compute-engines/Index.tsx @@ -0,0 +1,548 @@ +import React, { useMemo, useState } from "react"; +import { Route, Routes, useNavigate, useParams } from "react-router-dom"; + +import { + EuiPageTemplate, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiSpacer, + EuiPanel, + EuiHorizontalRule, + EuiTitle, + EuiText, + EuiBadge, + EuiBasicTable, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiHealth, + EuiFilterGroup, + EuiFilterButton, + EuiCallOut, +} from "@elastic/eui"; + +import { ComputeEngineIcon } from "../../graphics/ComputeEngineIcon"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import { + useLoadComputeEngine, + FeatureViewEngineInfo, +} from "../../queries/useLoadComputeEngine"; +import EuiCustomLink from "../../components/EuiCustomLink"; + +interface MaterializationJobRow { + id: string; + featureView: string; + status: "SUCCEEDED" | "RUNNING" | "ERROR" | "WAITING"; + rangeStart: string; + rangeEnd: string; + sortKey: number; +} + +function formatRange(iso: string | undefined): string { + if (!iso) return "—"; + try { + return new Date(iso).toLocaleString(); + } catch { + return iso; + } +} + +function buildJobsFromFeatureViews( + featureViewInfos: FeatureViewEngineInfo[], +): MaterializationJobRow[] { + const jobs: MaterializationJobRow[] = []; + + featureViewInfos.forEach((fv) => { + if (fv.materializationIntervals && fv.materializationIntervals.length > 0) { + fv.materializationIntervals.forEach((interval, idx) => { + const startTime = + (interval as any).startTime || (interval as any).start_time; + const endTime = (interval as any).endTime || (interval as any).end_time; + + const sortMs = endTime + ? new Date(endTime).getTime() + : startTime + ? new Date(startTime).getTime() + : 0; + + jobs.push({ + id: `${fv.name}-${idx}`, + featureView: fv.name, + status: "SUCCEEDED", + rangeStart: formatRange(startTime), + rangeEnd: formatRange(endTime), + sortKey: sortMs, + }); + }); + } + }); + + return jobs.sort((a, b) => { + try { + return b.sortKey - a.sortKey; + } catch { + return 0; + } + }); +} + +const statusColorMap: Record = { + SUCCEEDED: "success", + RUNNING: "primary", + ERROR: "danger", + WAITING: "subdued", +}; + +const OverviewTab = ({ + engineInfo, + featureViewInfos, + projectName, +}: { + engineInfo: any; + featureViewInfos: FeatureViewEngineInfo[]; + projectName: string | undefined; +}) => { + const materializedCount = featureViewInfos.filter( + (fv) => fv.lastMaterialized, + ).length; + const overrideCount = featureViewInfos.filter((fv) => fv.hasOverride).length; + + const fvColumns = [ + { + name: "Feature View", + field: "name", + sortable: true, + render: (name: string) => ( + + {name} + + ), + }, + { + name: "Type", + field: "type", + sortable: true, + render: (type: string) => {type}, + }, + { + name: "Online", + field: "online", + render: (online: boolean) => ( + + {online ? "Yes" : "No"} + + ), + }, + { + name: "Last Materialized", + field: "lastMaterialized", + render: (val: string | undefined) => { + if (!val) + return ( + + Never + + ); + try { + return new Date(val).toLocaleString(); + } catch { + return val; + } + }, + }, + { + name: "Engine Override", + field: "hasOverride", + render: (hasOverride: boolean, item: any) => { + if (!hasOverride) { + return ( + + None + + ); + } + const keys = item.overrides ? Object.keys(item.overrides) : []; + return ( + + {keys.length > 0 ? keys.join(", ") : "Custom"} + + ); + }, + }, + ]; + + return ( + + + + + + + + + + + + + + + + + + + + +

Engine Configuration

+
+ + + Type + + + {engineInfo?.engineType || "local"} + + + + Class + + {engineInfo?.engineClass || "LocalComputeEngine"} + + + + {engineInfo?.config && + Object.entries(engineInfo.config).filter( + ([key, value]) => key !== "type" && value != null && value !== "", + ).length > 0 && ( + <> + + +

Parameters

+
+ + + {Object.entries(engineInfo.config) + .filter( + ([key, value]) => + key !== "type" && value != null && value !== "", + ) + .map(([key, value]) => ( + + {key} + + {typeof value === "object" + ? JSON.stringify(value, null, 2) + : String(value)} + + + ))} + + + )} +
+ + + + +

Feature Views Using This Engine

+
+ + {featureViewInfos.length > 0 ? ( + ({ + "data-test-subj": `row-${item.name}`, + })} + /> + ) : ( + + No feature views found in this project. + + )} +
+ ); +}; + +const JobsTab = ({ + engineInfo, + featureViewInfos, + projectName, +}: { + engineInfo: any; + featureViewInfos: FeatureViewEngineInfo[]; + projectName: string | undefined; +}) => { + const [statusFilter, setStatusFilter] = useState(null); + + const jobs = useMemo( + () => buildJobsFromFeatureViews(featureViewInfos), + [featureViewInfos], + ); + + const filteredJobs = statusFilter + ? jobs.filter((j) => j.status === statusFilter) + : jobs; + + const succeededCount = jobs.filter((j) => j.status === "SUCCEEDED").length; + const failedCount = jobs.filter((j) => j.status === "ERROR").length; + const runningCount = jobs.filter((j) => j.status === "RUNNING").length; + + const columns = [ + { + name: "Job ID", + field: "id", + sortable: true, + render: (id: string) => ( + + {id} + + ), + }, + { + name: "Feature View", + field: "featureView", + sortable: true, + render: (name: string) => ( + + {name} + + ), + }, + { + name: "Engine", + field: "id", + render: () => ( + {engineInfo?.engineType || "local"} + ), + }, + { + name: "Status", + field: "status", + sortable: true, + render: (status: string) => ( + + {status} + + ), + }, + { + name: "Range Start", + field: "rangeStart", + sortable: true, + }, + { + name: "Range End", + field: "rangeEnd", + }, + ]; + + return ( + + + + + + + + + + + + + + + + + + + + + +

Filter by Status

+
+
+ + + setStatusFilter(null)} + > + All + + + setStatusFilter( + statusFilter === "SUCCEEDED" ? null : "SUCCEEDED", + ) + } + numFilters={succeededCount} + > + Succeeded + + + setStatusFilter(statusFilter === "RUNNING" ? null : "RUNNING") + } + numFilters={runningCount} + > + Running + + + setStatusFilter(statusFilter === "ERROR" ? null : "ERROR") + } + numFilters={failedCount} + > + Failed + + + +
+ + + + {filteredJobs.length > 0 ? ( + ({ + "data-test-subj": `row-${item.id}`, + })} + /> + ) : ( + + {jobs.length === 0 + ? "No materialization jobs found. Run 'feast materialize' to create jobs." + : "No jobs match the selected filter."} + + )} +
+ ); +}; + +const Index = () => { + const { projectName } = useParams(); + const navigate = useNavigate(); + + const { + isLoading, + isSuccess, + isError, + isPermissionDenied, + engineInfo, + featureViewInfos, + } = useLoadComputeEngine(projectName); + + useDocumentTitle(`Compute & Jobs | Feast`); + + return ( + + navigate(""), + }, + { + label: "Jobs", + isSelected: useMatchSubpath("jobs"), + onClick: () => navigate("jobs"), + }, + ]} + /> + + {isLoading && ( +

+ Loading +

+ )} + {isPermissionDenied && ( + +

You do not have permission to view compute engines.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} + {isSuccess && ( + + + } + /> + + } + /> + + )} +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/data-sources/DataSourceCatalog.tsx b/ui/src/pages/data-sources/DataSourceCatalog.tsx new file mode 100644 index 00000000000..61653b6cc0e --- /dev/null +++ b/ui/src/pages/data-sources/DataSourceCatalog.tsx @@ -0,0 +1,379 @@ +import React, { useState } from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiText, + EuiTitle, + EuiSpacer, + EuiButton, + EuiBadge, +} from "@elastic/eui"; +import { feast } from "../../protos"; +import { + BigQueryIcon, + SnowflakeIcon, + RedshiftIcon, + KafkaIcon, + SparkIcon, + FileIcon, + RequestSourceIcon, + PushSourceIcon, + KinesisIcon, + TrinoIcon, + AthenaIcon, + CustomSourceIcon, + RayIcon, + PostgresIcon, + MongoDBIcon, + SqlServerIcon, + OracleIcon, + CouchbaseIcon, + ClickHouseIcon, +} from "../../graphics/data-source-icons"; + +interface DataSourceTypeInfo { + id: string; + sourceType: string; + name: string; + description: string; + icon: React.FC>; + category: "batch" | "stream" | "on-demand"; + color: string; + contrib?: boolean; +} + +const DATA_SOURCE_TYPES: DataSourceTypeInfo[] = [ + { + id: "bigquery", + sourceType: String(feast.core.DataSource.SourceType.BATCH_BIGQUERY), + name: "BigQuery", + description: + "Google Cloud's serverless data warehouse. Ideal for large-scale analytics and ML feature computation.", + icon: BigQueryIcon, + category: "batch", + color: "#4285F4", + }, + { + id: "snowflake", + sourceType: String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE), + name: "Snowflake", + description: + "Cloud-native data platform with elastic scaling. Connect to your Snowflake tables for feature engineering.", + icon: SnowflakeIcon, + category: "batch", + color: "#29B5E8", + }, + { + id: "redshift", + sourceType: String(feast.core.DataSource.SourceType.BATCH_REDSHIFT), + name: "Redshift", + description: + "AWS fully managed data warehouse. Pull features from your Redshift clusters with fast parallel queries.", + icon: RedshiftIcon, + category: "batch", + color: "#205B97", + }, + { + id: "spark", + sourceType: String(feast.core.DataSource.SourceType.BATCH_SPARK), + name: "Spark", + description: + "Apache Spark data source for distributed processing. Access tables via Spark catalog or direct file paths.", + icon: SparkIcon, + category: "batch", + color: "#E25A1C", + }, + { + id: "file", + sourceType: String(feast.core.DataSource.SourceType.BATCH_FILE), + name: "File (Parquet / CSV)", + description: + "Read features from Parquet or CSV files stored in S3, GCS, HDFS, or local filesystem.", + icon: FileIcon, + category: "batch", + color: "#4CAF50", + }, + { + id: "trino", + sourceType: String(feast.core.DataSource.SourceType.BATCH_TRINO), + name: "Trino", + description: + "Distributed SQL query engine for big data analytics. Query data across heterogeneous sources via Trino catalog.", + icon: TrinoIcon, + category: "batch", + color: "#DD00A1", + }, + { + id: "athena", + sourceType: String(feast.core.DataSource.SourceType.BATCH_ATHENA), + name: "AWS Athena", + description: + "Serverless interactive query service on AWS. Run SQL queries directly against data in S3 without infrastructure.", + icon: AthenaIcon, + category: "batch", + color: "#8C4FFF", + }, + { + id: "iceberg", + sourceType: String(feast.core.DataSource.SourceType.BATCH_ICEBERG), + name: "Iceberg / Unity Catalog", + description: + "Apache Iceberg REST Catalog source. Connect to Unity Catalog or any Iceberg REST-compatible catalog for governed feature data.", + icon: CustomSourceIcon, + category: "batch", + color: "#3B82F6", + }, + { + id: "kafka", + sourceType: String(feast.core.DataSource.SourceType.STREAM_KAFKA), + name: "Kafka", + description: + "Real-time event streaming platform. Ingest features from Kafka topics for low-latency serving.", + icon: KafkaIcon, + category: "stream", + color: "#231F20", + }, + { + id: "kinesis", + sourceType: String(feast.core.DataSource.SourceType.STREAM_KINESIS), + name: "AWS Kinesis", + description: + "Managed real-time data streaming on AWS. Capture and process streaming data at scale for real-time features.", + icon: KinesisIcon, + category: "stream", + color: "#FF9900", + }, + { + id: "request-source", + sourceType: String(feast.core.DataSource.SourceType.REQUEST_SOURCE), + name: "Request Source", + description: + "Features provided at request time by the caller. No external storage needed — values come from the client.", + icon: RequestSourceIcon, + category: "on-demand", + color: "#7B61FF", + }, + { + id: "push-source", + sourceType: String(feast.core.DataSource.SourceType.PUSH_SOURCE), + name: "Push Source", + description: + "Push-based ingestion source. Clients push feature values directly to the online/offline store.", + icon: PushSourceIcon, + category: "on-demand", + color: "#FF6B35", + }, + { + id: "ray", + sourceType: "RAY_SOURCE", + name: "Ray", + description: + "Multi-format data source powered by Ray. Read images, HuggingFace datasets, Parquet, CSV, MongoDB, and more via Ray Data.", + icon: RayIcon, + category: "batch", + color: "#00A2E8", + contrib: true, + }, + { + id: "postgres", + sourceType: "POSTGRES_SOURCE", + name: "PostgreSQL", + description: + "Open-source relational database. Query feature data from PostgreSQL tables with full SQL support.", + icon: PostgresIcon, + category: "batch", + color: "#336791", + contrib: true, + }, + { + id: "mongodb", + sourceType: "MONGODB_SOURCE", + name: "MongoDB", + description: + "Document-oriented NoSQL database. Access feature data stored in MongoDB collections.", + icon: MongoDBIcon, + category: "batch", + color: "#00684A", + contrib: true, + }, + { + id: "clickhouse", + sourceType: "CLICKHOUSE_SOURCE", + name: "ClickHouse", + description: + "Column-oriented OLAP database for real-time analytics. High-performance queries for feature retrieval.", + icon: ClickHouseIcon, + category: "batch", + color: "#FFCC00", + contrib: true, + }, + { + id: "mssql", + sourceType: "MSSQL_SOURCE", + name: "SQL Server", + description: + "Microsoft SQL Server data source. Connect to MSSQL databases for enterprise feature data.", + icon: SqlServerIcon, + category: "batch", + color: "#CC2927", + contrib: true, + }, + { + id: "oracle", + sourceType: "ORACLE_SOURCE", + name: "Oracle", + description: + "Oracle Database data source. Pull features from Oracle tables and views for enterprise workloads.", + icon: OracleIcon, + category: "batch", + color: "#F80000", + contrib: true, + }, + { + id: "couchbase", + sourceType: "COUCHBASE_SOURCE", + name: "Couchbase", + description: + "Couchbase Columnar analytics source. Run SQL++ queries across distributed data in Couchbase.", + icon: CouchbaseIcon, + category: "batch", + color: "#EA2328", + contrib: true, + }, + { + id: "custom-source", + sourceType: String(feast.core.DataSource.SourceType.CUSTOM_SOURCE), + name: "Custom Source", + description: + "Plugin-based data source for custom integrations. Extend Feast with your own data source implementation.", + icon: CustomSourceIcon, + category: "batch", + color: "#607D8B", + }, +]; + +const CATEGORY_LABELS: Record = { + batch: { label: "Batch", color: "primary" }, + stream: { label: "Streaming", color: "accent" }, + "on-demand": { label: "On-Demand", color: "warning" }, +}; + +interface DataSourceCatalogProps { + onSelectType: (sourceType: string) => void; +} + +const SourceCard: React.FC<{ + dsType: DataSourceTypeInfo; + isHovered: boolean; + onHover: (id: string | null) => void; + onSelect: (sourceType: string) => void; +}> = ({ dsType, isHovered, onHover, onSelect }) => { + const categoryInfo = CATEGORY_LABELS[dsType.category]; + + return ( + + onHover(dsType.id)} + onMouseLeave={() => onHover(null)} + style={{ + height: "100%", + display: "flex", + flexDirection: "column", + transition: "all 0.2s ease", + transform: isHovered ? "translateY(-2px)" : "none", + borderTop: `3px solid ${dsType.color}`, + cursor: "pointer", + }} + onClick={() => onSelect(dsType.sourceType)} + > + + +
+ +
+
+ + +

{dsType.name}

+
+
+ + + {categoryInfo.label} + + +
+ + + + +

{dsType.description}

+
+ + + + { + e.stopPropagation(); + onSelect(dsType.sourceType); + }} + iconType="plusInCircle" + size="s" + > + Create Connection + +
+
+ ); +}; + +const DataSourceCatalog: React.FC = ({ + onSelectType, +}) => { + const [hoveredId, setHoveredId] = useState(null); + + return ( +
+ +

+ Choose a data source type to create a new connection. Each type has + its own configuration tailored to the underlying storage system. +

+
+ + + + {DATA_SOURCE_TYPES.map((dsType) => ( + + ))} + +
+ ); +}; + +export default DataSourceCatalog; +export { DATA_SOURCE_TYPES }; +export type { DataSourceTypeInfo }; diff --git a/ui/src/pages/data-sources/DataSourceInstance.tsx b/ui/src/pages/data-sources/DataSourceInstance.tsx index 1ed2cfa5eab..574da82114e 100644 --- a/ui/src/pages/data-sources/DataSourceInstance.tsx +++ b/ui/src/pages/data-sources/DataSourceInstance.tsx @@ -1,21 +1,343 @@ -import React from "react"; +import React, { useState } from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, +} from "@elastic/eui"; import { DataSourceIcon } from "../../graphics/DataSourceIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import DataSourceRawData from "./DataSourceRawData"; import DataSourceOverviewTab from "./DataSourceOverviewTab"; +import DataSourceFormModal, { + DataSourceFormData, +} from "../../components/DataSourceFormModal"; +import { + useApplyDataSource, + useDeleteDataSource, +} from "../../queries/mutations/useDataSourceMutations"; +import useLoadDataSource from "./useLoadDataSource"; +import { feast } from "../../protos"; import { useDataSourceCustomTabs, useDataSourceCustomTabRoutes, } from "../../custom-tabs/TabsRegistryContext"; +const buildEditFormData = (ds: any): DataSourceFormData => { + const spec = ds.spec || ds; + const tags = spec.tags + ? Object.entries(spec.tags).map(([key, value]) => ({ + key, + value: value as string, + })) + : []; + + return { + name: spec.name || ds.name || "", + description: spec.description || ds.description || "", + owner: spec.owner || ds.owner || "", + sourceType: String(spec.type ?? ds.type ?? 0), + timestampField: spec.timestampField || ds.timestampField || "", + createdTimestampColumn: + spec.createdTimestampColumn || ds.createdTimestampColumn || "", + tags, + // File + fileUri: spec.fileOptions?.uri || ds.fileOptions?.uri || "", + fileFormat: + spec.fileOptions?.fileFormat || ds.fileOptions?.fileFormat || "parquet", + fileS3EndpointOverride: + spec.fileOptions?.s3EndpointOverride || + ds.fileOptions?.s3EndpointOverride || + "", + // BigQuery + bigqueryTable: + spec.bigqueryOptions?.table || ds.bigqueryOptions?.table || "", + bigqueryQuery: + spec.bigqueryOptions?.query || ds.bigqueryOptions?.query || "", + bigqueryDatePartitionColumn: + spec.datePartitionColumn || ds.datePartitionColumn || "", + // Snowflake + snowflakeTable: + spec.snowflakeOptions?.table || ds.snowflakeOptions?.table || "", + snowflakeDatabase: + spec.snowflakeOptions?.database || ds.snowflakeOptions?.database || "", + snowflakeSchema: + spec.snowflakeOptions?.schema || ds.snowflakeOptions?.schema || "", + snowflakeQuery: + spec.snowflakeOptions?.query || ds.snowflakeOptions?.query || "", + snowflakeWarehouse: + spec.snowflakeOptions?.warehouse || ds.snowflakeOptions?.warehouse || "", + // Redshift + redshiftTable: + spec.redshiftOptions?.table || ds.redshiftOptions?.table || "", + redshiftDatabase: + spec.redshiftOptions?.database || ds.redshiftOptions?.database || "", + redshiftSchema: + spec.redshiftOptions?.schema || ds.redshiftOptions?.schema || "", + redshiftQuery: + spec.redshiftOptions?.query || ds.redshiftOptions?.query || "", + // Kafka + kafkaBootstrapServers: + spec.kafkaOptions?.kafkaBootstrapServers || + ds.kafkaOptions?.kafkaBootstrapServers || + "", + kafkaTopic: spec.kafkaOptions?.topic || ds.kafkaOptions?.topic || "", + kafkaMessageFormat: + spec.kafkaOptions?.messageFormat || + ds.kafkaOptions?.messageFormat || + "json", + kafkaWatermarkDelay: + spec.kafkaOptions?.watermarkDelayThreshold || + ds.kafkaOptions?.watermarkDelayThreshold || + "", + // Spark + sparkTable: spec.sparkOptions?.table || ds.sparkOptions?.table || "", + sparkPath: spec.sparkOptions?.path || ds.sparkOptions?.path || "", + sparkQuery: spec.sparkOptions?.query || ds.sparkOptions?.query || "", + sparkFileFormat: + spec.sparkOptions?.fileFormat || ds.sparkOptions?.fileFormat || "", + sparkTableFormat: + spec.sparkOptions?.tableFormat?.formatType || + ds.sparkOptions?.tableFormat?.formatType || + "", + sparkTableFormatCatalog: + spec.sparkOptions?.tableFormat?.catalog || + ds.sparkOptions?.tableFormat?.catalog || + "", + sparkTableFormatNamespace: + spec.sparkOptions?.tableFormat?.namespace || + ds.sparkOptions?.tableFormat?.namespace || + "", + sparkTableFormatProperties: (() => { + const props = + spec.sparkOptions?.tableFormat?.properties || + ds.sparkOptions?.tableFormat?.properties; + return props ? JSON.stringify(props) : ""; + })(), + sparkDatePartitionColumn: + spec.sparkOptions?.datePartitionColumn || + ds.sparkOptions?.datePartitionColumn || + spec.datePartitionColumn || + ds.datePartitionColumn || + "", + sparkDatePartitionFormat: + spec.sparkOptions?.datePartitionColumnFormat || + ds.sparkOptions?.datePartitionColumnFormat || + "%Y-%m-%d", + // Kinesis + kinesisRegion: + spec.kinesisOptions?.region || ds.kinesisOptions?.region || "", + kinesisStreamName: + spec.kinesisOptions?.streamName || ds.kinesisOptions?.streamName || "", + kinesisRecordFormat: + spec.kinesisOptions?.recordFormat || + ds.kinesisOptions?.recordFormat || + "json", + // Trino + trinoTable: spec.trinoOptions?.table || ds.trinoOptions?.table || "", + trinoQuery: spec.trinoOptions?.query || ds.trinoOptions?.query || "", + // Athena + athenaTable: spec.athenaOptions?.table || ds.athenaOptions?.table || "", + athenaQuery: spec.athenaOptions?.query || ds.athenaOptions?.query || "", + athenaDatabase: + spec.athenaOptions?.database || ds.athenaOptions?.database || "", + athenaDataSource: + spec.athenaOptions?.dataSource || ds.athenaOptions?.dataSource || "", + athenaDatePartitionColumn: + spec.datePartitionColumn || ds.datePartitionColumn || "", + // Custom + customSourceClassName: + spec.customOptions?.className || ds.customOptions?.className || "", + customSourceConfig: + spec.customOptions?.config || ds.customOptions?.config || "", + // Iceberg + ...(() => { + const configStr = + spec.customOptions?.configuration || + ds.customOptions?.configuration || + ""; + if ( + String(spec.type ?? ds.type ?? 0) === + String(feast.core.DataSource.SourceType.BATCH_ICEBERG) && + configStr + ) { + try { + const cfg = JSON.parse(configStr); + return { + icebergCatalogType: cfg.catalog_type || "rest", + icebergEndpoint: cfg.endpoint || "", + icebergWarehouse: cfg.warehouse || "", + icebergNamespace: cfg.namespace || "", + icebergTable: cfg.table || "", + icebergTokenEnvVar: cfg.token_env_var || "", + icebergCredentialVending: String(cfg.credential_vending ?? true), + icebergCatalogProperties: cfg.catalog_properties + ? JSON.stringify(cfg.catalog_properties) + : "", + }; + } catch { + /* ignore parse errors */ + } + } + return { + icebergCatalogType: "rest", + icebergEndpoint: "", + icebergWarehouse: "", + icebergNamespace: "", + icebergTable: "", + icebergTokenEnvVar: "", + icebergCredentialVending: "true", + icebergCatalogProperties: "", + }; + })(), + // Ray + rayReaderType: "", + rayPath: "", + rayReaderOptions: "", + // Postgres + postgresTable: "", + postgresQuery: "", + // MongoDB + mongodbCollection: "", + // ClickHouse + clickhouseTable: "", + clickhouseQuery: "", + // MSSQL + mssqlTable: "", + mssqlConnectionStr: "", + mssqlDatePartitionColumn: "", + // Oracle + oracleTable: "", + oracleConnectionStr: "", + oracleDatePartitionColumn: "", + // Couchbase + couchbaseDatabase: "", + couchbaseScope: "", + couchbaseCollection: "", + couchbaseQuery: "", + }; +}; + +const formDataToPayload = (formData: DataSourceFormData, project: string) => { + const payload: Record = { + name: formData.name, + project, + type: parseInt(formData.sourceType, 10), + timestamp_field: formData.timestampField, + created_timestamp_column: formData.createdTimestampColumn, + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + }; + + const st = formData.sourceType; + if (st === String(feast.core.DataSource.SourceType.BATCH_FILE)) { + payload.file_options = { + uri: formData.fileUri, + file_format: formData.fileFormat || "parquet", + s3_endpoint_override: formData.fileS3EndpointOverride || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_BIGQUERY)) { + payload.bigquery_options = { + table: formData.bigqueryTable, + query: formData.bigqueryQuery, + }; + if (formData.bigqueryDatePartitionColumn) { + payload.date_partition_column = formData.bigqueryDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE)) { + payload.snowflake_options = { + table: formData.snowflakeTable, + database: formData.snowflakeDatabase, + schema_: formData.snowflakeSchema, + query: formData.snowflakeQuery || "", + warehouse: formData.snowflakeWarehouse || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_REDSHIFT)) { + payload.redshift_options = { + table: formData.redshiftTable, + database: formData.redshiftDatabase, + schema_: formData.redshiftSchema, + query: formData.redshiftQuery || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KAFKA)) { + payload.kafka_options = { + kafka_bootstrap_servers: formData.kafkaBootstrapServers, + topic: formData.kafkaTopic, + message_format: formData.kafkaMessageFormat || "json", + watermark_delay_threshold: formData.kafkaWatermarkDelay || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SPARK)) { + payload.spark_options = { + table: formData.sparkTable, + path: formData.sparkPath, + query: formData.sparkQuery || "", + file_format: formData.sparkFileFormat || "", + table_format: formData.sparkTableFormat || "", + table_format_catalog: formData.sparkTableFormatCatalog || "", + table_format_namespace: formData.sparkTableFormatNamespace || "", + table_format_properties: formData.sparkTableFormatProperties || "", + date_partition_column: formData.sparkDatePartitionColumn || "", + date_partition_column_format: formData.sparkDatePartitionFormat || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_TRINO)) { + payload.trino_options = { + table: formData.trinoTable, + query: formData.trinoQuery, + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ATHENA)) { + payload.athena_options = { + table: formData.athenaTable, + query: formData.athenaQuery, + database: formData.athenaDatabase, + data_source: formData.athenaDataSource, + }; + if (formData.athenaDatePartitionColumn) { + payload.date_partition_column = formData.athenaDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ICEBERG)) { + const catalogProps = formData.icebergCatalogProperties.trim() + ? JSON.parse(formData.icebergCatalogProperties) + : {}; + payload.custom_options = { + configuration: JSON.stringify({ + catalog_type: formData.icebergCatalogType || "rest", + endpoint: formData.icebergEndpoint, + warehouse: formData.icebergWarehouse, + namespace: formData.icebergNamespace, + table: formData.icebergTable, + token_env_var: formData.icebergTokenEnvVar || null, + credential_vending: formData.icebergCredentialVending !== "false", + catalog_properties: catalogProps, + }), + }; + payload.data_source_class_type = + "feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source.IcebergSource"; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KINESIS)) { + payload.kinesis_options = { + region: formData.kinesisRegion, + stream_name: formData.kinesisStreamName, + record_format: formData.kinesisRecordFormat || "json", + }; + } else if (st === String(feast.core.DataSource.SourceType.CUSTOM_SOURCE)) { + payload.custom_options = { + class_name: formData.customSourceClassName, + config: formData.customSourceConfig, + }; + } + + return payload; +}; + const DataSourceInstance = () => { const navigate = useNavigate(); - let { dataSourceName } = useParams(); + let { dataSourceName, projectName } = useParams(); useDocumentTitle(`${dataSourceName} | Data Source | Feast`); @@ -34,12 +356,66 @@ const DataSourceInstance = () => { const CustomTabRoutes = useDataSourceCustomTabRoutes(); + const { data } = useLoadDataSource(dataSourceName || ""); + const applyDataSource = useApplyDataSource(); + const deleteDataSource = useDeleteDataSource(); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editError, setEditError] = useState(null); + + const handleDelete = () => { + deleteDataSource.mutate( + { name: dataSourceName || "", project: projectName || "" }, + { + onSuccess: () => { + navigate(`/p/${projectName}/data-source`); + }, + }, + ); + }; + + const handleEditSubmit = (formData: DataSourceFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyDataSource.mutate(payload as any, { + onSuccess: () => { + setIsEditModalOpen(false); + setEditError(null); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setEditError(message); + }, + }); + }; + return ( { + setEditError(null); + setIsEditModalOpen(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={tabs} /> @@ -49,6 +425,37 @@ const DataSourceInstance = () => { {CustomTabRoutes} + + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteDataSource.isLoading} + > +

+ This will permanently remove the data source. This action cannot be + undone. +

+
+ )} + + {isEditModalOpen && data && ( + { + setIsEditModalOpen(false); + setEditError(null); + }} + onSubmit={handleEditSubmit} + initialData={buildEditFormData(data)} + isEdit + isSubmitting={applyDataSource.isLoading} + submitError={editError} + /> + )}
); }; diff --git a/ui/src/pages/data-sources/DataSourceOverviewTab.tsx b/ui/src/pages/data-sources/DataSourceOverviewTab.tsx index d702034a558..831e90b91d1 100644 --- a/ui/src/pages/data-sources/DataSourceOverviewTab.tsx +++ b/ui/src/pages/data-sources/DataSourceOverviewTab.tsx @@ -13,29 +13,34 @@ import { EuiDescriptionListDescription, EuiSpacer, } from "@elastic/eui"; -import React, { useContext } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; -import PermissionsDisplay from "../../components/PermissionsDisplay"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import { FEAST_FCO_TYPES } from "../../parsers/types"; -import { feast } from "../../protos"; -import useLoadRegistry from "../../queries/useLoadRegistry"; -import { getEntityPermissions } from "../../utils/permissionUtils"; import BatchSourcePropertiesView from "./BatchSourcePropertiesView"; import FeatureViewEdgesList from "../entities/FeatureViewEdgesList"; import RequestDataSourceSchemaTable from "./RequestDataSourceSchemaTable"; import useLoadDataSource from "./useLoadDataSource"; +import { feast } from "../../protos"; const DataSourceOverviewTab = () => { - let { dataSourceName, projectName } = useParams(); - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl, projectName); + const { dataSourceName } = useParams(); const dsName = dataSourceName === undefined ? "" : dataSourceName; const { isLoading, isSuccess, isError, data, consumingFeatureViews } = useLoadDataSource(dsName); const isEmpty = data === undefined; + const viewTypesForDs: Record | undefined = + consumingFeatureViews && consumingFeatureViews.length > 0 + ? consumingFeatureViews.reduce((acc: Record, f: any) => { + acc[f.target.name] = + f.target.type === "labelView" ? "labelView" : "featureView"; + return acc; + }, {}) + : undefined; + + const spec = data?.spec || data; + const sourceType = spec?.type; + return ( {isLoading && ( @@ -56,16 +61,82 @@ const DataSourceOverviewTab = () => {

Properties

- {data.fileOptions || data.bigqueryOptions ? ( - - ) : data.type ? ( + {spec?.fileOptions || spec?.bigqueryOptions ? ( + + ) : String(sourceType) === + String(feast.core.DataSource.SourceType.BATCH_ICEBERG) ? ( + (() => { + let cfg: any = {}; + try { + cfg = JSON.parse( + spec?.customOptions?.configuration || "{}", + ); + } catch { + /* ignore */ + } + return ( + + + Source Type + + + Iceberg / Unity Catalog + + + Catalog Type + + + {cfg.catalog_type || "rest"} + + {cfg.endpoint && ( + <> + + Endpoint + + + {cfg.endpoint} + + + )} + + Warehouse + + + {cfg.warehouse || "—"} + + + Namespace + + + {cfg.namespace || "—"} + + + Table + + + {cfg.table || "—"} + + {cfg.token_env_var && ( + <> + + Token Env Variable + + + {cfg.token_env_var} + + + )} + + ); + })() + ) : sourceType ? ( Source Type - {feast.core.DataSource.SourceType[data.type]} + {sourceType} @@ -78,7 +149,7 @@ const DataSourceOverviewTab = () => { - {data.requestDataOptions ? ( + {spec?.requestDataOptions ? (

Request Source Schema

@@ -86,7 +157,7 @@ const DataSourceOverviewTab = () => { { + data?.requestDataOptions?.schema!.map((obj: any) => { return { fieldName: obj.name!, valueType: obj.valueType!, @@ -104,37 +175,18 @@ const DataSourceOverviewTab = () => { -

Consuming Feature Views

+

Consuming Views

{consumingFeatureViews && consumingFeatureViews.length > 0 ? ( { + fvNames={consumingFeatureViews.map((f: any) => { return f.target.name; })} + viewTypes={viewTypesForDs} /> ) : ( - No consuming feature views - )} -
- - - -

Permissions

-
- - {registryQuery.data?.permissions ? ( - - ) : ( - - No permissions defined for this data source. - + No consuming views )}
diff --git a/ui/src/pages/data-sources/DataSourcesListingTable.tsx b/ui/src/pages/data-sources/DataSourcesListingTable.tsx index c314a4dfb94..c08060dd0e4 100644 --- a/ui/src/pages/data-sources/DataSourcesListingTable.tsx +++ b/ui/src/pages/data-sources/DataSourcesListingTable.tsx @@ -32,8 +32,11 @@ const DatasourcesListingTable = ({ name: "Type", field: "type", sortable: true, - render: (valueType: feast.core.DataSource.SourceType) => { - return feast.core.DataSource.SourceType[valueType]; + render: (valueType: feast.core.DataSource.SourceType | string) => { + if (typeof valueType === "string") { + return valueType; + } + return feast.core.DataSource.SourceType[valueType] || String(valueType); }, }, ]; diff --git a/ui/src/pages/data-sources/Index.tsx b/ui/src/pages/data-sources/Index.tsx index 96aef712aec..c8422d3943a 100644 --- a/ui/src/pages/data-sources/Index.tsx +++ b/ui/src/pages/data-sources/Index.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { @@ -9,52 +9,175 @@ import { EuiTitle, EuiFieldSearch, EuiSpacer, + EuiButton, + EuiCallOut, } from "@elastic/eui"; -import useLoadRegistry from "../../queries/useLoadRegistry"; import DatasourcesListingTable from "./DataSourcesListingTable"; +import DataSourceCatalog from "./DataSourceCatalog"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import DataSourceIndexEmptyState from "./DataSourceIndexEmptyState"; import { DataSourceIcon } from "../../graphics/DataSourceIcon"; import { useSearchQuery } from "../../hooks/useSearchInputWithTags"; import { feast } from "../../protos"; import ExportButton from "../../components/ExportButton"; +import DataSourceFormModal, { + DataSourceFormData, +} from "../../components/DataSourceFormModal"; +import { useApplyDataSource } from "../../queries/mutations/useDataSourceMutations"; +import useResourceQuery, { + dataSourceListPath, +} from "../../queries/useResourceQuery"; const useLoadDatasources = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.dataSources; - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: "data-sources-list", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources, + }); }; -const filterFn = (data: feast.core.IDataSource[], searchTokens: string[]) => { - let filteredByTags = data; - +const filterFn = (data: any[], searchTokens: string[]) => { if (searchTokens.length) { - return filteredByTags.filter((entry) => { + return data.filter((entry) => { + const name = entry.name || entry.spec?.name || ""; return searchTokens.find((token) => { - return ( - token.length >= 3 && entry.name && entry.name.indexOf(token) >= 0 - ); + return token.length >= 3 && name.indexOf(token) >= 0; }); }); } - return filteredByTags; + return data; +}; + +const formDataToPayload = (formData: DataSourceFormData, project: string) => { + const payload: Record = { + name: formData.name, + project, + type: parseInt(formData.sourceType, 10), + timestamp_field: formData.timestampField, + created_timestamp_column: formData.createdTimestampColumn, + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + }; + + const st = formData.sourceType; + if (st === String(feast.core.DataSource.SourceType.BATCH_FILE)) { + payload.file_options = { + uri: formData.fileUri, + file_format: formData.fileFormat || "parquet", + s3_endpoint_override: formData.fileS3EndpointOverride || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_BIGQUERY)) { + payload.bigquery_options = { + table: formData.bigqueryTable, + query: formData.bigqueryQuery, + }; + if (formData.bigqueryDatePartitionColumn) { + payload.date_partition_column = formData.bigqueryDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE)) { + payload.snowflake_options = { + table: formData.snowflakeTable, + database: formData.snowflakeDatabase, + schema_: formData.snowflakeSchema, + query: formData.snowflakeQuery || "", + warehouse: formData.snowflakeWarehouse || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_REDSHIFT)) { + payload.redshift_options = { + table: formData.redshiftTable, + database: formData.redshiftDatabase, + schema_: formData.redshiftSchema, + query: formData.redshiftQuery || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KAFKA)) { + payload.kafka_options = { + kafka_bootstrap_servers: formData.kafkaBootstrapServers, + topic: formData.kafkaTopic, + message_format: formData.kafkaMessageFormat || "json", + watermark_delay_threshold: formData.kafkaWatermarkDelay || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SPARK)) { + payload.spark_options = { + table: formData.sparkTable, + path: formData.sparkPath, + query: formData.sparkQuery || "", + file_format: formData.sparkFileFormat || "", + table_format: formData.sparkTableFormat || "", + table_format_catalog: formData.sparkTableFormatCatalog || "", + table_format_namespace: formData.sparkTableFormatNamespace || "", + table_format_properties: formData.sparkTableFormatProperties || "", + date_partition_column: formData.sparkDatePartitionColumn || "", + date_partition_column_format: formData.sparkDatePartitionFormat || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_TRINO)) { + payload.trino_options = { + table: formData.trinoTable, + query: formData.trinoQuery, + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ATHENA)) { + payload.athena_options = { + table: formData.athenaTable, + query: formData.athenaQuery, + database: formData.athenaDatabase, + data_source: formData.athenaDataSource, + }; + if (formData.athenaDatePartitionColumn) { + payload.date_partition_column = formData.athenaDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ICEBERG)) { + const catalogProps = formData.icebergCatalogProperties.trim() + ? JSON.parse(formData.icebergCatalogProperties) + : {}; + payload.custom_options = { + configuration: JSON.stringify({ + catalog_type: formData.icebergCatalogType || "rest", + endpoint: formData.icebergEndpoint, + warehouse: formData.icebergWarehouse, + namespace: formData.icebergNamespace, + table: formData.icebergTable, + token_env_var: formData.icebergTokenEnvVar || null, + credential_vending: formData.icebergCredentialVending !== "false", + catalog_properties: catalogProps, + }), + }; + payload.data_source_class_type = + "feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source.IcebergSource"; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KINESIS)) { + payload.kinesis_options = { + region: formData.kinesisRegion, + stream_name: formData.kinesisStreamName, + record_format: formData.kinesisRecordFormat || "json", + }; + } else if (st === String(feast.core.DataSource.SourceType.CUSTOM_SOURCE)) { + payload.custom_options = { + class_name: formData.customSourceClassName, + config: formData.customSourceConfig, + }; + } + + return payload; }; const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadDatasources(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadDatasources(); + const isAllProjects = projectName === "all"; + + const [showCatalog, setShowCatalog] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [preselectedSourceType, setPreselectedSourceType] = useState< + string | null + >(null); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const applyDataSource = useApplyDataSource(); useDocumentTitle(`Data Sources | Feast`); @@ -62,6 +185,115 @@ const Index = () => { const filterResult = data ? filterFn(data, searchTokens) : data; + const hasExistingSources = isSuccess && data && data.length > 0; + const isEmpty = isSuccess && (!data || data.length === 0); + + const modalInitialData = useMemo(() => { + if (!preselectedSourceType) return undefined; + return { + name: "", + description: "", + owner: "", + sourceType: preselectedSourceType, + timestampField: "", + createdTimestampColumn: "", + tags: [] as { key: string; value: string }[], + fileUri: "", + fileFormat: "parquet", + fileS3EndpointOverride: "", + bigqueryTable: "", + bigqueryQuery: "", + bigqueryDatePartitionColumn: "", + snowflakeTable: "", + snowflakeDatabase: "", + snowflakeSchema: "", + snowflakeQuery: "", + snowflakeWarehouse: "", + redshiftTable: "", + redshiftDatabase: "", + redshiftSchema: "", + redshiftQuery: "", + kafkaBootstrapServers: "", + kafkaTopic: "", + kafkaMessageFormat: "json", + kafkaWatermarkDelay: "", + sparkTable: "", + sparkPath: "", + sparkQuery: "", + sparkFileFormat: "parquet", + sparkTableFormat: "", + sparkTableFormatCatalog: "", + sparkTableFormatNamespace: "", + sparkTableFormatProperties: "", + sparkDatePartitionColumn: "", + sparkDatePartitionFormat: "%Y-%m-%d", + kinesisRegion: "", + kinesisStreamName: "", + kinesisRecordFormat: "json", + trinoTable: "", + trinoQuery: "", + athenaTable: "", + athenaQuery: "", + athenaDatabase: "", + athenaDataSource: "", + athenaDatePartitionColumn: "", + customSourceClassName: "", + customSourceConfig: "", + icebergCatalogType: "rest", + icebergEndpoint: "", + icebergWarehouse: "", + icebergNamespace: "", + icebergTable: "", + icebergTokenEnvVar: "", + icebergCredentialVending: "true", + icebergCatalogProperties: "", + rayReaderType: "parquet", + rayPath: "", + rayReaderOptions: "", + postgresTable: "", + postgresQuery: "", + mongodbCollection: "", + clickhouseTable: "", + clickhouseQuery: "", + mssqlTable: "", + mssqlConnectionStr: "", + mssqlDatePartitionColumn: "", + oracleTable: "", + oracleConnectionStr: "", + oracleDatePartitionColumn: "", + couchbaseDatabase: "", + couchbaseScope: "", + couchbaseCollection: "", + couchbaseQuery: "", + }; + }, [preselectedSourceType]); + + const handleSelectType = (sourceType: string) => { + setPreselectedSourceType(sourceType); + setIsModalOpen(true); + }; + + const handleCreateSubmit = (formData: DataSourceFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyDataSource.mutate(payload as any, { + onSuccess: () => { + setIsModalOpen(false); + setPreselectedSourceType(null); + setShowCatalog(false); + setErrorMessage(null); + setSuccessMessage( + `Data source "${formData.name}" created successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setErrorMessage(message); + }, + }); + }; + return ( { iconType={DataSourceIcon} pageTitle="Data Sources" rightSideItems={[ + ...(isAllProjects || showCatalog + ? [] + : [ + setShowCatalog(true)} + key="create" + > + Create Data Source + , + ]), , ]} /> - {isLoading && ( -

- Loading -

+ {successMessage && ( + <> + + + )} - {isError &&

We encountered an error while loading.

} - {isSuccess && !data && } - {isSuccess && data && data.length > 0 && filterResult && ( - - - - -

Search

+ {errorMessage && !isModalOpen && ( + <> + + + + )} + + {showCatalog && !isAllProjects && ( + <> + + + +

Select a Data Source Type

- { - setSearchString(e.target.value); - }} - />
+ {hasExistingSources && ( + + setShowCatalog(false)} + iconType="arrowLeft" + size="s" + > + Back to Data Sources + + + )}
- -
+ + + )} + + {!showCatalog && ( + <> + {isLoading && ( +

+ Loading +

+ )} + {isPermissionDenied && ( + +

You do not have permission to view data sources.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} + {isEmpty && !isAllProjects && ( + <> + +

No data sources yet — create your first connection

+
+ + + + )} + {isEmpty && isAllProjects && ( +

No data sources found across projects.

+ )} + {hasExistingSources && filterResult && ( + + + + +

Search

+
+ { + setSearchString(e.target.value); + }} + /> +
+
+ + +
+ )} + )}
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setPreselectedSourceType(null); + setErrorMessage(null); + }} + onSubmit={handleCreateSubmit} + isSubmitting={applyDataSource.isLoading} + submitError={errorMessage} + initialData={modalInitialData} + /> + )}
); }; diff --git a/ui/src/pages/data-sources/RequestDataSourceSchemaTable.tsx b/ui/src/pages/data-sources/RequestDataSourceSchemaTable.tsx index a55aeac0280..f671042dd85 100644 --- a/ui/src/pages/data-sources/RequestDataSourceSchemaTable.tsx +++ b/ui/src/pages/data-sources/RequestDataSourceSchemaTable.tsx @@ -20,8 +20,9 @@ const RequestDataSourceSchemaTable = ({ fields }: RequestDataSourceSchema) => { { name: "Value Type", field: "valueType", - render: (valueType: feast.types.ValueType.Enum) => { - return feast.types.ValueType.Enum[valueType]; + render: (valueType: feast.types.ValueType.Enum | string) => { + if (typeof valueType === "string") return valueType; + return feast.types.ValueType.Enum[valueType] || String(valueType || ""); }, }, ]; diff --git a/ui/src/pages/data-sources/useLoadDataSource.ts b/ui/src/pages/data-sources/useLoadDataSource.ts index 43f697fca03..c099e78f917 100644 --- a/ui/src/pages/data-sources/useLoadDataSource.ts +++ b/ui/src/pages/data-sources/useLoadDataSource.ts @@ -1,35 +1,35 @@ -import { useContext } from "react"; import { useParams } from "react-router-dom"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import { FEAST_FCO_TYPES } from "../../parsers/types"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import useResourceQuery, { + dataSourceDetailPath, +} from "../../queries/useResourceQuery"; const useLoadDataSource = (dataSourceName: string) => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.dataSources?.find( - (ds) => ds.name === dataSourceName, - ); + const dsQuery = useResourceQuery({ + resourceType: `data-source:${dataSourceName}`, + project: projectName, + restPath: dataSourceDetailPath(dataSourceName, projectName || ""), + restSelect: (d) => ({ + dataSource: d, + relationships: d?.relationships || [], + }), + enabled: !!dataSourceName, + }); - const consumingFeatureViews = - registryQuery.data === undefined - ? undefined - : registryQuery.data.relationships.filter((relationship) => { - return ( - relationship.source.type === FEAST_FCO_TYPES.dataSource && - relationship.source.name === data?.name && - relationship.target.type === FEAST_FCO_TYPES.featureView - ); - }); + const dataSource = dsQuery.data?.dataSource; + const relationships = dsQuery.data?.relationships || []; + + const consumingFeatureViews = relationships.filter( + (rel: any) => + rel?.source?.type === "dataSource" && + (rel?.target?.type === "featureView" || + rel?.target?.type === "labelView"), + ); return { - ...registryQuery, - data, + ...dsQuery, + data: dataSource, consumingFeatureViews, }; }; diff --git a/ui/src/pages/document-labeling/ClassificationTab.tsx b/ui/src/pages/document-labeling/ClassificationTab.tsx deleted file mode 100644 index 302b03cd9fa..00000000000 --- a/ui/src/pages/document-labeling/ClassificationTab.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import React, { useState } from "react"; -import { - EuiPageSection, - EuiCallOut, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiButton, - EuiPanel, - EuiTitle, - EuiText, - EuiTable, - EuiTableHeader, - EuiTableHeaderCell, - EuiTableBody, - EuiTableRow, - EuiTableRowCell, - EuiSelect, - EuiLoadingSpinner, -} from "@elastic/eui"; - -interface ClassificationData { - id: number; - text: string; - currentClass: string; - originalClass?: string; -} - -const ClassificationTab = () => { - const [csvPath, setCsvPath] = useState("./src/sample-data.csv"); - const [isLoading, setIsLoading] = useState(false); - const [data, setData] = useState([]); - const [error, setError] = useState(null); - const [availableClasses] = useState(["positive", "negative", "neutral"]); - - const loadCsvData = async () => { - if (!csvPath) return; - - setIsLoading(true); - setError(null); - - try { - if (csvPath === "./src/sample-data.csv") { - const sampleData: ClassificationData[] = [ - { - id: 1, - text: "This product is amazing! I love the quality and design.", - currentClass: "positive", - originalClass: "positive", - }, - { - id: 2, - text: "The service was terrible and the food was cold.", - currentClass: "negative", - originalClass: "negative", - }, - { - id: 3, - text: "It's an okay product, nothing special but does the job.", - currentClass: "neutral", - originalClass: "neutral", - }, - { - id: 4, - text: "Excellent customer support and fast delivery!", - currentClass: "positive", - originalClass: "positive", - }, - { - id: 5, - text: "I'm not sure how I feel about this purchase.", - currentClass: "neutral", - originalClass: "positive", - }, - ]; - - setData(sampleData); - } else { - throw new Error( - "CSV file not found. Please use the sample data path: ./src/sample-data.csv", - ); - } - } catch (err) { - setError( - err instanceof Error - ? err.message - : "An error occurred while loading the CSV data", - ); - } finally { - setIsLoading(false); - } - }; - - const handleClassChange = (id: number, newClass: string) => { - setData( - data.map((item) => - item.id === id ? { ...item, currentClass: newClass } : item, - ), - ); - }; - - const getChangedItems = () => { - return data.filter((item) => item.currentClass !== item.originalClass); - }; - - const resetChanges = () => { - setData( - data.map((item) => ({ ...item, currentClass: item.originalClass || "" })), - ); - }; - - const saveChanges = () => { - const changedItems = getChangedItems(); - console.log("Saving classification changes:", changedItems); - alert(`Saved ${changedItems.length} classification changes!`); - }; - - const columns = [ - { - field: "id", - name: "ID", - width: "60px", - }, - { - field: "text", - name: "Text", - width: "60%", - }, - { - field: "originalClass", - name: "Original Class", - width: "15%", - }, - { - field: "currentClass", - name: "Current Class", - width: "20%", - }, - ]; - - return ( - - -

- Load a CSV file containing text samples and edit their classification - labels. This helps improve your classification models by providing - corrected training data. -

-
- - - - - - - setCsvPath(e.target.value)} - /> - - - - - - Load CSV Data - - - - - - - - {isLoading && ( - - - - - - Loading CSV data... - - - )} - - {error && ( - -

{error}

-
- )} - - {data.length > 0 && ( - <> - - - -

Classification Data ({data.length} samples)

-
-
- - - - - Reset Changes - - - - - Save Changes ({getChangedItems().length}) - - - - -
- - - - - - - {columns.map((column, index) => ( - - {column.name} - - ))} - - - {data.map((item) => ( - - {item.id} - - {item.text} - - - - {item.originalClass} - - - - ({ - value: cls, - text: cls, - }))} - value={item.currentClass} - onChange={(e) => - handleClassChange(item.id, e.target.value) - } - compressed - /> - - - ))} - - - - - {getChangedItems().length > 0 && ( - <> - - -

- You have unsaved changes. Click "Save Changes" to persist your - modifications. -

-
- - )} - - )} -
- ); -}; - -export default ClassificationTab; diff --git a/ui/src/pages/document-labeling/DocumentLabelingPage.tsx b/ui/src/pages/document-labeling/DocumentLabelingPage.tsx deleted file mode 100644 index 5563d6328c1..00000000000 --- a/ui/src/pages/document-labeling/DocumentLabelingPage.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useState } from "react"; -import { - EuiPage, - EuiPageBody, - EuiPageSection, - EuiPageHeader, - EuiTitle, - EuiSpacer, - EuiTabs, - EuiTab, -} from "@elastic/eui"; -import RagTab from "./RagTab"; -import ClassificationTab from "./ClassificationTab"; - -const DocumentLabelingPage = () => { - const [selectedTab, setSelectedTab] = useState("rag"); - - const tabs = [ - { - id: "rag", - name: "RAG", - content: , - }, - { - id: "classification", - name: "Classification", - content: , - }, - ]; - - const selectedTabContent = tabs.find( - (tab) => tab.id === selectedTab, - )?.content; - - return ( - - - - -

Data Labeling

-
-
- - - - {tabs.map((tab) => ( - setSelectedTab(tab.id)} - isSelected={tab.id === selectedTab} - > - {tab.name} - - ))} - - - - - {selectedTabContent} - -
-
- ); -}; - -export default DocumentLabelingPage; diff --git a/ui/src/pages/document-labeling/RagTab.tsx b/ui/src/pages/document-labeling/RagTab.tsx deleted file mode 100644 index ae5fd22aea7..00000000000 --- a/ui/src/pages/document-labeling/RagTab.tsx +++ /dev/null @@ -1,615 +0,0 @@ -import React, { useState } from "react"; -import { - EuiPageSection, - EuiCallOut, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiButton, - EuiPanel, - EuiTitle, - EuiText, - EuiLoadingSpinner, - EuiButtonGroup, - EuiCode, - EuiTextArea, -} from "@elastic/eui"; -import { useTheme } from "../../contexts/ThemeContext"; - -interface DocumentContent { - content: string; - file_path: string; -} - -interface TextSelection { - text: string; - start: number; - end: number; -} - -interface DocumentLabel { - text: string; - start: number; - end: number; - label: string; - timestamp: number; - groundTruthLabel: string; -} - -const RagTab = () => { - const { colorMode } = useTheme(); - const [filePath, setFilePath] = useState("./src/test-document.txt"); - const [selectedText, setSelectedText] = useState(null); - const [labelingMode, setLabelingMode] = useState("relevant"); - const [labels, setLabels] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [documentContent, setDocumentContent] = - useState(null); - const [error, setError] = useState(null); - const [prompt, setPrompt] = useState(""); - const [query, setQuery] = useState(""); - const [groundTruthLabel, setGroundTruthLabel] = useState(""); - const [isSaving, setIsSaving] = useState(false); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - - const loadDocument = async () => { - if (!filePath) return; - - setIsLoading(true); - setError(null); - - try { - if (filePath === "./src/test-document.txt") { - const testContent = `This is a sample document for testing the data labeling functionality in Feast UI. - -The document contains multiple paragraphs and sections that can be used to test the text highlighting and labeling features. - -This paragraph discusses machine learning and artificial intelligence concepts. It covers topics like neural networks, deep learning, and natural language processing. Users should be able to select and label relevant portions of this text for RAG retrieval systems. - -Another section focuses on data engineering and ETL pipelines. This content explains how to process large datasets and build scalable data infrastructure. The labeling system should allow users to mark this as relevant or irrelevant for their specific use cases. - -The final paragraph contains information about feature stores and real-time machine learning systems. This text can be used to test the highlighting functionality and ensure that labels are properly stored and displayed in the user interface.`; - - setDocumentContent({ - content: testContent, - file_path: filePath, - }); - - loadSavedLabels(); - } else { - throw new Error( - "Document not found. Please use the test document path: ./src/test-document.txt", - ); - } - } catch (err) { - setError( - err instanceof Error - ? err.message - : "An error occurred while loading the document", - ); - } finally { - setIsLoading(false); - } - }; - - const handleTextSelection = () => { - const selection = window.getSelection(); - if (selection && selection.toString().trim() && documentContent) { - const selectedTextContent = selection.toString().trim(); - const range = selection.getRangeAt(0); - - const textContent = documentContent.content; - - let startIndex = -1; - let endIndex = -1; - - const rangeText = range.toString(); - if (rangeText) { - startIndex = textContent.indexOf(rangeText); - if (startIndex !== -1) { - endIndex = startIndex + rangeText.length; - } - } - - if (startIndex !== -1 && endIndex !== -1) { - setSelectedText({ - text: selectedTextContent, - start: startIndex, - end: endIndex, - }); - } - } - }; - - const handleLabelSelection = () => { - if (selectedText) { - const newLabel: DocumentLabel = { - text: selectedText.text, - start: selectedText.start, - end: selectedText.end, - label: labelingMode, - timestamp: Date.now(), - groundTruthLabel: groundTruthLabel, - }; - - setLabels([...labels, newLabel]); - setSelectedText(null); - setHasUnsavedChanges(true); - - const selection = window.getSelection(); - if (selection) { - selection.removeAllRanges(); - } - } - }; - - const handleRemoveLabel = (index: number) => { - setLabels(labels.filter((_: DocumentLabel, i: number) => i !== index)); - setHasUnsavedChanges(true); - }; - - const saveLabels = () => { - setIsSaving(true); - - setTimeout(() => { - try { - const saveData = { - filePath: filePath, - prompt: prompt, - query: query, - groundTruthLabel: groundTruthLabel, - labels: labels, - timestamp: new Date().toISOString(), - }; - - const pathParts = filePath.split("/"); - const filename = pathParts[pathParts.length - 1]; - const nameWithoutExt = filename.replace(/\.[^/.]+$/, ""); - const downloadFilename = `${nameWithoutExt}-labels.json`; - - const jsonString = JSON.stringify(saveData, null, 2); - const blob = new Blob([jsonString], { type: "application/json" }); - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = downloadFilename; - link.style.display = "none"; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - setHasUnsavedChanges(false); - alert( - `Successfully saved ${labels.length} labels. File downloaded as ${downloadFilename}`, - ); - } catch (error) { - console.error("Error saving labels:", error); - alert("Error saving labels. Please try again."); - } finally { - setIsSaving(false); - } - }, 100); - }; - - const loadSavedLabels = () => { - try { - const savedData = JSON.parse(localStorage.getItem("ragLabels") || "[]"); - const fileData = savedData.find( - (item: any) => item.filePath === filePath, - ); - - if (fileData) { - setPrompt(fileData.prompt || ""); - setQuery(fileData.query || ""); - setGroundTruthLabel(fileData.groundTruthLabel || ""); - setLabels(fileData.labels || []); - setHasUnsavedChanges(false); - } - } catch (error) { - console.error("Error loading saved labels:", error); - } - }; - - const renderDocumentWithHighlights = ( - content: string, - ): (string | React.ReactElement)[] => { - const allHighlights = [...labels]; - - if (selectedText) { - allHighlights.push({ - text: selectedText.text, - start: selectedText.start, - end: selectedText.end, - label: "temp-selection", - timestamp: 0, - groundTruthLabel: "", - }); - } - - if (allHighlights.length === 0) { - return [content]; - } - - const sortedHighlights = [...allHighlights].sort( - (a, b) => a.start - b.start, - ); - const result: (string | React.ReactElement)[] = []; - let lastIndex = 0; - - sortedHighlights.forEach((highlight, index) => { - result.push(content.slice(lastIndex, highlight.start)); - - let highlightColor, borderColor; - - if (highlight.label === "temp-selection") { - if (colorMode === "dark") { - highlightColor = "#1a4d66"; - borderColor = "#2d6b8a"; - } else { - highlightColor = "#add8e6"; - borderColor = "#87ceeb"; - } - } else if (highlight.label === "irrelevant") { - if (colorMode === "dark") { - highlightColor = "#4d1a1a"; - borderColor = "#6b2d2d"; - } else { - highlightColor = "#f8d7da"; - borderColor = "#f5c6cb"; - } - } else { - if (colorMode === "dark") { - highlightColor = "#1a4d1a"; - borderColor = "#2d6b2d"; - } else { - highlightColor = "#d4edda"; - borderColor = "#c3e6cb"; - } - } - - result.push( - - {highlight.text} - , - ); - - lastIndex = highlight.end; - }); - - result.push(content.slice(lastIndex)); - return result; - }; - - const labelingOptions = [ - { - id: "relevant", - label: "Relevant", - }, - { - id: "irrelevant", - label: "Irrelevant", - }, - ]; - - return ( - - -

- Load a document and highlight text chunks to label them for chunk - extraction/retrieval. Add prompt and query context, then provide - ground truth labels for generation evaluation. -

-
- - - - - - - setFilePath(e.target.value)} - /> - - - - - - Load Document - - - - - - - - {isLoading && ( - - - - - - Loading document... - - - )} - - {error && ( - -

{error}

-
- )} - - {documentContent && ( - <> - - -

RAG Context

-
- - - - - { - setPrompt(e.target.value); - setHasUnsavedChanges(true); - }} - rows={3} - /> - - - - - { - setQuery(e.target.value); - setHasUnsavedChanges(true); - }} - rows={3} - /> - - - -
- - - - -

Step 1: Label for Chunk Extraction

-
- - - - - - setLabelingMode(id)} - buttonSize="s" - /> - - - - - - Label Selected Text - - - - - - - - {selectedText && ( - - {selectedText.text} - - )} - - - - - -

Document Content

-
- - -
- {renderDocumentWithHighlights(documentContent.content)} -
-
-
- - - - -

Step 2: Label for Generation

-
- - - - { - setGroundTruthLabel(e.target.value); - setHasUnsavedChanges(true); - }} - rows={3} - /> - - - - - - - - Save Labels - - - - - - - {(labels.length > 0 || groundTruthLabel || prompt || query) && ( - <> - -

- Click "Save Labels" to download your labeled data as a JSON - file. -

-
- - - )} - - - - {hasUnsavedChanges && ( - <> - -

- You have unsaved changes. Click "Save Labels" to persist your - work. -

-
- - - )} - - {labels.length > 0 && ( - <> - - - -

Extracted Chunk Labels ({labels.length})

-
- - {labels.map((label, index) => ( - - - - Chunk: {label.label} - - - {label.groundTruthLabel && ( - - - GT: {label.groundTruthLabel} - - - )} - - - "{label.text.substring(0, 80)} - {label.text.length > 80 ? "..." : ""}" - - - - handleRemoveLabel(index)} - > - Remove - - - - ))} -
- - )} - - )} -
- ); -}; - -export default RagTab; diff --git a/ui/src/pages/document-labeling/index.ts b/ui/src/pages/document-labeling/index.ts deleted file mode 100644 index f3f4012b362..00000000000 --- a/ui/src/pages/document-labeling/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./DocumentLabelingPage"; diff --git a/ui/src/pages/entities/EntitiesListingTable.tsx b/ui/src/pages/entities/EntitiesListingTable.tsx index 51ffb7c8609..625ec7d5fbb 100644 --- a/ui/src/pages/entities/EntitiesListingTable.tsx +++ b/ui/src/pages/entities/EntitiesListingTable.tsx @@ -20,7 +20,8 @@ const EntitiesListingTable = ({ entities }: EntitiesListingTableProps) => { sortable: true, render: (name: string, item: feast.core.IEntity) => { // For "All Projects" view, link to the specific project - const itemProject = item?.spec?.project || projectName; + const itemProject = + item?.spec?.project || (item as any)?.project || projectName; return ( {name} @@ -28,12 +29,19 @@ const EntitiesListingTable = ({ entities }: EntitiesListingTableProps) => { ); }, }, + { + name: "Join Key", + field: "spec.joinKey", + sortable: true, + }, { name: "Type", field: "spec.valueType", sortable: true, - render: (valueType: feast.types.ValueType.Enum) => { - return feast.types.ValueType.Enum[valueType]; + render: (valueType: feast.types.ValueType.Enum | string | undefined) => { + if (!valueType) return "—"; + if (typeof valueType === "string") return valueType; + return feast.types.ValueType.Enum[valueType] || String(valueType); }, }, { @@ -52,7 +60,7 @@ const EntitiesListingTable = ({ entities }: EntitiesListingTableProps) => { if (projectName === "all") { columns.splice(1, 0, { name: "Project", - field: "spec.project", + field: "project", sortable: true, render: (project: string) => { return {project || "Unknown"}; diff --git a/ui/src/pages/entities/EntityInstance.tsx b/ui/src/pages/entities/EntityInstance.tsx index e3be0ef167f..4807568ce2c 100644 --- a/ui/src/pages/entities/EntityInstance.tsx +++ b/ui/src/pages/entities/EntityInstance.tsx @@ -1,31 +1,128 @@ -import React from "react"; +import React, { useState } from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, +} from "@elastic/eui"; import { EntityIcon } from "../../graphics/EntityIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import EntityOverviewTab from "./EntityOverviewTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import EntityFormModal, { + EntityFormData, +} from "../../components/EntityFormModal"; +import { + useApplyEntity, + useDeleteEntity, +} from "../../queries/mutations/useEntityMutations"; +import useLoadEntity from "./useLoadEntity"; import { useEntityCustomTabs, useEntityCustomTabRoutes, } from "../../custom-tabs/TabsRegistryContext"; +import { feast } from "../../protos"; + +const buildEditFormData = (entity: feast.core.IEntity): EntityFormData => { + const tags = entity.spec?.tags + ? Object.entries(entity.spec.tags).map(([key, value]) => ({ + key, + value: String(value), + })) + : []; + + const joinKeys = entity.spec?.joinKey ? [entity.spec.joinKey] : [""]; + + return { + name: entity.spec?.name || "", + description: entity.spec?.description || "", + joinKeys, + valueType: String(entity.spec?.valueType ?? 0), + tags, + }; +}; const EntityInstance = () => { const navigate = useNavigate(); - let { entityName } = useParams(); + let { entityName, projectName } = useParams(); const { customNavigationTabs } = useEntityCustomTabs(navigate); const CustomTabRoutes = useEntityCustomTabRoutes(); useDocumentTitle(`${entityName} | Entity | Feast`); + const { data } = useLoadEntity(entityName || ""); + const applyEntity = useApplyEntity(); + const deleteEntity = useDeleteEntity(); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editError, setEditError] = useState(null); + + const handleDelete = () => { + deleteEntity.mutate( + { name: entityName || "", project: projectName || "" }, + { + onSuccess: () => { + navigate(`/p/${projectName}/entity`); + }, + }, + ); + }; + + const handleEditSubmit = (formData: EntityFormData) => { + const payload = { + name: formData.name, + project: projectName || "", + join_key: formData.joinKeys[0] || formData.name, + value_type: parseInt(formData.valueType, 10), + description: formData.description, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + owner: "", + }; + applyEntity.mutate(payload, { + onSuccess: () => { + setIsEditModalOpen(false); + setEditError(null); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setEditError(message); + }, + }); + }; + return ( { + setEditError(null); + setIsEditModalOpen(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={[ { label: "Overview", @@ -43,6 +140,37 @@ const EntityInstance = () => { {CustomTabRoutes}
+ + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteEntity.isLoading} + > +

+ This will permanently remove the entity. This action cannot be + undone. +

+
+ )} + + {isEditModalOpen && data && ( + { + setIsEditModalOpen(false); + setEditError(null); + }} + onSubmit={handleEditSubmit} + initialData={buildEditFormData(data)} + isEdit + isSubmitting={applyEntity.isLoading} + submitError={editError} + /> + )}
); }; diff --git a/ui/src/pages/entities/EntityOverviewTab.tsx b/ui/src/pages/entities/EntityOverviewTab.tsx index 8a20688d140..c590eeb3b8e 100644 --- a/ui/src/pages/entities/EntityOverviewTab.tsx +++ b/ui/src/pages/entities/EntityOverviewTab.tsx @@ -19,7 +19,7 @@ import PermissionsDisplay from "../../components/PermissionsDisplay"; import TagsDisplay from "../../components/TagsDisplay"; import RegistryPathContext from "../../contexts/RegistryPathContext"; import { FEAST_FCO_TYPES } from "../../parsers/types"; -import { feast } from "../../protos"; + import useLoadRegistry from "../../queries/useLoadRegistry"; import { getEntityPermissions } from "../../utils/permissionUtils"; import { toDate } from "../../utils/timestamp"; @@ -40,6 +40,15 @@ const EntityOverviewTab = () => { const fvEdgesSuccess = fvEdges.isSuccess; const fvEdgesData = fvEdges.data; + const viewTypesForEntity: Record | undefined = + fvEdgesSuccess && fvEdgesData && fvEdgesData[eName] + ? fvEdgesData[eName].reduce((acc: Record, r) => { + acc[r.target.name] = + r.target.type === "labelView" ? "labelView" : "featureView"; + return acc; + }, {}) + : undefined; + return ( {isLoading && ( @@ -64,14 +73,22 @@ const EntityOverviewTab = () => { {data?.spec?.joinKey} - Description - - {data?.spec?.description} - + {data?.spec?.valueType && ( + <> + + Value Type + + + {typeof data.spec.valueType === "string" + ? data.spec.valueType + : String(data.spec.valueType)} + + + )} - Value Type + Description - {feast.types.ValueType.Enum[data?.spec?.valueType!]} + {data?.spec?.description || "—"} @@ -109,7 +126,7 @@ const EntityOverviewTab = () => { -

Feature Views

+

Consuming Views

{fvEdgesSuccess && fvEdgesData ? ( @@ -118,13 +135,14 @@ const EntityOverviewTab = () => { fvNames={fvEdgesData[eName].map((r) => { return r.target.name; })} + viewTypes={viewTypesForEntity} /> ) : ( - No feature views have this entity + No views consume this entity ) ) : ( - Error loading feature views that have this entity. + Error loading views that consume this entity. )}
diff --git a/ui/src/pages/entities/FeatureViewEdgesList.tsx b/ui/src/pages/entities/FeatureViewEdgesList.tsx index eca599c852a..79f8eb89251 100644 --- a/ui/src/pages/entities/FeatureViewEdgesList.tsx +++ b/ui/src/pages/entities/FeatureViewEdgesList.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { EuiBasicTable, EuiLoadingSpinner } from "@elastic/eui"; +import { EuiBasicTable, EuiBadge, EuiLoadingSpinner } from "@elastic/eui"; import EuiCustomLink from "../../components/EuiCustomLink"; import { useParams } from "react-router-dom"; import useLoadRelationshipData from "../../queries/useLoadRelationshipsData"; @@ -8,6 +8,7 @@ import { FEAST_FCO_TYPES } from "../../parsers/types"; interface FeatureViewEdgesListInterace { fvNames: string[]; + viewTypes?: Record; } const whereFSconsumesThisFv = (fvName: string) => { @@ -42,7 +43,10 @@ const useGetFSConsumersOfFV = (fvList: string[]) => { }; }; -const FeatureViewEdgesList = ({ fvNames }: FeatureViewEdgesListInterace) => { +const FeatureViewEdgesList = ({ + fvNames, + viewTypes, +}: FeatureViewEdgesListInterace) => { const { projectName } = useParams(); const { isLoading, data } = useGetFSConsumersOfFV(fvNames); @@ -52,10 +56,20 @@ const FeatureViewEdgesList = ({ fvNames }: FeatureViewEdgesListInterace) => { name: "Name", field: "", render: ({ name }: { name: string }) => { + const isLabelView = viewTypes?.[name] === "labelView"; + const path = isLabelView + ? `/p/${projectName}/label-view/${name}` + : `/p/${projectName}/feature-view/${name}`; return ( - - {name} - + + {name} + {isLabelView && ( + <> + {" "} + label view + + )} + ); }, }, diff --git a/ui/src/pages/entities/Index.tsx b/ui/src/pages/entities/Index.tsx index 070c53d38fa..57a2584b071 100644 --- a/ui/src/pages/entities/Index.tsx +++ b/ui/src/pages/entities/Index.tsx @@ -1,38 +1,83 @@ -import React, { useContext } from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; -import { EuiPageTemplate, EuiLoadingSpinner } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiLoadingSpinner, + EuiButton, + EuiCallOut, + EuiSpacer, +} from "@elastic/eui"; import { EntityIcon } from "../../graphics/EntityIcon"; -import useLoadRegistry from "../../queries/useLoadRegistry"; import EntitiesListingTable from "./EntitiesListingTable"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; import EntityIndexEmptyState from "./EntityIndexEmptyState"; import ExportButton from "../../components/ExportButton"; +import EntityFormModal, { + EntityFormData, +} from "../../components/EntityFormModal"; +import { useApplyEntity } from "../../queries/mutations/useEntityMutations"; +import useResourceQuery, { + entityListPath, +} from "../../queries/useResourceQuery"; const useLoadEntities = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.entities; - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: "entities-list", + project: projectName, + restPath: entityListPath(projectName), + restSelect: (d) => d.entities, + }); }; +const formDataToPayload = (formData: EntityFormData, project: string) => ({ + name: formData.name, + project, + join_key: formData.joinKeys[0] || formData.name, + value_type: parseInt(formData.valueType, 10), + description: formData.description, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + owner: "", +}); + const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadEntities(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadEntities(); + const isAllProjects = projectName === "all"; + + const [isModalOpen, setIsModalOpen] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); + const [submitErrorMessage, setSubmitErrorMessage] = useState( + null, + ); + const applyEntity = useApplyEntity(); useDocumentTitle(`Entities | Feast`); + const handleCreateSubmit = (formData: EntityFormData) => { + setSubmitErrorMessage(null); + const payload = formDataToPayload(formData, projectName || ""); + applyEntity.mutate(payload, { + onSuccess: () => { + setIsModalOpen(false); + setSubmitErrorMessage(null); + setSuccessMessage(`Entity "${formData.name}" created successfully.`); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setSubmitErrorMessage(message); + }, + }); + }; + return ( { iconType={EntityIcon} pageTitle="Entities" rightSideItems={[ + ...(isAllProjects + ? [] + : [ + setIsModalOpen(true)} + key="create" + > + Create Entity + , + ]), , ]} /> + {successMessage && ( + <> + + + + )} {isLoading && (

Loading

)} - {isError &&

We encountered an error while loading.

} + {isPermissionDenied && ( + +

You do not have permission to view entities.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} {isSuccess && !data && } {isSuccess && data && }
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setSubmitErrorMessage(null); + }} + onSubmit={handleCreateSubmit} + isSubmitting={applyEntity.isLoading} + submitError={submitErrorMessage} + /> + )}
); }; diff --git a/ui/src/pages/entities/useLoadEntity.ts b/ui/src/pages/entities/useLoadEntity.ts index fdb4a7968f1..cf20c33bd8f 100644 --- a/ui/src/pages/entities/useLoadEntity.ts +++ b/ui/src/pages/entities/useLoadEntity.ts @@ -1,24 +1,18 @@ -import { useContext } from "react"; import { useParams } from "react-router-dom"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import useResourceQuery, { + entityDetailPath, +} from "../../queries/useResourceQuery"; const useLoadEntity = (entityName: string) => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.entities?.find( - (fv) => fv?.spec?.name === entityName, - ); - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: `entity:${entityName}`, + project: projectName, + restPath: entityDetailPath(entityName, projectName || ""), + restSelect: (d) => d, + enabled: !!entityName, + }); }; export default useLoadEntity; diff --git a/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx b/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx index a8080d0a68b..e2905f1fd02 100644 --- a/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx +++ b/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx @@ -2,28 +2,41 @@ import React from "react"; import { EuiEmptyPrompt, EuiTitle, EuiLink, EuiButton } from "@elastic/eui"; import FeastIconBlue from "../../graphics/FeastIconBlue"; -const FeatureServiceIndexEmptyState = () => { +interface FeatureServiceIndexEmptyStateProps { + onCreate?: () => void; +} + +const FeatureServiceIndexEmptyState: React.FC< + FeatureServiceIndexEmptyStateProps +> = ({ onCreate }) => { return ( There are no feature services} body={

- This project does not have any Feature Services. Learn more about - creating Feature Services in Feast Docs. + Feature services group related features from one or more feature views + for training or online serving. Create your first feature service to + get started.

} actions={ - { - window.open( - "https://docs.feast.dev/getting-started/concepts/feature-retrieval#feature-services", - "_blank", - ); - }} - > - Open Feature Services Docs - + onCreate ? ( + + Create Feature Service + + ) : ( + { + window.open( + "https://docs.feast.dev/getting-started/concepts/feature-retrieval#feature-services", + "_blank", + ); + }} + > + Open Feature Services Docs + + ) } footer={ <> diff --git a/ui/src/pages/feature-services/FeatureServiceInstance.tsx b/ui/src/pages/feature-services/FeatureServiceInstance.tsx index b88d2f4bdbf..4c16b383e94 100644 --- a/ui/src/pages/feature-services/FeatureServiceInstance.tsx +++ b/ui/src/pages/feature-services/FeatureServiceInstance.tsx @@ -1,11 +1,24 @@ -import React from "react"; +import React, { useState } from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, +} from "@elastic/eui"; import { FeatureServiceIcon } from "../../graphics/FeatureServiceIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import FeatureServiceOverviewTab from "./FeatureServiceOverviewTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import FeatureServiceFormModal, { + FeatureServiceFormData, +} from "../../components/FeatureServiceFormModal"; +import { + useApplyFeatureService, + useDeleteFeatureService, +} from "../../queries/mutations/useFeatureServiceMutations"; +import useLoadFeatureService from "./useLoadFeatureService"; import { useFeatureServiceCustomTabs, @@ -14,19 +27,107 @@ import { const FeatureServiceInstance = () => { const navigate = useNavigate(); - let { featureServiceName } = useParams(); + let { featureServiceName, projectName } = useParams(); useDocumentTitle(`${featureServiceName} | Feature Service | Feast`); const { customNavigationTabs } = useFeatureServiceCustomTabs(navigate); const CustomTabRoutes = useFeatureServiceCustomTabRoutes(); + const { data } = useLoadFeatureService(featureServiceName || ""); + const deleteFeatureService = useDeleteFeatureService(); + const applyFeatureService = useApplyFeatureService(); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editError, setEditError] = useState(null); + + const handleDelete = () => { + deleteFeatureService.mutate( + { name: featureServiceName || "", project: projectName || "" }, + { + onSuccess: () => { + navigate(`/p/${projectName}/feature-service`); + }, + }, + ); + }; + + const buildInitialEditData = (): FeatureServiceFormData | undefined => { + if (!data?.spec) return undefined; + const spec = data.spec; + return { + name: spec.name || featureServiceName || "", + description: spec.description || "", + owner: spec.owner || "", + projections: (spec.features || []).map((proj: any) => ({ + featureViewName: proj.featureViewName || "", + featureNames: (proj.featureColumns || []) + .map((col: any) => col.name) + .filter(Boolean), + })), + tags: Object.entries(spec.tags || {}).map(([key, value]) => ({ + key, + value: value as string, + })), + }; + }; + + const handleEditSubmit = (formData: FeatureServiceFormData) => { + const payload = { + name: formData.name, + project: projectName || "", + features: formData.projections.map((projection) => ({ + feature_view_name: projection.featureViewName, + feature_names: projection.featureNames, + })), + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags + .filter((tag) => tag.key.trim()) + .map((tag) => [tag.key, tag.value]), + ), + }; + applyFeatureService.mutate(payload, { + onSuccess: () => { + setIsEditModalOpen(false); + setEditError(null); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setEditError(message); + }, + }); + }; + return ( { + setEditError(null); + setIsEditModalOpen(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={[ { label: "Overview", @@ -44,6 +145,37 @@ const FeatureServiceInstance = () => { {CustomTabRoutes}
+ + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteFeatureService.isLoading} + > +

+ This will permanently remove the feature service. This action cannot + be undone. +

+
+ )} + + {isEditModalOpen && ( + { + setIsEditModalOpen(false); + setEditError(null); + }} + onSubmit={handleEditSubmit} + initialData={buildInitialEditData()} + isEdit={true} + isSubmitting={applyFeatureService.isLoading} + submitError={editError} + /> + )}
); }; diff --git a/ui/src/pages/feature-services/FeatureServiceListingTable.tsx b/ui/src/pages/feature-services/FeatureServiceListingTable.tsx index acc68b6e619..c26b976e197 100644 --- a/ui/src/pages/feature-services/FeatureServiceListingTable.tsx +++ b/ui/src/pages/feature-services/FeatureServiceListingTable.tsx @@ -29,8 +29,8 @@ const FeatureServiceListingTable = ({ name: "Name", field: "spec.name", render: (name: string, item: feast.core.IFeatureService) => { - // For "All Projects" view, link to the specific project - const itemProject = item?.spec?.project || projectName; + const itemProject = + item?.spec?.project || (item as any)?.project || projectName; return ( {name} @@ -41,10 +41,12 @@ const FeatureServiceListingTable = ({ { name: "# of Features", field: "spec.features", - render: (featureViews: feast.core.IFeatureViewProjection[]) => { - var numFeatures = 0; - featureViews.forEach((featureView) => { - numFeatures += featureView.featureColumns!.length; + render: ( + featureViews: feast.core.IFeatureViewProjection[] | undefined, + ) => { + let numFeatures = 0; + (featureViews || []).forEach((featureView) => { + numFeatures += (featureView.featureColumns || []).length; }); return numFeatures; }, @@ -58,11 +60,10 @@ const FeatureServiceListingTable = ({ }, ]; - // Add Project column when viewing all projects if (projectName === "all") { columns.splice(1, 0, { name: "Project", - field: "spec.project", + field: "project", sortable: true, render: (project: string) => { return project || "Unknown"; diff --git a/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx b/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx index be922e41261..f1ac3c5e349 100644 --- a/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx +++ b/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx @@ -34,11 +34,19 @@ const FeatureServiceOverviewTab = () => { const isEmpty = data === undefined; let numFeatures = 0; - let numFeatureViews = 0; + let numLabels = 0; + const featureProjections: any[] = []; + const labelProjections: any[] = []; if (data) { - data?.spec?.features?.forEach((featureView) => { - numFeatureViews += 1; - numFeatures += featureView?.featureColumns!.length; + data?.spec?.features?.forEach((featureView: any) => { + const columnCount = (featureView?.featureColumns || []).length; + if (featureView.viewType === "labelView") { + numLabels += columnCount; + labelProjections.push(featureView); + } else { + numFeatures += columnCount; + featureProjections.push(featureView); + } }); } @@ -57,7 +65,7 @@ const FeatureServiceOverviewTab = () => { - + @@ -66,7 +74,7 @@ const FeatureServiceOverviewTab = () => { @@ -90,14 +98,43 @@ const FeatureServiceOverviewTab = () => {

Features

- {data?.spec?.features ? ( - + {featureProjections.length > 0 ? ( + ) : ( No features specified for this feature service. )} + {labelProjections.length > 0 && ( + + + + + + + + +

from

+
+
+ + + +
+ + + +

Labels

+
+ + +
+
+ )} @@ -153,16 +190,28 @@ const FeatureServiceOverviewTab = () => { -

All Feature Views

+

All Views

{data?.spec?.features?.length! > 0 ? ( { + data?.spec?.features?.map((f: any) => { return f.featureViewName!; })! } + viewTypes={ + data?.spec?.features?.reduce( + (acc: Record, f: any) => { + if (f.featureViewName) { + acc[f.featureViewName] = + f.viewType || "featureView"; + } + return acc; + }, + {}, + ) || {} + } /> ) : ( No feature views in this feature service diff --git a/ui/src/pages/feature-services/Index.tsx b/ui/src/pages/feature-services/Index.tsx index 260a9b821dc..b50b6066a24 100644 --- a/ui/src/pages/feature-services/Index.tsx +++ b/ui/src/pages/feature-services/Index.tsx @@ -1,19 +1,20 @@ -import React, { useContext } from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { EuiPageTemplate, EuiLoadingSpinner, - EuiTitle, EuiSpacer, + EuiTitle, + EuiFieldSearch, EuiFlexGroup, EuiFlexItem, - EuiFieldSearch, + EuiButton, + EuiCallOut, } from "@elastic/eui"; import { FeatureServiceIcon } from "../../graphics/FeatureServiceIcon"; -import useLoadRegistry from "../../queries/useLoadRegistry"; import FeatureServiceListingTable from "./FeatureServiceListingTable"; import { useSearchQuery, @@ -22,27 +23,29 @@ import { tagTokenGroupsType, } from "../../hooks/useSearchInputWithTags"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; import FeatureServiceIndexEmptyState from "./FeatureServiceIndexEmptyState"; import TagSearch from "../../components/TagSearch"; import ExportButton from "../../components/ExportButton"; +import FeatureServiceFormModal, { + FeatureServiceFormData, +} from "../../components/FeatureServiceFormModal"; +import { useApplyFeatureService } from "../../queries/mutations/useFeatureServiceMutations"; import { useFeatureServiceTagsAggregation } from "../../hooks/useTagsAggregation"; import { feast } from "../../protos"; +import useResourceQuery, { + featureServiceListPath, + featureViewListPath, + restFeatureViewsToMergedList, +} from "../../queries/useResourceQuery"; const useLoadFeatureServices = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.featureServices; - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: "feature-services-list", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices, + }); }; const shouldIncludeFSsGivenTokenGroups = ( @@ -54,7 +57,7 @@ const shouldIncludeFSsGivenTokenGroups = ( if (entryTagValue) { return values.every((value) => { - return value.length > 0 ? entryTagValue.indexOf(value) >= 0 : true; // Don't filter if the string is empty + return value.length > 0 ? entryTagValue.indexOf(value) >= 0 : true; }); } else { return false; @@ -88,10 +91,46 @@ const filterFn = ( return filteredByTags; }; +const formDataToPayload = ( + formData: FeatureServiceFormData, + project: string, +) => ({ + name: formData.name, + project, + features: formData.projections.map((projection) => ({ + feature_view_name: projection.featureViewName, + feature_names: projection.featureNames, + })), + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags + .filter((tag) => tag.key.trim()) + .map((tag) => [tag.key, tag.value]), + ), +}); + const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadFeatureServices(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadFeatureServices(); + const isAllProjects = projectName === "all"; const tagAggregationQuery = useFeatureServiceTagsAggregation(); + const featureViewsQuery = useResourceQuery({ + resourceType: "feature-views-list-fs-prereq", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: restFeatureViewsToMergedList, + enabled: !isAllProjects, + }); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [prereqWarning, setPrereqWarning] = useState(null); + const applyFeatureService = useApplyFeatureService(); + useDocumentTitle(`Feature Services | Feast`); const { searchString, searchTokens, setSearchString } = useSearchQuery(); @@ -112,6 +151,40 @@ const Index = () => { ? filterFn(data, { tagTokenGroups, searchTokens }) : data; + const handleCreateClick = () => { + const featureViews = featureViewsQuery.data || []; + if (featureViews.length === 0) { + setPrereqWarning( + "Feature services require at least one feature view. Create a feature view first, or proceed and add views later.", + ); + } else { + setPrereqWarning(null); + } + setIsModalOpen(true); + }; + + const handleCreateSubmit = (formData: FeatureServiceFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyFeatureService.mutate(payload, { + onSuccess: () => { + setIsModalOpen(false); + setErrorMessage(null); + setPrereqWarning(null); + setSuccessMessage( + `Feature service "${formData.name}" created successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setErrorMessage(message); + }, + }); + }; + + const showEmptyState = isSuccess && (!data || data.length === 0); + return ( { iconType={FeatureServiceIcon} pageTitle="Feature Services" rightSideItems={[ + ...(isAllProjects + ? [] + : [ + + Create Feature Service + , + ]), , ]} /> + {successMessage && ( + <> + + + + )} + {prereqWarning && !isModalOpen && ( + <> + + + + )} {isLoading && (

Loading

)} - {isError &&

We encountered an error while loading.

} - {isSuccess && !data && } - {isSuccess && filterResult && ( + {isPermissionDenied && ( + +

You do not have permission to view feature services.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} + {showEmptyState && ( + + )} + {isSuccess && filterResult && filterResult.length > 0 && ( @@ -169,6 +288,18 @@ const Index = () => { )}
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setErrorMessage(null); + }} + onSubmit={handleCreateSubmit} + isSubmitting={applyFeatureService.isLoading} + submitError={errorMessage} + /> + )}
); }; diff --git a/ui/src/pages/feature-services/useLoadFeatureService.ts b/ui/src/pages/feature-services/useLoadFeatureService.ts index 004ab35b927..81fff2e931d 100644 --- a/ui/src/pages/feature-services/useLoadFeatureService.ts +++ b/ui/src/pages/feature-services/useLoadFeatureService.ts @@ -1,53 +1,50 @@ -import { FEAST_FCO_TYPES } from "../../parsers/types"; -import { useContext } from "react"; import { useParams } from "react-router-dom"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; - -import useLoadRegistry from "../../queries/useLoadRegistry"; import { EntityReference } from "../../parsers/parseEntityRelationships"; +import useResourceQuery, { + featureServiceDetailPath, +} from "../../queries/useResourceQuery"; const useLoadFeatureService = (featureServiceName: string) => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.featureServices?.find( - (fs) => fs?.spec?.name === featureServiceName, - ); + const fsQuery = useResourceQuery({ + resourceType: `feature-service:${featureServiceName}`, + project: projectName, + restPath: featureServiceDetailPath(featureServiceName, projectName || ""), + restSelect: (d) => ({ + featureService: d, + indirectRelationships: d?.relationships || [], + permissions: d?.permissions || [], + }), + enabled: !!featureServiceName, + }); + + const featureService = fsQuery.data?.featureService; + const indirectRelationships = fsQuery.data?.indirectRelationships || []; + const permissions = fsQuery.data?.permissions || []; - let entities = - data === undefined + let entities: EntityReference[] | undefined = + featureService === undefined ? undefined - : registryQuery.data?.indirectRelationships - .filter((relationship) => { - return ( - relationship.target.type === FEAST_FCO_TYPES.featureService && - relationship.target.name === data?.spec?.name && - relationship.source.type === FEAST_FCO_TYPES.entity - ); - }) - .map((relationship) => { - return relationship.source; - }); - // Deduplicate on name of entity + : indirectRelationships + .filter( + (rel: any) => + rel?.target?.type === "featureService" && + rel?.source?.type === "entity", + ) + .map((rel: any) => rel.source); + if (entities) { - let entityToName: { [key: string]: EntityReference } = {}; - for (let entity of entities) { + const entityToName: { [key: string]: EntityReference } = {}; + for (const entity of entities) { entityToName[entity.name] = entity; } entities = Object.values(entityToName); } + return { - ...registryQuery, - data: data - ? { - ...data, - permissions: registryQuery.data?.permissions, - } - : undefined, + ...fsQuery, + data: featureService ? { ...featureService, permissions } : undefined, entities, }; }; diff --git a/ui/src/pages/feature-views/CurlGeneratorTab.tsx b/ui/src/pages/feature-views/CurlGeneratorTab.tsx index 147921458ee..5c83440a2a0 100644 --- a/ui/src/pages/feature-views/CurlGeneratorTab.tsx +++ b/ui/src/pages/feature-views/CurlGeneratorTab.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { EuiPanel, EuiTitle, @@ -16,23 +16,22 @@ import { import { CodeBlock, github } from "react-code-blocks"; import { RegularFeatureViewCustomTabProps } from "../../custom-tabs/types"; +const defaultServerUrl = + process.env.REACT_APP_FEAST_FEATURE_SERVER_URL || "http://localhost:6566"; + const CurlGeneratorTab = ({ feastObjectQuery, }: RegularFeatureViewCustomTabProps) => { const data = feastObjectQuery.data as any; const [serverUrl, setServerUrl] = useState(() => { const savedUrl = localStorage.getItem("feast-feature-server-url"); - return savedUrl || "http://localhost:6566"; + return savedUrl || defaultServerUrl; }); const [entityValues, setEntityValues] = useState>({}); const [selectedFeatures, setSelectedFeatures] = useState< Record >({}); - useEffect(() => { - localStorage.setItem("feast-feature-server-url", serverUrl); - }, [serverUrl]); - if (feastObjectQuery.isLoading) { return Loading...; } @@ -106,8 +105,12 @@ const CurlGeneratorTab = ({ setServerUrl(e.target.value)} - placeholder="http://localhost:6566" + onChange={(e) => { + const nextValue = e.target.value; + setServerUrl(nextValue); + localStorage.setItem("feast-feature-server-url", nextValue); + }} + placeholder={defaultServerUrl} /> diff --git a/ui/src/pages/feature-views/FeatureViewListingTable.tsx b/ui/src/pages/feature-views/FeatureViewListingTable.tsx index e865abe6e74..9fd6f8f8fd7 100644 --- a/ui/src/pages/feature-views/FeatureViewListingTable.tsx +++ b/ui/src/pages/feature-views/FeatureViewListingTable.tsx @@ -31,7 +31,10 @@ const FeatureViewListingTable = ({ sortable: true, render: (name: string, item: genericFVType) => { // For "All Projects" view, link to the specific project - const itemProject = item.object?.spec?.project || projectName; + const itemProject = + item.object?.spec?.project || + (item.object as any)?.project || + projectName; return ( {name}{" "} @@ -49,6 +52,13 @@ const FeatureViewListingTable = ({ return features.length; }, }, + { + name: "Version", + render: (item: genericFVType) => { + const ver = (item.object as any)?.meta?.currentVersionNumber; + return ver != null && ver > 0 ? `v${ver}` : "—"; + }, + }, ]; // Add Project column when viewing all projects @@ -56,7 +66,13 @@ const FeatureViewListingTable = ({ columns.splice(1, 0, { name: "Project", render: (item: genericFVType) => { - return {item.object?.spec?.project || "Unknown"}; + return ( + + {item.object?.spec?.project || + (item.object as any)?.project || + "Unknown"} + + ); }, }); } diff --git a/ui/src/pages/feature-views/FeatureViewUsagePanel.tsx b/ui/src/pages/feature-views/FeatureViewUsagePanel.tsx new file mode 100644 index 00000000000..71eaebedd02 --- /dev/null +++ b/ui/src/pages/feature-views/FeatureViewUsagePanel.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import { + EuiBadge, + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiPanel, + EuiText, + EuiTitle, + EuiToolTip, +} from "@elastic/eui"; +import useLoadFeatureUsage, { + FeatureUsageEntry, +} from "../../queries/useLoadFeatureUsage"; + +interface FeatureViewUsagePanelProps { + featureViewName: string; +} + +const formatTimestamp = (ts: number | null): string => { + if (ts == null) return "Never"; + const date = new Date(ts); + return date.toLocaleString(); +}; + +const formatRelativeTime = (ts: number | null): string => { + if (ts == null) return ""; + const now = Date.now(); + const diffMs = now - ts; + const diffMin = Math.floor(diffMs / 60000); + if (diffMin < 1) return "just now"; + if (diffMin < 60) return `${diffMin}m ago`; + const diffHr = Math.floor(diffMin / 60); + if (diffHr < 24) return `${diffHr}h ago`; + const diffDays = Math.floor(diffHr / 24); + return `${diffDays}d ago`; +}; + +const FeatureViewUsagePanel = ({ + featureViewName, +}: FeatureViewUsagePanelProps) => { + const { data, isLoading, isError } = useLoadFeatureUsage(); + + if (isLoading) { + return ( + + +

MLflow Usage

+
+ + + + + + +
+ ); + } + + if (isError || !data || !data.mlflow_enabled) { + return null; + } + + const usage: FeatureUsageEntry | undefined = + data.feature_usage?.[featureViewName]; + + if (!usage || usage.run_count === 0) { + return ( + + +

MLflow Usage

+
+ + + No MLflow training runs have used this feature view. + +
+ ); + } + + const modelItems = usage.models.map((name) => ({ name })); + + const modelColumns = [ + { + name: "Registered Model", + field: "name", + render: (name: string) => {name}, + }, + ]; + + return ( + + +

MLflow Usage

+
+ + + + + Training runs: {usage.run_count} + + + + + + Last used: {formatRelativeTime(usage.last_used)} + + + + + {modelItems.length > 0 && ( + <> + + + Registered models using this feature view: + + + + )} +
+ ); +}; + +export default FeatureViewUsagePanel; diff --git a/ui/src/pages/feature-views/FeatureViewVersionsTab.tsx b/ui/src/pages/feature-views/FeatureViewVersionsTab.tsx new file mode 100644 index 00000000000..f388d247da4 --- /dev/null +++ b/ui/src/pages/feature-views/FeatureViewVersionsTab.tsx @@ -0,0 +1,268 @@ +import React, { useContext, useState, useMemo } from "react"; +import { + EuiBasicTable, + EuiText, + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import { feast } from "../../protos"; +import { toDate } from "../../utils/timestamp"; +import { useParams } from "react-router-dom"; + +interface FeatureViewVersionsTabProps { + featureViewName: string; +} + +interface DecodedVersion { + record: feast.core.IFeatureViewVersionRecord; + features: feast.core.IFeatureSpecV2[]; + entities: string[]; + description: string; + udfBody: string | null; +} + +const decodeVersionProto = ( + record: feast.core.IFeatureViewVersionRecord, +): DecodedVersion => { + const result: DecodedVersion = { + record, + features: [], + entities: [], + description: "", + udfBody: null, + }; + + if (!record.featureViewProto || record.featureViewProto.length === 0) { + return result; + } + + try { + const bytes = + record.featureViewProto instanceof Uint8Array + ? record.featureViewProto + : new Uint8Array(record.featureViewProto); + + if (record.featureViewType === "on_demand_feature_view") { + const odfv = feast.core.OnDemandFeatureView.decode(bytes); + result.features = odfv.spec?.features || []; + result.description = odfv.spec?.description || ""; + result.udfBody = + odfv.spec?.featureTransformation?.userDefinedFunction?.bodyText || + odfv.spec?.userDefinedFunction?.bodyText || + null; + } else if (record.featureViewType === "stream_feature_view") { + const sfv = feast.core.StreamFeatureView.decode(bytes); + result.features = sfv.spec?.features || []; + result.entities = sfv.spec?.entities || []; + result.description = sfv.spec?.description || ""; + } else if (record.featureViewType === "label_view") { + const lv = feast.core.LabelView.decode(bytes); + result.features = lv.spec?.features || []; + result.entities = lv.spec?.entities || []; + result.description = lv.spec?.description || ""; + } else { + const fv = feast.core.FeatureView.decode(bytes); + result.features = fv.spec?.features || []; + result.entities = fv.spec?.entities || []; + result.description = fv.spec?.description || ""; + } + } catch (e) { + console.error("Failed to decode version proto:", e); + } + + return result; +}; + +const VersionDetail = ({ decoded }: { decoded: DecodedVersion }) => { + return ( + + {decoded.description && ( + + + {decoded.description} + + + )} + {decoded.udfBody && ( + + + +

Transformation

+
+ + + {decoded.udfBody} + +
+
+ )} + + + + +

Features ({decoded.features.length})

+
+ + {decoded.features.length > 0 ? ( + + typeof vt === "string" + ? vt + : feast.types.ValueType.Enum[vt] || String(vt || ""), + }, + ]} + /> + ) : ( + No features in this version. + )} +
+
+ {decoded.entities.length > 0 && ( + + + +

Entities

+
+ + + {decoded.entities.map((entity) => ( + + {entity} + + ))} + +
+
+ )} +
+
+ ); +}; + +const FeatureViewVersionsTab = ({ + featureViewName, +}: FeatureViewVersionsTabProps) => { + const registryUrl = useContext(RegistryPathContext); + const { projectName } = useParams(); + const registryQuery = useLoadRegistry(registryUrl, projectName); + const [expandedRows, setExpandedRows] = useState>({}); + + const records = useMemo( + () => + registryQuery.data?.objects?.featureViewVersionHistory?.records?.filter( + (r: feast.core.IFeatureViewVersionRecord) => + r.featureViewName === featureViewName, + ) || [], + [ + registryQuery.data?.objects?.featureViewVersionHistory?.records, + featureViewName, + ], + ); + + const decodedVersions: DecodedVersion[] = useMemo( + () => records.map(decodeVersionProto), + [records], + ); + + if (records.length === 0) { + return No version history available.; + } + + const toggleRow = (versionNumber: number) => { + setExpandedRows((prev) => ({ + ...prev, + [versionNumber]: !prev[versionNumber], + })); + }; + + const columns = [ + { + field: "record.versionNumber", + name: "Version", + render: (_: unknown, item: DecodedVersion) => + `v${item.record.versionNumber}`, + sortable: true, + width: "80px", + }, + { + name: "Features", + render: (item: DecodedVersion) => `${item.features.length}`, + width: "80px", + }, + { + field: "record.featureViewType", + name: "Type", + render: (_: unknown, item: DecodedVersion) => + item.record.featureViewType || "—", + }, + { + field: "record.createdTimestamp", + name: "Created", + render: (_: unknown, item: DecodedVersion) => + item.record.createdTimestamp + ? toDate(item.record.createdTimestamp).toLocaleString() + : "—", + }, + { + field: "record.versionId", + name: "Version ID", + render: (_: unknown, item: DecodedVersion) => + item.record.versionId || "—", + }, + ]; + + const itemIdToExpandedRowMap: Record = {}; + decodedVersions.forEach((decoded) => { + const vn = decoded.record.versionNumber!; + if (expandedRows[vn]) { + itemIdToExpandedRowMap[String(vn)] = ; + } + }); + + return ( + String(item.record.versionNumber)} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + columns={[ + { + isExpander: true, + width: "40px", + render: (item: DecodedVersion) => { + const vn = item.record.versionNumber!; + return ( + + ); + }, + }, + ...columns, + ]} + /> + ); +}; + +export default FeatureViewVersionsTab; diff --git a/ui/src/pages/feature-views/Index.tsx b/ui/src/pages/feature-views/Index.tsx index b1c28895370..879aa717574 100644 --- a/ui/src/pages/feature-views/Index.tsx +++ b/ui/src/pages/feature-views/Index.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { @@ -9,11 +9,12 @@ import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, + EuiButton, + EuiCallOut, } from "@elastic/eui"; import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; -import useLoadRegistry from "../../queries/useLoadRegistry"; import FeatureViewListingTable from "./FeatureViewListingTable"; import { filterInputInterface, @@ -22,26 +23,29 @@ import { } from "../../hooks/useSearchInputWithTags"; import { genericFVType, regularFVInterface } from "../../parsers/mergedFVTypes"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; import FeatureViewIndexEmptyState from "./FeatureViewIndexEmptyState"; import { useFeatureViewTagsAggregation } from "../../hooks/useTagsAggregation"; import TagSearch from "../../components/TagSearch"; import ExportButton from "../../components/ExportButton"; +import FeatureViewFormModal, { + FeatureViewFormData, +} from "../../components/FeatureViewFormModal"; +import { useApplyFeatureView } from "../../queries/mutations/useFeatureViewMutations"; +import useResourceQuery, { + featureViewListPath, + restFeatureViewsToMergedList, + entityListPath, + dataSourceListPath, +} from "../../queries/useResourceQuery"; const useLoadFeatureViews = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.mergedFVList; - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: "feature-views-list", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: restFeatureViewsToMergedList, + }); }; const shouldIncludeFVsGivenTokenGroups = ( @@ -49,13 +53,12 @@ const shouldIncludeFVsGivenTokenGroups = ( tagTokenGroups: Record, ) => { return Object.entries(tagTokenGroups).every(([key, values]) => { - const entryTagValue = entry?.object?.spec!.tags - ? entry.object.spec.tags[key] - : undefined; + const tags = entry?.object?.spec?.tags; + const entryTagValue = tags ? (tags as any)[key] : undefined; if (entryTagValue) { return values.every((value) => { - return value.length > 0 ? entryTagValue.indexOf(value) >= 0 : true; // Don't filter if the string is empty + return value.length > 0 ? entryTagValue.indexOf(value) >= 0 : true; }); } else { return false; @@ -74,7 +77,7 @@ const filterFn = (data: genericFVType[], filterInput: filterInputInterface) => { filterInput.tagTokenGroups, ); } else { - return false; // ODFVs don't have tags yet + return false; } }); } @@ -90,9 +93,73 @@ const filterFn = (data: genericFVType[], filterInput: filterInputInterface) => { return filteredByTags; }; +const TTL_UNITS: Record = { + days: 86400, + hours: 3600, + minutes: 60, + seconds: 1, +}; + +const formDataToPayload = (formData: FeatureViewFormData, project: string) => ({ + name: formData.name, + project, + entities: formData.entities, + features: formData.features.map((f) => ({ + name: f.name, + value_type: parseInt(f.valueType, 10), + description: f.description, + })), + batch_source: formData.batchSource, + ttl_seconds: formData.ttlValue * (TTL_UNITS[formData.ttlUnit] || 1), + online: formData.online, + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), +}); + const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadFeatureViews(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadFeatureViews(); + const isAllProjects = projectName === "all"; + + const entitiesQuery = useResourceQuery({ + resourceType: "entities-list-fv-prereq", + project: projectName, + restPath: entityListPath(projectName), + restSelect: (d) => d.entities, + }); + const dataSourcesQuery = useResourceQuery({ + resourceType: "data-sources-list-fv-prereq", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources, + }); + const tagAggregationQuery = useFeatureViewTagsAggregation(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [prereqWarning, setPrereqWarning] = useState(null); + const applyFeatureView = useApplyFeatureView(); + + const handleCreateClick = () => { + const missingDeps: string[] = []; + const entities = entitiesQuery.data || []; + const dataSources = dataSourcesQuery.data || []; + + if (entities.length === 0) missingDeps.push("entities"); + if (dataSources.length === 0) missingDeps.push("data sources"); + + if (missingDeps.length > 0) { + setPrereqWarning( + `Feature views require at least one entity and one data source. Missing: ${missingDeps.join(" and ")}. You can still proceed — the form will let you create them inline.`, + ); + } + setIsModalOpen(true); + }; useDocumentTitle(`Feature Views | Feast`); @@ -114,6 +181,27 @@ const Index = () => { ? filterFn(data, { tagTokenGroups, searchTokens }) : data; + const handleCreateSubmit = (formData: FeatureViewFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyFeatureView.mutate(payload, { + onSuccess: () => { + setIsModalOpen(false); + setErrorMessage(null); + setPrereqWarning(null); + setSuccessMessage( + `Feature view "${formData.name}" created successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + // Error shown inside the modal via submitError prop + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setErrorMessage(message); + }, + }); + }; + return ( { iconType={FeatureViewIcon} pageTitle="Feature Views" rightSideItems={[ + ...(isAllProjects + ? [] + : [ + + Create Feature View + , + ]), , ]} /> + {prereqWarning && ( + <> + +

{prereqWarning}

+
+ + + )} + {successMessage && ( + <> + + + + )} + {errorMessage && ( + <> + + + + )} {isLoading && (

Loading

)} - {isError &&

We encountered an error while loading.

} + {isPermissionDenied && ( + +

You do not have permission to view feature views.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} {isSuccess && data?.length === 0 && } {isSuccess && data && data.length > 0 && filterResult && ( @@ -171,6 +314,19 @@ const Index = () => { )}
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setPrereqWarning(null); + setErrorMessage(null); + }} + onSubmit={handleCreateSubmit} + isSubmitting={applyFeatureView.isLoading} + submitError={errorMessage} + /> + )}
); }; diff --git a/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx b/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx index 5a4b48f6d6d..70824219aaa 100644 --- a/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx @@ -1,11 +1,12 @@ import React from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { EuiBadge, EuiPageTemplate } from "@elastic/eui"; import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; -import { useMatchExact } from "../../hooks/useMatchSubpath"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import OnDemandFeatureViewOverviewTab from "./OnDemandFeatureViewOverviewTab"; +import FeatureViewVersionsTab from "./FeatureViewVersionsTab"; import { useOnDemandFeatureViewCustomTabs, @@ -29,7 +30,17 @@ const OnDemandFeatureInstance = ({ data }: OnDemandFeatureInstanceProps) => { + {featureViewName} + {data?.meta?.currentVersionNumber != null && + data.meta.currentVersionNumber > 0 && ( + + v{data.meta.currentVersionNumber} + + )} + + } tabs={[ { label: "Overview", @@ -38,6 +49,13 @@ const OnDemandFeatureInstance = ({ data }: OnDemandFeatureInstanceProps) => { navigate(""); }, }, + { + label: "Versions", + isSelected: useMatchSubpath("versions"), + onClick: () => { + navigate("versions"); + }, + }, ...customNavigationTabs, ]} /> @@ -47,6 +65,12 @@ const OnDemandFeatureInstance = ({ data }: OnDemandFeatureInstanceProps) => { path="/" element={} /> + + } + /> {CustomTabRoutes} diff --git a/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx b/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx index 48d61e45f8f..b800a861481 100644 --- a/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx @@ -1,12 +1,26 @@ -import React, { useContext } from "react"; -import { Route, Routes, useNavigate } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import React, { useContext, useState } from "react"; +import { Route, Routes, useNavigate, useParams } from "react-router-dom"; +import { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, + EuiPageTemplate, +} from "@elastic/eui"; import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import RegularFeatureViewOverviewTab from "./RegularFeatureViewOverviewTab"; import FeatureViewLineageTab from "./FeatureViewLineageTab"; +import FeatureViewVersionsTab from "./FeatureViewVersionsTab"; +import FeatureViewFormModal, { + FeatureViewFormData, +} from "../../components/FeatureViewFormModal"; +import { + useApplyFeatureView, + useDeleteFeatureView, +} from "../../queries/mutations/useFeatureViewMutations"; import { useRegularFeatureViewCustomTabs, @@ -20,12 +34,69 @@ interface RegularFeatureInstanceProps { permissions?: any[]; } +const buildEditFormData = ( + fv: feast.core.IFeatureView, +): FeatureViewFormData => { + const tags = fv.spec?.tags + ? Object.entries(fv.spec.tags).map(([key, value]) => ({ key, value })) + : []; + + const features = (fv.spec?.features || []).map((f) => ({ + name: f.name || "", + valueType: String(f.valueType ?? 0), + description: f.description || "", + })); + + let ttlValue = 0; + let ttlUnit = "seconds"; + if (fv.spec?.ttl?.seconds) { + const secs = + typeof fv.spec.ttl.seconds === "number" + ? fv.spec.ttl.seconds + : ((fv.spec.ttl.seconds as any).toNumber?.() ?? 0); + if (secs > 0 && secs % 86400 === 0) { + ttlValue = secs / 86400; + ttlUnit = "days"; + } else if (secs > 0 && secs % 3600 === 0) { + ttlValue = secs / 3600; + ttlUnit = "hours"; + } else if (secs > 0 && secs % 60 === 0) { + ttlValue = secs / 60; + ttlUnit = "minutes"; + } else { + ttlValue = secs; + ttlUnit = "seconds"; + } + } + + return { + name: fv.spec?.name || "", + description: fv.spec?.description || "", + owner: fv.spec?.owner || "", + entities: fv.spec?.entities || [], + features, + batchSource: fv.spec?.batchSource?.name || "", + ttlValue, + ttlUnit, + online: fv.spec?.online ?? true, + tags, + }; +}; + +const TTL_UNITS: Record = { + days: 86400, + hours: 3600, + minutes: 60, + seconds: 1, +}; + const RegularFeatureInstance = ({ data, permissions, }: RegularFeatureInstanceProps) => { const { enabledFeatureStatistics } = useContext(FeatureFlagsContext); const navigate = useNavigate(); + const { projectName } = useParams(); const { customNavigationTabs } = useRegularFeatureViewCustomTabs(navigate); let tabs = [ @@ -57,16 +128,104 @@ const RegularFeatureInstance = ({ }); } + tabs.push({ + label: "Versions", + isSelected: useMatchSubpath("versions"), + onClick: () => { + navigate("versions"); + }, + }); + tabs.push(...customNavigationTabs); const TabRoutes = useRegularFeatureViewCustomTabRoutes(); + const applyFeatureView = useApplyFeatureView(); + const deleteFeatureView = useDeleteFeatureView(); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editError, setEditError] = useState(null); + + const handleDelete = () => { + deleteFeatureView.mutate( + { name: data?.spec?.name || "", project: projectName || "" }, + { + onSuccess: () => { + navigate(`/p/${projectName}/feature-view`); + }, + }, + ); + }; + + const handleEditSubmit = (formData: FeatureViewFormData) => { + const payload = { + name: formData.name, + project: projectName || "", + entities: formData.entities, + features: formData.features.map((f) => ({ + name: f.name, + value_type: parseInt(f.valueType, 10), + description: f.description, + })), + batch_source: formData.batchSource, + ttl_seconds: formData.ttlValue * (TTL_UNITS[formData.ttlUnit] || 1), + online: formData.online, + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + }; + applyFeatureView.mutate(payload, { + onSuccess: () => { + setIsEditModalOpen(false); + setEditError(null); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setEditError(message); + }, + }); + }; + return ( + {data?.spec?.name} + {data?.meta?.currentVersionNumber != null && + data.meta.currentVersionNumber > 0 && ( + + v{data.meta.currentVersionNumber} + + )} + + } + rightSideItems={[ + { + setEditError(null); + setIsEditModalOpen(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={tabs} /> @@ -84,9 +243,46 @@ const RegularFeatureInstance = ({ path="/lineage" element={} /> + + } + /> {TabRoutes} + + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteFeatureView.isLoading} + > +

+ This will permanently remove the feature view. This action cannot be + undone. +

+
+ )} + + {isEditModalOpen && data && ( + { + setIsEditModalOpen(false); + setEditError(null); + }} + onSubmit={handleEditSubmit} + initialData={buildEditFormData(data)} + isEdit + isSubmitting={applyFeatureView.isLoading} + submitError={editError} + /> + )}
); }; diff --git a/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx b/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx index e766e4fd0ab..e58e690c04e 100644 --- a/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx +++ b/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx @@ -8,6 +8,7 @@ import { EuiStat, EuiText, EuiTitle, + EuiToolTip, } from "@elastic/eui"; import React from "react"; @@ -19,9 +20,11 @@ import { encodeSearchQueryString } from "../../hooks/encodeSearchQueryString"; import { EntityRelation } from "../../parsers/parseEntityRelationships"; import { FEAST_FCO_TYPES } from "../../parsers/types"; import useLoadRelationshipData from "../../queries/useLoadRelationshipsData"; +import useLoadFeatureUsage from "../../queries/useLoadFeatureUsage"; import { getEntityPermissions } from "../../utils/permissionUtils"; import BatchSourcePropertiesView from "../data-sources/BatchSourcePropertiesView"; import ConsumingFeatureServicesList from "./ConsumingFeatureServicesList"; +import FeatureViewUsagePanel from "./FeatureViewUsagePanel"; import { feast } from "../../protos"; import { toDate } from "../../utils/timestamp"; @@ -51,6 +54,7 @@ const RegularFeatureViewOverviewTab = ({ const fvName = featureViewName === undefined ? "" : featureViewName; const relationshipQuery = useLoadRelationshipData(); + const { data: usageData } = useLoadFeatureUsage(); const fsNames = relationshipQuery.data ? relationshipQuery.data.filter(whereFSconsumesThisFv(fvName)).map((fs) => { @@ -59,12 +63,43 @@ const RegularFeatureViewOverviewTab = ({ : []; const numOfFs = fsNames.length; + const fvUsage = usageData?.feature_usage?.[fvName]; + const runCount = fvUsage?.run_count ?? 0; + const lastUsed = fvUsage?.last_used ?? null; + const lastUsedLabel = + lastUsed != null ? new Date(lastUsed).toLocaleDateString() : "N/A"; + return ( + {usageData?.mlflow_enabled && ( + <> + + + + + + + + + + )} @@ -128,6 +163,10 @@ const RegularFeatureViewOverviewTab = ({ )}
+ {usageData?.mlflow_enabled && ( + + )} +

Tags

diff --git a/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx b/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx index 0e22a6c2e5d..c0b9627bca5 100644 --- a/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx @@ -1,11 +1,12 @@ import React from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { EuiBadge, EuiPageTemplate } from "@elastic/eui"; import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; -import { useMatchExact } from "../../hooks/useMatchSubpath"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import StreamFeatureViewOverviewTab from "./StreamFeatureViewOverviewTab"; +import FeatureViewVersionsTab from "./FeatureViewVersionsTab"; import { useStreamFeatureViewCustomTabs, @@ -30,7 +31,17 @@ const StreamFeatureInstance = ({ data }: StreamFeatureInstanceProps) => { restrictWidth paddingSize="l" iconType={FeatureViewIcon} - pageTitle={`${featureViewName}`} + pageTitle={ + <> + {featureViewName} + {data?.meta?.currentVersionNumber != null && + data.meta.currentVersionNumber > 0 && ( + + v{data.meta.currentVersionNumber} + + )} + + } tabs={[ { label: "Overview", @@ -39,6 +50,13 @@ const StreamFeatureInstance = ({ data }: StreamFeatureInstanceProps) => { navigate(""); }, }, + { + label: "Versions", + isSelected: useMatchSubpath("versions"), + onClick: () => { + navigate("versions"); + }, + }, ...customNavigationTabs, ]} /> @@ -48,6 +66,12 @@ const StreamFeatureInstance = ({ data }: StreamFeatureInstanceProps) => { path="/" element={} /> + + } + /> {CustomTabRoutes} diff --git a/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx b/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx index 1e31df47d69..4abb5d02f75 100644 --- a/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx +++ b/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx @@ -1,9 +1,9 @@ import React from "react"; import { + EuiBadge, EuiBasicTable, EuiPanel, EuiSpacer, - EuiText, EuiTitle, } from "@elastic/eui"; import { useParams } from "react-router-dom"; @@ -17,6 +17,8 @@ const FeatureViewProjectionDisplayPanel = ( featureViewProjection: RequestDataDisplayPanelProps, ) => { const { projectName } = useParams(); + const isLabelView = (featureViewProjection as any).viewType === "labelView"; + const viewPath = isLabelView ? "label-view" : "feature-view"; const columns = [ { @@ -27,20 +29,21 @@ const FeatureViewProjectionDisplayPanel = ( name: "Type", field: "valueType", render: (valueType: any) => { - return feast.types.ValueType.Enum[valueType]; + if (typeof valueType === "string") return valueType; + return feast.types.ValueType.Enum[valueType] || String(valueType || ""); }, }, ]; return ( - - Feature View - + + {isLabelView ? "label view" : "feature view"} + {featureViewProjection?.featureViewName} @@ -48,7 +51,7 @@ const FeatureViewProjectionDisplayPanel = ( ); diff --git a/ui/src/pages/feature-views/useLoadFeatureView.ts b/ui/src/pages/feature-views/useLoadFeatureView.ts index 08e8646f60f..5f88aab0a7d 100644 --- a/ui/src/pages/feature-views/useLoadFeatureView.ts +++ b/ui/src/pages/feature-views/useLoadFeatureView.ts @@ -1,71 +1,56 @@ -import { useContext } from "react"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import { useParams } from "react-router-dom"; +import useResourceQuery, { + featureViewDetailPath, + restFeatureViewDetailToGeneric, +} from "../../queries/useResourceQuery"; +import type { genericFVType } from "../../parsers/mergedFVTypes"; const useLoadFeatureView = (featureViewName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.mergedFVMap[featureViewName]; - - return { - ...registryQuery, - data, - }; + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `feature-view:${featureViewName}`, + project: projectName, + restPath: featureViewDetailPath(featureViewName, projectName || ""), + restSelect: restFeatureViewDetailToGeneric, + enabled: !!featureViewName, + }); }; const useLoadRegularFeatureView = (featureViewName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.featureViews?.find((fv) => { - return fv?.spec?.name === featureViewName; - }); - - return { - ...registryQuery, - data, - }; + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `regular-fv:${featureViewName}`, + project: projectName, + restPath: featureViewDetailPath(featureViewName, projectName || ""), + restSelect: (d) => (d?.type === "featureView" ? d : undefined), + enabled: !!featureViewName, + }); }; const useLoadOnDemandFeatureView = (featureViewName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.onDemandFeatureViews?.find((fv) => { - return fv?.spec?.name === featureViewName; - }); - - return { - ...registryQuery, - data, - }; + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `odfv:${featureViewName}`, + project: projectName, + restPath: featureViewDetailPath(featureViewName, projectName || ""), + restSelect: (d) => (d?.type === "onDemandFeatureView" ? d : undefined), + enabled: !!featureViewName, + }); }; const useLoadStreamFeatureView = (featureViewName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.streamFeatureViews?.find((fv) => { - return fv.spec?.name === featureViewName; - }); - - return { - ...registryQuery, - data, - }; + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `sfv:${featureViewName}`, + project: projectName, + restPath: featureViewDetailPath(featureViewName, projectName || ""), + restSelect: (d) => (d?.type === "streamFeatureView" ? d : undefined), + enabled: !!featureViewName, + }); }; export default useLoadFeatureView; diff --git a/ui/src/pages/features/FeatureInstance.tsx b/ui/src/pages/features/FeatureInstance.tsx index fe81c6e619f..aa73db7c8c1 100644 --- a/ui/src/pages/features/FeatureInstance.tsx +++ b/ui/src/pages/features/FeatureInstance.tsx @@ -3,8 +3,9 @@ import { Route, Routes, useNavigate, useParams } from "react-router-dom"; import { EuiPageTemplate } from "@elastic/eui"; import { FeatureIcon } from "../../graphics/FeatureIcon"; -import { useMatchExact } from "../../hooks/useMatchSubpath"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import FeatureOverviewTab from "./FeatureOverviewTab"; +import FeatureMonitoringTab from "./FeatureMonitoringTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import { useFeatureCustomTabs, @@ -34,12 +35,20 @@ const FeatureInstance = () => { navigate(""); }, }, + { + label: "Monitoring", + isSelected: useMatchSubpath("monitoring"), + onClick: () => { + navigate("monitoring"); + }, + }, ...customNavigationTabs, ]} /> } /> + } /> {CustomTabRoutes} diff --git a/ui/src/pages/features/FeatureListPage.tsx b/ui/src/pages/features/FeatureListPage.tsx index 36087f98bc0..d03ed0ec508 100644 --- a/ui/src/pages/features/FeatureListPage.tsx +++ b/ui/src/pages/features/FeatureListPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useContext } from "react"; +import React, { useState } from "react"; import { EuiBasicTable, EuiTableFieldDataColumnType, @@ -15,13 +15,19 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiBadge, + EuiCallOut, } from "@elastic/eui"; import EuiCustomLink from "../../components/EuiCustomLink"; import ExportButton from "../../components/ExportButton"; import { useParams } from "react-router-dom"; -import useLoadRegistry from "../../queries/useLoadRegistry"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadFeatureModels, { + FeatureModelInfo, +} from "../../queries/useLoadFeatureModels"; import { FeatureIcon } from "../../graphics/FeatureIcon"; +import useResourceQuery, { + featuresListPath, +} from "../../queries/useResourceQuery"; import { FEAST_FCO_TYPES } from "../../parsers/types"; import { getEntityPermissions, @@ -35,6 +41,7 @@ interface Feature { type: string; project?: string; permissions?: any[]; + models?: FeatureModelInfo[]; } type FeatureColumn = @@ -43,11 +50,24 @@ type FeatureColumn = const FeatureListPage = () => { const { projectName } = useParams(); - const registryUrl = useContext(RegistryPathContext); - const { data, isLoading, isError } = useLoadRegistry( - registryUrl, - projectName, - ); + const { + data: features, + isLoading, + isError, + isPermissionDenied, + } = useResourceQuery({ + resourceType: "features-list", + project: projectName, + restPath: featuresListPath(projectName), + restSelect: (d) => d.features, + }); + const { data: permissions } = useResourceQuery({ + resourceType: "permissions", + project: projectName, + restPath: `/permissions?project=${encodeURIComponent(projectName || "")}`, + restSelect: (d) => d.permissions, + }); + const { data: featureModelsData } = useLoadFeatureModels(); const [searchText, setSearchText] = useState(""); const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); @@ -57,27 +77,24 @@ const FeatureListPage = () => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(100); - const featuresWithPermissions: Feature[] = (data?.allFeatures || []).map( - (feature) => { - return { - ...feature, - permissions: getEntityPermissions( - selectedPermissionAction - ? filterPermissionsByAction( - data?.permissions, - selectedPermissionAction, - ) - : data?.permissions, - FEAST_FCO_TYPES.featureView, - feature.featureView, - ), - }; - }, - ); + const featuresWithPermissions: Feature[] = (features || []).map((feature) => { + const featureRef = `${feature.featureView}:${feature.name}`; + return { + ...feature, + models: featureModelsData?.feature_models?.[featureRef] || [], + permissions: getEntityPermissions( + selectedPermissionAction + ? filterPermissionsByAction(permissions, selectedPermissionAction) + : permissions, + FEAST_FCO_TYPES.featureView, + feature.featureView, + ), + }; + }); - const features: Feature[] = featuresWithPermissions; + const enrichedFeatures: Feature[] = featuresWithPermissions; - const filteredFeatures = features.filter((feature) => + const filteredFeatures = enrichedFeatures.filter((feature) => feature.name.toLowerCase().includes(searchText.toLowerCase()), ); @@ -100,7 +117,6 @@ const FeatureListPage = () => { field: "name", sortable: true, render: (name: string, feature: Feature) => { - // For "All Projects" view, link to the specific project const itemProject = feature.project || projectName; return ( { field: "featureView", sortable: true, render: (featureView: string, feature: Feature) => { - // For "All Projects" view, link to the specific project const itemProject = feature.project || projectName; return ( @@ -126,6 +141,47 @@ const FeatureListPage = () => { }, }, { name: "Type", field: "type", sortable: true }, + { + name: "Models", + field: "models", + sortable: false, + render: (models: FeatureModelInfo[]) => { + if (!models || models.length === 0) { + return ( + + -- + + ); + } + if (models.length === 1) { + return ( + + {models[0].model_name} v{models[0].version} + + ); + } + return ( + + {models.map((m) => ( +
+ {m.model_name} v{m.version} +
+ ))} +
+ } + > + {models.length} models + + ); + }, + }, { name: "Permissions", field: "permissions", @@ -203,6 +259,10 @@ const FeatureListPage = () => { {isLoading ? (

Loading...

+ ) : isPermissionDenied ? ( + +

You do not have permission to view features.

+
) : isError ? (

We encountered an error while loading.

) : ( diff --git a/ui/src/pages/features/FeatureMonitoringTab.tsx b/ui/src/pages/features/FeatureMonitoringTab.tsx new file mode 100644 index 00000000000..bc8e2c2cf9f --- /dev/null +++ b/ui/src/pages/features/FeatureMonitoringTab.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSkeletonText, + EuiEmptyPrompt, + EuiButton, +} from "@elastic/eui"; +import { + useFeatureMetrics, + useBaselineMetrics, +} from "../../queries/useMonitoringApi"; +import type { + NumericHistogram, + CategoricalHistogram, +} from "../../queries/useMonitoringApi"; +import { + NumericHistogramChart, + CategoricalHistogramChart, +} from "../monitoring/components/HistogramChart"; +import StatsPanel from "../monitoring/components/StatsPanel"; + +const FeatureMonitoringTab = () => { + const { projectName, FeatureViewName, FeatureName } = useParams(); + + const { + data: metrics, + isLoading, + isError, + } = useFeatureMetrics({ + project: projectName || "", + feature_view_name: FeatureViewName, + feature_name: FeatureName, + }); + + const { data: baselineMetrics } = useBaselineMetrics( + projectName || "", + FeatureViewName, + FeatureName, + ); + + if (isLoading) { + return ; + } + + const latestMetric = (() => { + if (!metrics || metrics.length === 0) return null; + const withData = metrics.filter((m) => m.row_count > 0); + const candidates = withData.length > 0 ? withData : metrics; + return candidates.reduce((a, b) => (a.metric_date > b.metric_date ? a : b)); + })(); + + const baselineMetric = + baselineMetrics && baselineMetrics.length > 0 ? baselineMetrics[0] : null; + + if (isError || !latestMetric) { + return ( + No Monitoring Data} + body={ +

+ No monitoring metrics available for this feature. Run a monitoring + compute job to generate data quality metrics. +

+ } + actions={ + + Go to Monitoring + + } + /> + ); + } + + const isNumeric = latestMetric.feature_type === "numeric"; + + return ( + <> + + + {isNumeric && latestMetric.histogram && ( + + )} + {!isNumeric && latestMetric.histogram && ( + + )} + {!latestMetric.histogram && ( + No Histogram} + body={

Histogram data is not available.

} + /> + )} +
+ + + +
+ + + ); +}; + +export default FeatureMonitoringTab; diff --git a/ui/src/pages/features/FeatureOverviewTab.tsx b/ui/src/pages/features/FeatureOverviewTab.tsx index 6b613abc589..d895f52c585 100644 --- a/ui/src/pages/features/FeatureOverviewTab.tsx +++ b/ui/src/pages/features/FeatureOverviewTab.tsx @@ -1,4 +1,5 @@ import { + EuiBadge, EuiFlexGroup, EuiHorizontalRule, EuiLoadingSpinner, @@ -63,7 +64,9 @@ const FeatureOverviewTab = () => { Value Type - {feast.types.ValueType.Enum[featureData?.valueType!]} + {featureData?.valueType + ? feast.types.ValueType.Enum[featureData.valueType] + : featureData?.type || data?.type || "—"} Description @@ -71,13 +74,21 @@ const FeatureOverviewTab = () => { {featureData?.description} - FeatureView + + {data?.kind === "label" ? "Label View" : "Feature View"} + {FeatureViewName} + {data?.kind === "label" && ( + <> + {" "} + label view + + )} diff --git a/ui/src/pages/features/useLoadFeature.ts b/ui/src/pages/features/useLoadFeature.ts index 54bf31e996f..be322a9c8aa 100644 --- a/ui/src/pages/features/useLoadFeature.ts +++ b/ui/src/pages/features/useLoadFeature.ts @@ -1,27 +1,32 @@ -import { useContext } from "react"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import { useParams } from "react-router-dom"; +import useResourceQuery, { + featureDetailPath, +} from "../../queries/useResourceQuery"; const useLoadFeature = (featureViewName: string, featureName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); + const { projectName } = useParams(); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.featureViews?.find((fv) => { - return fv?.spec?.name === featureViewName; - }); + const fvQuery = useResourceQuery({ + resourceType: `feature:${featureViewName}:${featureName}`, + project: projectName, + restPath: featureDetailPath( + featureViewName, + featureName, + projectName || "", + ), + restSelect: (d) => d, + enabled: !!featureViewName && !!featureName, + }); const featureData = - data === undefined + fvQuery.data === undefined ? undefined - : data?.spec?.features?.find((f) => { - return f.name === featureName; - }); + : fvQuery.data?.spec?.features?.find( + (f: any) => f.name === featureName, + ) || fvQuery.data; return { - ...registryQuery, + ...fvQuery, featureData, }; }; diff --git a/ui/src/pages/label-views/ActiveLearningTab.tsx b/ui/src/pages/label-views/ActiveLearningTab.tsx new file mode 100644 index 00000000000..e880c2aaa55 --- /dev/null +++ b/ui/src/pages/label-views/ActiveLearningTab.tsx @@ -0,0 +1,739 @@ +import React, { useContext, useState, useMemo, useCallback } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiSpacer, + EuiCallOut, + EuiText, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiBadge, + EuiBasicTable, + EuiBasicTableColumn, + EuiCodeBlock, + EuiIcon, + EuiEmptyPrompt, + EuiFieldNumber, + EuiSuperSelect, + EuiFieldSearch, + EuiTablePagination, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiOverlayMask, + EuiGlobalToastList, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import useAnnotationConfig from "./useAnnotationConfig"; + +interface CandidateData { + unlabeled_entities: Record[]; + total_labeled: number; + total_unlabeled: number; + entity_names: string[]; + feature_names: string[]; + label_view: string; + reference_feature_view: string | null; +} + +const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; + +const ActiveLearningTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, data } = useLoadLabelView(name); + const { data: registryData } = useLoadRegistry(registryUrl); + const { data: annotationConfig } = useAnnotationConfig(name); + + const [refFV, setRefFV] = useState(""); + const [limit, setLimit] = useState(50); + const [candidates, setCandidates] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [selectedItems, setSelectedItems] = useState[]>([]); + const [labelValues, setLabelValues] = useState>({}); + const [submitting, setSubmitting] = useState(false); + const [submitSuccess, setSubmitSuccess] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + const [toasts, setToasts] = useState< + Array<{ + id: string; + title: string; + color: "success" | "danger"; + iconType: string; + }> + >([]); + + const removeToast = useCallback((removedToast: { id: string }) => { + setToasts((prev) => prev.filter((t) => t.id !== removedToast.id)); + }, []); + + const spec = data?.object?.spec || data?.spec || {}; + const labelFields: { name: string; valueType?: string }[] = + spec.features || []; + const labelerField: string | null = + spec.labelerField || spec.labeler_field || null; + const configLabelValues = annotationConfig?.label_values || {}; + const configLabelWidgets = annotationConfig?.label_widgets || {}; + + const featureViewOptions = (registryData?.objects?.featureViews || []) + .filter((fv: any) => { + const fvName = fv.spec?.name || fv.name || ""; + return fvName !== name; + }) + .map((fv: any) => ({ + value: fv.spec?.name || fv.name || "", + inputDisplay: fv.spec?.name || fv.name || "Unknown", + dropdownDisplay: ( + + {fv.spec?.name || fv.name} + {fv.spec?.description && ( + +

{fv.spec.description}

+
+ )} +
+ ), + })); + + const fetchCandidates = async () => { + setLoading(true); + setError(null); + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch(`${baseUrl}/active-learning/candidates`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + feature_view: name, + reference_feature_view: refFV || undefined, + limit: limit, + }), + }); + const result = await response.json(); + if (!response.ok) { + const detail = result.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : "Failed to fetch candidates", + ); + } else { + setCandidates(result); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setLoading(false); + } + }; + + const entityColumns: EuiBasicTableColumn>[] = candidates + ? Object.keys(candidates.unlabeled_entities[0] || {}).map((key) => ({ + field: key, + name: key, + sortable: true, + })) + : []; + + const filteredCandidates = useMemo(() => { + if (!candidates) return []; + if (!searchQuery.trim()) return candidates.unlabeled_entities; + const query = searchQuery.toLowerCase(); + return candidates.unlabeled_entities.filter((row) => + Object.values(row).some( + (val) => val != null && String(val).toLowerCase().includes(query), + ), + ); + }, [candidates, searchQuery]); + + const paginatedCandidates = useMemo(() => { + const start = pageIndex * pageSize; + return filteredCandidates.slice(start, start + pageSize); + }, [filteredCandidates, pageIndex, pageSize]); + + const handleSubmitLabels = async () => { + if (selectedItems.length === 0) return; + setSubmitting(true); + setSubmitSuccess(null); + setError(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const pushSourceName = + spec.source?.pushSourceName || + spec.source?.name || + `${name}_push_source`; + + const rows = selectedItems.map((entity) => { + const row: Record = { ...entity, ...labelValues }; + row["event_timestamp"] = new Date().toISOString(); + return row; + }); + + const columnar: Record = {}; + if (rows.length > 0) { + for (const key of Object.keys(rows[0])) { + columnar[key] = rows.map((r) => r[key]); + } + } + + const response = await fetch(`${baseUrl}/push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + df: columnar, + to: "online_and_offline", + }), + }); + + if (response.ok) { + const msg = `Successfully labeled ${selectedItems.length} record${selectedItems.length !== 1 ? "s" : ""}`; + setSubmitSuccess(msg); + setToasts((prev) => [ + ...prev, + { + id: String(Date.now()), + title: msg, + color: "success", + iconType: "check", + }, + ]); + setSelectedItems([]); + setLabelValues({}); + fetchCandidates(); + } else { + const errData = await response.json().catch(() => null); + const detail = errData?.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : `Label submission failed (${response.status})`, + ); + } + } catch (e: any) { + setError(e.message || "Network error during label submission"); + } finally { + setSubmitting(false); + } + }; + + if (isLoading) { + return ( +

+ Loading... +

+ ); + } + + const exportJSON = () => { + if (!candidates) return; + const blob = new Blob( + [JSON.stringify(candidates.unlabeled_entities, null, 2)], + { type: "application/json" }, + ); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${name}_unlabeled_candidates.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const argillaPushCode = candidates + ? `import argilla as rg +import requests + +# Fetch unlabeled candidates from Feast +response = requests.post( + "${window.location.origin}/api/v1/active-learning/candidates", + json={ + "feature_view": "${name}", + ${refFV ? `"reference_feature_view": "${refFV}",` : ""} + "limit": ${limit}, + }, +) +candidates = response.json()["unlabeled_entities"] + +# Create Argilla records for annotation +records = [] +for entity in candidates: + records.append( + rg.Record( + fields={"text": f"Review entity: {entity}"}, + metadata=entity, + suggestions=[], # Add model predictions here for pre-labeling + ) + ) + +# Push to Argilla dataset for human annotation +dataset = rg.Dataset(name="${name}_annotation", settings=your_settings) +dataset.records.log(records) +print(f"Pushed {len(records)} candidates for annotation")` + : ""; + + return ( + + + + Find records that exist in your feature views but have NOT been + labeled yet. Label them directly here in the Feast UI, or export + candidates to your annotation tool (Argilla, Label Studio) for + targeted human review. + + + + + + + +

Find Unlabeled Records

+
+ + + + + {featureViewOptions.length > 0 ? ( + setRefFV(value)} + placeholder="Select a feature view..." + hasDividers + /> + ) : ( + setRefFV(e.target.value)} + /> + )} + + + + setLimit(parseInt(e.target.value) || 50)} + /> + + + + + + Find Unlabeled Records + + +
+ + {error && !isModalOpen && ( + + + + {error} + + + )} + + {candidates && ( + + + + + + + + + + + + + + + + + 0 + ? `${( + (candidates.total_labeled / + (candidates.total_labeled + + candidates.total_unlabeled)) * + 100 + ).toFixed(1)}%` + : "N/A" + } + description="Label Coverage" + titleColor="primary" + /> + + + + + + + {candidates.unlabeled_entities.length > 0 ? ( + + + + + +

+ Unlabeled Records{" "} + + {candidates.unlabeled_entities.length} + + {selectedItems.length > 0 && ( + <> + {" "} + + {selectedItems.length} selected + + + )} +

+
+
+ + + {selectedItems.length > 0 && ( + + setIsModalOpen(true)} + iconType="tag" + > + Label Selected ({selectedItems.length}) + + + )} + + + Export JSON + + + + +
+ + + Select records below, then click "Label Selected" to + assign labels. + + + { + setSearchQuery(e.target.value); + setPageIndex(0); + }} + isClearable + fullWidth + /> + + ) => + Object.values(item).join("_") + } + selection={{ + onSelectionChange: (items: Record[]) => + setSelectedItems(items), + selectable: () => true, + selectableMessage: () => "Select to label", + }} + /> + {filteredCandidates.length > pageSize && ( + <> + + setPageIndex(page)} + itemsPerPage={pageSize} + onChangeItemsPerPage={(size) => { + setPageSize(size); + setPageIndex(0); + }} + itemsPerPageOptions={PAGE_SIZE_OPTIONS} + /> + + )} +
+ + {submitSuccess && ( + + + + + )} + + {isModalOpen && ( + + setIsModalOpen(false)} + maxWidth={500} + > + + + Label {selectedItems.length}{" "} + Record + {selectedItems.length !== 1 ? "s" : ""} + + + + + Assign label values to all {selectedItems.length}{" "} + selected records. Labels will be pushed via{" "} + FeatureStore.push(). + + + + {labelFields + .filter((f) => f.name !== labelerField) + .map((field) => { + const widget = configLabelWidgets[field.name]; + const values = configLabelValues[field.name]; + let input; + + if ( + widget === "binary" && + values && + values.length === 2 + ) { + input = ( + ({ + value: v, + inputDisplay: + v === "1" + ? "Yes (1)" + : v === "0" + ? "No (0)" + : v, + })), + ]} + valueOfSelected={ + labelValues[field.name] || "" + } + onChange={(val) => + setLabelValues((prev) => ({ + ...prev, + [field.name]: val, + })) + } + /> + ); + } else if (values && values.length > 0) { + input = ( + ({ + value: v, + inputDisplay: v, + })), + ]} + valueOfSelected={ + labelValues[field.name] || "" + } + onChange={(val) => + setLabelValues((prev) => ({ + ...prev, + [field.name]: val, + })) + } + /> + ); + } else if (widget === "number") { + input = ( + + setLabelValues((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + /> + ); + } else { + input = ( + + setLabelValues((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + /> + ); + } + + return ( + + {input} + + ); + })} + {labelerField && ( + + + setLabelValues((prev) => ({ + ...prev, + [labelerField]: e.target.value, + })) + } + /> + + )} + + {error && ( + + + + {error} + + + )} + + + { + setIsModalOpen(false); + setLabelValues({}); + }} + > + Cancel + + { + await handleSubmitLabels(); + if (!error) { + setIsModalOpen(false); + } + }} + isLoading={submitting} + iconType="check" + > + Submit Labels + + + + + )} +
+ ) : ( + All records are labeled!} + body="No unlabeled records found in the reference feature view." + /> + )} + + + + + +

Push Candidates to Argilla

+
+ + + Use this script to push unlabeled candidates to Argilla for + annotation: + + + + {argillaPushCode} + +
+
+ )} + +
+ +
+
+ ); +}; + +export default ActiveLearningTab; diff --git a/ui/src/pages/label-views/AnnotateTab.tsx b/ui/src/pages/label-views/AnnotateTab.tsx new file mode 100644 index 00000000000..f378e2dfcb7 --- /dev/null +++ b/ui/src/pages/label-views/AnnotateTab.tsx @@ -0,0 +1,149 @@ +import React, { useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiButtonGroup, + EuiSpacer, + EuiPanel, + EuiText, + EuiLoadingSpinner, + EuiCallOut, + EuiBadge, + EuiFlexGroup, + EuiFlexItem, +} from "@elastic/eui"; +import ActiveLearningTab from "./ActiveLearningTab"; +import RagLabelingMethod from "./RagLabelingMethod"; +import ClassificationMethod from "./ClassificationMethod"; +import EntityFormMethod from "./EntityFormMethod"; +import useAnnotationConfig from "./useAnnotationConfig"; + +const PROFILE_DESCRIPTIONS: Record = { + "document-span": + "Load documents, highlight text spans, and label them for RAG retrieval or citation evaluation.", + "review-edit": + "Review and edit existing label records in a table view. Supports inline editing and batch push.", + "entity-form": + "Fill label fields per entity using a structured form. One record at a time.", + "active-learning": + "Surface unlabeled entities from a reference feature view and label them.", +}; + +const AnnotateTab = () => { + const { labelViewName } = useParams(); + const { + data: config, + isLoading, + isError, + } = useAnnotationConfig(labelViewName || ""); + + const detectedProfile = config?.profile || "table"; + + const availableMethods = React.useMemo(() => { + const methods: { id: string; label: string }[] = []; + + if (detectedProfile === "document-span") { + methods.push({ id: "document-span", label: "Document Span" }); + methods.push({ id: "review-edit", label: "Review & Edit" }); + return methods; + } + + if (detectedProfile === "entity-form") { + methods.push({ id: "entity-form", label: "Entity Form" }); + methods.push({ id: "review-edit", label: "Review & Edit" }); + methods.push({ id: "active-learning", label: "Active Learning" }); + return methods; + } + + if (detectedProfile === "active-learning") { + methods.push({ id: "active-learning", label: "Active Learning" }); + methods.push({ id: "entity-form", label: "Entity Form" }); + methods.push({ id: "review-edit", label: "Review & Edit" }); + return methods; + } + + methods.push({ id: "review-edit", label: "Review & Edit" }); + methods.push({ id: "active-learning", label: "Active Learning" }); + methods.push({ id: "entity-form", label: "Entity Form" }); + return methods; + }, [detectedProfile]); + + const [selectedMethod, setSelectedMethod] = useState(null); + const activeMethod = + selectedMethod || availableMethods[0]?.id || "review-edit"; + + if (isLoading) { + return ( + + + + + + Loading labeling configuration... + + + ); + } + + if (isError || !config) { + return ( + +

+ Falling back to Review & Edit view. Define{" "} + feast.io/labeling-method in your LabelView tags to + configure the labeling experience. +

+
+ ); + } + + return ( + + + + + + Labeling Method + + + + profile: {detectedProfile} + + + + setSelectedMethod(id)} + buttonSize="m" + isFullWidth={false} + /> + {PROFILE_DESCRIPTIONS[activeMethod] && ( + <> + + + {PROFILE_DESCRIPTIONS[activeMethod]} + + + )} + + + + + {activeMethod === "active-learning" && } + {activeMethod === "document-span" && ( + + )} + {activeMethod === "review-edit" && } + {activeMethod === "entity-form" && ( + + )} + + ); +}; + +export default AnnotateTab; diff --git a/ui/src/pages/label-views/BatchUploadTab.tsx b/ui/src/pages/label-views/BatchUploadTab.tsx new file mode 100644 index 00000000000..6f3930c5d92 --- /dev/null +++ b/ui/src/pages/label-views/BatchUploadTab.tsx @@ -0,0 +1,332 @@ +import React, { useContext, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiForm, + EuiFormRow, + EuiButton, + EuiSpacer, + EuiCallOut, + EuiText, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiSelect, + EuiFilePicker, + EuiBasicTable, + EuiBasicTableColumn, + EuiCodeBlock, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; + +const BatchUploadTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, data } = useLoadLabelView(name); + + const [fileData, setFileData] = useState(null); + const [fileName, setFileName] = useState(""); + const [pushTarget, setPushTarget] = useState("online"); + const [uploading, setUploading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [parseError, setParseError] = useState(null); + + if (isLoading) { + return ( +

+ Loading... +

+ ); + } + + const spec = data?.object?.spec || data?.spec || {}; + const pushSourceName = + spec.streamSource?.pushOptions?.pushSourceName || + spec.batchSource?.name?.replace("_batch", "_push_source") || + `${name}_push_source`; + + const features = data?.features || spec.features || []; + const entities = spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || []; + + const handleFileChange = (files: FileList | null) => { + if (!files || files.length === 0) { + setFileData(null); + setFileName(""); + setParseError(null); + return; + } + + const file = files[0]; + setFileName(file.name); + setParseError(null); + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const text = e.target?.result as string; + if (file.name.endsWith(".json")) { + const parsed = JSON.parse(text); + const rows = Array.isArray(parsed) + ? parsed + : parsed.data || parsed.records || [parsed]; + setFileData(rows); + } else if (file.name.endsWith(".csv")) { + const lines = text.trim().split("\n"); + if (lines.length < 2) { + setParseError( + "CSV must have at least a header row and one data row", + ); + return; + } + const headers = lines[0] + .split(",") + .map((h) => h.trim().replace(/"/g, "")); + const rows = []; + for (let i = 1; i < lines.length; i++) { + const values = lines[i] + .split(",") + .map((v) => v.trim().replace(/"/g, "")); + const row: Record = {}; + headers.forEach((h, idx) => { + const val = values[idx] || ""; + const numVal = Number(val); + row[h] = val === "" ? null : isNaN(numVal) ? val : numVal; + }); + rows.push(row); + } + setFileData(rows); + } else { + setParseError("Unsupported file format. Use .csv or .json"); + } + } catch (e: any) { + setParseError(`Failed to parse file: ${e.message}`); + setFileData(null); + } + }; + reader.readAsText(file); + }; + + const handleUpload = async () => { + if (!fileData || fileData.length === 0) return; + + setUploading(true); + setError(null); + setResult(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + + const dataWithTimestamp = fileData.map((row) => ({ + ...row, + event_timestamp: row.event_timestamp || new Date().toISOString(), + })); + + const response = await fetch(`${baseUrl}/batch-push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + data: dataWithTimestamp, + to: pushTarget, + }), + }); + + const res = await response.json(); + if (!response.ok) { + const detail = res.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : "Upload failed", + ); + } else { + setResult(res); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setUploading(false); + } + }; + + const previewColumns: EuiBasicTableColumn[] = fileData + ? Object.keys(fileData[0] || {}).map((col) => ({ + field: col, + name: col, + truncateText: true, + width: "120px", + })) + : []; + + const csvTemplate = [ + entities.join(",") + + "," + + features.map((f: any) => f.name || f).join(",") + + ",event_timestamp", + entities.map(() => "").join(",") + + "," + + features.map(() => "").join(",") + + ",2026-01-01T00:00:00Z", + ].join("\n"); + + return ( + + + + Upload a CSV or JSON file to push labels in bulk. Useful for + correcting labels, importing from external systems, or backfilling + historical labels. + + + + + + + +

Upload File

+
+ + + + + + + + {parseError && ( + + + + {parseError} + + + )} + + + setPushTarget(e.target.value)} + /> + + + + + + Push {fileData ? `${fileData.length} rows` : "Labels"} + + +
+ + {/* Preview */} + {fileData && fileData.length > 0 && ( + + + + + + +

+ Preview{" "} + {fileData.length} rows{" "} + {fileName} +

+
+
+
+ + + {fileData.length > 10 && ( + + Showing first 10 of {fileData.length} rows + + )} +
+
+ )} + + {error && ( + + + + {error} + + + )} + + {result && ( + + + + + Pushed {result.rows_pushed} rows to{" "} + {pushTarget} store. + + + + )} + + + + {/* Template */} + + +

CSV Template

+
+ + + Expected columns for this LabelView: + + + + {csvTemplate} + +
+
+ ); +}; + +export default BatchUploadTab; diff --git a/ui/src/pages/label-views/ClassificationMethod.tsx b/ui/src/pages/label-views/ClassificationMethod.tsx new file mode 100644 index 00000000000..9d4366f2e00 --- /dev/null +++ b/ui/src/pages/label-views/ClassificationMethod.tsx @@ -0,0 +1,465 @@ +import React, { + useState, + useContext, + useEffect, + useCallback, + useMemo, +} from "react"; +import { useParams } from "react-router-dom"; +import { + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiPanel, + EuiText, + EuiLoadingSpinner, + EuiBasicTable, + EuiBasicTableColumn, + EuiSelect, + EuiBadge, + EuiFieldSearch, + EuiTablePagination, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; +import useAnnotationConfig from "./useAnnotationConfig"; + +interface LabelRow { + _id: string; + [key: string]: any; +} + +const PAGE_SIZE_OPTIONS = [10, 25, 50]; + +const ClassificationMethod = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { data } = useLoadLabelView(labelViewName || ""); + const { data: annotationConfig } = useAnnotationConfig(labelViewName || ""); + + const [rows, setRows] = useState([]); + const [originalRows, setOriginalRows] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [pushSuccess, setPushSuccess] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + + const spec = data?.object?.spec || data?.spec || {}; + const labelFields: { name: string; valueType?: string }[] = useMemo( + () => spec.features || [], + [spec.features], + ); + const entities: string[] = useMemo( + () => + spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || [], + [spec.entityColumns, spec.entities], + ); + + const configuredValues = annotationConfig?.label_values || {}; + const fieldRoles = annotationConfig?.field_roles || {}; + const labelWidgets = annotationConfig?.label_widgets || {}; + + const fetchLabels = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch(`${baseUrl}/list-labels`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + feature_view: labelViewName || "", + limit: 200, + }), + }); + if (!response.ok) { + throw new Error(`Failed to fetch labels (${response.status})`); + } + const result = await response.json(); + const labelData: Record[] = result.labels || []; + const mapped = labelData.map((row, idx) => ({ + ...row, + _id: `row_${idx}_${Object.values(row).join("_")}`, + })); + setRows(mapped); + setOriginalRows(JSON.parse(JSON.stringify(mapped))); + } catch (e: any) { + setError(e.message || "Failed to load labels"); + } finally { + setIsLoading(false); + } + }, [labelViewName, registryUrl]); + + useEffect(() => { + if (labelViewName) { + fetchLabels(); + } + }, [labelViewName, fetchLabels]); + + const handleFieldChange = (rowId: string, field: string, value: string) => { + setRows((prev) => + prev.map((row) => (row._id === rowId ? { ...row, [field]: value } : row)), + ); + }; + + const getChangedRows = () => { + return rows.filter((row) => { + const original = originalRows.find((o) => o._id === row._id); + if (!original) return false; + return labelFields.some((f) => row[f.name] !== original[f.name]); + }); + }; + + const resetChanges = () => { + setRows(JSON.parse(JSON.stringify(originalRows))); + }; + + const saveToLabelView = async () => { + const changed = getChangedRows(); + if (changed.length === 0) return; + + setIsSaving(true); + setError(null); + setPushSuccess(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const pushSourceName = + spec.source?.pushSourceName || + spec.source?.name || + `${labelViewName}_push_source`; + + const pushRows = changed.map((row) => { + const pushRow: Record = {}; + entities.forEach((e) => { + pushRow[e] = row[e]; + }); + labelFields.forEach((f) => { + pushRow[f.name] = row[f.name]; + }); + pushRow["event_timestamp"] = new Date().toISOString(); + return pushRow; + }); + + const columnar: Record = {}; + if (pushRows.length > 0) { + for (const key of Object.keys(pushRows[0])) { + columnar[key] = pushRows.map((r) => r[key]); + } + } + + const response = await fetch(`${baseUrl}/push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + df: columnar, + to: "online_and_offline", + }), + }); + + if (response.ok) { + setPushSuccess( + `Successfully pushed ${changed.length} updated labels to ${labelViewName}`, + ); + setOriginalRows(JSON.parse(JSON.stringify(rows))); + } else { + const errData = await response.json().catch(() => null); + setError(errData?.detail || `Push failed (${response.status})`); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setIsSaving(false); + } + }; + + const exportJSON = () => { + const exportData = rows.map((row) => { + const { _id, ...rest } = row; + return rest; + }); + const blob = new Blob([JSON.stringify(exportData, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${labelViewName}_labels.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const filteredRows = useMemo(() => { + if (!searchQuery.trim()) return rows; + const q = searchQuery.toLowerCase(); + return rows.filter((row) => + Object.entries(row) + .filter(([key]) => key !== "_id") + .some( + ([, val]) => val != null && String(val).toLowerCase().includes(q), + ), + ); + }, [rows, searchQuery]); + + const paginatedRows = useMemo(() => { + const start = pageIndex * pageSize; + return filteredRows.slice(start, start + pageSize); + }, [filteredRows, pageIndex, pageSize]); + + const uniqueValuesForField = useMemo(() => { + const result: Record = {}; + labelFields.forEach((field) => { + const values = new Set(); + rows.forEach((row) => { + if (row[field.name] != null && String(row[field.name]).trim() !== "") { + values.add(String(row[field.name])); + } + }); + result[field.name] = Array.from(values).sort(); + }); + return result; + }, [rows, labelFields]); + + const metadataFields = ["event_timestamp", "labeler"]; + + const entityColumns: EuiBasicTableColumn[] = entities.map( + (ent) => ({ + field: ent, + name: ent, + sortable: true, + truncateText: true, + }), + ); + + const labelColumns: EuiBasicTableColumn[] = labelFields + .filter((field) => !metadataFields.includes(field.name)) + .map((field) => { + const configVals = configuredValues[field.name]; + const role = fieldRoles[field.name]; + const widget = labelWidgets[field.name]; + + return { + field: field.name, + name: field.name, + render: (value: any, row: LabelRow) => { + if (widget === "binary" && configVals && configVals.length === 2) { + return ( + ({ + value: o, + text: o === "1" ? "Yes (1)" : o === "0" ? "No (0)" : o, + })), + ]} + value={value != null ? String(value) : ""} + onChange={(e) => + handleFieldChange(row._id, field.name, e.target.value) + } + /> + ); + } + const dropdownOptions = + configVals || uniqueValuesForField[field.name] || []; + if ( + (role === "label" || dropdownOptions.length > 0) && + dropdownOptions.length <= 30 + ) { + return ( + ({ + value: o, + text: o, + })), + ]} + value={value != null ? String(value) : ""} + onChange={(e) => + handleFieldChange(row._id, field.name, e.target.value) + } + /> + ); + } + return {value != null ? String(value) : ""}; + }, + }; + }); + + const metadataColumns: EuiBasicTableColumn[] = labelFields + .filter((field) => metadataFields.includes(field.name)) + .map((field) => ({ + field: field.name, + name: field.name, + sortable: true, + truncateText: true, + })); + + const columns = [...entityColumns, ...labelColumns, ...metadataColumns]; + + const changedCount = getChangedRows().length; + + if (isLoading) { + return ( + + + + + + Loading labels from {labelViewName}... + + + ); + } + + return ( + + +

+ Review and correct existing labels in the table below. Changes are + pushed to {labelViewName} via its PushSource and + governed by the configured conflict policy. +

+
+ + {error && ( + <> + + +

{error}

+
+ + )} + + {pushSuccess && ( + <> + + + + )} + + + + {rows.length === 0 ? ( + + +

+ No labels found. Use Entity Form or Active Learning to add labels + first, then review them here. +

+
+
+ ) : ( + <> + + + { + setSearchQuery(e.target.value); + setPageIndex(0); + }} + isClearable + /> + + + + {changedCount > 0 && ( + + {changedCount} changed + + )} + + + Export JSON + + + + + Reset + + + + + Save ({changedCount}) + + + + + + + + + + { + const original = originalRows.find((o) => o._id === row._id); + const isChanged = + original && + labelFields.some((f) => row[f.name] !== original[f.name]); + return isChanged + ? { style: { backgroundColor: "rgba(255, 200, 0, 0.05)" } } + : {}; + }} + /> + + + {filteredRows.length > pageSize && ( + <> + + setPageIndex(page)} + itemsPerPage={pageSize} + onChangeItemsPerPage={(size) => { + setPageSize(size); + setPageIndex(0); + }} + itemsPerPageOptions={PAGE_SIZE_OPTIONS} + /> + + )} + + )} +
+ ); +}; + +export default ClassificationMethod; diff --git a/ui/src/pages/label-views/EntityFormMethod.tsx b/ui/src/pages/label-views/EntityFormMethod.tsx new file mode 100644 index 00000000000..025f08b277c --- /dev/null +++ b/ui/src/pages/label-views/EntityFormMethod.tsx @@ -0,0 +1,484 @@ +import React, { useState, useContext, useEffect, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiPanel, + EuiTitle, + EuiText, + EuiFormRow, + EuiFieldText, + EuiFieldNumber, + EuiSelect, + EuiTextArea, + EuiButtonGroup, + EuiBadge, + EuiIcon, + EuiEmptyPrompt, + EuiForm, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; +import { AnnotationConfig } from "./useAnnotationConfig"; + +interface EntityFormMethodProps { + annotationConfig: AnnotationConfig; +} + +const EntityFormMethod = ({ annotationConfig }: EntityFormMethodProps) => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { data } = useLoadLabelView(labelViewName || ""); + + const spec = data?.object?.spec || data?.spec || {}; + const entities: string[] = useMemo( + () => + spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || [], + [spec.entityColumns, spec.entities], + ); + const labelFields: { name: string; valueType?: string }[] = useMemo( + () => spec.features || [], + [spec.features], + ); + + const fieldRoles = annotationConfig.field_roles; + const labelValues = annotationConfig.label_values; + const labelWidgets = annotationConfig.label_widgets; + const labelerField = annotationConfig.labeler_field || "labeler"; + + const [entityValues, setEntityValues] = useState>({}); + const [fieldInputs, setFieldInputs] = useState>({}); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const [pushSuccess, setPushSuccess] = useState(null); + const [labelCount, setLabelCount] = useState(0); + + const editableFields = useMemo( + () => + labelFields.filter( + (f) => f.name !== labelerField && f.name !== "event_timestamp", + ), + [labelFields, labelerField], + ); + + useEffect(() => { + const defaults: Record = {}; + editableFields.forEach((f) => { + const vals = labelValues[f.name]; + if (vals && vals.length > 0) { + defaults[f.name] = ""; + } else { + defaults[f.name] = ""; + } + }); + setFieldInputs(defaults); + }, [editableFields, labelValues]); + + const resetForm = () => { + const defaults: Record = {}; + editableFields.forEach((f) => { + defaults[f.name] = ""; + }); + setFieldInputs(defaults); + setError(null); + setPushSuccess(null); + }; + + const isFormValid = useMemo(() => { + const hasEntity = entities.every( + (e) => entityValues[e] && entityValues[e].trim() !== "", + ); + const hasAtLeastOneLabel = editableFields.some( + (f) => + fieldRoles[f.name] === "label" && + fieldInputs[f.name] && + fieldInputs[f.name].trim() !== "", + ); + return hasEntity && hasAtLeastOneLabel; + }, [entities, entityValues, editableFields, fieldInputs, fieldRoles]); + + const submitLabel = async () => { + if (!isFormValid) return; + + setIsSaving(true); + setError(null); + setPushSuccess(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const pushSourceName = + annotationConfig.push_source_name || + spec.source?.pushSourceName || + spec.source?.name || + `${labelViewName}_push_source`; + + const pushRow: Record = {}; + entities.forEach((e) => { + pushRow[e] = entityValues[e]; + }); + editableFields.forEach((f) => { + if (fieldInputs[f.name] && fieldInputs[f.name].trim() !== "") { + const widget = labelWidgets[f.name]; + if (widget === "number" || widget === "binary") { + pushRow[f.name] = Number(fieldInputs[f.name]); + } else { + pushRow[f.name] = fieldInputs[f.name]; + } + } + }); + pushRow[labelerField] = fieldInputs[labelerField] || "human_reviewer"; + pushRow["event_timestamp"] = new Date().toISOString(); + + const columnar: Record = {}; + for (const key of Object.keys(pushRow)) { + columnar[key] = [pushRow[key]]; + } + + const response = await fetch(`${baseUrl}/push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + df: columnar, + to: "online_and_offline", + }), + }); + + if (response.ok) { + setLabelCount((c) => c + 1); + setPushSuccess( + `Label pushed for ${entities.map((e) => `${e}=${entityValues[e]}`).join(", ")}`, + ); + resetForm(); + } else { + const errData = await response.json().catch(() => null); + setError(errData?.detail || `Push failed (${response.status})`); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setIsSaving(false); + } + }; + + const renderFieldInput = (field: { name: string; valueType?: string }) => { + const widget = labelWidgets[field.name]; + const values = labelValues[field.name]; + const role = fieldRoles[field.name]; + const currentValue = fieldInputs[field.name] || ""; + + if (widget === "binary" && values && values.length === 2) { + const options = values.map((v) => ({ + id: v, + label: v === "1" ? "Yes" : v === "0" ? "No" : v, + })); + return ( + + setFieldInputs((prev) => ({ ...prev, [field.name]: id })) + } + buttonSize="m" + /> + ); + } + + if ( + widget === "enum" || + (values && values.length > 0 && values.length <= 30) + ) { + return ( + ({ value: v, text: v })), + ]} + value={currentValue} + onChange={(e) => + setFieldInputs((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + /> + ); + } + + if (widget === "number") { + return ( + + setFieldInputs((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + placeholder={`Enter ${field.name}`} + /> + ); + } + + if (widget === "text" || role === "metadata") { + return ( + + setFieldInputs((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + placeholder={`Enter ${field.name}`} + rows={2} + compressed + /> + ); + } + + return ( + + setFieldInputs((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + placeholder={`Enter ${field.name}`} + /> + ); + }; + + if (!labelViewName) { + return ( + No label view selected} + /> + ); + } + + return ( + + +

+ Fill in the entity identifier and label values below. Each submission + pushes one label record to {labelViewName}. +

+
+ + {error && ( + <> + + +

{error}

+
+ + )} + + {pushSuccess && ( + <> + + + + )} + + + + + + + + +

+ Entity +

+
+ + + {entities.map((entity) => ( + + + setEntityValues((prev) => ({ + ...prev, + [entity]: e.target.value, + })) + } + placeholder={`Enter ${entity} value`} + /> + + ))} + + + +

+ Labels +

+
+ + + {editableFields + .filter((f) => fieldRoles[f.name] === "label") + .map((field) => ( + + {renderFieldInput(field)} + + ))} + + {editableFields.filter( + (f) => + fieldRoles[f.name] !== "label" && + fieldRoles[f.name] !== undefined, + ).length > 0 && ( + <> + + +

+ Additional Fields +

+
+ + {editableFields + .filter( + (f) => + fieldRoles[f.name] !== "label" && + fieldRoles[f.name] !== undefined, + ) + .map((field) => ( + + {renderFieldInput(field)} + + ))} + + )} + + {editableFields.filter((f) => !fieldRoles[f.name]).length > 0 && ( + <> + + {editableFields + .filter((f) => !fieldRoles[f.name]) + .map((field) => ( + + {renderFieldInput(field)} + + ))} + + )} + + + + + + setFieldInputs((prev) => ({ + ...prev, + [labelerField]: e.target.value, + })) + } + placeholder="your_name or reviewer_id" + /> + + + + + + + + Submit Label + + + + + Clear + + + +
+
+
+ + + + +

Session

+
+ + +

+ Labels submitted:{" "} + {labelCount} +

+

+ Conflict policy:{" "} + + {spec.conflictPolicy || "LAST_WRITE_WINS"} + +

+

+ Labeler field: {labelerField} +

+
+ + +

Schema

+
+ + + {entities.map((e) => ( +

+ entity {e} +

+ ))} + {editableFields.map((f) => ( +

+ + {fieldRoles[f.name] || "field"} + {" "} + {f.name} +

+ ))} +
+
+
+
+
+ ); +}; + +export default EntityFormMethod; diff --git a/ui/src/pages/label-views/Index.tsx b/ui/src/pages/label-views/Index.tsx new file mode 100644 index 00000000000..ae8fe324f7d --- /dev/null +++ b/ui/src/pages/label-views/Index.tsx @@ -0,0 +1,208 @@ +import React from "react"; +import { useParams } from "react-router-dom"; + +import { + EuiPageTemplate, + EuiLoadingSpinner, + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiEmptyPrompt, + EuiTitle, + EuiLink, + EuiCallOut, +} from "@elastic/eui"; + +import { LabelViewIcon } from "../../graphics/LabelViewIcon"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import useResourceQuery, { + labelViewListPath, + restLabelViewsFromResponse, +} from "../../queries/useResourceQuery"; + +const useLoadLabelViews = () => { + const { projectName } = useParams(); + return useResourceQuery({ + resourceType: "label-views-list", + project: projectName, + restPath: labelViewListPath(projectName), + restSelect: restLabelViewsFromResponse, + }); +}; + +interface LabelViewRow { + name: string; + entities: string[]; + conflictPolicy: string; + annotationProfile: string; + labelerField: string; + online: boolean; + description: string; +} + +const LabelViewsListingTable = ({ labelViews }: { labelViews: any[] }) => { + const { projectName } = useParams(); + + const rows: LabelViewRow[] = labelViews.map((lv: any) => { + const spec = lv.spec || {}; + const tags = spec.tags || {}; + return { + name: spec.name || "Unknown", + entities: spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || [], + conflictPolicy: spec.conflictPolicy || "LAST_WRITE_WINS", + annotationProfile: tags["feast.io/labeling-method"] || "table", + labelerField: spec.labelerField || "labeler", + online: spec.online !== false, + description: spec.description || "", + }; + }); + + const columns: EuiBasicTableColumn[] = [ + { + field: "name", + name: "Name", + sortable: true, + render: (name: string) => ( + {name} + ), + }, + { + field: "entities", + name: "Entities", + render: (entities: string[]) => + entities.length > 0 + ? entities.map((e, i) => ( + + {i > 0 && ", "} + {e} + + )) + : "-", + }, + { + field: "conflictPolicy", + name: "Conflict Policy", + render: (policy: string) => { + const color = + policy === "LAST_WRITE_WINS" + ? "default" + : policy === "MAJORITY_VOTE" + ? "primary" + : "accent"; + return {policy}; + }, + }, + { + field: "annotationProfile", + name: "Labeling Method", + render: (profile: string) => { + const color = + profile === "document-span" + ? "warning" + : profile === "entity-form" + ? "success" + : profile === "active-learning" + ? "accent" + : "hollow"; + return {profile}; + }, + }, + { + field: "labelerField", + name: "Labeler Field", + }, + { + field: "online", + name: "Online", + render: (online: boolean) => ( + + {online ? "Yes" : "No"} + + ), + }, + { + field: "description", + name: "Description", + render: (desc: string) => ( + + {desc || "-"} + + ), + }, + ]; + + return ( + + items={rows} + columns={columns} + tableLayout="auto" + /> + ); +}; + +const LabelViewIndexEmptyState = () => ( + +

No Label Views

+ + } + body={ +

+ Label views manage mutable labels and annotations for agent + interactions, safety monitoring, and RLHF pipelines. Define a LabelView + in your feature repository to get started. +

+ } + /> +); + +const Index = () => { + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadLabelViews(); + + useDocumentTitle(`Label Views | Feast`); + + return ( + + + + {isLoading && ( +

+ Loading +

+ )} + {isPermissionDenied && ( + +

You do not have permission to view label views.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} + {isSuccess && (!data || data.length === 0) && ( + + )} + {isSuccess && data && data.length > 0 && ( + + )} +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/label-views/IntegrationsTab.tsx b/ui/src/pages/label-views/IntegrationsTab.tsx new file mode 100644 index 00000000000..feca543bd10 --- /dev/null +++ b/ui/src/pages/label-views/IntegrationsTab.tsx @@ -0,0 +1,288 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiText, + EuiLoadingSpinner, + EuiCallOut, + EuiCodeBlock, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiIcon, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; + +const IntegrationsTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, isSuccess, data } = useLoadLabelView(name); + + const [webhookConfig, setWebhookConfig] = useState(null); + const [configLoading, setConfigLoading] = useState(true); + + useEffect(() => { + if (isSuccess && data) { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + fetch(`${baseUrl}/webhook/config/${name}`) + .then((r) => r.json()) + .then((d) => { + setWebhookConfig(d); + setConfigLoading(false); + }) + .catch(() => setConfigLoading(false)); + } + }, [isSuccess, data, name, registryUrl]); + + if (isLoading || configLoading) { + return ( +

+ Loading integration config... +

+ ); + } + + if (!webhookConfig) { + return ( + + Could not fetch webhook configuration. + + ); + } + + const webhookPayload = JSON.stringify(webhookConfig.payload_example, null, 2); + const baseUrl = window.location.origin; + const webhookFullUrl = `${baseUrl}${webhookConfig.webhook_url}`; + const batchFullUrl = `${baseUrl}${webhookConfig.batch_url}`; + + const argillaPython = `import argilla as rg +import requests +from datetime import datetime, timezone + +# After annotation is complete, export and push to Feast LabelView +dataset = rg.load("your_dataset_name") +submitted = dataset.records(status="submitted").to_list(flatten=True) + +records = [] +for record in submitted: + records.append({ + ${webhookConfig.entity_fields.map((e: string) => `"${e}": record.metadata["${e}"],`).join("\n ")} + ${webhookConfig.label_fields.map((f: string) => `"${f}": record.responses["${f}"],`).join("\n ")} + ${webhookConfig.labeler_field ? `"${webhookConfig.labeler_field}": record.user_id,` : ""} + "event_timestamp": datetime.now(timezone.utc).isoformat(), + }) + +response = requests.post( + "${webhookFullUrl}", + json={ + "push_source_name": "${webhookConfig.push_source_name}", + "records": records, + }, +) +print(f"Pushed {len(records)} labels: {response.json()}")`; + + const labelStudioPython = `import requests +from datetime import datetime, timezone +from label_studio_sdk import Client + +ls = Client(url="http://localhost:8080", api_key="YOUR_KEY") +project = ls.get_project(PROJECT_ID) + +# Export completed annotations +tasks = project.get_labeled_tasks() + +records = [] +for task in tasks: + annotation = task["annotations"][0]["result"][0] + records.append({ + ${webhookConfig.entity_fields.map((e: string) => `"${e}": task["data"]["${e}"],`).join("\n ")} + ${webhookConfig.label_fields.map((f: string) => `"${f}": annotation["value"].get("${f}", ""),`).join("\n ")} + ${webhookConfig.labeler_field ? `"${webhookConfig.labeler_field}": str(task["annotations"][0]["completed_by"]),` : ""} + "event_timestamp": datetime.now(timezone.utc).isoformat(), + }) + +response = requests.post( + "${webhookFullUrl}", + json={ + "push_source_name": "${webhookConfig.push_source_name}", + "records": records, + }, +) +print(f"Pushed {len(records)} labels: {response.json()}")`; + + const curlExample = `curl -X POST "${webhookFullUrl}" \\ + -H "Content-Type: application/json" \\ + -d '${webhookPayload}'`; + + return ( + + + + Connect Argilla, Label Studio, or any annotation tool to push labels + into this LabelView via webhook or batch API. + + + + + + {/* Webhook Configuration */} + + + + + + + +

Webhook Endpoint

+
+
+ + POST + +
+ + + Real-time label ingestion from annotation tools. Automatically adds + timestamps if not provided. + + + + + URL: + + {webhookFullUrl} + + + + + + + Required fields: + + {webhookConfig.entity_fields.map((f: string) => ( + + {f} (entity) + + ))} + {webhookConfig.label_fields.map((f: string) => ( + + {f} (label) + + ))} + {webhookConfig.labeler_field && ( + + + {webhookConfig.labeler_field} (labeler) + + + )} + + + +

Example payload:

+
+ + {webhookPayload} + +
+ + + + {/* Batch Push */} + + + + + + + +

Batch Push Endpoint

+
+
+ + POST + +
+ + + Upload bulk labels from CSV/parquet exports. Same schema as webhook. + + + + {batchFullUrl} + +
+ + + + {/* cURL Example */} + + +

cURL Example

+
+ + + {curlExample} + +
+ + + + {/* Argilla Integration */} + + + + + + + +

Argilla Integration

+
+
+
+ + + Export annotated records from Argilla and push to this LabelView. + + + + {argillaPython} + +
+ + + + {/* Label Studio Integration */} + + + + + + + +

Label Studio Integration

+
+
+
+ + + Export completed annotations from Label Studio and ingest via webhook. + + + + {labelStudioPython} + +
+
+ ); +}; + +export default IntegrationsTab; diff --git a/ui/src/pages/label-views/LabelBrowseTab.tsx b/ui/src/pages/label-views/LabelBrowseTab.tsx new file mode 100644 index 00000000000..7ed1b51a8b1 --- /dev/null +++ b/ui/src/pages/label-views/LabelBrowseTab.tsx @@ -0,0 +1,385 @@ +import React, { useContext, useState, useEffect, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiFieldSearch, + EuiButton, + EuiSpacer, + EuiCallOut, + EuiText, + EuiLoadingSpinner, + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiTablePagination, + EuiEmptyPrompt, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; + +interface LabelRow { + [key: string]: any; +} + +const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; + +const LabelBrowseTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, isSuccess, data } = useLoadLabelView(name); + + const [allLabels, setAllLabels] = useState(null); + const [allEntityNames, setAllEntityNames] = useState([]); + const [totalEntities, setTotalEntities] = useState(0); + const [loadingAll, setLoadingAll] = useState(false); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + const initialLoadDone = React.useRef(false); + + useEffect(() => { + if (isSuccess && data && !initialLoadDone.current) { + initialLoadDone.current = true; + loadLabels(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess, data, name, registryUrl]); + + const loadLabels = async () => { + setLoadingAll(true); + setError(null); + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch(`${baseUrl}/list-labels`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ feature_view: name, limit: 1000 }), + }); + if (response.ok) { + const respData = await response.json(); + setAllLabels(respData.labels || []); + setAllEntityNames(respData.entity_names || []); + setTotalEntities(respData.total_entities || 0); + } else { + const errData = await response.json().catch(() => null); + setError( + errData?.detail || `Failed to load labels (${response.status})`, + ); + } + } catch (err: any) { + setError(err.message || "Network error."); + } finally { + setLoadingAll(false); + } + }; + + const spec = data?.spec || data?.object?.spec || {}; + const entities: string[] = spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || []; + const features: any[] = useMemo(() => spec.features || [], [spec.features]); + const conflictPolicy = + spec.conflictPolicy || spec.conflict_policy || "LAST_WRITE_WINS"; + const policyLabel = + typeof conflictPolicy === "number" + ? ["LAST_WRITE_WINS", "LABELER_PRIORITY", "MAJORITY_VOTE"][ + conflictPolicy + ] || "LAST_WRITE_WINS" + : String(conflictPolicy).replace("CONFLICT_POLICY_", ""); + const labelerField: string = + spec.labelerField || spec.labeler_field || "labeler"; + + const entityCols = allEntityNames.length > 0 ? allEntityNames : entities; + + const filteredLabels = useMemo(() => { + if (!allLabels) return []; + if (!searchQuery.trim()) return allLabels; + + const query = searchQuery.toLowerCase(); + return allLabels.filter((row) => + Object.values(row).some( + (val) => val != null && String(val).toLowerCase().includes(query), + ), + ); + }, [allLabels, searchQuery]); + + const paginatedLabels = useMemo(() => { + const start = pageIndex * pageSize; + return filteredLabels.slice(start, start + pageSize); + }, [filteredLabels, pageIndex, pageSize]); + + const columns: EuiBasicTableColumn[] = useMemo(() => { + const cols: EuiBasicTableColumn[] = []; + + for (const entity of entityCols) { + cols.push({ + field: entity, + name: entity, + sortable: true, + render: (value: any) => ( + + {value != null ? String(value) : "\u2014"} + + ), + }); + } + + for (const feature of features) { + cols.push({ + field: feature.name, + name: feature.name, + sortable: true, + render: (value: any) => { + if (value === null || value === undefined) { + return ( + + + + ); + } + return {String(value)}; + }, + }); + } + + cols.push({ + field: "_event_ts", + name: "Last Updated", + sortable: true, + render: (value: any) => + value ? new Date(value * 1000).toLocaleString() : "\u2014", + }); + + return cols; + }, [entityCols, features]); + + if (isLoading) { + return ( +

+ Loading schema... +

+ ); + } + + if (!isSuccess || !data) { + return

Unable to load label view schema.

; + } + + return ( + + + + + +

Schema

+
+ + ({ + name: e, + type: "ENTITY", + role: "entity", + })), + ...features.map((f: any) => ({ + name: f.name, + type: f.valueType || "STRING", + role: f.name === labelerField ? "labeler" : "label", + })), + ]} + columns={[ + { field: "name", name: "Field", width: "40%" }, + { field: "type", name: "Type", width: "30%" }, + { + field: "role", + name: "Role", + width: "30%", + render: (role: string) => ( + + {role} + + ), + }, + ]} + tableLayout="fixed" + compressed + /> +
+ + +

Properties

+
+ + + + + Conflict Policy + +
+ + {policyLabel} + +
+
+ + + Labeler Field + + + {labelerField} + + +
+
+
+
+ + + + + + + +

+ Label Records{" "} + {allLabels && ( + {totalEntities} total + )} + {searchQuery && + filteredLabels.length !== (allLabels || []).length && ( + <> + {" "} + + {filteredLabels.length} matching + + + )} +

+
+
+ + + Refresh + + +
+ + + + +

+ All label records in the online store, resolved by conflict policy. + Use the search bar to filter by any field value. +

+
+ + + + { + setSearchQuery(e.target.value); + setPageIndex(0); + }} + isClearable + fullWidth + /> + + + + {allLabels === null && loadingAll && ( + + + + + Loading label records... + + )} + + {allLabels !== null && allLabels.length === 0 && ( + No labels submitted yet} + body="No labels have been pushed to the online store for this label view." + /> + )} + + {allLabels !== null && + allLabels.length > 0 && + filteredLabels.length === 0 && ( + No matching records} + body={ +

+ No records match "{searchQuery}". + Try a different search term. +

+ } + /> + )} + + {filteredLabels.length > 0 && ( + <> + + items={paginatedLabels} + columns={columns} + tableLayout="auto" + /> + + setPageIndex(page)} + itemsPerPage={pageSize} + onChangeItemsPerPage={(size) => { + setPageSize(size); + setPageIndex(0); + }} + itemsPerPageOptions={PAGE_SIZE_OPTIONS} + /> + + )} +
+ + {error && ( + <> + + + {error} + + + )} +
+ ); +}; + +export default LabelBrowseTab; diff --git a/ui/src/pages/label-views/LabelViewInstance.tsx b/ui/src/pages/label-views/LabelViewInstance.tsx new file mode 100644 index 00000000000..8ef720ed278 --- /dev/null +++ b/ui/src/pages/label-views/LabelViewInstance.tsx @@ -0,0 +1,152 @@ +import React, { useState } from "react"; +import { Route, Routes, useNavigate, useParams } from "react-router-dom"; +import { + EuiPageTemplate, + EuiPopover, + EuiContextMenu, + EuiButton, +} from "@elastic/eui"; + +import { LabelViewIcon } from "../../graphics/LabelViewIcon"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; +import LabelBrowseTab from "./LabelBrowseTab"; +import QualityDashboardTab from "./QualityDashboardTab"; +import AnnotateTab from "./AnnotateTab"; +import TrainingExportTab from "./TrainingExportTab"; +import IntegrationsTab from "./IntegrationsTab"; +import BatchUploadTab from "./BatchUploadTab"; +import LabelViewLineageTab from "./LabelViewLineageTab"; +import FeatureViewVersionsTab from "../feature-views/FeatureViewVersionsTab"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; + +const LabelViewInstance = () => { + const navigate = useNavigate(); + const { labelViewName } = useParams(); + const [isMoreOpen, setIsMoreOpen] = useState(false); + + useDocumentTitle(`${labelViewName} | Label View | Feast`); + + const moreMenuItems = [ + { + id: "more-panel", + items: [ + { + name: "Export", + icon: "exportAction", + onClick: () => { + setIsMoreOpen(false); + navigate("export"); + }, + }, + { + name: "Upload", + icon: "importAction", + onClick: () => { + setIsMoreOpen(false); + navigate("upload"); + }, + }, + { + name: "Versions", + icon: "copyClipboard", + onClick: () => { + setIsMoreOpen(false); + navigate("versions"); + }, + }, + { + name: "Lineage", + icon: "graphApp", + onClick: () => { + setIsMoreOpen(false); + navigate("lineage"); + }, + }, + { + name: "Integrations", + icon: "gear", + onClick: () => { + setIsMoreOpen(false); + navigate("integrations"); + }, + }, + ], + }, + ]; + + return ( + + setIsMoreOpen(!isMoreOpen)} + > + More + + } + isOpen={isMoreOpen} + closePopover={() => setIsMoreOpen(false)} + panelPaddingSize="none" + anchorPosition="downRight" + > + + , + ]} + tabs={[ + { + label: "Labels", + isSelected: useMatchExact(""), + onClick: () => { + navigate(""); + }, + }, + { + label: "Quality", + isSelected: useMatchSubpath("quality"), + onClick: () => { + navigate("quality"); + }, + }, + { + label: "Label Data", + isSelected: useMatchSubpath("annotate"), + onClick: () => { + navigate("annotate"); + }, + }, + ]} + /> + + + } /> + } /> + } /> + } /> + } /> + + } + /> + } /> + } /> + + + + ); +}; + +export default LabelViewInstance; diff --git a/ui/src/pages/label-views/LabelViewLineageTab.tsx b/ui/src/pages/label-views/LabelViewLineageTab.tsx new file mode 100644 index 00000000000..0da3d92dab8 --- /dev/null +++ b/ui/src/pages/label-views/LabelViewLineageTab.tsx @@ -0,0 +1,93 @@ +import React, { useContext, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiSpacer, + EuiSelect, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, +} from "@elastic/eui"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import RegistryVisualization from "../../components/RegistryVisualization"; +import { FEAST_FCO_TYPES } from "../../parsers/types"; +import { filterPermissionsByAction } from "../../utils/permissionUtils"; + +const LabelViewLineageTab = () => { + const registryUrl = useContext(RegistryPathContext); + const { labelViewName, projectName } = useParams(); + const { + isLoading, + isSuccess, + isError, + data: registryData, + } = useLoadRegistry(registryUrl, projectName); + const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); + + const filterNode = { + type: FEAST_FCO_TYPES.labelView, + name: labelViewName || "", + }; + + return ( + <> + {isLoading && ( +
+ +
+ )} + {isError && ( + Error loading lineage} + body={

Could not load lineage data for this label view.

} + /> + )} + {isSuccess && registryData && ( + <> + + + + setSelectedPermissionAction(e.target.value)} + aria-label="Filter by permissions" + /> + + + + + + + )} + + ); +}; + +export default LabelViewLineageTab; diff --git a/ui/src/pages/label-views/LabelViewOverviewTab.tsx b/ui/src/pages/label-views/LabelViewOverviewTab.tsx new file mode 100644 index 00000000000..bbe65071c39 --- /dev/null +++ b/ui/src/pages/label-views/LabelViewOverviewTab.tsx @@ -0,0 +1,340 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiText, + EuiSpacer, + EuiBadge, + EuiLoadingSpinner, + EuiBasicTable, + EuiBasicTableColumn, + EuiCallOut, + EuiLink, + EuiCode, +} from "@elastic/eui"; + +import useLoadLabelView from "./useLoadLabelView"; +import useAnnotationConfig from "./useAnnotationConfig"; + +const CONFLICT_POLICY_MAP: Record = { + "0": "LAST_WRITE_WINS", + "1": "LABELER_PRIORITY", + "2": "MAJORITY_VOTE", + LAST_WRITE_WINS: "LAST_WRITE_WINS", + LABELER_PRIORITY: "LABELER_PRIORITY", + MAJORITY_VOTE: "MAJORITY_VOTE", +}; + +interface SchemaField { + name: string; + valueType: string; +} + +const PROFILE_COLORS: Record = { + "document-span": "primary", + table: "default", + "entity-form": "accent", + "active-learning": "success", +}; + +interface FieldRoleRow { + field: string; + role: string; + values?: string; + widget?: string; +} + +const LabelViewOverviewTab = () => { + const { labelViewName, projectName } = useParams(); + const name = labelViewName || ""; + const { isLoading, isSuccess, isError, data } = useLoadLabelView(name); + const { data: annotationConfig } = useAnnotationConfig(name); + + if (isLoading) { + return ( +

+ Loading +

+ ); + } + if (isError) { + return

Error loading label view: {name}

; + } + if (!isSuccess || !data) { + return

No label view found with name: {name}

; + } + + const spec = data.spec || {}; + const meta = data.meta || {}; + const conflictPolicy = + CONFLICT_POLICY_MAP[spec.conflictPolicy] || + spec.conflictPolicy || + "LAST_WRITE_WINS"; + const labelerField = spec.labelerField || "labeler"; + const entities: string[] = spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || []; + const features: any[] = spec.features || []; + + const schemaColumns: EuiBasicTableColumn[] = [ + { field: "name", name: "Field Name", sortable: true }, + { field: "valueType", name: "Value Type" }, + ]; + + const schemaRows: SchemaField[] = features.map((f: any) => ({ + name: f.name || "Unknown", + valueType: f.valueType != null ? String(f.valueType) : "Unknown", + })); + + return ( + + +

+ conflict_policy is enforced for offline store reads + (training data, Browse, Quality). The offline store always retains + full write history. The online store uses last-write-wins for serving. +

+
+ + + + + +

Properties

+
+ + + Conflict Policy + + + {conflictPolicy} + + + + Labeler Field + + {labelerField} + + + Entities + + {entities.length > 0 + ? entities.map((ent: string, i: number) => ( + + {i > 0 && ", "} + + {ent} + + + )) + : "-"} + + + {spec.description && ( + <> + Description + + {spec.description} + + + )} + +
+ + + +

Metadata

+
+ + + Created + + {meta.createdTimestamp + ? new Date( + typeof meta.createdTimestamp === "string" + ? meta.createdTimestamp + : Number(meta.createdTimestamp.seconds) * 1000, + ).toLocaleDateString("en-CA") + : "N/A"} + + + Last Updated + + {meta.lastUpdatedTimestamp + ? new Date( + typeof meta.lastUpdatedTimestamp === "string" + ? meta.lastUpdatedTimestamp + : Number(meta.lastUpdatedTimestamp.seconds) * 1000, + ).toLocaleDateString("en-CA") + : "N/A"} + + +
+ {annotationConfig && ( + <> + + + +

Labeling Method

+
+ + + Profile + + + {annotationConfig.profile} + + + + Push Source + + + {annotationConfig.push_source_name || "N/A"} + + + + + {Object.keys(annotationConfig.field_roles).length > 0 && ( + <> + + + Field Roles + + + + items={Object.entries(annotationConfig.field_roles).map( + ([field, role]) => ({ + field, + role, + values: + annotationConfig.label_values[field]?.join(", "), + widget: annotationConfig.label_widgets[field], + }), + )} + columns={[ + { field: "field", name: "Field", width: "30%" }, + { + field: "role", + name: "Role", + width: "25%", + render: (role: string) => ( + + {role} + + ), + }, + { + field: "values", + name: "Values", + render: (v: string) => v || "\u2014", + }, + { + field: "widget", + name: "Widget", + render: (w: string) => w || "\u2014", + }, + ]} + tableLayout="auto" + compressed + /> + + )} +
+ + )} +
+ + + +

Labels

+
+ + {schemaRows.length > 0 ? ( + + items={schemaRows} + columns={[ + { + field: "name", + name: "Label Name", + sortable: true, + render: (labelName: string) => ( + + {labelName} + + ), + }, + { field: "valueType", name: "Value Type" }, + ]} + tableLayout="auto" + /> + ) : ( + No labels defined. + )} +
+ + {spec.source && ( + + +

Data Source

+
+ + + Source Type + + {spec.source.type || "PushSource"} + + {spec.source.name && ( + <> + + Source Name + + + + {spec.source.name} + + + + )} + +
+ )} +
+
+
+ ); +}; + +export default LabelViewOverviewTab; diff --git a/ui/src/pages/label-views/QualityDashboardTab.tsx b/ui/src/pages/label-views/QualityDashboardTab.tsx new file mode 100644 index 00000000000..7d0a8a0f3f4 --- /dev/null +++ b/ui/src/pages/label-views/QualityDashboardTab.tsx @@ -0,0 +1,406 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiText, + EuiLoadingSpinner, + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiBadge, + EuiBasicTable, + EuiBasicTableColumn, + EuiProgress, + EuiButton, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; + +interface QualityData { + total_entities: number; + feature_names: string[]; + distributions: Record>; + coverage_pct: Record; + null_counts: Record; + labeler_stats: Record; + staleness_seconds: number | null; + oldest_label_ts: string | null; + newest_label_ts: string | null; + labeler_field: string | null; +} + +const formatStaleness = (seconds: number | null): string => { + if (seconds === null) return "N/A"; + if (seconds < 60) return `${Math.round(seconds)}s ago`; + if (seconds < 3600) return `${Math.round(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.round(seconds / 3600)}h ago`; + return `${Math.round(seconds / 86400)}d ago`; +}; + +const getStalenessColor = (seconds: number | null): string => { + if (seconds === null) return "subdued"; + if (seconds < 3600) return "success"; + if (seconds < 86400) return "warning"; + return "danger"; +}; + +const DistributionBar = ({ + distribution, + label, +}: { + distribution: Record; + label: string; +}) => { + const entries = Object.entries(distribution).sort((a, b) => b[1] - a[1]); + const total = entries.reduce((acc, [, count]) => acc + count, 0); + const colors = [ + "#0569EA", + "#00BFB3", + "#F5A623", + "#BD271E", + "#6092C0", + "#D36086", + "#9170B8", + "#CA8EAE", + ]; + + if (entries.length === 0) { + return ( + + No data + + ); + } + + return ( +
+ + {label} ({total} values, {entries.length} unique) + + +
+ {entries.slice(0, 8).map(([val, count], idx) => ( +
+ ))} +
+ + + {entries.slice(0, 6).map(([val, count], idx) => ( + + + {val.length > 12 ? val.slice(0, 12) + "\u2026" : val}: {count} + + + ))} + {entries.length > 6 && ( + + +{entries.length - 6} more + + )} + +
+ ); +}; + +const QualityDashboardTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading: lvLoading, isSuccess } = useLoadLabelView(name); + + const [quality, setQuality] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchQuality = async () => { + setLoading(true); + setError(null); + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch(`${baseUrl}/label-quality`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ feature_view: name, limit: 500 }), + }); + const result = await response.json(); + if (!response.ok) { + const detail = result.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : "Failed to load quality metrics", + ); + } else { + setQuality(result); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (isSuccess) { + fetchQuality(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess, name]); + + if (lvLoading) { + return ( +

+ Loading... +

+ ); + } + + if (loading) { + return ( + + + + + + Computing label quality metrics... + + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + if (!quality) return null; + + const labelerColumns: EuiBasicTableColumn<{ name: string; count: number }>[] = + [ + { field: "name", name: "Labeler", sortable: true }, + { + field: "count", + name: "Labels Submitted", + sortable: true, + render: (count: number) => {count}, + }, + { + field: "count", + name: "Share", + render: (count: number) => { + const total = Object.values(quality.labeler_stats).reduce( + (a, b) => a + b, + 0, + ); + return `${((count / total) * 100).toFixed(1)}%`; + }, + }, + ]; + + const labelerData = Object.entries(quality.labeler_stats) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count); + + return ( + + {/* Time Range + Refresh at top */} + + + + + + + Oldest label:{" "} + {quality.oldest_label_ts + ? new Date(quality.oldest_label_ts).toLocaleString() + : "N/A"} + + + + + Newest label:{" "} + {quality.newest_label_ts + ? new Date(quality.newest_label_ts).toLocaleString() + : "N/A"} + + + + + + + Refresh Metrics + + + + + + + + {/* Summary Stats */} + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Coverage */} + + +

Field Coverage

+
+ + Percentage of records with non-null values for each label field + + + {quality.feature_names.map((fn) => ( + + + + + {fn} + + + + 80 + ? "success" + : (quality.coverage_pct[fn] || 0) > 50 + ? "warning" + : "danger" + } + label={`${(quality.coverage_pct[fn] || 0).toFixed(1)}%`} + /> + + + + {quality.null_counts[fn] || 0} nulls + + + + + + ))} +
+ + + + {/* Distributions */} + + +

Value Distributions

+
+ + Distribution of label values across all records + + + {quality.feature_names.map((fn) => ( + + + + + + ))} +
+ + + + {/* Per-Labeler Stats */} + {labelerData.length > 0 && ( + + + + +

Per-Labeler Statistics

+
+
+ {quality.labeler_field && ( + + + Tracked via: {quality.labeler_field} + + + )} +
+ + +
+ )} +
+ ); +}; + +export default QualityDashboardTab; diff --git a/ui/src/pages/label-views/RagLabelingMethod.tsx b/ui/src/pages/label-views/RagLabelingMethod.tsx new file mode 100644 index 00000000000..84ad696a149 --- /dev/null +++ b/ui/src/pages/label-views/RagLabelingMethod.tsx @@ -0,0 +1,899 @@ +import React, { useState, useContext, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiPanel, + EuiTitle, + EuiText, + EuiLoadingSpinner, + EuiButtonGroup, + EuiCode, + EuiTextArea, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiOverlayMask, + EuiForm, + EuiIcon, + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from "@elastic/eui"; +import { useTheme } from "../../contexts/ThemeContext"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import type { AnnotationConfig } from "./useAnnotationConfig"; + +interface TextSelection { + text: string; + start: number; + end: number; +} + +interface DocumentLabel { + text: string; + start: number; + end: number; + label: string; + timestamp: number; +} + +interface RagLabelingMethodProps { + annotationConfig: AnnotationConfig; +} + +const RagLabelingMethod = ({ annotationConfig }: RagLabelingMethodProps) => { + const { labelViewName } = useParams(); + const { colorMode } = useTheme(); + const registryUrl = useContext(RegistryPathContext); + + const fieldRoles = annotationConfig.field_roles; + const labelValues = annotationConfig.label_values; + const labelWidgets = annotationConfig.label_widgets; + + const contentRefField = useMemo( + () => + Object.entries(fieldRoles).find( + ([, role]) => role === "content_ref", + )?.[0] || null, + [fieldRoles], + ); + const contentField = useMemo( + () => + Object.entries(fieldRoles).find(([, role]) => role === "content")?.[0] || + null, + [fieldRoles], + ); + const spanStartField = useMemo( + () => + Object.entries(fieldRoles).find( + ([, role]) => role === "span_start", + )?.[0] || null, + [fieldRoles], + ); + const spanEndField = useMemo( + () => + Object.entries(fieldRoles).find(([, role]) => role === "span_end")?.[0] || + null, + [fieldRoles], + ); + + const labelFieldEntries = useMemo( + () => Object.entries(fieldRoles).filter(([, role]) => role === "label"), + [fieldRoles], + ); + const primaryLabelField = labelFieldEntries[0]?.[0] || null; + const secondaryLabelFields = labelFieldEntries.slice(1).map(([name]) => name); + + const primaryLabelOptions = useMemo(() => { + if (!primaryLabelField) + return [ + { id: "relevant", label: "Relevant" }, + { id: "irrelevant", label: "Irrelevant" }, + ]; + const values = labelValues[primaryLabelField]; + if (values && values.length > 0) { + return values.map((v) => ({ + id: v, + label: v.charAt(0).toUpperCase() + v.slice(1), + })); + } + return [ + { id: "relevant", label: "Relevant" }, + { id: "irrelevant", label: "Irrelevant" }, + ]; + }, [primaryLabelField, labelValues]); + + const [filePath, setFilePath] = useState(""); + const [selectedText, setSelectedText] = useState(null); + const [labelingMode, setLabelingMode] = useState( + primaryLabelOptions[0]?.id || "relevant", + ); + const [labels, setLabels] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [documentContent, setDocumentContent] = useState(null); + const [error, setError] = useState(null); + const [groundTruthLabel, setGroundTruthLabel] = useState(""); + const [isSaving, setIsSaving] = useState(false); + const [pushSuccess, setPushSuccess] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [extraFieldValues, setExtraFieldValues] = useState< + Record + >({}); + + const loadDocument = async () => { + if (!filePath) return; + setIsLoading(true); + setError(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch( + `${baseUrl}/document-content?path=${encodeURIComponent(filePath)}`, + ); + if (response.ok) { + const result = await response.json(); + setDocumentContent(result.content); + } else { + setDocumentContent( + `This is a sample document for testing RAG labeling in Feast UI. + +The document contains multiple paragraphs that can be used to test text highlighting and labeling. + +This paragraph discusses machine learning and artificial intelligence concepts. It covers topics like neural networks, deep learning, and natural language processing. Users should be able to select and label relevant portions of this text for RAG retrieval systems. + +Another section focuses on data engineering and ETL pipelines. This content explains how to process large datasets and build scalable data infrastructure. + +The final paragraph contains information about feature stores and real-time machine learning systems.`, + ); + } + } catch { + setDocumentContent( + `This is a sample document for testing RAG labeling in Feast UI. + +The document contains multiple paragraphs that can be used to test text highlighting and labeling. + +This paragraph discusses machine learning and artificial intelligence concepts. It covers topics like neural networks, deep learning, and natural language processing. + +Another section focuses on data engineering and ETL pipelines. This content explains how to process large datasets and build scalable data infrastructure. + +The final paragraph contains information about feature stores and real-time machine learning systems.`, + ); + } finally { + setIsLoading(false); + } + }; + + const handleTextSelection = () => { + const selection = window.getSelection(); + if (selection && selection.toString().trim() && documentContent) { + const selectedTextContent = selection.toString().trim(); + const range = selection.getRangeAt(0); + const rangeText = range.toString(); + if (rangeText) { + const startIndex = documentContent.indexOf(rangeText); + if (startIndex !== -1) { + setSelectedText({ + text: selectedTextContent, + start: startIndex, + end: startIndex + rangeText.length, + }); + } + } + } + }; + + const handleLabelSelection = () => { + if (selectedText) { + const newLabel: DocumentLabel = { + text: selectedText.text, + start: selectedText.start, + end: selectedText.end, + label: labelingMode, + timestamp: Date.now(), + }; + setLabels([...labels, newLabel]); + setSelectedText(null); + const selection = window.getSelection(); + if (selection) selection.removeAllRanges(); + } + }; + + const handleRemoveLabel = (index: number) => { + setLabels(labels.filter((_: DocumentLabel, i: number) => i !== index)); + }; + + const generateChunkId = (docName: string, start: number, end: number) => { + const raw = `${docName}:${start}:${end}`; + let hash = 0; + for (let i = 0; i < raw.length; i++) { + const char = raw.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash |= 0; + } + return `chunk_${Math.abs(hash).toString(36)}`; + }; + + const openSubmitModal = () => { + const defaults: Record = {}; + if (annotationConfig.labeler_field) { + defaults[annotationConfig.labeler_field] = "rag_labeling_ui"; + } + setExtraFieldValues(defaults); + setIsModalOpen(true); + }; + + const buildPushRows = () => { + const docName = filePath || "document"; + const entityField = annotationConfig.entities[0] || "entity_id"; + const labelerField = annotationConfig.labeler_field; + + return labels.map((label) => { + const row: Record = {}; + + row[entityField] = generateChunkId(docName, label.start, label.end); + + if (annotationConfig.entities.length > 1) { + for (let i = 1; i < annotationConfig.entities.length; i++) { + const ent = annotationConfig.entities[i]; + if (extraFieldValues[ent]) { + row[ent] = extraFieldValues[ent]; + } + } + } + + if (contentRefField) row[contentRefField] = filePath; + if (contentField) row[contentField] = label.text; + if (spanStartField) row[spanStartField] = label.start; + if (spanEndField) row[spanEndField] = label.end; + if (primaryLabelField) row[primaryLabelField] = label.label; + + for (const secField of secondaryLabelFields) { + const widget = labelWidgets[secField]; + if (widget === "text" && groundTruthLabel) { + row[secField] = groundTruthLabel; + } else if (extraFieldValues[secField]) { + row[secField] = extraFieldValues[secField]; + } + } + + if (labelerField) { + row[labelerField] = extraFieldValues[labelerField] || "rag_labeling_ui"; + } + + row["event_timestamp"] = new Date().toISOString(); + return row; + }); + }; + + const saveToLabelView = async () => { + if (labels.length === 0) return; + setIsSaving(true); + setError(null); + setPushSuccess(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const pushSourceName = + annotationConfig.push_source_name || `${labelViewName}_push_source`; + const rows = buildPushRows(); + + const columnar: Record = {}; + if (rows.length > 0) { + for (const key of Object.keys(rows[0])) { + columnar[key] = rows.map((r) => r[key]); + } + } + + const response = await fetch(`${baseUrl}/push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + df: columnar, + to: "online_and_offline", + }), + }); + + if (response.ok) { + setPushSuccess( + `Pushed ${labels.length} span labels to ${labelViewName}`, + ); + setLabels([]); + setIsModalOpen(false); + } else { + const errData = await response.json().catch(() => null); + setError( + typeof errData?.detail === "string" + ? errData.detail + : `Push failed (${response.status})`, + ); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setIsSaving(false); + } + }; + + const exportJSON = () => { + const rows = buildPushRows(); + const saveData = { + labelView: labelViewName, + profile: annotationConfig.profile, + filePath, + groundTruthLabel, + rows, + timestamp: new Date().toISOString(), + }; + const blob = new Blob([JSON.stringify(saveData, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${labelViewName}_rag_labels.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const renderDocumentWithHighlights = ( + content: string, + ): (string | React.ReactElement)[] => { + const allHighlights = [...labels]; + if (selectedText) { + allHighlights.push({ + text: selectedText.text, + start: selectedText.start, + end: selectedText.end, + label: "temp-selection", + timestamp: 0, + }); + } + if (allHighlights.length === 0) return [content]; + + const sortedHighlights = [...allHighlights].sort( + (a, b) => a.start - b.start, + ); + const result: (string | React.ReactElement)[] = []; + let lastIndex = 0; + + const positiveValues = new Set( + primaryLabelOptions.length > 0 + ? [primaryLabelOptions[0].id] + : ["relevant"], + ); + + sortedHighlights.forEach((highlight, index) => { + result.push(content.slice(lastIndex, highlight.start)); + let highlightColor: string; + let borderColor: string; + + if (highlight.label === "temp-selection") { + highlightColor = colorMode === "dark" ? "#1a4d66" : "#add8e6"; + borderColor = colorMode === "dark" ? "#2d6b8a" : "#87ceeb"; + } else if (!positiveValues.has(highlight.label)) { + highlightColor = colorMode === "dark" ? "#4d1a1a" : "#f8d7da"; + borderColor = colorMode === "dark" ? "#6b2d2d" : "#f5c6cb"; + } else { + highlightColor = colorMode === "dark" ? "#1a4d1a" : "#d4edda"; + borderColor = colorMode === "dark" ? "#2d6b2d" : "#c3e6cb"; + } + + result.push( + + {highlight.text} + , + ); + lastIndex = highlight.end; + }); + + result.push(content.slice(lastIndex)); + return result; + }; + + const chunkColumns: EuiBasicTableColumn[] = [ + { + field: "label", + name: primaryLabelField || "Label", + width: "120px", + render: (value: string) => { + const isPositive = + primaryLabelOptions.length > 0 && value === primaryLabelOptions[0].id; + return ( + {value} + ); + }, + }, + { + field: "text", + name: "Span Text", + truncateText: true, + render: (value: string) => + value.substring(0, 100) + (value.length > 100 ? "..." : ""), + }, + { + name: "Offsets", + width: "100px", + render: (item: DocumentLabel) => ( + + {item.start}–{item.end} + + ), + }, + ]; + + const unmappedFeatures = annotationConfig.features.filter((f) => { + if (f === annotationConfig.labeler_field) return false; + if (fieldRoles[f]) return false; + return true; + }); + + return ( + + +

+ Load a document, highlight text spans, and label them. Span positions + and labels are auto-mapped to your schema fields and pushed to{" "} + {labelViewName} via its PushSource. +

+
+ + + + + +

Field Mapping

+
+ + + {contentRefField && ( + <> + Document path + + {contentRefField} + + + )} + {contentField && ( + <> + Span text + + {contentField} + + + )} + {spanStartField && spanEndField && ( + <> + Offsets + + {spanStartField},{" "} + {spanEndField} + + + )} + {primaryLabelField && ( + <> + Primary label + + {primaryLabelField} + {" → "} + {primaryLabelOptions.map((o) => o.id).join(", ")} + + + )} + {secondaryLabelFields.map((f) => ( + + {f} + + {labelWidgets[f] || "text"} + + + ))} + +
+ + + + + + + setFilePath(e.target.value)} + /> + + + + + + Load Document + + + + + + {isLoading && ( + <> + + + + + + + Loading document... + + + + )} + + {error && ( + <> + + +

{error}

+
+ + )} + + {pushSuccess && ( + <> + + + + )} + + {documentContent && ( + <> + + + + + + setLabelingMode(id)} + buttonSize="s" + /> + + + + + + Label Selected Text + + + + + + {selectedText && ( + <> + + + {selectedText.text.substring(0, 120)} + + + )} + + + + + +

Document Content

+
+ + +
+ {renderDocumentWithHighlights(documentContent)} +
+
+
+ + {secondaryLabelFields.length > 0 && ( + <> + + {secondaryLabelFields.map((f) => { + const widget = labelWidgets[f] || "text"; + if (widget === "text") { + return ( + + { + if (f === secondaryLabelFields[0]) { + setGroundTruthLabel(e.target.value); + } else { + setExtraFieldValues((prev) => ({ + ...prev, + [f]: e.target.value, + })); + } + }} + rows={3} + /> + + ); + } + return null; + })} + + )} + + + + + + + Export JSON + + + + + Save to LabelView ({labels.length}) + + + + + {labels.length > 0 && ( + <> + + + +

Labeled Spans ({labels.length})

+
+ + {labels.map((label, index) => ( + + + + {label.label} + + + + + {label.start}–{label.end} + + + + + "{label.text.substring(0, 80)} + {label.text.length > 80 ? "..." : ""}" + + + + handleRemoveLabel(index)} + > + Remove + + + + ))} +
+ + )} + + )} + + {isModalOpen && ( + + setIsModalOpen(false)} maxWidth={600}> + + + Push {labels.length} Span + {labels.length !== 1 ? "s" : ""} to {labelViewName} + + + + +

+ Each span will be pushed as a row. Entity key ( + {annotationConfig.entities[0] || "entity_id"} + ) is auto-generated from document path + span offsets. Fields + are mapped from the annotation profile. +

+
+ + + + +

Spans to push:

+
+ + +
+ + + +

Additional Fields

+
+ + + + + setExtraFieldValues((prev) => ({ + ...prev, + [annotationConfig.labeler_field]: e.target.value, + })) + } + /> + + + {annotationConfig.entities.length > 1 && + annotationConfig.entities.slice(1).map((ent) => ( + + + setExtraFieldValues((prev) => ({ + ...prev, + [ent]: e.target.value, + })) + } + /> + + ))} + + {unmappedFeatures.map((f) => ( + + + setExtraFieldValues((prev) => ({ + ...prev, + [f]: e.target.value, + })) + } + /> + + ))} + + + {error && ( + <> + + + {error} + + + )} +
+ + { + setIsModalOpen(false); + setError(null); + }} + > + Cancel + + + Push Labels + + +
+
+ )} +
+ ); +}; + +export default RagLabelingMethod; diff --git a/ui/src/pages/label-views/TrainingExportTab.tsx b/ui/src/pages/label-views/TrainingExportTab.tsx new file mode 100644 index 00000000000..7a19741c182 --- /dev/null +++ b/ui/src/pages/label-views/TrainingExportTab.tsx @@ -0,0 +1,421 @@ +import React, { useContext, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiSpacer, + EuiCallOut, + EuiText, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiDatePicker, + EuiSuperSelect, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import moment from "moment"; + +const TrainingExportTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, data } = useLoadLabelView(name); + const { data: registryData } = useLoadRegistry(registryUrl); + + const [featureService, setFeatureService] = useState(""); + const [entityColumn, setEntityColumn] = useState(""); + const [entityValues, setEntityValues] = useState(""); + const [startDate, setStartDate] = useState( + moment().subtract(30, "days"), + ); + const [endDate, setEndDate] = useState(moment()); + const [exporting, setExporting] = useState(false); + const [exportResult, setExportResult] = useState(null); + const [error, setError] = useState(null); + + if (isLoading) { + return ( +

+ Loading... +

+ ); + } + + const spec = data?.object?.spec || data?.spec || {}; + const entities: string[] = spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || []; + + const allFeatureServices = registryData?.objects?.featureServices || []; + const relevantFeatureServices = allFeatureServices.filter((fs: any) => { + const projections = [ + ...(fs.spec?.features || []), + ...(fs.spec?.featureViewProjections || []), + ]; + return projections.some( + (proj: any) => + proj.featureViewName === name || + proj.name === name || + proj.featureViewProjection?.featureViewName === name, + ); + }); + const servicesToShow = + relevantFeatureServices.length > 0 + ? relevantFeatureServices + : allFeatureServices; + + const featureServiceOptions = servicesToShow.map((fs: any) => ({ + value: fs.spec?.name || fs.name || "", + inputDisplay: fs.spec?.name || fs.name || "Unknown", + dropdownDisplay: ( + + {fs.spec?.name || fs.name} + {fs.spec?.description && ( + +

{fs.spec.description}

+
+ )} +
+ ), + })); + + const entityColumnOptions = entities.map((e: string) => ({ + value: e, + inputDisplay: e, + })); + + const handleExport = async () => { + setExporting(true); + setError(null); + setExportResult(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const entityKey = + entityColumn || (entities.length > 0 ? entities[0] : "entity_id"); + const values = entityValues + .split(",") + .map((v: string) => v.trim()) + .filter((v: string) => v.length > 0); + + if (values.length === 0) { + setError("Please provide at least one entity value"); + setExporting(false); + return; + } + + const serviceName = featureService || `${name}_service`; + const entityDf: Record = { + [entityKey]: values, + }; + + const response = await fetch(`${baseUrl}/training-dataset/export`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + feature_service: serviceName, + entity_df: entityDf, + start_date: startDate?.toISOString() || null, + end_date: endDate?.toISOString() || null, + }), + }); + + const result = await response.json(); + if (!response.ok) { + const detail = result.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : "Export failed", + ); + } else { + setExportResult(result); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setExporting(false); + } + }; + + const downloadCSV = () => { + if (!exportResult?.data) return; + const cols = exportResult.columns; + const csvRows = [cols.join(",")]; + for (const row of exportResult.data) { + csvRows.push( + cols + .map((c: string) => { + const val = row[c]; + if (val === null || val === undefined) return ""; + const str = String(val); + return str.includes(",") ? `"${str}"` : str; + }) + .join(","), + ); + } + const blob = new Blob([csvRows.join("\n")], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${exportResult.feature_service}_training_data.csv`; + a.click(); + URL.revokeObjectURL(url); + }; + + const downloadJSON = () => { + if (!exportResult?.data) return; + const blob = new Blob([JSON.stringify(exportResult.data, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${exportResult.feature_service}_training_data.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const columns: EuiBasicTableColumn[] = exportResult + ? exportResult.columns.map((col: string) => ({ + field: col, + name: col, + truncateText: true, + render: (val: any) => + val === null || val === undefined ? ( + null + ) : ( + String(val) + ), + })) + : []; + + return ( + + + + Create a point-in-time correct training dataset by joining features + with labels via get_historical_features. Export as CSV or + JSON for model training. + + + + + + + +

Export Configuration

+
+ + + + + setFeatureService(value)} + placeholder="Select a feature service..." + hasDividers + /> + + + + {entityColumnOptions.length > 0 ? ( + setEntityColumn(value)} + /> + ) : ( + setEntityColumn(e.target.value)} + /> + )} + + + + setEntityValues(e.target.value)} + /> + + + + + + + + + + + + + + + + + + + Generate Training Dataset + + +
+ + {error && ( + + + + {error} + + + )} + + {exportResult && ( + + + + + + +

+ Training Dataset{" "} + + {exportResult.row_count} rows + +

+
+
+ + + + + Download CSV + + + + + Download JSON + + + + +
+ + + Feature service: {exportResult.feature_service} | + Columns: {exportResult.columns.length} | Point-in-time correct + + + + {exportResult.data.length > 50 && ( + + Showing first 50 of {exportResult.data.length} rows. Download + for full dataset. + + )} +
+
+ )} + + + + + +

SDK Equivalent

+
+ + + This UI action is equivalent to the following Python SDK call: + + +
+          {`from feast import FeatureStore
+import pandas as pd
+
+store = FeatureStore(".")
+entity_df = pd.DataFrame({
+    "${entityColumn || entities[0] || "entity_id"}": [${
+      entityValues
+        ? entityValues
+            .split(",")
+            .map((v) => `"${v.trim()}"`)
+            .join(", ")
+        : '"user_1", "user_2"'
+    }],
+    "event_timestamp": pd.Timestamp("${endDate?.toISOString() || "now"}"),
+})
+
+training_df = store.get_historical_features(
+    entity_df=entity_df,
+    features=store.get_feature_service("${featureService || "your_service"}"),
+).to_df()
+
+training_df.to_parquet("training_data.parquet")`}
+        
+
+
+ ); +}; + +export default TrainingExportTab; diff --git a/ui/src/pages/label-views/useAnnotationConfig.ts b/ui/src/pages/label-views/useAnnotationConfig.ts new file mode 100644 index 00000000000..558fd3067e8 --- /dev/null +++ b/ui/src/pages/label-views/useAnnotationConfig.ts @@ -0,0 +1,41 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; + +export interface AnnotationConfig { + label_view: string; + profile: string; + field_roles: Record; + label_values: Record; + label_widgets: Record; + entities: string[]; + features: string[]; + labeler_field: string; + push_source_name: string | null; +} + +const useAnnotationConfig = (labelViewName: string) => { + const registryUrl = useContext(RegistryPathContext); + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + + return useQuery( + ["annotation-config", labelViewName, registryUrl], + async () => { + const response = await fetch( + `${baseUrl}/annotation-config/${encodeURIComponent(labelViewName)}`, + ); + if (!response.ok) { + throw new Error( + `Failed to load annotation config (${response.status})`, + ); + } + return response.json(); + }, + { + enabled: !!labelViewName && !!registryUrl, + staleTime: 60_000, + }, + ); +}; + +export default useAnnotationConfig; diff --git a/ui/src/pages/label-views/useLoadLabelView.ts b/ui/src/pages/label-views/useLoadLabelView.ts new file mode 100644 index 00000000000..8ddb44cd22c --- /dev/null +++ b/ui/src/pages/label-views/useLoadLabelView.ts @@ -0,0 +1,18 @@ +import { useParams } from "react-router-dom"; +import useResourceQuery, { + labelViewDetailPath, +} from "../../queries/useResourceQuery"; + +const useLoadLabelView = (labelViewName: string) => { + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `label-view:${labelViewName}`, + project: projectName, + restPath: labelViewDetailPath(labelViewName, projectName || ""), + restSelect: (d) => d, + enabled: !!labelViewName, + }); +}; + +export default useLoadLabelView; diff --git a/ui/src/pages/lineage/Index.tsx b/ui/src/pages/lineage/Index.tsx index a3a9ca19296..40bf7f83afc 100644 --- a/ui/src/pages/lineage/Index.tsx +++ b/ui/src/pages/lineage/Index.tsx @@ -1,18 +1,29 @@ -import React, { useContext } from "react"; +import React, { useContext, useState } from "react"; import { EuiPageTemplate, EuiTitle, EuiSpacer, EuiSkeletonText, EuiEmptyPrompt, + EuiButtonGroup, } from "@elastic/eui"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import useLoadRegistry from "../../queries/useLoadRegistry"; import RegistryPathContext from "../../contexts/RegistryPathContext"; import RegistryVisualizationTab from "../../components/RegistryVisualizationTab"; +import { LineageGraph } from "../../components/OpenLineageGraph"; +import LineageEventsList from "../../components/LineageEventsList"; +import { useLoadOpenLineageGraph } from "../../queries/useLoadOpenLineageGraph"; import { useParams } from "react-router-dom"; +type ActiveTab = "lineage" | "events"; + +const tabButtons = [ + { id: "lineage", label: "Lineage" }, + { id: "events", label: "Events" }, +]; + const LineagePage = () => { useDocumentTitle("Feast Lineage"); const registryUrl = useContext(RegistryPathContext); @@ -22,7 +33,14 @@ const LineagePage = () => { projectName, ); - // Show message for "All Projects" view + const [activeTab, setActiveTab] = useState("lineage"); + const [registryOnly, setRegistryOnly] = useState(false); + + const olGraphQuery = useLoadOpenLineageGraph(); + + const olConsumerAvailable = + !olGraphQuery.isError && olGraphQuery.data !== undefined; + if (projectName === "all") { return ( @@ -81,7 +99,66 @@ const LineagePage = () => { /> )} - {isSuccess && } + {isSuccess && ( + <> + {olConsumerAvailable ? ( + <> + setActiveTab(id as ActiveTab)} + buttonSize="m" + isFullWidth={false} + /> + + + {activeTab === "lineage" && ( + <> + {registryOnly ? ( + + + setRegistryOnly(e.target.checked) + } + /> + {" Feast Only Lineage"} + + } + /> + ) : ( + + + setRegistryOnly(e.target.checked) + } + /> + {" Feast Only Lineage"} + + } + /> + )} + + )} + + {activeTab === "events" && } + + ) : ( + + )} + + )} ); diff --git a/ui/src/pages/monitoring/FeatureMetricsDetail.tsx b/ui/src/pages/monitoring/FeatureMetricsDetail.tsx new file mode 100644 index 00000000000..51f690ffa55 --- /dev/null +++ b/ui/src/pages/monitoring/FeatureMetricsDetail.tsx @@ -0,0 +1,259 @@ +import React, { useState, useMemo } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + EuiPageTemplate, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSkeletonText, + EuiEmptyPrompt, + EuiButton, + EuiBreadcrumbs, + EuiSuperSelect, + EuiFormRow, +} from "@elastic/eui"; +import { FeatureIcon } from "../../graphics/FeatureIcon"; +import { + useFeatureMetrics, + useBaselineMetrics, +} from "../../queries/useMonitoringApi"; +import type { + NumericHistogram, + CategoricalHistogram, +} from "../../queries/useMonitoringApi"; +import { + NumericHistogramChart, + CategoricalHistogramChart, +} from "./components/HistogramChart"; +import StatsPanel from "./components/StatsPanel"; +import TimeSeriesAnalysis from "./components/TimeSeriesAnalysis"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; + +const BASELINE_KEY = "__baseline__"; + +const GRANULARITY_LABELS: Record = { + daily: "Daily", + weekly: "Weekly", + biweekly: "Biweekly", + monthly: "Monthly", + quarterly: "Quarterly", + [BASELINE_KEY]: "Baseline", +}; + +const FeatureMetricsDetail = () => { + const { projectName, featureViewName, featureName } = useParams(); + const navigate = useNavigate(); + const [selectedGranularity, setSelectedGranularity] = useState(""); + + useDocumentTitle(`${featureName} Monitoring | ${featureViewName} | Feast`); + + const { + data: metrics, + isLoading, + isError, + } = useFeatureMetrics({ + project: projectName || "", + feature_view_name: featureViewName, + feature_name: featureName, + }); + + const { data: baselineMetrics } = useBaselineMetrics( + projectName || "", + featureViewName, + featureName, + ); + + const baselineMetric = + baselineMetrics && baselineMetrics.length > 0 ? baselineMetrics[0] : null; + + const availableGranularities = useMemo(() => { + const granularities = new Set(); + if (metrics) { + for (const m of metrics) { + if (m.row_count > 0) granularities.add(m.granularity); + } + } + return Array.from(granularities).sort(); + }, [metrics]); + + const granularityOptions = useMemo(() => { + const options = availableGranularities.map((g) => ({ + value: g, + inputDisplay: GRANULARITY_LABELS[g] || g, + dropdownDisplay: GRANULARITY_LABELS[g] || g, + })); + if (baselineMetric) { + options.push({ + value: BASELINE_KEY, + inputDisplay: "Baseline", + dropdownDisplay: "Baseline (all data)", + }); + } + return options; + }, [availableGranularities, baselineMetric]); + + const effectiveGranularity = + selectedGranularity || availableGranularities[0] || ""; + + const activeMetric = useMemo(() => { + if (effectiveGranularity === BASELINE_KEY && baselineMetric) { + return baselineMetric; + } + if (!metrics || metrics.length === 0) return null; + const matching = metrics.filter( + (m) => m.granularity === effectiveGranularity && m.row_count > 0, + ); + if (matching.length === 0) { + const withData = metrics.filter((m) => m.row_count > 0); + const candidates = withData.length > 0 ? withData : metrics; + return candidates.reduce((a, b) => + a.metric_date > b.metric_date ? a : b, + ); + } + return matching.reduce((a, b) => (a.metric_date > b.metric_date ? a : b)); + }, [metrics, effectiveGranularity, baselineMetric]); + + const breadcrumbs = [ + { + text: "Monitoring", + onClick: () => navigate(`/p/${projectName}/monitoring`), + }, + { + text: featureViewName || "", + }, + { + text: featureName || "", + }, + ]; + + if (isLoading) { + return ( + + + + + + ); + } + + if (isError || !activeMetric) { + return ( + + + + + No Metrics Available} + body={ +

+ No monitoring metrics found for feature{" "} + {featureName} in feature view{" "} + {featureViewName}. Run a monitoring compute job + first. +

+ } + actions={ + navigate(`/p/${projectName}/monitoring`)} + > + Back to Monitoring + + } + /> +
+
+ ); + } + + const isNumeric = activeMetric.feature_type === "numeric"; + + return ( + + navigate(`/p/${projectName}/monitoring`)} + > + Back to Monitoring + , + ]} + /> + + + + + {granularityOptions.length > 0 && ( + <> + + + + setSelectedGranularity(val)} + compressed + /> + + + + + + )} + + + + {isNumeric && activeMetric.histogram && ( + + )} + {!isNumeric && activeMetric.histogram && ( + + )} + {!activeMetric.histogram && ( + No Histogram Data} + body={

Histogram data is not available for this metric.

} + /> + )} +
+ + + + +
+ + {metrics && metrics.length > 1 && ( + <> + + + + )} +
+
+ ); +}; + +export default FeatureMetricsDetail; diff --git a/ui/src/pages/monitoring/FeatureMetricsTable.tsx b/ui/src/pages/monitoring/FeatureMetricsTable.tsx new file mode 100644 index 00000000000..5b998c98aee --- /dev/null +++ b/ui/src/pages/monitoring/FeatureMetricsTable.tsx @@ -0,0 +1,426 @@ +import React, { useState, useMemo, useEffect } from "react"; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiButtonIcon, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLink, + EuiPopover, + EuiProgress, + EuiTitle, + EuiToolTip, + Criteria, +} from "@elastic/eui"; +import type { + FeatureMetric, + NumericHistogram, + CategoricalHistogram, +} from "../../queries/useMonitoringApi"; + +const healthColor = (nullRate: number): string => { + if (nullRate >= 0.5) return "danger"; + if (nullRate >= 0.1) return "warning"; + return "success"; +}; + +const healthLabel = (nullRate: number): string => { + if (nullRate >= 0.5) return "High null rate"; + if (nullRate >= 0.1) return "Moderate null rate"; + return "Healthy"; +}; + +const formatNum = (val: number | null, decimals = 2): string => { + if (val === null || val === undefined) return "—"; + if (Number.isInteger(val)) return val.toLocaleString(); + return val.toFixed(decimals); +}; + +const formatFreshness = (computedAt: string | null): string => { + if (!computedAt) return "—"; + const diff = Date.now() - new Date(computedAt).getTime(); + const mins = Math.floor(diff / 60_000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + const days = Math.floor(hrs / 24); + if (days < 30) return `${days}d ago`; + return `${Math.floor(days / 30)}mo ago`; +}; + +const freshnessColor = (computedAt: string | null): string => { + if (!computedAt) return "subdued"; + const hrs = (Date.now() - new Date(computedAt).getTime()) / 3_600_000; + if (hrs < 24) return "success"; + if (hrs < 72) return "warning"; + return "danger"; +}; + +const MiniHistogram = ({ metric }: { metric: FeatureMetric }) => { + if (!metric.histogram) return ; + + const width = 120; + const height = 28; + + if (metric.feature_type === "numeric") { + const hist = metric.histogram as NumericHistogram; + const maxCount = Math.max(...hist.counts, 1); + const barW = Math.max(Math.floor(width / hist.counts.length) - 1, 2); + + return ( + + + {hist.counts.map((count, i) => { + const h = (count / maxCount) * (height - 2); + return ( + + ); + })} + + + ); + } + + const hist = metric.histogram as CategoricalHistogram; + const maxCount = Math.max(...hist.values.map((v) => v.count), 1); + const barW = Math.max( + Math.floor(width / Math.min(hist.values.length, 10)) - 1, + 6, + ); + + return ( + + + {hist.values.slice(0, 10).map((v, i) => { + const h = (v.count / maxCount) * (height - 2); + return ( + + ); + })} + + + ); +}; + +interface FeatureMetricsTableProps { + metrics: FeatureMetric[]; + isLoading: boolean; + onFeatureClick: (fvName: string, featureName: string) => void; +} + +const PAGE_SIZE_OPTIONS = [10, 20, 50]; + +const FeatureMetricsTable = ({ + metrics, + isLoading, + onFeatureClick, +}: FeatureMetricsTableProps) => { + const [sortField, setSortField] = + useState("feature_view_name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(20); + + useEffect(() => { + setPageIndex(0); + }, [metrics]); + + const latestMetrics = useMemo(() => { + const byKey = new Map(); + for (const m of metrics) { + const key = `${m.feature_view_name}::${m.feature_name}`; + const existing = byKey.get(key); + if (!existing) { + byKey.set(key, m); + } else { + const preferNew = + m.row_count > 0 && existing.row_count === 0 + ? true + : existing.row_count > 0 && m.row_count === 0 + ? false + : m.metric_date > existing.metric_date; + if (preferNew) byKey.set(key, m); + } + } + return Array.from(byKey.values()); + }, [metrics]); + + const sortedItems = useMemo(() => { + return [...latestMetrics].sort((a, b) => { + const aVal = a[sortField]; + const bVal = b[sortField]; + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + if (aVal < bVal) return sortDirection === "asc" ? -1 : 1; + if (aVal > bVal) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + }, [latestMetrics, sortField, sortDirection]); + + const pageOfItems = useMemo(() => { + const start = pageIndex * pageSize; + return sortedItems.slice(start, start + pageSize); + }, [sortedItems, pageIndex, pageSize]); + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount: sortedItems.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }), + [pageIndex, pageSize, sortedItems.length], + ); + + const onTableChange = ({ sort, page }: Criteria) => { + if (sort) { + setSortField(sort.field as keyof FeatureMetric); + setSortDirection(sort.direction); + } + if (page) { + setPageIndex(page.index); + setPageSize(page.size); + } + }; + + const [isLegendOpen, setIsLegendOpen] = useState(false); + + const columnLegend = [ + { + title: "Feature", + description: + "Name of the individual feature. Click to view full distribution and detailed statistics.", + }, + { + title: "Feature View", + description: + "The feature view this feature belongs to — a logical grouping of related features sharing the same data source.", + }, + { + title: "Type", + description: + "Data type: numeric (continuous/discrete numbers) or categorical (strings/labels).", + }, + { + title: "Distribution", + description: + "Compact histogram showing the value distribution. Blue bars = numeric, orange bars = categorical.", + }, + { + title: "Rows", + description: + "Total number of rows (data points) observed for this feature in the computed time window.", + }, + { + title: "Null Rate", + description: + "Percentage of rows with missing (null) values. Shown as a progress bar colored by severity.", + }, + { + title: "Health", + description: + "Data quality indicator based on null rate: Healthy (< 10%), Moderate (10–49%), High (>= 50%).", + }, + { + title: "Mean", + description: + "Arithmetic mean of the feature values. Only shown for numeric features.", + }, + { + title: "Std Dev", + description: + "Standard deviation — measures how spread out the values are from the mean. Only for numeric features.", + }, + { + title: "Freshness", + description: + "Recency of the underlying data. Green (< 24h old), Yellow (24–72h), Red (> 72h). Hover for the data date.", + }, + { + title: "Source", + description: + "Data source type used for metric computation (e.g. batch, stream).", + }, + ]; + + const columns: EuiBasicTableColumn[] = [ + { + field: "feature_name", + name: "Feature", + sortable: true, + render: (name: string, item: FeatureMetric) => ( + onFeatureClick(item.feature_view_name, name)}> + {name} + + ), + }, + { + field: "feature_view_name", + name: "Feature View", + sortable: true, + }, + { + field: "feature_type", + name: "Type", + sortable: true, + width: "100px", + render: (type: string) => ( + + {type} + + ), + }, + { + name: "Distribution", + width: "140px", + render: (item: FeatureMetric) => , + }, + { + field: "row_count", + name: "Rows", + sortable: true, + width: "90px", + render: (val: number) => formatNum(val, 0), + }, + { + field: "null_rate", + name: "Null Rate", + sortable: true, + width: "150px", + render: (val: number) => ( +
+ + {(val * 100).toFixed(1)}% +
+ ), + }, + { + field: "null_rate", + name: "Health", + width: "130px", + render: (val: number) => ( + {healthLabel(val)} + ), + }, + { + field: "mean", + name: "Mean", + sortable: true, + width: "100px", + render: (val: number | null) => formatNum(val), + }, + { + field: "stddev", + name: "Std Dev", + sortable: true, + width: "100px", + render: (val: number | null) => formatNum(val), + }, + { + field: "metric_date", + name: "Freshness", + sortable: true, + width: "110px", + render: (val: string) => ( + + + {formatFreshness(val)} + + + ), + }, + { + field: "data_source_type", + name: "Source", + width: "80px", + render: (val: string) => {val}, + }, + ]; + + return ( + <> + + + setIsLegendOpen(!isLegendOpen)} + /> + } + isOpen={isLegendOpen} + closePopover={() => setIsLegendOpen(false)} + anchorPosition="downRight" + panelPaddingSize="m" + panelStyle={{ maxWidth: 420 }} + > + +

Column Legend

+
+ +
+
+
+ ({ + "data-test-subj": `row-${item.feature_name}`, + })} + noItemsMessage={ + isLoading + ? "Loading metrics..." + : "No metrics found. Run a monitoring compute job to generate metrics." + } + /> + + ); +}; + +export default FeatureMetricsTable; diff --git a/ui/src/pages/monitoring/FeatureServiceMetricsPanel.tsx b/ui/src/pages/monitoring/FeatureServiceMetricsPanel.tsx new file mode 100644 index 00000000000..536c5d56c6c --- /dev/null +++ b/ui/src/pages/monitoring/FeatureServiceMetricsPanel.tsx @@ -0,0 +1,225 @@ +import React, { useState, useMemo } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiBasicTable, + EuiBasicTableColumn, + EuiProgress, + EuiBadge, + EuiSkeletonText, + Criteria, +} from "@elastic/eui"; +import type { FeatureServiceMetric } from "../../queries/useMonitoringApi"; + +const healthColor = (nullRate: number): string => { + if (nullRate >= 0.5) return "danger"; + if (nullRate >= 0.1) return "warning"; + return "success"; +}; + +interface FeatureServiceMetricsPanelProps { + metrics: FeatureServiceMetric[]; + isLoading: boolean; +} + +const FeatureServiceMetricsPanel = ({ + metrics, + isLoading, +}: FeatureServiceMetricsPanelProps) => { + if (isLoading) { + return ( + + +

Feature Service Metrics

+
+ + +
+ ); + } + + const latestByFS = new Map(); + for (const m of metrics) { + const existing = latestByFS.get(m.feature_service_name); + if (!existing || m.metric_date > existing.metric_date) { + latestByFS.set(m.feature_service_name, m); + } + } + const latestMetrics = Array.from(latestByFS.values()); + + const totalViews = latestMetrics.reduce( + (sum, m) => sum + (m.total_feature_views || 0), + 0, + ); + const totalFeatures = latestMetrics.reduce( + (sum, m) => sum + (m.total_features || 0), + 0, + ); + const avgNullRate = + latestMetrics.length > 0 + ? latestMetrics.reduce((sum, m) => sum + (m.avg_null_rate || 0), 0) / + latestMetrics.length + : 0; + + const columns: EuiBasicTableColumn[] = [ + { + field: "feature_service_name", + name: "Feature Service", + sortable: true, + }, + { + field: "total_feature_views", + name: "Feature Views", + sortable: true, + width: "110px", + }, + { + field: "total_features", + name: "Features", + sortable: true, + width: "80px", + }, + { + field: "avg_null_rate", + name: "Avg Null Rate", + sortable: true, + render: (val: number) => ( +
+ + {((val || 0) * 100).toFixed(1)}% +
+ ), + }, + { + field: "max_null_rate", + name: "Max Null Rate", + sortable: true, + width: "110px", + render: (val: number) => `${((val || 0) * 100).toFixed(1)}%`, + }, + { + field: "metric_date", + name: "Date", + sortable: true, + width: "110px", + }, + { + field: "data_source_type", + name: "Source", + width: "80px", + render: (val: string) => {val}, + }, + ]; + + return ( + + +

Feature Service Metrics

+
+

+ Aggregated data quality metrics across feature services. +

+ + + + + + + + + + + + + + + + + + + + {latestMetrics.length > 0 && ( + + )} +
+ ); +}; + +const SortableFSTable = ({ + items, + columns, +}: { + items: FeatureServiceMetric[]; + columns: EuiBasicTableColumn[]; +}) => { + const [sortField, setSortField] = useState("feature_service_name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + + const sortedItems = useMemo(() => { + return [...items].sort((a, b) => { + const aVal = (a as any)[sortField]; + const bVal = (b as any)[sortField]; + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + if (aVal < bVal) return sortDirection === "asc" ? -1 : 1; + if (aVal > bVal) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + }, [items, sortField, sortDirection]); + + const onTableChange = ({ sort }: Criteria) => { + if (sort) { + setSortField(sort.field as string); + setSortDirection(sort.direction); + } + }; + + return ( + + ); +}; + +export default FeatureServiceMetricsPanel; diff --git a/ui/src/pages/monitoring/FeatureViewMetricsPanel.tsx b/ui/src/pages/monitoring/FeatureViewMetricsPanel.tsx new file mode 100644 index 00000000000..a0dcc78a8ab --- /dev/null +++ b/ui/src/pages/monitoring/FeatureViewMetricsPanel.tsx @@ -0,0 +1,241 @@ +import React, { useState, useMemo } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiBasicTable, + EuiBasicTableColumn, + EuiProgress, + EuiBadge, + EuiSkeletonText, + Criteria, +} from "@elastic/eui"; +import type { FeatureViewMetric } from "../../queries/useMonitoringApi"; + +const healthColor = (nullRate: number): string => { + if (nullRate >= 0.5) return "danger"; + if (nullRate >= 0.1) return "warning"; + return "success"; +}; + +interface FeatureViewMetricsPanelProps { + metrics: FeatureViewMetric[]; + isLoading: boolean; + title: string; + description?: string; +} + +const FeatureViewMetricsPanel = ({ + metrics, + isLoading, + title, + description, +}: FeatureViewMetricsPanelProps) => { + if (isLoading) { + return ( + + +

{title}

+
+ + +
+ ); + } + + const latestByFV = new Map(); + for (const m of metrics) { + const existing = latestByFV.get(m.feature_view_name); + if (!existing || m.metric_date > existing.metric_date) { + latestByFV.set(m.feature_view_name, m); + } + } + const latestMetrics = Array.from(latestByFV.values()); + + const totalRows = latestMetrics.reduce( + (sum, m) => sum + (m.total_row_count || 0), + 0, + ); + const totalFeatures = latestMetrics.reduce( + (sum, m) => sum + (m.total_features || 0), + 0, + ); + const avgNullRate = + latestMetrics.length > 0 + ? latestMetrics.reduce((sum, m) => sum + (m.avg_null_rate || 0), 0) / + latestMetrics.length + : 0; + const healthyViews = latestMetrics.filter( + (m) => m.avg_null_rate < 0.1, + ).length; + + const columns: EuiBasicTableColumn[] = [ + { + field: "feature_view_name", + name: "Feature View", + sortable: true, + }, + { + field: "total_row_count", + name: "Total Rows", + sortable: true, + render: (val: number) => (val || 0).toLocaleString(), + }, + { + field: "total_features", + name: "Features", + sortable: true, + width: "80px", + }, + { + field: "features_with_nulls", + name: "With Nulls", + sortable: true, + width: "90px", + }, + { + field: "avg_null_rate", + name: "Avg Null Rate", + sortable: true, + render: (val: number) => ( +
+ + {((val || 0) * 100).toFixed(1)}% +
+ ), + }, + { + field: "max_null_rate", + name: "Max Null Rate", + sortable: true, + width: "110px", + render: (val: number) => `${((val || 0) * 100).toFixed(1)}%`, + }, + { + field: "metric_date", + name: "Date", + sortable: true, + width: "110px", + }, + { + field: "data_source_type", + name: "Source", + width: "80px", + render: (val: string) => {val}, + }, + ]; + + return ( + + +

{title}

+
+ {description && ( +

+ {description} +

+ )} + + + + + + + + + + + + + + + + + + + + {latestMetrics.length > 0 && ( + + )} +
+ ); +}; + +const SortableTable = ({ + items, + columns, +}: { + items: FeatureViewMetric[]; + columns: EuiBasicTableColumn[]; +}) => { + const [sortField, setSortField] = useState("feature_view_name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + + const sortedItems = useMemo(() => { + return [...items].sort((a, b) => { + const aVal = (a as any)[sortField]; + const bVal = (b as any)[sortField]; + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + if (aVal < bVal) return sortDirection === "asc" ? -1 : 1; + if (aVal > bVal) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + }, [items, sortField, sortDirection]); + + const onTableChange = ({ sort }: Criteria) => { + if (sort) { + setSortField(sort.field as string); + setSortDirection(sort.direction); + } + }; + + return ( + + ); +}; + +export default FeatureViewMetricsPanel; diff --git a/ui/src/pages/monitoring/Index.tsx b/ui/src/pages/monitoring/Index.tsx new file mode 100644 index 00000000000..c3576d07c3d --- /dev/null +++ b/ui/src/pages/monitoring/Index.tsx @@ -0,0 +1,327 @@ +import React, { useState, useContext, useMemo } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + EuiPageTemplate, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, + EuiEmptyPrompt, + EuiButton, + EuiCallOut, +} from "@elastic/eui"; + +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { + isServiceUnavailable, + useFeatureMetrics, + useFeatureViewMetrics, + useFeatureServiceMetrics, + useComputeMetrics, +} from "../../queries/useMonitoringApi"; +import FeatureMetricsTable from "./FeatureMetricsTable"; +import FeatureViewMetricsPanel from "./FeatureViewMetricsPanel"; +import FeatureServiceMetricsPanel from "./FeatureServiceMetricsPanel"; +import MetricsFilters from "./components/MetricsFilters"; + +const MonitoringIndex = () => { + useDocumentTitle("Monitoring | Feast"); + + const { projectName } = useParams(); + const navigate = useNavigate(); + const registryUrl = useContext(RegistryPathContext); + const { data: registryData } = useLoadRegistry(registryUrl, projectName); + + const [selectedFV, setSelectedFV] = useState(""); + const [granularity, setGranularity] = useState("daily"); + const [dataSourceType, setDataSourceType] = useState(""); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + + const isBaseline = granularity === "baseline"; + + const handleGranularityChange = (g: string) => { + setGranularity(g); + if (g === "baseline") { + setStartDate(""); + setEndDate(""); + } + }; + + const filters = useMemo( + () => ({ + project: projectName || "", + feature_view_name: selectedFV || undefined, + granularity: isBaseline ? undefined : granularity || undefined, + data_source_type: dataSourceType || undefined, + start_date: isBaseline ? undefined : startDate || undefined, + end_date: isBaseline ? undefined : endDate || undefined, + is_baseline: isBaseline || undefined, + }), + [ + projectName, + selectedFV, + granularity, + isBaseline, + dataSourceType, + startDate, + endDate, + ], + ); + + const featureQuery = useFeatureMetrics(filters); + const fvQuery = useFeatureViewMetrics(filters); + const fsQuery = useFeatureServiceMetrics({ + project: projectName || "", + granularity: isBaseline ? undefined : granularity || undefined, + data_source_type: dataSourceType || undefined, + start_date: startDate || undefined, + end_date: endDate || undefined, + is_baseline: isBaseline || undefined, + }); + const computeMutation = useComputeMetrics(); + + const featureViews = useMemo(() => { + if (!registryData?.mergedFVList) return []; + return registryData.mergedFVList.map((fv: any) => fv.name as string); + }, [registryData]); + + const handleFeatureClick = (fvName: string, featureName: string) => { + navigate(`/p/${projectName}/monitoring/feature/${fvName}/${featureName}`); + }; + + const uniqueFeatureCount = useMemo(() => { + if (!featureQuery.data) return 0; + const seen = new Set(); + for (const m of featureQuery.data) { + seen.add(`${m.feature_view_name}::${m.feature_name}`); + } + return seen.size; + }, [featureQuery.data]); + + const handleRefresh = () => { + featureQuery.refetch(); + fvQuery.refetch(); + fsQuery.refetch(); + }; + + const handleCompute = () => { + computeMutation.mutate({ + project: projectName || "", + feature_view_name: selectedFV || undefined, + }); + }; + + const allFailed = featureQuery.isError && fvQuery.isError && fsQuery.isError; + const monitoringNotEnabled = + allFailed && + isServiceUnavailable(featureQuery.error) && + isServiceUnavailable(fvQuery.error) && + isServiceUnavailable(fsQuery.error); + const hasError = allFailed && !monitoringNotEnabled; + const hasData = + (featureQuery.data && featureQuery.data.length > 0) || + (fvQuery.data && fvQuery.data.length > 0); + + const tabs: EuiTabbedContentTab[] = [ + { + id: "features", + name: `Features${uniqueFeatureCount > 0 ? ` (${uniqueFeatureCount})` : ""}`, + content: ( + <> + + + + ), + }, + { + id: "feature-views", + name: "Feature Views", + content: ( + <> + + + + ), + }, + { + id: "feature-services", + name: "Feature Services", + content: ( + <> + + + + ), + }, + ]; + + if (monitoringNotEnabled) { + return ( + + + + Monitoring Is Not Enabled} + body={ + <> +

+ Data quality monitoring is not configured for this Feast + deployment. +

+

+ To enable monitoring, add the following to your{" "} + feature_store.yaml: +

+
+                  {`data_quality_monitoring:
+  auto_baseline: true`}
+                
+

Then restart the Feast registry server.

+ + } + /> +
+
+ ); + } + + return ( + + + Compute Metrics + , + ]} + /> + + {hasError && ( + <> + +

+ Could not connect to the monitoring API. Make sure the Feast + registry server is running with monitoring enabled. +

+
+ + + )} + + + + + + {!hasData && !featureQuery.isLoading && !hasError && ( + No Metrics Yet} + body={ +

+ No monitoring data has been computed for this project. Click + "Compute Metrics" to run data quality analysis on your + feature views, or use the CLI:{" "} + feast monitor run --data-source batch +

+ } + actions={ + + Compute Metrics + + } + /> + )} + + {(hasData || featureQuery.isLoading) && ( + + )} + + {computeMutation.isSuccess && ( + <> + + +

+ Data quality metrics have been computed. The table above has + been refreshed. +

+
+ + )} + + {computeMutation.isError && ( + <> + + +

{(computeMutation.error as Error)?.message}

+
+ + )} +
+
+ ); +}; + +export default MonitoringIndex; diff --git a/ui/src/pages/monitoring/components/HistogramChart.tsx b/ui/src/pages/monitoring/components/HistogramChart.tsx new file mode 100644 index 00000000000..a716b025021 --- /dev/null +++ b/ui/src/pages/monitoring/components/HistogramChart.tsx @@ -0,0 +1,435 @@ +import React, { useState } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from "@elastic/eui"; +import type { + NumericHistogram, + CategoricalHistogram, +} from "../../../queries/useMonitoringApi"; + +const BAR_COLOR = "#006BB4"; +const BAR_COLOR_BASELINE = "#BD271E55"; + +interface ChartDimensions { + chartHeight: number; + axisHeight: number; + leftPad: number; + barGap: number; + minBarWidth: number; + targetBarsWidth: number; + fontSize: number; + xTickCount: number; +} + +const COMPACT: ChartDimensions = { + chartHeight: 160, + axisHeight: 28, + leftPad: 54, + barGap: 2, + minBarWidth: 6, + targetBarsWidth: 460, + fontSize: 10, + xTickCount: 2, +}; + +const EXPANDED: ChartDimensions = { + chartHeight: 400, + axisHeight: 48, + leftPad: 72, + barGap: 3, + minBarWidth: 12, + targetBarsWidth: 800, + fontSize: 12, + xTickCount: 6, +}; + +const formatNumber = (val: number, compact: boolean): string => { + if (val === 0) return "0"; + const abs = Math.abs(val); + if (compact && abs >= 1_000_000) return (val / 1_000_000).toFixed(1) + "M"; + if (compact && abs >= 1_000) return (val / 1_000).toFixed(1) + "K"; + if (abs >= 1) + return val.toLocaleString(undefined, { maximumFractionDigits: 1 }); + if (abs >= 0.01) return val.toFixed(2); + return val.toExponential(1); +}; + +const renderNumericSvg = ( + histogram: NumericHistogram, + baseline: NumericHistogram | null | undefined, + dim: ChartDimensions, +) => { + const maxCount = Math.max( + ...histogram.counts, + ...(baseline ? baseline.counts : []), + 1, + ); + const numBars = histogram.counts.length; + const barWidth = Math.max( + Math.floor(dim.targetBarsWidth / numBars) - dim.barGap, + dim.minBarWidth, + ); + const barsWidth = (barWidth + dim.barGap) * numBars; + const svgWidth = dim.leftPad + barsWidth + 24; + const isCompact = dim === COMPACT; + + const yTickFractions = [0, 0.25, 0.5, 0.75, 1]; + const yTicks = yTickFractions.map((f) => ({ + label: formatNumber(Math.round(maxCount * f), isCompact), + y: dim.chartHeight - f * dim.chartHeight, + })); + + const xTickStep = Math.max(1, Math.floor(numBars / dim.xTickCount)); + const xTicks: { label: string; x: number }[] = []; + for (let i = 0; i < numBars; i += xTickStep) { + xTicks.push({ + label: formatNumber(histogram.bins[i], isCompact), + x: dim.leftPad + i * (barWidth + dim.barGap) + barWidth / 2, + }); + } + if (numBars > 0) { + const lastBin = histogram.bins[histogram.bins.length - 1]; + xTicks.push({ + label: formatNumber(lastBin, isCompact), + x: dim.leftPad + (numBars - 1) * (barWidth + dim.barGap) + barWidth / 2, + }); + } + + return ( + + {yTicks.map((t, i) => ( + + + + {t.label} + + + ))} + {histogram.counts.map((count, i) => { + const height = (count / maxCount) * dim.chartHeight; + const x = dim.leftPad + i * (barWidth + dim.barGap); + const binStart = histogram.bins[i]; + const binEnd = + i < histogram.bins.length - 1 + ? histogram.bins[i + 1] + : binStart + histogram.bin_width; + const baselineHeight = + baseline && baseline.counts[i] + ? (baseline.counts[i] / maxCount) * dim.chartHeight + : 0; + + return ( + + {baselineHeight > 0 && ( + + )} + + {`${formatNumber(binStart, false)} – ${formatNumber(binEnd, false)}: ${count.toLocaleString()}`} + + + ); + })} + + {xTicks.map((t, i) => ( + + {t.label} + + ))} + + ); +}; + +const NumericHistogramChart = ({ + histogram, + baseline, + title, +}: { + histogram: NumericHistogram; + baseline?: NumericHistogram | null; + title?: string; +}) => { + const [expanded, setExpanded] = useState(false); + + return ( + <> + + + + {title && ( + +

{title}

+
+ )} +
+ + + setExpanded(true)} + /> + + +
+ {title && } +
+ {renderNumericSvg(histogram, baseline, COMPACT)} +
+ {baseline && ( + + + Baseline + + )} +
+ + {expanded && ( + setExpanded(false)} maxWidth={960}> + + {title || "Histogram"} + + +
+ {renderNumericSvg(histogram, baseline, EXPANDED)} +
+ {baseline && ( + <> + + + + Baseline + + + )} +
+
+ )} + + ); +}; + +const LABEL_WIDTH = 60; +const BAR_MAX_WIDTH = 320; +const COUNT_PAD = 80; + +const LABEL_WIDTH_EXP = 120; +const BAR_MAX_WIDTH_EXP = 560; +const COUNT_PAD_EXP = 100; + +const renderCategoricalSvg = ( + histogram: CategoricalHistogram, + isExpanded: boolean, +) => { + const labelW = isExpanded ? LABEL_WIDTH_EXP : LABEL_WIDTH; + const barMax = isExpanded ? BAR_MAX_WIDTH_EXP : BAR_MAX_WIDTH; + const countPad = isExpanded ? COUNT_PAD_EXP : COUNT_PAD; + const totalW = labelW + barMax + countPad; + const truncLen = isExpanded ? 20 : 8; + const fontSize = isExpanded ? 13 : 12; + const barHeight = isExpanded ? 30 : 24; + const rowHeight = barHeight + 6; + + const maxCount = Math.max(...histogram.values.map((v) => v.count), 1); + const chartHeight = histogram.values.length * rowHeight; + + return ( + + {histogram.values.map((v, i) => { + const width = (v.count / maxCount) * barMax; + const y = i * rowHeight; + return ( + + + {v.value.length > truncLen + ? v.value.slice(0, truncLen) + "…" + : v.value} + + + {`${v.value}: ${v.count.toLocaleString()}`} + + + {v.count.toLocaleString()} + + + ); + })} + + ); +}; + +const CategoricalHistogramChart = ({ + histogram, + title, +}: { + histogram: CategoricalHistogram; + title?: string; +}) => { + const [expanded, setExpanded] = useState(false); + + return ( + <> + + + + {title && ( + +

{title}

+
+ )} +
+ + + setExpanded(true)} + /> + + +
+ {title && } +
+ {renderCategoricalSvg(histogram, false)} +
+ + {histogram.unique_count} unique values + {histogram.other_count > 0 && + ` (${histogram.other_count.toLocaleString()} in other categories)`} + +
+ + {expanded && ( + setExpanded(false)} maxWidth={960}> + + + {title || "Category Distribution"} + + + +
+ {renderCategoricalSvg(histogram, true)} +
+ + + {histogram.unique_count} unique values + {histogram.other_count > 0 && + ` (${histogram.other_count.toLocaleString()} in other categories)`} + +
+
+ )} + + ); +}; + +export { NumericHistogramChart, CategoricalHistogramChart }; diff --git a/ui/src/pages/monitoring/components/MetricsFilters.tsx b/ui/src/pages/monitoring/components/MetricsFilters.tsx new file mode 100644 index 00000000000..977044d495a --- /dev/null +++ b/ui/src/pages/monitoring/components/MetricsFilters.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSelect, + EuiFieldText, + EuiFormRow, + EuiButton, +} from "@elastic/eui"; + +interface MetricsFiltersProps { + featureViews: string[]; + selectedFeatureView: string; + onFeatureViewChange: (fv: string) => void; + granularity: string; + onGranularityChange: (g: string) => void; + dataSourceType: string; + onDataSourceTypeChange: (ds: string) => void; + startDate: string; + onStartDateChange: (d: string) => void; + endDate: string; + onEndDateChange: (d: string) => void; + onRefresh: () => void; + isLoading?: boolean; + datesDisabled?: boolean; +} + +const GRANULARITY_OPTIONS = [ + { value: "baseline", text: "Baseline" }, + { value: "daily", text: "Daily" }, + { value: "weekly", text: "Weekly" }, + { value: "biweekly", text: "Biweekly" }, + { value: "monthly", text: "Monthly" }, + { value: "quarterly", text: "Quarterly" }, +]; + +const DATA_SOURCE_OPTIONS = [ + { value: "", text: "All Sources" }, + { value: "batch", text: "Batch" }, + { value: "log", text: "Log" }, +]; + +const MetricsFilters = ({ + featureViews, + selectedFeatureView, + onFeatureViewChange, + granularity, + onGranularityChange, + dataSourceType, + onDataSourceTypeChange, + startDate, + onStartDateChange, + endDate, + onEndDateChange, + onRefresh, + isLoading, + datesDisabled, +}: MetricsFiltersProps) => { + const fvOptions = [ + { value: "", text: "All Feature Views" }, + ...featureViews.map((fv) => ({ value: fv, text: fv })), + ]; + + return ( + + + + onFeatureViewChange(e.target.value)} + compressed + /> + + + + + onGranularityChange(e.target.value)} + compressed + /> + + + + + onDataSourceTypeChange(e.target.value)} + compressed + /> + + + + + onStartDateChange(e.target.value)} + compressed + disabled={datesDisabled} + /> + + + + + onEndDateChange(e.target.value)} + compressed + disabled={datesDisabled} + /> + + + + + Refresh + + + + ); +}; + +export default MetricsFilters; diff --git a/ui/src/pages/monitoring/components/StatsPanel.tsx b/ui/src/pages/monitoring/components/StatsPanel.tsx new file mode 100644 index 00000000000..070b99373e7 --- /dev/null +++ b/ui/src/pages/monitoring/components/StatsPanel.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, +} from "@elastic/eui"; +import type { FeatureMetric } from "../../../queries/useMonitoringApi"; + +const formatNumber = (val: number | null, decimals = 4): string => { + if (val === null || val === undefined) return "—"; + if (Number.isInteger(val)) return val.toLocaleString(); + return val.toFixed(decimals); +}; + +const formatPercent = (val: number | null): string => { + if (val === null || val === undefined) return "—"; + return `${(val * 100).toFixed(2)}%`; +}; + +const StatsPanel = ({ + metric, + baseline, +}: { + metric: FeatureMetric; + baseline?: FeatureMetric | null; +}) => { + const isNumeric = metric.feature_type === "numeric"; + + return ( + + + + +

Statistics

+
+
+ + + {metric.feature_type} + + +
+ + + Row Count + + {formatNumber(metric.row_count, 0)} + {baseline && ( + + (baseline: {formatNumber(baseline.row_count, 0)}) + + )} + + + Null Rate + + 0.1 ? "#BD271E" : "inherit", + fontWeight: metric.null_rate > 0.1 ? 600 : 400, + }} + > + {formatPercent(metric.null_rate)} + + {baseline && ( + + (baseline: {formatPercent(baseline.null_rate)}) + + )} + + + {isNumeric && ( + <> + Mean + + {formatNumber(metric.mean)} + {baseline && ( + + (baseline: {formatNumber(baseline.mean)}) + + )} + + + Std Dev + + {formatNumber(metric.stddev)} + + + Min / Max + + {formatNumber(metric.min_val)} / {formatNumber(metric.max_val)} + + + Percentiles + + P50: {formatNumber(metric.p50)} | P75: {formatNumber(metric.p75)}{" "} + | P90: {formatNumber(metric.p90)} | P95:{" "} + {formatNumber(metric.p95)} | P99: {formatNumber(metric.p99)} + + + )} + + Data Source + + {metric.data_source_type} + + + Granularity + + {metric.granularity} + + + Computed At + + {metric.computed_at + ? new Date(metric.computed_at).toLocaleString() + : "—"} + + +
+ ); +}; + +export default StatsPanel; diff --git a/ui/src/pages/monitoring/components/TimeSeriesAnalysis.tsx b/ui/src/pages/monitoring/components/TimeSeriesAnalysis.tsx new file mode 100644 index 00000000000..23acd0be434 --- /dev/null +++ b/ui/src/pages/monitoring/components/TimeSeriesAnalysis.tsx @@ -0,0 +1,651 @@ +import React, { useState, useMemo } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, +} from "@elastic/eui"; +import type { + FeatureMetric, + CategoricalHistogram, +} from "../../../queries/useMonitoringApi"; + +const COLORS = [ + "#006BB4", + "#54B399", + "#E7664C", + "#9170B8", + "#D36086", + "#6092C0", + "#D6BF57", + "#B9A888", +]; + +const RANGE_OPTIONS = [ + { value: "24h", inputDisplay: "Last 24 hours" }, + { value: "7d", inputDisplay: "Last 7 days" }, + { value: "30d", inputDisplay: "Last 30 days" }, + { value: "90d", inputDisplay: "Last 90 days" }, + { value: "all", inputDisplay: "All time" }, +]; + +const rangeToMs: Record = { + "24h": 24 * 3600_000, + "7d": 7 * 86400_000, + "30d": 30 * 86400_000, + "90d": 90 * 86400_000, + all: Infinity, +}; + +interface ChartDims { + width: number; + height: number; + padLeft: number; + padRight: number; + padTop: number; + padBottom: number; +} + +const DIMS: ChartDims = { + width: 860, + height: 220, + padLeft: 60, + padRight: 20, + padTop: 10, + padBottom: 30, +}; + +const formatDate = (d: string): string => { + const dt = new Date(d); + const mm = String(dt.getMonth() + 1).padStart(2, "0"); + const dd = String(dt.getDate()).padStart(2, "0"); + const hh = String(dt.getHours()).padStart(2, "0"); + const mi = String(dt.getMinutes()).padStart(2, "0"); + return `${mm}-${dd} ${hh}:${mi}`; +}; + +const formatAxisVal = (v: number): string => { + if (v === 0) return "0"; + const abs = Math.abs(v); + if (abs >= 1_000_000) return (v / 1_000_000).toFixed(1) + "M"; + if (abs >= 1_000) return (v / 1_000).toFixed(1) + "K"; + if (abs >= 1) return v.toFixed(1); + return (v * 100).toFixed(1) + "%"; +}; + +const niceYTicks = (min: number, max: number, count = 5): number[] => { + if (max === min) return [min]; + const step = (max - min) / (count - 1); + return Array.from({ length: count }, (_, i) => min + step * i); +}; + +interface LineSeriesData { + label: string; + color: string; + dashArray?: string; + points: { x: number; y: number; date: string; value: number }[]; +} + +const renderMultiLineChart = ( + series: LineSeriesData[], + dims: ChartDims, + yLabel: string, + yFormatter: (v: number) => string = formatAxisVal, +) => { + const allPoints = series.flatMap((s) => s.points); + if (allPoints.length === 0) return null; + + const xMin = Math.min(...allPoints.map((p) => p.x)); + const xMax = Math.max(...allPoints.map((p) => p.x)); + const yMin = Math.min(...allPoints.map((p) => p.value), 0); + const yMax = Math.max(...allPoints.map((p) => p.value), 0.01); + + const plotW = dims.width - dims.padLeft - dims.padRight; + const plotH = dims.height - dims.padTop - dims.padBottom; + + const scaleX = (x: number) => + xMax === xMin + ? dims.padLeft + plotW / 2 + : dims.padLeft + ((x - xMin) / (xMax - xMin)) * plotW; + const scaleY = (v: number) => + dims.padTop + plotH - ((v - yMin) / (yMax - yMin || 1)) * plotH; + + const yTicks = niceYTicks(yMin, yMax); + const xDates = allPoints + .map((p) => ({ x: p.x, date: p.date })) + .filter((v, i, arr) => arr.findIndex((a) => a.date === v.date) === i) + .sort((a, b) => a.x - b.x); + + const maxXLabels = Math.floor(plotW / 80); + const xStep = Math.max(1, Math.ceil(xDates.length / maxXLabels)); + const xLabels = xDates.filter((_, i) => i % xStep === 0); + + return ( + + {/* Y axis grid + labels */} + {yTicks.map((v, i) => { + const y = scaleY(v); + return ( + + + + {yFormatter(v)} + + + ); + })} + + {/* Y axis label */} + + {yLabel} + + + {/* X axis labels */} + {xLabels.map((xl, i) => ( + + {formatDate(xl.date)} + + ))} + + {/* Lines */} + {series.map((s, si) => { + if (s.points.length === 0) return null; + const sorted = [...s.points].sort((a, b) => a.x - b.x); + const pathD = sorted + .map( + (p, i) => + `${i === 0 ? "M" : "L"} ${scaleX(p.x)} ${scaleY(p.value)}`, + ) + .join(" "); + return ( + + + {sorted.map((p, i) => ( + + ))} + + ); + })} + + ); +}; + +const renderAreaChart = ( + points: { x: number; value: number; date: string }[], + dims: ChartDims, + yLabel: string, + lineColor: string, + fillColor: string, +) => { + if (points.length === 0) return null; + + const sorted = [...points].sort((a, b) => a.x - b.x); + const xMin = Math.min(...sorted.map((p) => p.x)); + const xMax = Math.max(...sorted.map((p) => p.x)); + const yMin = 0; + const yMax = Math.max(...sorted.map((p) => p.value), 0.01); + + const plotW = dims.width - dims.padLeft - dims.padRight; + const plotH = dims.height - dims.padTop - dims.padBottom; + + const scaleX = (x: number) => + xMax === xMin + ? dims.padLeft + plotW / 2 + : dims.padLeft + ((x - xMin) / (xMax - xMin)) * plotW; + const scaleY = (v: number) => + dims.padTop + plotH - ((v - yMin) / (yMax - yMin || 1)) * plotH; + + const yTicks = niceYTicks(yMin, yMax); + const baseline = scaleY(0); + + const areaPath = + `M ${scaleX(sorted[0].x)} ${baseline} ` + + sorted.map((p) => `L ${scaleX(p.x)} ${scaleY(p.value)}`).join(" ") + + ` L ${scaleX(sorted[sorted.length - 1].x)} ${baseline} Z`; + + const linePath = sorted + .map((p, i) => `${i === 0 ? "M" : "L"} ${scaleX(p.x)} ${scaleY(p.value)}`) + .join(" "); + + const maxXLabels = Math.floor(plotW / 80); + const xStep = Math.max(1, Math.ceil(sorted.length / maxXLabels)); + const xLabels = sorted.filter((_, i) => i % xStep === 0); + + return ( + + {yTicks.map((v, i) => { + const y = scaleY(v); + return ( + + + + {(v * 100).toFixed(0)}% + + + ); + })} + + + {yLabel} + + + {xLabels.map((xl, i) => ( + + {formatDate(xl.date)} + + ))} + + + + + ); +}; + +const Legend = ({ + items, +}: { + items: { label: string; color: string; dashed?: boolean }[]; +}) => ( +
+ {items.map((item, i) => ( +
+ {item.dashed ? ( + + + + ) : ( +
+ )} + {item.label} +
+ ))} +
+); + +interface TimeSeriesAnalysisProps { + metrics: FeatureMetric[]; + featureType: string; +} + +const TimeSeriesAnalysis = ({ + metrics, + featureType, +}: TimeSeriesAnalysisProps) => { + const [range, setRange] = useState("all"); + + const filteredMetrics = useMemo(() => { + if (range === "all") return metrics; + const cutoff = Date.now() - rangeToMs[range]; + return metrics.filter((m) => new Date(m.metric_date).getTime() >= cutoff); + }, [metrics, range]); + + const sorted = useMemo( + () => + [...filteredMetrics] + .filter((m) => m.row_count > 0) + .sort((a, b) => a.metric_date.localeCompare(b.metric_date)), + [filteredMetrics], + ); + + const toX = (m: FeatureMetric) => new Date(m.metric_date).getTime(); + const isNumeric = featureType === "numeric"; + const hasData = sorted.length >= 1; + + return ( + + + + +

Time-Series Analysis

+
+

+ Historical trends for central aggregates and quality signals. +

+
+ + + +
+ + + + {!hasData ? ( +

+ No data points available for the selected time range. Try a wider + range. +

+ ) : isNumeric ? ( + + ) : ( + + )} +
+ ); +}; + +const NumericTimeSeries = ({ + metrics, + toX, +}: { + metrics: FeatureMetric[]; + toX: (m: FeatureMetric) => number; +}) => { + const driftSeries: LineSeriesData[] = useMemo(() => { + const build = ( + label: string, + color: string, + accessor: (m: FeatureMetric) => number | null, + dashArray?: string, + ): LineSeriesData => ({ + label, + color, + dashArray, + points: metrics + .filter((m) => accessor(m) !== null) + .map((m) => ({ + x: toX(m), + y: 0, + value: accessor(m)!, + date: m.metric_date, + })), + }); + return [ + build("Mean", COLORS[0], (m) => m.mean, "6,3"), + build("P50", COLORS[1], (m) => m.p50), + build("P95", COLORS[2], (m) => m.p95), + ]; + }, [metrics, toX]); + + const nullPoints = useMemo( + () => + metrics.map((m) => ({ + x: toX(m), + value: m.null_rate, + date: m.metric_date, + })), + [metrics, toX], + ); + + return ( + <> +

+ Aggregate Metrics Drift (Mean/P50/P95) +

+
+ {renderMultiLineChart(driftSeries, DIMS, "Metric Value")} +
+ + + + +

+ Null Rate Evolution (%) +

+
+ {renderAreaChart( + nullPoints, + { ...DIMS, height: 160 }, + "Percentage (%)", + "#BD271E", + "rgba(189, 39, 30, 0.15)", + )} +
+ + ); +}; + +const CategoricalTimeSeries = ({ + metrics, + toX, +}: { + metrics: FeatureMetric[]; + toX: (m: FeatureMetric) => number; +}) => { + const { cardinalitySeries, shareSeries, topCategories } = useMemo(() => { + const catCounts = new Map(); + for (const m of metrics) { + const hist = m.histogram as CategoricalHistogram | null; + if (!hist) continue; + for (const v of hist.values) { + catCounts.set(v.value, (catCounts.get(v.value) || 0) + v.count); + } + } + const topCats = Array.from(catCounts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([v]) => v); + + const cardSeries: LineSeriesData[] = [ + { + label: "Cardinality", + color: COLORS[0], + points: metrics + .filter((m) => m.histogram) + .map((m) => ({ + x: toX(m), + y: 0, + value: (m.histogram as CategoricalHistogram).unique_count || 0, + date: m.metric_date, + })), + }, + ...topCats.map((cat, i) => ({ + label: cat, + color: COLORS[(i + 1) % COLORS.length], + points: metrics + .filter((m) => m.histogram) + .map((m) => { + const hist = m.histogram as CategoricalHistogram; + const entry = hist.values.find((v) => v.value === cat); + return { + x: toX(m), + y: 0, + value: entry?.count || 0, + date: m.metric_date, + }; + }), + })), + ]; + + const shrSeries: LineSeriesData[] = topCats.map((cat, i) => ({ + label: cat, + color: COLORS[(i + 1) % COLORS.length], + points: metrics + .filter((m) => m.histogram && m.row_count > 0) + .map((m) => { + const hist = m.histogram as CategoricalHistogram; + const entry = hist.values.find((v) => v.value === cat); + return { + x: toX(m), + y: 0, + value: ((entry?.count || 0) / m.row_count) * 100, + date: m.metric_date, + }; + }), + })); + + return { + cardinalitySeries: cardSeries, + shareSeries: shrSeries, + topCategories: topCats, + }; + }, [metrics, toX]); + + const nullPoints = useMemo( + () => + metrics.map((m) => ({ + x: toX(m), + value: m.null_rate, + date: m.metric_date, + })), + [metrics, toX], + ); + + const shareYFormatter = (v: number) => `${v.toFixed(0)}%`; + + return ( + <> +

+ Cardinality over time +

+
+ {renderMultiLineChart(cardinalitySeries, DIMS, "Count")} +
+ ({ + label: s.label, + color: s.color, + }))} + /> + + + +

+ Top category share over time (%) +

+
+ {renderMultiLineChart( + shareSeries, + DIMS, + "Percentage (%)", + shareYFormatter, + )} +
+ ({ + label: cat, + color: COLORS[(i + 1) % COLORS.length], + }))} + /> + + + +

+ Null Rate Evolution (%) +

+
+ {renderAreaChart( + nullPoints, + { ...DIMS, height: 160 }, + "Percentage (%)", + "#BD271E", + "rgba(189, 39, 30, 0.15)", + )} +
+ + ); +}; + +export default TimeSeriesAnalysis; diff --git a/ui/src/pages/permissions/Index.tsx b/ui/src/pages/permissions/Index.tsx index 76dde026e90..6ef6dea9bd7 100644 --- a/ui/src/pages/permissions/Index.tsx +++ b/ui/src/pages/permissions/Index.tsx @@ -1,96 +1,561 @@ -import React from "react"; +import React, { useState, useMemo } from "react"; import { EuiPageTemplate, - EuiTitle, EuiSpacer, - EuiPanel, + EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, + EuiButton, + EuiCallOut, + EuiFieldSearch, + EuiTitle, + EuiBasicTable, + EuiBadge, + EuiButtonIcon, + EuiConfirmModal, EuiText, - EuiLoadingSpinner, - EuiHorizontalRule, + EuiToolTip, EuiSelect, EuiFormRow, + EuiEmptyPrompt, } from "@elastic/eui"; -import { useContext, useState } from "react"; import { useParams } from "react-router-dom"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; -import PermissionsDisplay from "../../components/PermissionsDisplay"; -import { filterPermissionsByAction } from "../../utils/permissionUtils"; +import { PermissionsIcon } from "../../graphics/PermissionsIcon"; +import PermissionFormModal, { + PermissionFormData, +} from "../../components/PermissionFormModal"; +import { + useApplyPermission, + useDeletePermission, + ApplyPermissionPayload, +} from "../../queries/mutations/usePermissionMutations"; +import useResourceQuery, { + permissionListPath, +} from "../../queries/useResourceQuery"; + +const ACTION_NAMES = [ + "CREATE", + "DESCRIBE", + "UPDATE", + "DELETE", + "READ_ONLINE", + "READ_OFFLINE", + "WRITE_ONLINE", + "WRITE_OFFLINE", +]; + +const TYPE_DISPLAY_NAMES: Record = { + FEATURE_VIEW: "Feature View", + ON_DEMAND_FEATURE_VIEW: "On-Demand Feature View", + BATCH_FEATURE_VIEW: "Batch Feature View", + STREAM_FEATURE_VIEW: "Stream Feature View", + ENTITY: "Entity", + FEATURE_SERVICE: "Feature Service", + DATA_SOURCE: "Data Source", + VALIDATION_REFERENCE: "Validation Reference", + SAVED_DATASET: "Saved Dataset", + PERMISSION: "Permission", + PROJECT: "Project", + LABEL_VIEW: "Label View", +}; + +const resolveType = (t: string | number): string => { + if (typeof t === "string") return t; + const numericMap: Record = { + 0: "FEATURE_VIEW", + 1: "ON_DEMAND_FEATURE_VIEW", + 2: "BATCH_FEATURE_VIEW", + 3: "STREAM_FEATURE_VIEW", + 4: "ENTITY", + 5: "FEATURE_SERVICE", + 6: "DATA_SOURCE", + 7: "VALIDATION_REFERENCE", + 8: "SAVED_DATASET", + 9: "PERMISSION", + 10: "PROJECT", + 11: "LABEL_VIEW", + }; + return numericMap[t] || `Type ${t}`; +}; + +const resolveAction = (a: string | number): string => { + if (typeof a === "string") return a; + return ACTION_NAMES[a] || `Action ${a}`; +}; + +const getActionColor = (action: string) => { + if (action.startsWith("READ")) return "success"; + if (action.startsWith("WRITE")) return "warning"; + if (action === "CREATE") return "primary"; + if (action === "UPDATE") return "accent"; + if (action === "DELETE") return "danger"; + if (action === "DESCRIBE") return "hollow"; + return "default"; +}; + +const getPolicyDescription = (policy: any): string => { + if (!policy) return "Allow All"; + if (policy.roleBasedPolicy?.roles) { + return `Roles: ${policy.roleBasedPolicy.roles.join(", ")}`; + } + if (policy.groupBasedPolicy?.groups) { + return `Groups: ${policy.groupBasedPolicy.groups.join(", ")}`; + } + if (policy.namespaceBasedPolicy?.namespaces) { + return `Namespaces: ${policy.namespaceBasedPolicy.namespaces.join(", ")}`; + } + if (policy.combinedGroupNamespacePolicy) { + const parts = []; + if (policy.combinedGroupNamespacePolicy.groups?.length) { + parts.push( + `Groups: ${policy.combinedGroupNamespacePolicy.groups.join(", ")}`, + ); + } + if (policy.combinedGroupNamespacePolicy.namespaces?.length) { + parts.push( + `Namespaces: ${policy.combinedGroupNamespacePolicy.namespaces.join(", ")}`, + ); + } + return parts.join(" | "); + } + return "Allow All"; +}; + +const getPolicyType = ( + policy: any, +): "role_based" | "group_based" | "namespace_based" | "combined" => { + if (!policy) return "role_based"; + if (policy.roleBasedPolicy) return "role_based"; + if (policy.groupBasedPolicy) return "group_based"; + if (policy.namespaceBasedPolicy) return "namespace_based"; + if (policy.combinedGroupNamespacePolicy) return "combined"; + return "role_based"; +}; + +const permissionToFormData = (permission: any): PermissionFormData => { + const spec = permission.spec || permission; + const policy = spec.policy; + + const rawTypes: string[] = (spec.types || []).map(resolveType); + const rawActions: string[] = (spec.actions || []).map(resolveAction); + + return { + name: spec.name || "", + types: Array.from(new Set(rawTypes)), + namePatterns: spec.namePatterns || spec.name_patterns || [], + actions: Array.from(new Set(rawActions)), + policyType: getPolicyType(policy), + roles: policy?.roleBasedPolicy?.roles || [], + groups: + policy?.groupBasedPolicy?.groups || + policy?.combinedGroupNamespacePolicy?.groups || + [], + namespaces: + policy?.namespaceBasedPolicy?.namespaces || + policy?.combinedGroupNamespacePolicy?.namespaces || + [], + tags: Object.entries(spec.tags || {}).map(([key, value]) => ({ + key, + value: value as string, + })), + requiredTags: Object.entries( + spec.requiredTags || spec.required_tags || {}, + ).map(([key, value]) => ({ + key, + value: value as string, + })), + }; +}; + +const formDataToPayload = ( + formData: PermissionFormData, + project: string, +): ApplyPermissionPayload => { + const policy: ApplyPermissionPayload["policy"] = {}; + + if (formData.policyType === "role_based") { + policy.role_based_policy = { + roles: formData.roles.filter((r) => r.trim()), + }; + } else if (formData.policyType === "group_based") { + policy.group_based_policy = { + groups: formData.groups.filter((g) => g.trim()), + }; + } else if (formData.policyType === "namespace_based") { + policy.namespace_based_policy = { + namespaces: formData.namespaces.filter((n) => n.trim()), + }; + } else if (formData.policyType === "combined") { + policy.combined_group_namespace_policy = { + groups: formData.groups.filter((g) => g.trim()), + namespaces: formData.namespaces.filter((n) => n.trim()), + }; + } + + return { + name: formData.name, + project, + types: formData.types, + name_patterns: formData.namePatterns.filter((p) => p.trim()), + actions: formData.actions, + policy, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + required_tags: Object.fromEntries( + formData.requiredTags + .filter((t) => t.key.trim()) + .map((t) => [t.key, t.value]), + ), + }; +}; + +const useLoadPermissions = () => { + const { projectName } = useParams(); + return useResourceQuery({ + resourceType: "permissions-list", + project: projectName, + restPath: permissionListPath(projectName), + restSelect: (d) => d.permissions, + }); +}; const PermissionsIndex = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const { isLoading, isSuccess, isError, data } = useLoadRegistry( - registryUrl, - projectName, - ); - const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadPermissions(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingPermission, setEditingPermission] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [searchString, setSearchString] = useState(""); + const [actionFilter, setActionFilter] = useState(""); + + const applyPermission = useApplyPermission(); + const deletePermissionMutation = useDeletePermission(); + + const permissions = useMemo(() => { + if (!data) return []; + let filtered = data; + + if (searchString.trim()) { + const lower = searchString.toLowerCase(); + filtered = filtered.filter((p) => + (p.spec?.name || "").toLowerCase().includes(lower), + ); + } + + if (actionFilter) { + filtered = filtered.filter((p) => { + const actions = (p.spec?.actions || []).map(resolveAction); + return actions.includes(actionFilter); + }); + } + + return filtered; + }, [data, searchString, actionFilter]); + + const handleCreate = () => { + setEditingPermission(null); + setIsModalOpen(true); + }; + + const handleEdit = (permission: any) => { + setEditingPermission(permission); + setIsModalOpen(true); + }; + + const handleDelete = (permission: any) => { + setDeleteTarget(permission); + }; + + const confirmDelete = () => { + if (!deleteTarget) return; + const name = deleteTarget.spec?.name || deleteTarget.name; + deletePermissionMutation.mutate( + { name, project: projectName || "" }, + { + onSuccess: () => { + setDeleteTarget(null); + setErrorMessage(null); + setSuccessMessage(`Permission "${name}" deleted successfully.`); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + setDeleteTarget(null); + const message = + err instanceof Error + ? err.message + : "An unexpected error occurred."; + setErrorMessage(message); + setTimeout(() => setErrorMessage(null), 5000); + }, + }, + ); + }; + + const handleFormSubmit = (formData: PermissionFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyPermission.mutate(payload, { + onSuccess: () => { + setIsModalOpen(false); + setEditingPermission(null); + setErrorMessage(null); + const verb = editingPermission ? "updated" : "created"; + setSuccessMessage( + `Permission "${formData.name}" ${verb} successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setErrorMessage(message); + }, + }); + }; + + const columns = [ + { + field: "spec.name", + name: "Name", + sortable: true, + render: (_: string, item: any) => ( + {item.spec?.name || "—"} + ), + }, + { + field: "spec.types", + name: "Resource Types", + render: (_: any, item: any) => { + const rawTypes: string[] = (item.spec?.types || []).map(resolveType); + const types = Array.from(new Set(rawTypes)); + if (types.length === 0) return All; + const display = (t: string) => TYPE_DISPLAY_NAMES[t] || t; + if (types.length > 3) { + return ( + + + {types.slice(0, 3).map(display).join(", ")} +{types.length - 3}{" "} + more + + + ); + } + return {types.map(display).join(", ")}; + }, + }, + { + field: "spec.actions", + name: "Actions", + render: (_: any, item: any) => { + const rawActions: string[] = (item.spec?.actions || []).map( + resolveAction, + ); + const actions = Array.from(new Set(rawActions)); + return ( + + {actions.map((action: string, i: number) => ( + + {action} + + ))} + + ); + }, + }, + { + field: "spec.policy", + name: "Policy", + render: (_: any, item: any) => { + const desc = getPolicyDescription(item.spec?.policy); + return ( + + + {desc.length > 40 ? desc.substring(0, 40) + "..." : desc} + + + ); + }, + }, + { + name: "Actions", + width: "100px", + render: (item: any) => ( + + + + handleEdit(item)} + color="primary" + /> + + + + + handleDelete(item)} + color="danger" + /> + + + + ), + }, + ]; + + const hasPermissions = isSuccess && data && data.length > 0; + const isEmpty = isSuccess && (!data || data.length === 0); return ( - + + Create Permission + , + ]} /> + {successMessage && ( + <> + + + + )} + {errorMessage && !isModalOpen && ( + <> + + + + )} + {isLoading && ( - +

Loading - +

+ )} + {isPermissionDenied && ( + +

You do not have permission to view permissions.

+
+ )} + {isError && !isPermissionDenied &&

Error loading permissions.

} + + {isEmpty && ( + No permissions yet} + body={ +

+ Permissions let you control who can perform specific actions on + your Feast resources. Create your first permission to get + started. +

+ } + actions={ + + Create Permission + + } + /> )} - {isError &&

Error loading permissions

} - {isSuccess && data && ( - - - + + {hasPermissions && ( + <> + + + +

Search

+
+ setSearchString(e.target.value)} + /> +
+ ({ value: a, text: a })), ]} - value={selectedPermissionAction} - onChange={(e) => - setSelectedPermissionAction(e.target.value) - } - aria-label="Filter by action" + value={actionFilter} + onChange={(e) => setActionFilter(e.target.value)} />
- - -

Permissions

-
- - {data.permissions && data.permissions.length > 0 ? ( - - ) : ( - No permissions defined in this project. - )} -
-
+ + )}
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setEditingPermission(null); + setErrorMessage(null); + }} + onSubmit={handleFormSubmit} + isEdit={!!editingPermission} + initialData={ + editingPermission + ? permissionToFormData(editingPermission) + : undefined + } + isSubmitting={applyPermission.isLoading} + submitError={errorMessage} + /> + )} + + {deleteTarget && ( + setDeleteTarget(null)} + onConfirm={confirmDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deletePermissionMutation.isLoading} + > +

+ Are you sure you want to delete the permission{" "} + + "{deleteTarget.spec?.name || deleteTarget.name}" + + ? This action cannot be undone. +

+
+ )}
); }; diff --git a/ui/src/pages/saved-data-sets/AddToCatalogModal.tsx b/ui/src/pages/saved-data-sets/AddToCatalogModal.tsx new file mode 100644 index 00000000000..7f9d80a6bd8 --- /dev/null +++ b/ui/src/pages/saved-data-sets/AddToCatalogModal.tsx @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiTabbedContent, + EuiTabbedContentTab, + EuiIcon, +} from "@elastic/eui"; +import RegisterDatasetModal from "./RegisterDatasetModal"; +import type { RegisterDatasetPayload } from "./RegisterDatasetModal"; +import CreateDatasetForm from "./CreateDatasetForm"; + +interface AddToCatalogModalProps { + onClose: () => void; + onLinkSubmit: (data: RegisterDatasetPayload) => Promise; + isLinkSubmitting: boolean; + linkError?: string | null; +} + +const AddToCatalogModal = ({ + onClose, + onLinkSubmit, + isLinkSubmitting, + linkError, +}: AddToCatalogModalProps) => { + const [selectedTab, setSelectedTab] = useState("link"); + + const tabs: EuiTabbedContentTab[] = [ + { + id: "link", + name: "Link Existing", + prepend: , + content: ( + + ), + }, + { + id: "create", + name: "Create Dataset", + prepend: , + content: , + }, + ]; + + return ( + + + Add to Catalog + + + t.id === selectedTab)} + onTabClick={(tab) => setSelectedTab(tab.id)} + /> + + + ); +}; + +export default AddToCatalogModal; diff --git a/ui/src/pages/saved-data-sets/CreateDatasetForm.tsx b/ui/src/pages/saved-data-sets/CreateDatasetForm.tsx new file mode 100644 index 00000000000..88a9d3e0201 --- /dev/null +++ b/ui/src/pages/saved-data-sets/CreateDatasetForm.tsx @@ -0,0 +1,725 @@ +import React, { useState, useMemo, useCallback, useContext } from "react"; +import { + EuiSpacer, + EuiFormRow, + EuiFieldText, + EuiRadioGroup, + EuiComboBox, + EuiComboBoxOptionOption, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiCallOut, + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiSuperSelect, + EuiSuperSelectOption, + EuiDatePicker, + EuiDatePickerRange, + EuiTextArea, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import { useMutation, useQueryClient } from "react-query"; +import moment, { Moment } from "moment"; +import useResourceQuery, { + featureServiceListPath, + featureViewListPath, + dataSourceListPath, +} from "../../queries/useResourceQuery"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; +import { restPost } from "../../queries/restApiClient"; +import TagsEditor, { TagEntry } from "../../components/forms/TagsEditor"; +import JobStatusPanel from "./JobStatusPanel"; + +interface CreateDatasetFormProps { + onClose: () => void; +} + +const FEATURE_MODE_OPTIONS = [ + { id: "service", label: "Use a Feature Service" }, + { id: "individual", label: "Select individual features" }, +]; + +const ENTITY_SOURCE_OPTIONS = [ + { id: "inline", label: "Define entity keys and time range manually" }, + { id: "reference", label: "Reference an existing data source path" }, +]; + +const STORAGE_TYPES = [ + { + value: "file", + label: "File (Parquet)", + placeholder: "s3://bucket/path/output.parquet", + }, + { + value: "bigquery", + label: "BigQuery", + placeholder: "project.dataset.table", + }, + { + value: "snowflake", + label: "Snowflake", + placeholder: "database.schema.table", + }, + { value: "redshift", label: "Redshift", placeholder: "schema.table" }, + { value: "spark", label: "Spark", placeholder: "s3://bucket/path/" }, + { value: "trino", label: "Trino", placeholder: "catalog.schema.table" }, + { value: "athena", label: "Athena", placeholder: "database.table" }, + { + value: "postgres", + label: "PostgreSQL", + placeholder: "schema.table_name", + }, + { + value: "clickhouse", + label: "ClickHouse", + placeholder: "database.table_name", + }, + { + value: "couchbase", + label: "Couchbase Columnar", + placeholder: "database.scope.collection", + }, +]; + +const CreateDatasetForm = ({ onClose }: CreateDatasetFormProps) => { + const { projectName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + const queryClient = useQueryClient(); + + // Step 1: Feature selection + const [featureMode, setFeatureMode] = useState("service"); + const [selectedService, setSelectedService] = useState< + EuiComboBoxOptionOption[] + >([]); + const [selectedFeatures, setSelectedFeatures] = useState< + EuiComboBoxOptionOption[] + >([]); + + // Step 2: Entity source + const [entitySourceType, setEntitySourceType] = useState("inline"); + + // Inline mode state + const [entityKeys, setEntityKeys] = useState([]); + const [entityValues, setEntityValues] = useState(""); + const [startDate, setStartDate] = useState( + moment().subtract(30, "days"), + ); + const [endDate, setEndDate] = useState(moment()); + const [extraColumns, setExtraColumns] = useState(""); + + // Reference mode state + const [entitySourcePath, setEntitySourcePath] = useState(""); + + // Step 3: Storage & metadata + const [datasetName, setDatasetName] = useState(""); + const [namespace, setNamespace] = useState(""); + const [collection, setCollection] = useState(""); + const [description, setDescription] = useState(""); + const [storageType, setStorageType] = useState("file"); + const [storagePath, setStoragePath] = useState(""); + const [storageFileFormat, setStorageFileFormat] = useState("parquet"); + const [tags, setTags] = useState([]); + const [allowOverwrite] = useState(false); + + // Job tracking + const [jobId, setJobId] = useState(null); + const [submitError, setSubmitError] = useState(null); + + // Fetch data for suggestions + const { data: featureServicesRaw } = useResourceQuery({ + resourceType: "create-dataset-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices || [], + }); + + const { data: featureViewsRaw } = useResourceQuery({ + resourceType: "create-dataset-fv", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: (d) => d.featureViews || [], + }); + + const { data: dataSourcesRaw } = useResourceQuery({ + resourceType: "create-dataset-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources || [], + }); + + const featureServiceOptions: EuiComboBoxOptionOption[] = useMemo( + () => + (featureServicesRaw || []).map((fs: any) => ({ + label: fs.spec?.name || fs.name || "", + })), + [featureServicesRaw], + ); + + const featureOptions: EuiComboBoxOptionOption[] = useMemo( + () => + (featureViewsRaw || []) + .filter((fv: any) => fv.type !== "labelView") + .flatMap((fv: any) => { + const fvName = fv.spec?.name || ""; + const features = fv.spec?.features || []; + return features.map((f: any) => ({ + label: `${fvName}:${f.name || f}`, + })); + }), + [featureViewsRaw], + ); + + // Extract entity/join key options from feature views + const joinKeyOptions: EuiComboBoxOptionOption[] = useMemo(() => { + const seen = new Set(); + (featureViewsRaw || []).forEach((fv: any) => { + const entities = fv.spec?.entities || []; + entities.forEach((e: string) => { + if (e && !seen.has(e)) seen.add(e); + }); + }); + return Array.from(seen).map((k) => ({ label: k })); + }, [featureViewsRaw]); + + // Filter storage types based on configured data sources + const storageOptions: EuiSuperSelectOption[] = useMemo(() => { + if (!dataSourcesRaw || dataSourcesRaw.length === 0) { + return STORAGE_TYPES.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: {st.label}, + })); + } + + const detectedTypes = new Set(); + for (const ds of dataSourcesRaw) { + const spec = ds.spec || ds; + if (spec.fileOptions || ds.fileOptions) detectedTypes.add("file"); + if (spec.bigqueryOptions || ds.bigqueryOptions) + detectedTypes.add("bigquery"); + if (spec.snowflakeOptions || ds.snowflakeOptions) + detectedTypes.add("snowflake"); + if (spec.redshiftOptions || ds.redshiftOptions) + detectedTypes.add("redshift"); + if (spec.sparkOptions || ds.sparkOptions) detectedTypes.add("spark"); + if (spec.trinoOptions || ds.trinoOptions) detectedTypes.add("trino"); + if (spec.athenaOptions || ds.athenaOptions) detectedTypes.add("athena"); + const dsType = spec.type || ds.type; + if (dsType === 1) detectedTypes.add("file"); + if (dsType === 2) detectedTypes.add("bigquery"); + if (dsType === 3) detectedTypes.add("redshift"); + if (dsType === 5) detectedTypes.add("snowflake"); + if (dsType === 7) detectedTypes.add("spark"); + if (dsType === 8) detectedTypes.add("trino"); + if (dsType === 9) detectedTypes.add("athena"); + const classType = + spec.dataSourceClassType || ds.dataSourceClassType || ""; + if (classType.includes("postgres")) detectedTypes.add("postgres"); + if (classType.includes("clickhouse")) detectedTypes.add("clickhouse"); + if (classType.includes("couchbase")) detectedTypes.add("couchbase"); + } + + // Always include file as a fallback + detectedTypes.add("file"); + + const filtered = STORAGE_TYPES.filter((st) => detectedTypes.has(st.value)); + return filtered.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: {st.label}, + })); + }, [dataSourcesRaw]); + + // Extract available data source paths for the reference entity source option + const dataSourcePathOptions: EuiComboBoxOptionOption[] = useMemo(() => { + if (!dataSourcesRaw) return []; + const paths: EuiComboBoxOptionOption[] = []; + for (const ds of dataSourcesRaw) { + const spec = ds.spec || ds; + const name = spec.name || ds.name || ""; + const fileOpts = spec.fileOptions || ds.fileOptions; + const bqOpts = spec.bigqueryOptions || ds.bigqueryOptions; + const sfOpts = spec.snowflakeOptions || ds.snowflakeOptions; + const rsOpts = spec.redshiftOptions || ds.redshiftOptions; + const sparkOpts = spec.sparkOptions || ds.sparkOptions; + const trinoOpts = spec.trinoOptions || ds.trinoOptions; + const athenaOpts = spec.athenaOptions || ds.athenaOptions; + + let path = ""; + if (fileOpts?.uri) path = fileOpts.uri; + else if (fileOpts?.path) path = fileOpts.path; + else if (bqOpts?.table) path = bqOpts.table; + else if (sfOpts?.table) path = sfOpts.table; + else if (rsOpts?.table) path = rsOpts.table; + else if (sparkOpts?.path) path = sparkOpts.path; + else if (sparkOpts?.table) path = sparkOpts.table; + else if (trinoOpts?.table) path = trinoOpts.table; + else if (athenaOpts?.table) path = athenaOpts.table; + + if (path) { + paths.push({ label: path, key: name }); + } + } + return paths; + }, [dataSourcesRaw]); + + const currentStorageType = + STORAGE_TYPES.find((s) => s.value === storageType) || STORAGE_TYPES[0]; + + // Submit create job + const createMutation = useMutation( + async () => { + const payload: any = { + name: datasetName.trim(), + project: projectName || "", + storage_type: storageType, + storage_path: storagePath.trim(), + storage_file_format: + storageType === "spark" ? storageFileFormat : undefined, + entity_source_type: entitySourceType, + allow_overwrite: allowOverwrite, + tags: tags.reduce( + (acc, t) => { + if (t.key.trim() && t.value.trim()) + acc[t.key.trim()] = t.value.trim(); + return acc; + }, + {} as Record, + ), + }; + + if (namespace.trim()) payload.namespace = namespace.trim(); + if (collection.trim()) payload.collection = collection.trim(); + if (description.trim()) payload.description = description.trim(); + + if (featureMode === "service" && selectedService.length > 0) { + payload.feature_service_name = selectedService[0].label; + } else if (featureMode === "individual" && selectedFeatures.length > 0) { + payload.features = selectedFeatures.map((f) => f.label); + } + + if (entitySourceType === "inline") { + payload.entity_keys = entityKeys.map((k) => k.label); + payload.entity_values = entityValues.trim(); + if (startDate) payload.start_date = startDate.toISOString(); + if (endDate) payload.end_date = endDate.toISOString(); + } else if (entitySourceType === "reference") { + payload.entity_source_path = entitySourcePath.trim(); + } + + // Extra columns apply to all entity source methods + if (extraColumns.trim()) { + payload.extra_columns = extraColumns.trim(); + } + + return restPost( + registryUrl, + "/saved_datasets/create", + payload, + fetchOptions, + ); + }, + { + onSuccess: (data: any) => { + setJobId(data.job_id); + setSubmitError(null); + }, + onError: (err: Error) => { + setSubmitError(err.message); + }, + }, + ); + + const handleJobComplete = useCallback(() => { + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + }, [queryClient]); + + // Validation + const canProceedStep0 = + featureMode === "service" + ? selectedService.length > 0 + : selectedFeatures.length > 0; + + const canProceedStep1 = (() => { + if (entitySourceType === "inline") { + return entityKeys.length > 0 && entityValues.trim().length > 0; + } + return entitySourcePath.trim().length > 0; + })(); + + const canSubmit = + datasetName.trim().length > 0 && + storagePath.trim().length > 0 && + /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(datasetName.trim()); + + const handleRetry = useCallback(() => { + setJobId(null); + setSubmitError(null); + }, []); + + if (jobId) { + return ( +
+ +
+ ); + } + + return ( +
+ {submitError && ( + <> + +

{submitError}

+
+ + + )} + + + + Create a new dataset by running a feature retrieval job. Feast will + execute get_historical_features, persist the results to your chosen + storage, and register the dataset in the catalog. + + + + + {/* Step 1: Feature Selection */} + +

Step 1: Define Features

+
+ + + setFeatureMode(id)} + /> + + + {featureMode === "service" ? ( + + + + ) : ( + + + setSelectedFeatures([...selectedFeatures, { label: val }]) + } + placeholder="Search or type features..." + isClearable + fullWidth + /> + + )} + + + + + {/* Step 2: Entity Source */} + +

Step 2: Entity Source

+
+ + + setEntitySourceType(id)} + /> + + + {entitySourceType === "inline" && ( + <> + + + setEntityKeys([...entityKeys, { label: val }]) + } + placeholder="Select or type entity keys (e.g. driver_id, customer_id)..." + isClearable + fullWidth + /> + + + + setEntityValues(e.target.value)} + placeholder={ + "1001, 1002, 1003, 1004\nor for multiple keys:\n1001,A\n1002,B\n1003,C" + } + rows={4} + fullWidth + /> + + + + + } + endDateControl={ + + } + fullWidth + /> + + + )} + + {entitySourceType === "reference" && ( + + + setEntitySourcePath(selected.length > 0 ? selected[0].label : "") + } + onCreateOption={(val) => setEntitySourcePath(val)} + placeholder="Select a data source or type a path..." + isClearable + fullWidth + /> + + )} + + + + setExtraColumns(e.target.value)} + placeholder={"val_to_add=10\ndiscount_pct=0.15"} + rows={2} + fullWidth + /> + + + + + + {/* Step 3: Storage Destination & Metadata */} + +

Step 3: Output Destination

+
+ + + 0 && + !/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(datasetName) + } + error="Must start with letter/underscore, contain only letters, numbers, underscores, hyphens." + > + setDatasetName(e.target.value)} + placeholder="e.g. driver_training_2024_q1" + fullWidth + /> + + + + setDescription(e.target.value)} + placeholder="e.g. Training data for driver fraud model" + fullWidth + /> + + + + + + setNamespace(e.target.value)} + placeholder="e.g. fraud" + /> + + + + + setCollection(e.target.value)} + placeholder="e.g. training" + /> + + + + + + + + + + + + + + + setStoragePath(e.target.value)} + placeholder={currentStorageType.placeholder} + fullWidth + /> + + + + + {storageType === "spark" && ( + <> + + + Parquet, + }, + { + value: "avro", + inputDisplay: "Avro", + dropdownDisplay: Avro, + }, + { + value: "csv", + inputDisplay: "CSV", + dropdownDisplay: CSV, + }, + { + value: "json", + inputDisplay: "JSON", + dropdownDisplay: JSON, + }, + ]} + valueOfSelected={storageFileFormat} + onChange={setStorageFileFormat} + fullWidth + /> + + + )} + + + + + {/* Actions */} + + + + Cancel + + + createMutation.mutate()} + isLoading={createMutation.isLoading} + disabled={!canProceedStep0 || !canProceedStep1 || !canSubmit} + iconType="playFilled" + > + Create Dataset + + + +
+ ); +}; + +export default CreateDatasetForm; diff --git a/ui/src/pages/saved-data-sets/DatasetCatalogBrowser.tsx b/ui/src/pages/saved-data-sets/DatasetCatalogBrowser.tsx new file mode 100644 index 00000000000..bcb2bd844a5 --- /dev/null +++ b/ui/src/pages/saved-data-sets/DatasetCatalogBrowser.tsx @@ -0,0 +1,846 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiText, + EuiBadge, + EuiSpacer, + EuiIcon, + EuiBreadcrumbs, + EuiEmptyPrompt, + EuiTreeView, + EuiToolTip, + EuiButtonIcon, + EuiCopy, +} from "@elastic/eui"; +import type { Node as EuiTreeNode } from "@elastic/eui/src/components/tree_view/tree_view"; +import { useNavigate, useParams } from "react-router-dom"; + +/* ────────────────────────── types ────────────────────────── */ + +interface BrowsePath { + namespace?: string; + collection?: string; +} + +interface DatasetCatalogBrowserProps { + datasets: any[]; + onDelete?: (name: string) => void; +} + +/* ──────────────────── hierarchy builder ──────────────────── */ + +function buildHierarchy(datasets: any[]) { + const tree: Record> = {}; + for (const ds of datasets) { + const ns = ds.spec?.namespace || ""; + const col = ds.spec?.collection || ""; + if (!tree[ns]) tree[ns] = {}; + if (!tree[ns][col]) tree[ns][col] = []; + tree[ns][col].push(ds); + } + return tree; +} + +/* ──────────────────── colors ──────────────────── */ + +const NS_COLOR = "#0077CC"; +const COL_COLOR = "#8B5CF6"; + +const STORAGE_TYPE_CONFIG: Record< + string, + { label: string; color: string; icon: string } +> = { + file: { label: "File", color: "#4CAF50", icon: "document" }, + bigquery: { label: "BigQuery", color: "#4285F4", icon: "storage" }, + snowflake: { label: "Snowflake", color: "#29B5E8", icon: "snowflake" }, + redshift: { label: "Redshift", color: "#205B97", icon: "compute" }, + spark: { label: "Spark", color: "#E25A1C", icon: "bolt" }, + trino: { label: "Trino", color: "#DD00A1", icon: "database" }, + athena: { label: "Athena", color: "#8C4FFF", icon: "database" }, + custom: { label: "Custom", color: "#607D8B", icon: "gear" }, + unknown: { label: "Storage", color: "#98A2B3", icon: "database" }, +}; + +function detectStorageType(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return "unknown"; + if (storage.fileStorage) return "file"; + if (storage.bigqueryStorage) return "bigquery"; + if (storage.snowflakeStorage) return "snowflake"; + if (storage.redshiftStorage) return "redshift"; + if (storage.sparkStorage) return "spark"; + if (storage.trinoStorage) return "trino"; + if (storage.athenaStorage) return "athena"; + if (storage.customStorage) return "custom"; + return "unknown"; +} + +function extractStoragePath(dataset: any): string | undefined { + const storage = dataset?.spec?.storage; + if (!storage) return undefined; + if (storage.fileStorage?.uri) return storage.fileStorage.uri; + if (storage.bigqueryStorage?.table) return storage.bigqueryStorage.table; + if (storage.snowflakeStorage?.table) return storage.snowflakeStorage.table; + if (storage.redshiftStorage?.table) return storage.redshiftStorage.table; + if (storage.sparkStorage?.path) return storage.sparkStorage.path; + if (storage.sparkStorage?.table) return storage.sparkStorage.table; + if (storage.trinoStorage?.table) return storage.trinoStorage.table; + if (storage.athenaStorage?.table) return storage.athenaStorage.table; + if (storage.customStorage?.configuration) + return storage.customStorage.configuration; + return undefined; +} + +function truncatePath(path: string, maxLen: number = 42): string { + if (path.length <= maxLen) return path; + return `${path.slice(0, 18)}...${path.slice(-20)}`; +} + +function getRelativeTime(date: Date): string { + const diffMs = Date.now() - date.getTime(); + const diffDays = Math.floor(diffMs / 86400000); + if (diffDays === 0) return "Today"; + if (diffDays === 1) return "Yesterday"; + if (diffDays < 7) return `${diffDays}d ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; + if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`; + return `${Math.floor(diffDays / 365)}y ago`; +} + +/* ──────────────────── tile: dataset card ──────────────────── */ + +const DatasetCard: React.FC<{ + dataset: any; + onNavigate: () => void; + onDelete?: (name: string) => void; +}> = ({ dataset, onNavigate, onDelete }) => { + const [isHovered, setIsHovered] = useState(false); + const spec = dataset.spec || {}; + const meta = dataset.meta || {}; + const name = spec.name || "unknown"; + const features = spec.features || []; + const joinKeys = spec.joinKeys || spec.join_keys || []; + const tags = spec.tags || {}; + const description = spec.description || ""; + const featureServiceName = + spec.featureServiceName || spec.feature_service_name; + const createdTimestamp = meta.createdTimestamp || meta.created_timestamp; + const storagePath = extractStoragePath(dataset); + const storageType = detectStorageType(dataset); + const storageInfo = + STORAGE_TYPE_CONFIG[storageType] || STORAGE_TYPE_CONFIG.unknown; + + const formattedDate = createdTimestamp + ? new Date(createdTimestamp).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + : null; + const relativeTime = createdTimestamp + ? getRelativeTime(new Date(createdTimestamp)) + : null; + + const codeSnippet = `dataset = store.get_saved_dataset("${name}")\ndf = dataset.to_df()`; + + return ( + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onNavigate} + style={{ + height: "100%", + display: "flex", + flexDirection: "column", + transition: "all 0.2s ease", + transform: isHovered ? "translateY(-2px)" : "none", + borderTop: `3px solid ${storageInfo.color}`, + cursor: "pointer", + }} + > + {/* Header */} + + + +

+ {name} +

+
+
+ + + {storageInfo.label} + + +
+ + {/* Description */} + {description && ( + +

{description}

+
+ )} + + + + {/* Storage path */} + {storagePath && ( + + + {" "} + + {truncatePath(storagePath)} + + + + )} + + + + {/* Metrics */} + + + + Features + + + {features.length} + + + + + Retrieval Keys + + + {joinKeys.length} + + + {featureServiceName && ( + + + Service + + + {featureServiceName} + + + )} + + +
+ + + {/* Tags */} + {Object.keys(tags).length > 0 && ( + <> + + {Object.entries(tags) + .slice(0, 3) + .map(([key, value]) => ( + + + {key}: {value as string} + + + ))} + {Object.keys(tags).length > 3 && ( + + + +{Object.keys(tags).length - 3} more + + + )} + + + + )} + + {/* Footer */} + + + {formattedDate && ( + + + {relativeTime} + + + )} + + + + + + {(copy) => ( + + { + e.stopPropagation(); + copy(); + }} + /> + + )} + + + {onDelete && ( + + + { + e.stopPropagation(); + onDelete(name); + }} + /> + + + )} + + + + + + ); +}; + +/* ──────────────────── tile: folder card (same width as dataset) ──────────────────── */ + +const FolderCard: React.FC<{ + name: string; + type: "namespace" | "collection"; + datasetCount: number; + collectionCount?: number; + onClick: () => void; +}> = ({ name, type, datasetCount, collectionCount, onClick }) => { + const [isHovered, setIsHovered] = useState(false); + const color = type === "namespace" ? NS_COLOR : COL_COLOR; + + return ( + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + style={{ + cursor: "pointer", + transition: "all 0.2s ease", + transform: isHovered ? "translateY(-2px)" : "none", + borderTop: `3px solid ${color}`, + display: "flex", + flexDirection: "column", + }} + > + + + + + + +

{name}

+
+
+
+ + + + + {type === "namespace" && + collectionCount !== undefined && + collectionCount > 0 && ( + + + Collections + + + {collectionCount} + + + )} + + + Datasets + + + {datasetCount} + + + +
+
+ ); +}; + +/* ──────────────────── tree styles ──────────────────── */ + +const TREE_CSS = ` + .catalogTree .euiTreeView__node { + margin-bottom: 1px; + } + .catalogTree .euiTreeView__node--expanded > .euiTreeView__nodeInner { + background: rgba(0, 119, 204, 0.05); + border-radius: 4px; + } + .catalogTree .euiTreeView__nodeInner { + padding: 4px 6px; + border-radius: 4px; + transition: background 0.15s ease; + } + .catalogTree .euiTreeView__nodeInner:hover { + background: rgba(0, 119, 204, 0.08); + } + .catalogTree .euiTreeView__nodeInner--withArrow .euiTreeView__nodeInner__arrow { + margin-right: 2px; + } + .catalogTree .euiTreeView__nodeLabel { + font-size: 13px; + } +`; + +/* ──────────────────── main component ──────────────────── */ + +const DatasetCatalogBrowser: React.FC = ({ + datasets, + onDelete, +}) => { + const { projectName } = useParams(); + const navigate = useNavigate(); + const [browsePath, setBrowsePath] = useState({}); + const [showTree, setShowTree] = useState(true); + + const hierarchy = useMemo(() => buildHierarchy(datasets), [datasets]); + + const namespaceList = useMemo(() => { + const entries: Array<{ + name: string; + collections: string[]; + totalDatasets: number; + }> = []; + for (const [ns, cols] of Object.entries(hierarchy)) { + if (ns === "") continue; + const colNames = Object.keys(cols) + .filter((c) => c !== "") + .sort(); + const total = Object.values(cols).reduce((s, a) => s + a.length, 0); + entries.push({ name: ns, collections: colNames, totalDatasets: total }); + } + entries.sort((a, b) => a.name.localeCompare(b.name)); + return entries; + }, [hierarchy]); + + const rootDatasets = useMemo(() => { + const cols = hierarchy[""]; + if (!cols) return []; + const all: any[] = []; + for (const arr of Object.values(cols)) all.push(...arr); + return all; + }, [hierarchy]); + + const goToDataset = useCallback( + (dataset: any) => { + const p = dataset.project || dataset.spec?.project || projectName; + const n = dataset.spec?.name || dataset.name; + navigate(`/p/${p}/data-set/${n}`); + }, + [navigate, projectName], + ); + + /* ── tree sidebar (EuiTreeView) ── */ + + // Clickable label for parent nodes — onClick navigates, stopPropagation prevents toggle + const navLabel = useCallback( + ( + text: string, + onClick: () => void, + isActive: boolean, + activeColor?: string, + ) => ( + { + e.stopPropagation(); + onClick(); + }} + style={{ + cursor: "pointer", + fontWeight: isActive ? 600 : 400, + color: isActive ? activeColor || NS_COLOR : undefined, + }} + > + {text} + + ), + [], + ); + + const treeItems: EuiTreeNode[] = useMemo(() => { + const isRootSelected = browsePath.namespace === undefined; + const items: EuiTreeNode[] = [ + { + id: "_root", + label: navLabel( + "All Datasets", + () => setBrowsePath({}), + isRootSelected, + ), + icon: , + isExpanded: true, + }, + ]; + + for (const ns of namespaceList) { + const nsData = hierarchy[ns.name] || {}; + const nsChildren: EuiTreeNode[] = []; + const nsSelected = + browsePath.namespace === ns.name && !browsePath.collection; + + // Collection sub-folders + for (const col of ns.collections) { + const colDatasets = nsData[col] || []; + const colSelected = + browsePath.namespace === ns.name && browsePath.collection === col; + + const dsLeaves: EuiTreeNode[] = colDatasets.map((ds: any) => ({ + id: `ds:${ns.name}/${col}/${ds.spec?.name || ds.name}`, + label: ds.spec?.name || ds.name || "unknown", + icon: , + callback: () => { + goToDataset(ds); + return `ds:${ns.name}/${col}/${ds.spec?.name}`; + }, + })); + + nsChildren.push({ + id: `col:${ns.name}/${col}`, + label: navLabel( + `${col} (${colDatasets.length})`, + () => setBrowsePath({ namespace: ns.name, collection: col }), + colSelected, + COL_COLOR, + ), + icon: , + iconWhenExpanded: ( + + ), + children: dsLeaves.length > 0 ? dsLeaves : undefined, + }); + } + + // Direct datasets under namespace (no collection) + const directDatasets = nsData[""] || []; + for (const ds of directDatasets) { + nsChildren.push({ + id: `ds:${ns.name}/_/${ds.spec?.name || ds.name}`, + label: ds.spec?.name || ds.name || "unknown", + icon: , + callback: () => { + goToDataset(ds); + return `ds:${ns.name}/_/${ds.spec?.name}`; + }, + }); + } + + items.push({ + id: `ns:${ns.name}`, + label: navLabel( + `${ns.name} (${ns.totalDatasets})`, + () => setBrowsePath({ namespace: ns.name }), + nsSelected, + NS_COLOR, + ), + icon: , + iconWhenExpanded: ( + + ), + children: nsChildren.length > 0 ? nsChildren : undefined, + }); + } + + // Ungrouped datasets + if (rootDatasets.length > 0) { + const ungroupedLeaves: EuiTreeNode[] = rootDatasets.map((ds: any) => ({ + id: `ds:_ungrouped/${ds.spec?.name || ds.name}`, + label: ds.spec?.name || ds.name || "unknown", + icon: , + callback: () => { + goToDataset(ds); + return `ds:_ungrouped/${ds.spec?.name}`; + }, + })); + + items.push({ + id: "_ungrouped", + label: navLabel( + `Ungrouped (${rootDatasets.length})`, + () => setBrowsePath({}), + false, + "#98A2B3", + ), + icon: , + children: ungroupedLeaves, + }); + } + + return items; + }, [ + namespaceList, + rootDatasets, + hierarchy, + browsePath, + goToDataset, + navLabel, + ]); + + /* ── breadcrumbs ── */ + const breadcrumbs = useMemo(() => { + const crumbs: Array<{ text: string; onClick?: () => void }> = [ + { + text: "All Datasets", + onClick: + browsePath.namespace !== undefined + ? () => setBrowsePath({}) + : undefined, + }, + ]; + if (browsePath.namespace) { + crumbs.push({ + text: browsePath.namespace, + onClick: browsePath.collection + ? () => setBrowsePath({ namespace: browsePath.namespace }) + : undefined, + }); + if (browsePath.collection) { + crumbs.push({ text: browsePath.collection }); + } + } + return crumbs; + }, [browsePath]); + + /* ── content ── */ + const renderContent = () => { + // ─── Root level: namespace folders + datasets mixed in one grid ─── + if (browsePath.namespace === undefined) { + if (namespaceList.length === 0 && rootDatasets.length === 0) { + return ( + No datasets yet} + body={ +

+ Add datasets and organize them into namespaces and collections. +

+ } + /> + ); + } + + return ( + + {namespaceList.map((ns) => ( + setBrowsePath({ namespace: ns.name })} + /> + ))} + {rootDatasets.map((d: any) => ( + goToDataset(d)} + onDelete={onDelete} + /> + ))} + + ); + } + + // ─── Namespace level: collection folders + direct datasets mixed ─── + if (browsePath.namespace && !browsePath.collection) { + const ns = browsePath.namespace; + const cols = hierarchy[ns] || {}; + const colNames = Object.keys(cols) + .filter((c) => c !== "") + .sort(); + const directDs = cols[""] || []; + + return ( + + {colNames.map((col) => ( + setBrowsePath({ namespace: ns, collection: col })} + /> + ))} + {directDs.map((d: any) => ( + goToDataset(d)} + onDelete={onDelete} + /> + ))} + {colNames.length === 0 && directDs.length === 0 && ( + + + This namespace is empty. + + + )} + + ); + } + + // ─── Collection level: datasets only ─── + if (browsePath.namespace && browsePath.collection) { + const dsList = + hierarchy[browsePath.namespace]?.[browsePath.collection] || []; + + return ( + + {dsList.map((d: any) => ( + goToDataset(d)} + onDelete={onDelete} + /> + ))} + {dsList.length === 0 && ( + + + No datasets in this collection. + + + )} + + ); + } + + return null; + }; + + /* ──────────────────── render ──────────────────── */ + + const hasTree = namespaceList.length > 0; + + return ( +
+ {/* Tree sidebar */} + {hasTree && ( +
+ {showTree ? ( + <> +
+ + + Catalog + + + + setShowTree(false)} + /> + +
+ +
+ + +
+ + ) : ( + + setShowTree(true)} + /> + + )} +
+ )} + + {/* Content */} +
+ + + {renderContent()} +
+
+ ); +}; + +export default DatasetCatalogBrowser; diff --git a/ui/src/pages/saved-data-sets/DatasetInstance.tsx b/ui/src/pages/saved-data-sets/DatasetInstance.tsx index 0f7e3ab8984..2856af5f342 100644 --- a/ui/src/pages/saved-data-sets/DatasetInstance.tsx +++ b/ui/src/pages/saved-data-sets/DatasetInstance.tsx @@ -1,33 +1,116 @@ -import React from "react"; +import React, { useState, useContext, useCallback } from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, + EuiCallOut, + EuiSpacer, +} from "@elastic/eui"; +import { useMutation, useQueryClient } from "react-query"; import { DatasetIcon } from "../../graphics/DatasetIcon"; - import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import DatasetOverviewTab from "./DatasetOverviewTab"; +import DatasetUsageTab from "./DatasetUsageTab"; +import DatasetSampleTab from "./DatasetSampleTab"; +import EditDatasetModal from "./EditDatasetModal"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import DatasetExpectationsTab from "./DatasetExpectationsTab"; import { useDatasetCustomTabs, useDataSourceCustomTabRoutes, } from "../../custom-tabs/TabsRegistryContext"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; +import { restPost, restDelete } from "../../queries/restApiClient"; +import useLoadDataset from "./useLoadDataset"; const DatasetInstance = () => { const navigate = useNavigate(); - let { datasetName } = useParams(); + const { datasetName, projectName } = useParams(); - useDocumentTitle(`${datasetName} | Saved Datasets | Feast`); + useDocumentTitle(`${datasetName} | Datasets | Feast`); const { customNavigationTabs } = useDatasetCustomTabs(navigate); const CustomTabRoutes = useDataSourceCustomTabRoutes(); + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + const queryClient = useQueryClient(); + + const { data: datasetData } = useLoadDataset(datasetName || ""); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [editError, setEditError] = useState(null); + + const deleteMutation = useMutation( + () => + restDelete( + registryUrl, + `/saved_datasets/${encodeURIComponent(datasetName || "")}?project=${encodeURIComponent(projectName || "")}`, + fetchOptions, + ), + { + onSuccess: () => { + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + navigate(`/p/${projectName}/data-set`); + }, + }, + ); + + const editMutation = useMutation( + (payload: any) => + restPost(registryUrl, "/saved_datasets", payload, fetchOptions), + { + onSuccess: () => { + setShowEditModal(false); + setEditError(null); + queryClient.invalidateQueries(["rest", `saved-dataset:${datasetName}`]); + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + }, + onError: (err: Error) => { + setEditError(err.message); + }, + }, + ); + + const handleEditSubmit = useCallback( + async (payload: any) => { + payload.project = projectName || ""; + await editMutation.mutateAsync(payload); + }, + [projectName, editMutation], + ); + return ( { + setEditError(null); + setShowEditModal(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={[ { label: "Overview", @@ -37,22 +120,72 @@ const DatasetInstance = () => { }, }, { - label: "Expectations", - isSelected: useMatchSubpath("expectations"), + label: "Preview Data", + isSelected: useMatchSubpath("sample"), onClick: () => { - navigate("expectations"); + navigate("sample"); + }, + }, + { + label: "Usage", + isSelected: useMatchSubpath("usage"), + onClick: () => { + navigate("usage"); }, }, ...customNavigationTabs, ]} /> + {deleteMutation.isError && ( + <> + +

{(deleteMutation.error as Error)?.message}

+
+ + + )} } /> - } /> + } /> + } /> {CustomTabRoutes}
+ + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={() => deleteMutation.mutate()} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteMutation.isLoading} + > +

+ Are you sure you want to delete {datasetName}? +

+

+ This removes the dataset metadata from the registry. The underlying + data at the storage location will not be deleted. +

+
+ )} + + {showEditModal && datasetData && ( + setShowEditModal(false)} + onSubmit={handleEditSubmit} + isSubmitting={editMutation.isLoading} + error={editError} + /> + )}
); }; diff --git a/ui/src/pages/saved-data-sets/DatasetOverviewTab.tsx b/ui/src/pages/saved-data-sets/DatasetOverviewTab.tsx index 9ee7dd1aa42..65e3cc2732b 100644 --- a/ui/src/pages/saved-data-sets/DatasetOverviewTab.tsx +++ b/ui/src/pages/saved-data-sets/DatasetOverviewTab.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { EuiFlexGroup, EuiHorizontalRule, @@ -9,16 +10,54 @@ import { EuiDescriptionList, EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiBadge, + EuiText, + EuiCallOut, } from "@elastic/eui"; -import React from "react"; import { useParams } from "react-router-dom"; +import EuiCustomLink from "../../components/EuiCustomLink"; import DatasetFeaturesTable from "./DatasetFeaturesTable"; import DatasetJoinKeysTable from "./DatasetJoinKeysTable"; import useLoadDataset from "./useLoadDataset"; -import { toDate } from "../../utils/timestamp"; -const EntityOverviewTab = () => { - let { datasetName } = useParams(); +function extractStorageInfo(data: any): { type: string; path: string } { + const storage = data?.spec?.storage; + if (!storage) return { type: "Unknown", path: "—" }; + if (storage.fileStorage?.uri) + return { type: "File", path: storage.fileStorage.uri }; + if (storage.bigqueryStorage?.table) + return { type: "BigQuery", path: storage.bigqueryStorage.table }; + if (storage.snowflakeStorage?.table) + return { type: "Snowflake", path: storage.snowflakeStorage.table }; + if (storage.redshiftStorage?.table) + return { type: "Redshift", path: storage.redshiftStorage.table }; + if (storage.sparkStorage?.path) + return { type: "Spark", path: storage.sparkStorage.path }; + if (storage.sparkStorage?.table) + return { type: "Spark", path: storage.sparkStorage.table }; + if (storage.trinoStorage?.table) + return { type: "Trino", path: storage.trinoStorage.table }; + if (storage.athenaStorage?.table) + return { type: "Athena", path: storage.athenaStorage.table }; + if (storage.customStorage?.configuration) + return { type: "Custom", path: storage.customStorage.configuration }; + return { type: "Unknown", path: "—" }; +} + +function formatTimestamp(ts: any): string { + if (!ts) return "—"; + try { + return new Date(ts).toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }); + } catch { + return "—"; + } +} + +const DatasetOverviewTab = () => { + const { datasetName, projectName } = useParams(); if (!datasetName) { throw new Error( @@ -26,87 +65,226 @@ const EntityOverviewTab = () => { ); } - const { isLoading, isSuccess, isError, data } = useLoadDataset(datasetName); - const isEmpty = data === undefined; + const { isLoading, isError, data } = useLoadDataset(datasetName); + + if (isLoading) { + return ( + + + + + + Loading dataset details... + + + ); + } + + if (isError || !data) { + return ( + +

+ Could not load dataset {datasetName}. It may have + been deleted or the registry may be unavailable. +

+
+ ); + } + + const storageInfo = extractStorageInfo(data); + const features = data.spec?.features || []; + const joinKeys = data.spec?.joinKeys || data.spec?.join_keys || []; + const tags = data.spec?.tags || {}; + const featureServiceName = + data.spec?.featureServiceName || data.spec?.feature_service_name; + const namespace = data.spec?.namespace || ""; + const collection = data.spec?.collection || ""; + const description = data.spec?.description || ""; + const dataSources: string[] = data.spec?.dataSources || []; + const createdTs = data.meta?.createdTimestamp || data.meta?.created_timestamp; + const minEventTs = + data.meta?.minEventTimestamp || data.meta?.min_event_timestamp; + const maxEventTs = + data.meta?.maxEventTimestamp || data.meta?.max_event_timestamp; + + // Determine provenance: if min/max event timestamps exist, the dataset + // was likely created via SDK (persist), otherwise it was linked manually. + const provenance = minEventTs + ? "Created (feature retrieval)" + : "Linked (existing data)"; return ( - - {isLoading && ( - - Loading - - )} - {isEmpty &&

No dataset with name: {datasetName}

} - {isError &&

Error loading dataset: {datasetName}

} - {isSuccess && data && ( - - - - - -

Features

-
- - { - const [featureViewName, featureName] = - joinedName.split(":"); - - return { - featureViewName, - featureName, - }; - })! - } - /> -
- - - -

Join Keys

-
- - { - return { name: joinKey }; - })! - } - /> -
-
- - - -

Properties

-
- - - - Source Feature Service - - - {data?.spec?.featureServiceName!} - - -
- - - - Created - - {toDate(data?.meta?.createdTimestamp!).toLocaleDateString( - "en-CA", - )} - - - -
-
-
- )} -
+ + {/* Left: Schema */} + + + +

Features ({features.length})

+
+ + {features.length > 0 ? ( + { + const parts = joinedName.split(":"); + return { + featureViewName: parts.length > 1 ? parts[0] : "—", + featureName: parts.length > 1 ? parts[1] : joinedName, + }; + })} + /> + ) : ( + + No features defined + + )} +
+ + + + + +

Retrieval Keys ({joinKeys.length})

+
+ + {joinKeys.length > 0 ? ( + ({ name: joinKey }))} + /> + ) : ( + + No retrieval keys defined + + )} +
+
+ + {/* Right: Properties */} + + + +

Properties

+
+ + + Origin + + + {provenance} + + + + {description && ( + <> + Description + + {description} + + + )} + + {namespace && ( + <> + Namespace + + {namespace} + + + )} + + {collection && ( + <> + Collection + + {collection} + + + )} + + Storage Type + + {storageInfo.type} + + + Storage Path + + + {storageInfo.path} + + + + {dataSources.length > 0 && ( + <> + + Data Source{dataSources.length > 1 ? "s" : ""} + + + {dataSources.map((dsName, idx) => ( + + {idx > 0 && ", "} + + {dsName} + + + ))} + + + )} + + {featureServiceName && ( + <> + + Feature Service + + + {featureServiceName} + + + )} + + Created + + {formatTimestamp(createdTs)} + + + {minEventTs && ( + <> + + Event Time Range + + + {formatTimestamp(minEventTs)} → {formatTimestamp(maxEventTs)} + + + )} + +
+ + {Object.keys(tags).length > 0 && ( + <> + + + +

Tags

+
+ + + {Object.entries(tags).map(([key, value]) => ( + + {key} + + {value as string} + + + ))} + +
+ + )} +
+
); }; -export default EntityOverviewTab; + +export default DatasetOverviewTab; diff --git a/ui/src/pages/saved-data-sets/DatasetSampleTab.tsx b/ui/src/pages/saved-data-sets/DatasetSampleTab.tsx new file mode 100644 index 00000000000..66f41ea131d --- /dev/null +++ b/ui/src/pages/saved-data-sets/DatasetSampleTab.tsx @@ -0,0 +1,190 @@ +import React, { useState, useContext, useCallback } from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiText, + EuiCallOut, + EuiButton, + EuiSpacer, + EuiPanel, + EuiBasicTable, + EuiBasicTableColumn, + EuiFieldNumber, + EuiFormRow, + EuiBadge, + EuiTitle, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; + +const DatasetSampleTab = () => { + const { datasetName, projectName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [sampleData, setSampleData] = useState<{ + columns: string[]; + rows: Record[]; + total_rows: number; + sample_size: number; + } | null>(null); + const [limit, setLimit] = useState(10); + + const fetchSample = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch( + `${registryUrl}/saved_datasets/data/${encodeURIComponent(datasetName || "")}?project=${encodeURIComponent(projectName || "")}&limit=${limit}`, + { method: "GET", ...fetchOptions }, + ); + + if (!response.ok) { + const err = await response + .json() + .catch(() => ({ detail: "Failed to load sample" })); + throw new Error(err.detail || `Request failed: ${response.status}`); + } + + const data = await response.json(); + setSampleData(data); + } catch (err: any) { + setError(err.message || "Failed to load dataset sample."); + } finally { + setLoading(false); + } + }, [registryUrl, datasetName, projectName, limit, fetchOptions]); + + const columns: EuiBasicTableColumn>[] = sampleData + ? sampleData.columns.map((col) => ({ + field: col, + name: col, + sortable: true, + truncateText: true, + render: (value: any) => { + if (value === null || value === undefined || value === "") { + return ( + + null + + ); + } + return String(value); + }, + })) + : []; + + return ( + <> + + + + + setLimit(parseInt(e.target.value) || 10)} + min={1} + max={100} + compressed + style={{ width: 80 }} + /> + + + + + Load Preview + + + {sampleData && ( + + + {sampleData.sample_size} of {sampleData.total_rows} rows + + + )} + + + + + + {error && ( + <> + +

{error}

+
+ + + )} + + {loading && ( + + + + + + Reading dataset... + + + )} + + {!loading && !sampleData && !error && ( + + + + +

Preview dataset contents

+
+
+ + + Click "Load Preview" to read and display actual rows from the + dataset's storage location. This executes a read query against + the configured offline store. + + +
+
+ )} + + {sampleData && sampleData.rows.length > 0 && ( + + )} + + {sampleData && sampleData.rows.length === 0 && ( + +

No rows found in this dataset's storage location.

+
+ )} + + ); +}; + +export default DatasetSampleTab; diff --git a/ui/src/pages/saved-data-sets/DatasetUsageTab.tsx b/ui/src/pages/saved-data-sets/DatasetUsageTab.tsx new file mode 100644 index 00000000000..01a9778213e --- /dev/null +++ b/ui/src/pages/saved-data-sets/DatasetUsageTab.tsx @@ -0,0 +1,253 @@ +import React from "react"; +import { + EuiPanel, + EuiTitle, + EuiText, + EuiSpacer, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiLoadingSpinner, + EuiIcon, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import useLoadDataset from "./useLoadDataset"; + +const DatasetUsageTab = () => { + const { datasetName } = useParams(); + + if (!datasetName) { + throw new Error("Unable to get dataset name."); + } + + const { isLoading, isSuccess, data } = useLoadDataset(datasetName); + + if (isLoading) { + return ; + } + + if (!isSuccess || !data) { + return ( + +

Could not load dataset details.

+
+ ); + } + + const name = data.spec?.name || datasetName; + + const loadCode = `from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +# Load the saved dataset +dataset = store.get_saved_dataset("${name}") + +# Convert to pandas DataFrame +df = dataset.to_df() +print(f"Loaded {len(df)} rows, {len(df.columns)} columns") +print(df.head())`; + + const loadArrowCode = `# Load as PyArrow Table (more efficient for large datasets) +dataset = store.get_saved_dataset("${name}") +table = dataset.to_arrow() +print(f"Schema: {table.schema}") +print(f"Rows: {table.num_rows}")`; + + const torchCode = `import torch +from torch.utils.data import TensorDataset, DataLoader + +dataset = store.get_saved_dataset("${name}") +df = dataset.to_df() + +# Select your feature columns and target +feature_cols = df.select_dtypes(include=["number"]).columns.tolist() +X = torch.tensor(df[feature_cols].values, dtype=torch.float32) + +# Create DataLoader +torch_dataset = TensorDataset(X) +loader = DataLoader(torch_dataset, batch_size=32, shuffle=True) + +for batch in loader: + # Your training loop here + pass`; + + const registerCode = `from feast import FeatureStore +from feast.infra.offline_stores.file_source import SavedDatasetFileStorage + +store = FeatureStore(repo_path=".") + +# Create from a historical retrieval job +entity_df = ... # Your entity DataFrame with timestamps +job = store.get_historical_features( + entity_df=entity_df, + features=[ + "feature_view:feature_1", + "feature_view:feature_2", + ], +) + +# Persist and register +dataset = store.create_saved_dataset( + from_=job, + name="${name}", + storage=SavedDatasetFileStorage(path="data/${name}.parquet"), + tags={"team": "ml", "version": "1"}, +)`; + + const validationCode = `from feast.dqm.profilers.ge_profiler import GEProfiler + +dataset = store.get_saved_dataset("${name}") + +# Create a validation reference +profiler = GEProfiler() +ref = dataset.as_reference(name="${name}_ref", profiler=profiler) + +# Apply the reference to the registry +store.apply(ref) + +# Later: validate logged features against this reference +store.validate_logged_features( + source=feature_service, + start=start_time, + end=end_time, + reference=ref, +)`; + + return ( + + {/* Load Dataset */} + + + + + + + + +

Load Dataset

+
+
+
+ +

+ Retrieve this dataset as a pandas DataFrame or PyArrow Table for + analysis or training. +

+
+ + + {loadCode} + + + + {loadArrowCode} + +
+
+ + {/* Training Integration */} + + + + + + + + +

Use for Training (PyTorch)

+
+
+
+ +

Convert the dataset into PyTorch tensors for model training.

+
+ + + {torchCode} + +
+
+ + {/* Register via SDK */} + + + + + + + + +

Register via SDK

+
+
+
+ +

+ Create this dataset programmatically from a historical feature + retrieval job. +

+
+ + + {registerCode} + +
+
+ + {/* Validation */} + + + + + + + + +

Data Validation

+
+
+
+ +

+ Use this dataset as a reference for validating feature quality and + detecting drift. +

+
+ + + {validationCode} + +
+
+
+ ); +}; + +export default DatasetUsageTab; diff --git a/ui/src/pages/saved-data-sets/DatasetsCardGrid.tsx b/ui/src/pages/saved-data-sets/DatasetsCardGrid.tsx new file mode 100644 index 00000000000..d5c69082978 --- /dev/null +++ b/ui/src/pages/saved-data-sets/DatasetsCardGrid.tsx @@ -0,0 +1,396 @@ +import React, { useState } from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiText, + EuiBadge, + EuiSpacer, + EuiButtonIcon, + EuiToolTip, + EuiIcon, + EuiCopy, + EuiLink, +} from "@elastic/eui"; +import { useNavigate, useParams } from "react-router-dom"; + +const STORAGE_TYPE_CONFIG: Record< + string, + { label: string; color: string; icon: string } +> = { + file: { label: "File", color: "#4CAF50", icon: "document" }, + bigquery: { label: "BigQuery", color: "#4285F4", icon: "storage" }, + snowflake: { label: "Snowflake", color: "#29B5E8", icon: "snowflake" }, + redshift: { label: "Redshift", color: "#205B97", icon: "compute" }, + spark: { label: "Spark", color: "#E25A1C", icon: "bolt" }, + trino: { label: "Trino", color: "#DD00A1", icon: "database" }, + athena: { label: "Athena", color: "#8C4FFF", icon: "database" }, + custom: { label: "Custom", color: "#607D8B", icon: "gear" }, + unknown: { label: "Storage", color: "#98A2B3", icon: "database" }, +}; + +function detectStorageType(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return "unknown"; + if (storage.fileStorage) return "file"; + if (storage.bigqueryStorage) return "bigquery"; + if (storage.snowflakeStorage) return "snowflake"; + if (storage.redshiftStorage) return "redshift"; + if (storage.sparkStorage) return "spark"; + if (storage.trinoStorage) return "trino"; + if (storage.athenaStorage) return "athena"; + if (storage.customStorage) return "custom"; + return "unknown"; +} + +function extractStoragePath(dataset: any): string | undefined { + const storage = dataset?.spec?.storage; + if (!storage) return undefined; + if (storage.fileStorage?.uri) return storage.fileStorage.uri; + if (storage.bigqueryStorage?.table) return storage.bigqueryStorage.table; + if (storage.snowflakeStorage?.table) return storage.snowflakeStorage.table; + if (storage.redshiftStorage?.table) return storage.redshiftStorage.table; + if (storage.sparkStorage?.path) return storage.sparkStorage.path; + if (storage.sparkStorage?.table) return storage.sparkStorage.table; + if (storage.trinoStorage?.table) return storage.trinoStorage.table; + if (storage.athenaStorage?.table) return storage.athenaStorage.table; + if (storage.customStorage?.configuration) + return storage.customStorage.configuration; + return undefined; +} + +function truncatePath(path: string, maxLen: number = 42): string { + if (path.length <= maxLen) return path; + const start = path.slice(0, 18); + const end = path.slice(-20); + return `${start}...${end}`; +} + +function getRelativeTime(date: Date): string { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return "Today"; + if (diffDays === 1) return "Yesterday"; + if (diffDays < 7) return `${diffDays}d ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; + if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`; + return `${Math.floor(diffDays / 365)}y ago`; +} + +interface DatasetCardProps { + dataset: any; + isHovered: boolean; + onHover: (name: string | null) => void; + onDelete?: (name: string) => void; +} + +const DatasetCard: React.FC = ({ + dataset, + isHovered, + onHover, + onDelete, +}) => { + const { projectName } = useParams(); + const navigate = useNavigate(); + const spec = dataset.spec || dataset; + const meta = dataset.meta || {}; + const name = spec.name || "unknown"; + const datasetProject = dataset.project || spec.project || projectName; + const features = spec.features || []; + const joinKeys = spec.joinKeys || spec.join_keys || []; + const tags = spec.tags || {}; + const featureServiceName = + spec.featureServiceName || spec.feature_service_name; + const namespace = spec.namespace || ""; + const collection = spec.collection || ""; + const description = spec.description || ""; + const createdTimestamp = meta.createdTimestamp || meta.created_timestamp; + const storagePath = extractStoragePath(dataset); + const storageType = detectStorageType(dataset); + const storageInfo = + STORAGE_TYPE_CONFIG[storageType] || STORAGE_TYPE_CONFIG.unknown; + + const formattedDate = createdTimestamp + ? new Date(createdTimestamp).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + : null; + const relativeTime = createdTimestamp + ? getRelativeTime(new Date(createdTimestamp)) + : null; + + const codeSnippet = `dataset = store.get_saved_dataset("${name}")\ndf = dataset.to_df()`; + + return ( + + onHover(name)} + onMouseLeave={() => onHover(null)} + onClick={() => navigate(`/p/${datasetProject}/data-set/${name}`)} + style={{ + height: "100%", + display: "flex", + flexDirection: "column", + transition: "all 0.2s ease", + transform: isHovered ? "translateY(-2px)" : "none", + borderTop: `3px solid ${storageInfo.color}`, + cursor: "pointer", + }} + > + {/* Header: Name + Storage badge */} + + + +

+ {name} +

+
+
+ + + {storageInfo.label} + + +
+ + {/* Description */} + {description && ( + +

{description}

+
+ )} + + {/* Namespace / Collection badges */} + {(namespace || collection) && ( + <> + + + {namespace && ( + + {namespace} + + )} + {collection && ( + + {collection} + + )} + + + )} + + + + {/* Storage path */} + {storagePath && ( + + + {" "} + + {truncatePath(storagePath)} + + + + )} + + + + {/* Metrics */} + + + + Features + + + {features.length} + + + + + Retrieval Keys + + + {joinKeys.length} + + + {featureServiceName && ( + + + Service + + + {featureServiceName} + + + )} + {spec.dataSources && spec.dataSources.length > 0 && ( + + + Source + + + {spec.dataSources.map((dsName: string, idx: number) => ( + + {idx > 0 && ", "} + { + e.stopPropagation(); + navigate(`/p/${datasetProject}/data-source/${dsName}`); + }} + > + {dsName} + + + ))} + + + )} + + + {/* Spacer to push footer */} +
+ + + + {/* Tags row */} + {Object.keys(tags).length > 0 && ( + <> + + {Object.entries(tags) + .slice(0, 3) + .map(([key, value]) => ( + + + {key}: {value as string} + + + ))} + {Object.keys(tags).length > 3 && ( + + + +{Object.keys(tags).length - 3} more + + + )} + + + + )} + + {/* Footer: Timestamp + actions */} + + + {formattedDate && ( + + + {relativeTime} + + + )} + + + + + + {(copy) => ( + + { + e.stopPropagation(); + copy(); + }} + /> + + )} + + + {onDelete && ( + + + { + e.stopPropagation(); + onDelete(name); + }} + /> + + + )} + + + + + + ); +}; + +interface DatasetsCardGridProps { + datasets: any[]; + onDelete?: (name: string) => void; +} + +const DatasetsCardGrid = ({ datasets, onDelete }: DatasetsCardGridProps) => { + const [hoveredId, setHoveredId] = useState(null); + + if (datasets.length === 0) { + return ( + +

No datasets match your search.

+
+ ); + } + + return ( + + {datasets.map((dataset: any) => { + const name = dataset.spec?.name || dataset.name || "unknown"; + return ( + + ); + })} + + ); +}; + +export default DatasetsCardGrid; diff --git a/ui/src/pages/saved-data-sets/DatasetsIndexEmptyState.tsx b/ui/src/pages/saved-data-sets/DatasetsIndexEmptyState.tsx index 9f1a34be2a5..a8f039bc28f 100644 --- a/ui/src/pages/saved-data-sets/DatasetsIndexEmptyState.tsx +++ b/ui/src/pages/saved-data-sets/DatasetsIndexEmptyState.tsx @@ -1,29 +1,54 @@ import React from "react"; -import { EuiEmptyPrompt, EuiTitle, EuiLink, EuiButton } from "@elastic/eui"; +import { + EuiEmptyPrompt, + EuiTitle, + EuiLink, + EuiButton, + EuiFlexGroup, + EuiFlexItem, +} from "@elastic/eui"; import FeastIconBlue from "../../graphics/FeastIconBlue"; -const DatasetsIndexEmptyState = () => { +interface DatasetsIndexEmptyStateProps { + onRegister?: () => void; +} + +const DatasetsIndexEmptyState = ({ + onRegister, +}: DatasetsIndexEmptyStateProps) => { return ( There are no saved datasets} + title={

Your Data Catalog is empty

} body={

- You currently do not have any saved datasets. Learn more about - creating saved datasets in Feast Docs. + The Data Catalog lets you create, link, share, and reuse curated + feature datasets for model training and validation. Link an existing + data artifact or create a new one by running a feature retrieval job.

} actions={ - { - window.open( - "https://docs.feast.dev/getting-started/concepts/dataset#creating-saved-dataset-from-historical-retrieval", - "_blank", - ); - }} - > - Open Dataset Docs - + + {onRegister && ( + + + Add to Catalog + + + )} + + { + window.open( + "https://docs.feast.dev/getting-started/concepts/dataset#creating-saved-dataset-from-historical-retrieval", + "_blank", + ); + }} + > + Open Dataset Docs + + + } footer={ <> diff --git a/ui/src/pages/saved-data-sets/DatasetsListingTable.tsx b/ui/src/pages/saved-data-sets/DatasetsListingTable.tsx index 7b73e9cd6dc..0ce44b35dc0 100644 --- a/ui/src/pages/saved-data-sets/DatasetsListingTable.tsx +++ b/ui/src/pages/saved-data-sets/DatasetsListingTable.tsx @@ -1,12 +1,24 @@ import React from "react"; -import { EuiBasicTable } from "@elastic/eui"; +import { EuiBasicTable, EuiBadge } from "@elastic/eui"; import EuiCustomLink from "../../components/EuiCustomLink"; import { useParams } from "react-router-dom"; -import { feast } from "../../protos"; -import { toDate } from "../../utils/timestamp"; interface DatasetsListingTableProps { - datasets: feast.core.ISavedDataset[]; + datasets: any[]; +} + +function detectStorageType(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return "—"; + if (storage.fileStorage) return "File"; + if (storage.bigqueryStorage) return "BigQuery"; + if (storage.snowflakeStorage) return "Snowflake"; + if (storage.redshiftStorage) return "Redshift"; + if (storage.sparkStorage) return "Spark"; + if (storage.trinoStorage) return "Trino"; + if (storage.athenaStorage) return "Athena"; + if (storage.customStorage) return "Custom"; + return "—"; } const DatasetsListingTable = ({ datasets }: DatasetsListingTableProps) => { @@ -17,27 +29,116 @@ const DatasetsListingTable = ({ datasets }: DatasetsListingTableProps) => { name: "Name", field: "spec.name", sortable: true, - render: (name: string) => { + render: (name: string, item: any) => { + const itemProject = item.project || item.spec?.project || projectName; return ( - - {name} + + {name} ); }, }, { - name: "Source Feature Service", - field: "spec.featureService", + name: "Namespace", + render: (item: any) => { + const ns = item.spec?.namespace; + return ns ? ( + {ns} + ) : ( + + ); + }, + width: "140px", + }, + { + name: "Collection", + render: (item: any) => { + const col = item.spec?.collection; + return col ? ( + {col} + ) : ( + + ); + }, + width: "140px", + }, + { + name: "Description", + render: (item: any) => { + const desc = item.spec?.description; + return desc ? ( + + {desc.length > 50 ? desc.slice(0, 50) + "…" : desc} + + ) : ( + + ); + }, + width: "200px", + }, + { + name: "Features", + render: (item: any) => (item.spec?.features || []).length, + width: "90px", + }, + { + name: "Retrieval Keys", + render: (item: any) => + (item.spec?.joinKeys || item.spec?.join_keys || []).length, + width: "90px", + }, + { + name: "Storage", + render: (item: any) => ( + {detectStorageType(item)} + ), + width: "120px", + }, + { + name: "Data Source", + render: (item: any) => { + const dsList: string[] = item.spec?.dataSources || []; + if (dsList.length === 0) return "—"; + const itemProject = item.project || item.spec?.project || projectName; + return ( + <> + {dsList.map((dsName, idx) => ( + + {idx > 0 && ", "} + + {dsName} + + + ))} + + ); + }, + }, + { + name: "Feature Service", + render: (item: any) => + item.spec?.featureServiceName || item.spec?.feature_service_name || "—", }, { name: "Created", - render: (item: feast.core.ISavedDataset) => { - return toDate(item?.meta?.createdTimestamp!).toLocaleString("en-CA")!; + render: (item: any) => { + const ts = item.meta?.createdTimestamp || item.meta?.created_timestamp; + if (!ts) return "—"; + try { + return new Date(ts).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + } catch { + return "—"; + } }, + width: "140px", }, ]; - const getRowProps = (item: feast.core.ISavedDataset) => { + const getRowProps = (item: any) => { return { "data-test-subj": `row-${item.spec?.name}`, }; diff --git a/ui/src/pages/saved-data-sets/EditDatasetModal.tsx b/ui/src/pages/saved-data-sets/EditDatasetModal.tsx new file mode 100644 index 00000000000..507827e6dd0 --- /dev/null +++ b/ui/src/pages/saved-data-sets/EditDatasetModal.tsx @@ -0,0 +1,708 @@ +import React, { useState, useMemo } from "react"; +import { + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiHorizontalRule, + EuiText, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiComboBox, + EuiComboBoxOptionOption, + EuiCheckbox, + EuiSuperSelect, + EuiSuperSelectOption, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import FormModal from "../../components/forms/FormModal"; +import TagsEditor, { TagEntry } from "../../components/forms/TagsEditor"; +import useResourceQuery, { + featureServiceListPath, + featureViewListPath, + dataSourceListPath, +} from "../../queries/useResourceQuery"; + +interface EditDatasetModalProps { + dataset: any; + onClose: () => void; + onSubmit: (data: any) => Promise; + isSubmitting: boolean; + error?: string | null; +} + +interface StorageTypeDef { + value: string; + label: string; + description: string; + placeholder: string; + helpText: string; + sourceTypeMatch: string[]; +} + +const ALL_STORAGE_TYPES: StorageTypeDef[] = [ + { + value: "file", + label: "File (Parquet / CSV)", + description: "Local or remote file path (S3, GCS, HDFS)", + placeholder: "s3://my-bucket/datasets/training_v1.parquet", + helpText: "Path to the data file accessible by the Feast server.", + sourceTypeMatch: ["BATCH_FILE"], + }, + { + value: "bigquery", + label: "BigQuery", + description: "Google BigQuery table reference", + placeholder: "project_id.dataset.table_name", + helpText: "Full BigQuery table reference: project:dataset.table", + sourceTypeMatch: ["BATCH_BIGQUERY"], + }, + { + value: "snowflake", + label: "Snowflake", + description: "Snowflake table reference", + placeholder: "database.schema.table_name", + helpText: "Snowflake table: database.schema.table", + sourceTypeMatch: ["BATCH_SNOWFLAKE"], + }, + { + value: "redshift", + label: "Redshift", + description: "Amazon Redshift table reference", + placeholder: "schema.table_name", + helpText: "Redshift table: schema.table", + sourceTypeMatch: ["BATCH_REDSHIFT"], + }, + { + value: "spark", + label: "Spark", + description: "Apache Spark table or path", + placeholder: "s3://bucket/path/ or catalog.database.table", + helpText: "Spark path or catalog table reference.", + sourceTypeMatch: ["BATCH_SPARK"], + }, + { + value: "trino", + label: "Trino", + description: "Trino table reference", + placeholder: "catalog.schema.table", + helpText: "Trino table: catalog.schema.table", + sourceTypeMatch: ["BATCH_TRINO"], + }, + { + value: "athena", + label: "AWS Athena", + description: "AWS Athena table reference", + placeholder: "database.table_name", + helpText: "Athena table reference.", + sourceTypeMatch: ["BATCH_ATHENA"], + }, + { + value: "postgres", + label: "PostgreSQL", + description: "PostgreSQL table reference", + placeholder: "schema.table_name", + helpText: + "PostgreSQL table reference. Data is read via the PostgreSQL offline store.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "clickhouse", + label: "ClickHouse", + description: "ClickHouse table reference", + placeholder: "database.table_name", + helpText: + "ClickHouse table reference. Data is read via the ClickHouse offline store.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "couchbase", + label: "Couchbase Columnar", + description: "Couchbase Columnar collection reference", + placeholder: "database.scope.collection", + helpText: + "Couchbase Columnar reference in format: database.scope.collection", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "custom", + label: "Custom", + description: "Custom storage configuration", + placeholder: '{"class": "my.CustomStorage", "config": {}}', + helpText: "Serialized configuration for a custom storage implementation.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, +]; + +function detectDataSourceTypes(dataSources: any[]): Set { + const types = new Set(); + for (const ds of dataSources) { + const dsType = ds.spec?.type || ds.type; + if (dsType != null) { + const typeName = dataSourceTypeToName(dsType); + if (typeName) types.add(typeName); + } + if (ds.spec?.fileOptions || ds.fileOptions) types.add("BATCH_FILE"); + if (ds.spec?.bigqueryOptions || ds.bigqueryOptions) + types.add("BATCH_BIGQUERY"); + if (ds.spec?.snowflakeOptions || ds.snowflakeOptions) + types.add("BATCH_SNOWFLAKE"); + if (ds.spec?.redshiftOptions || ds.redshiftOptions) + types.add("BATCH_REDSHIFT"); + if (ds.spec?.sparkOptions || ds.sparkOptions) types.add("BATCH_SPARK"); + if (ds.spec?.trinoOptions || ds.trinoOptions) types.add("BATCH_TRINO"); + if (ds.spec?.athenaOptions || ds.athenaOptions) types.add("BATCH_ATHENA"); + if (ds.spec?.customOptions || ds.customOptions) types.add("CUSTOM_SOURCE"); + const classType = + ds.spec?.dataSourceClassType || ds.dataSourceClassType || ""; + if (classType.includes("postgres")) types.add("CUSTOM_SOURCE"); + if (classType.includes("clickhouse")) types.add("CUSTOM_SOURCE"); + if (classType.includes("couchbase")) types.add("CUSTOM_SOURCE"); + } + return types; +} + +function dataSourceTypeToName(typeNum: number | string): string | null { + const map: Record = { + "1": "BATCH_FILE", + "2": "BATCH_BIGQUERY", + "3": "BATCH_REDSHIFT", + "5": "BATCH_SNOWFLAKE", + "7": "BATCH_SPARK", + "8": "BATCH_TRINO", + "9": "BATCH_ATHENA", + "6": "STREAM_KAFKA", + "10": "STREAM_KINESIS", + "4": "REQUEST_SOURCE", + "12": "PUSH_SOURCE", + "11": "CUSTOM_SOURCE", + }; + return map[String(typeNum)] || null; +} + +function detectStorageType(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return "file"; + if (storage.fileStorage) return "file"; + if (storage.bigqueryStorage) return "bigquery"; + if (storage.snowflakeStorage) return "snowflake"; + if (storage.redshiftStorage) return "redshift"; + if (storage.sparkStorage) return "spark"; + if (storage.trinoStorage) return "trino"; + if (storage.athenaStorage) return "athena"; + if (storage.customStorage) { + try { + const config = storage.customStorage.configuration || ""; + const parsed = typeof config === "string" ? JSON.parse(config) : config; + if (parsed.database && parsed.scope && parsed.collection) + return "couchbase"; + if (parsed.table) { + const classType = + dataset?.spec?.dataSourceClassType || + dataset?.dataSourceClassType || + ""; + if (classType.includes("postgres")) return "postgres"; + if (classType.includes("clickhouse")) return "clickhouse"; + } + } catch { + // fall through + } + return "custom"; + } + return "file"; +} + +function extractStoragePath(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return ""; + if (storage.fileStorage?.uri) return storage.fileStorage.uri; + if (storage.bigqueryStorage?.table) return storage.bigqueryStorage.table; + if (storage.snowflakeStorage?.table) return storage.snowflakeStorage.table; + if (storage.redshiftStorage?.table) return storage.redshiftStorage.table; + if (storage.sparkStorage?.path) return storage.sparkStorage.path; + if (storage.sparkStorage?.table) return storage.sparkStorage.table; + if (storage.trinoStorage?.table) return storage.trinoStorage.table; + if (storage.athenaStorage?.table) return storage.athenaStorage.table; + if (storage.customStorage?.configuration) + return storage.customStorage.configuration; + return ""; +} + +function extractStorageFileFormat(dataset: any): string { + const storage = dataset?.spec?.storage; + if (storage?.sparkStorage?.fileFormat) return storage.sparkStorage.fileFormat; + if (storage?.sparkStorage?.file_format) + return storage.sparkStorage.file_format; + return "parquet"; +} + +const EditDatasetModal = ({ + dataset, + onClose, + onSubmit, + isSubmitting, + error, +}: EditDatasetModalProps) => { + const { projectName } = useParams(); + const spec = dataset?.spec || {}; + const datasetName = spec.name || ""; + + // Load data sources to filter storage type options + const { data: dataSourcesRaw } = useResourceQuery({ + resourceType: "edit-modal-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources || [], + }); + + // Load feature services for dropdown + const { data: featureServicesRaw } = useResourceQuery({ + resourceType: "edit-modal-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices || [], + }); + + // Load feature views for features/join key suggestions + const { data: featureViewsRaw } = useResourceQuery({ + resourceType: "edit-modal-fv", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: (d) => d.featureViews || [], + }); + + // Derive available storage types from project's data sources + const availableStorageOptions: EuiSuperSelectOption[] = + useMemo(() => { + const currentType = detectStorageType(dataset); + + if (!dataSourcesRaw || dataSourcesRaw.length === 0) { + return ALL_STORAGE_TYPES.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: ( + <> + {st.label} + +

{st.description}

+
+ + ), + })); + } + + const detectedTypes = detectDataSourceTypes(dataSourcesRaw); + + let matched = ALL_STORAGE_TYPES.filter((st) => + st.sourceTypeMatch.some((match) => detectedTypes.has(match)), + ); + + // Always include File as a fallback + if (!matched.some((st) => st.value === "file")) { + matched = [ALL_STORAGE_TYPES[0], ...matched]; + } + + // Always include the dataset's current storage type so it's visible + if (!matched.some((st) => st.value === currentType)) { + const currentDef = ALL_STORAGE_TYPES.find( + (st) => st.value === currentType, + ); + if (currentDef) matched = [currentDef, ...matched]; + } + + return matched.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: ( + <> + {st.label} + +

{st.description}

+
+ + ), + })); + }, [dataSourcesRaw, dataset]); + + // Build feature suggestions from loaded feature views + const featureOptions: EuiComboBoxOptionOption[] = useMemo( + () => + (featureViewsRaw || []) + .filter((fv: any) => fv.type !== "labelView") + .flatMap((fv: any) => { + const fvName = fv.spec?.name || ""; + const features = fv.spec?.features || []; + return features.map((f: any) => ({ + label: `${fvName}:${f.name || f}`, + })); + }), + [featureViewsRaw], + ); + + // Build join key suggestions from feature views' entities + const joinKeyOptions: EuiComboBoxOptionOption[] = useMemo(() => { + const seen = new Set(); + (featureViewsRaw || []).forEach((fv: any) => { + const entities = fv.spec?.entities || []; + entities.forEach((e: string) => { + if (!seen.has(e)) seen.add(e); + }); + }); + return Array.from(seen).map((k) => ({ label: k })); + }, [featureViewsRaw]); + + // Build feature service options for dropdown + const featureServiceOptions: EuiComboBoxOptionOption[] = useMemo( + () => + (featureServicesRaw || []).map((fs: any) => ({ + label: fs.spec?.name || fs.name || "", + })), + [featureServicesRaw], + ); + + // Form state + const [storagePath, setStoragePath] = useState(extractStoragePath(dataset)); + const [storageType, setStorageType] = useState(detectStorageType(dataset)); + const [namespace, setNamespace] = useState(spec.namespace || ""); + const [collection, setCollection] = useState(spec.collection || ""); + const [description, setDescription] = useState(spec.description || ""); + const [storageFileFormat, setStorageFileFormat] = useState( + extractStorageFileFormat(dataset), + ); + const [featuresInput, setFeaturesInput] = useState( + (spec.features || []).map((f: string) => ({ label: f })), + ); + const [joinKeysInput, setJoinKeysInput] = useState( + (spec.joinKeys || spec.join_keys || []).map((k: string) => ({ label: k })), + ); + const [tags, setTags] = useState( + Object.entries(spec.tags || {}).map(([key, value]) => ({ + key, + value: value as string, + })), + ); + const [featureServiceName, setFeatureServiceName] = useState( + spec.featureServiceName || spec.feature_service_name || "", + ); + const [fullFeatureNames, setFullFeatureNames] = useState( + spec.fullFeatureNames || spec.full_feature_names || false, + ); + const [errors, setErrors] = useState>({}); + const [submitted, setSubmitted] = useState(false); + + // Get current storage type config + const currentStorageConfig = + ALL_STORAGE_TYPES.find((st) => st.value === storageType) || + ALL_STORAGE_TYPES[0]; + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!storagePath.trim()) { + newErrors.storagePath = "Storage path is required."; + } + + const tagKeys = tags.map((t) => t.key).filter((k) => k.trim()); + if (new Set(tagKeys).size !== tagKeys.length) { + newErrors.tags = "Tag keys must be unique."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + setSubmitted(true); + if (!validate()) return; + + const tagsObj: Record = {}; + tags.forEach(({ key, value }) => { + if (key.trim() && value.trim()) tagsObj[key.trim()] = value.trim(); + }); + + const payload = { + name: datasetName, + project: "", + features: featuresInput.map((o) => o.label), + join_keys: joinKeysInput.map((o) => o.label), + storage_path: storagePath.trim(), + storage_type: storageType, + storage_file_format: + storageType === "spark" ? storageFileFormat : undefined, + tags: tagsObj, + full_feature_names: fullFeatureNames, + feature_service_name: featureServiceName || undefined, + namespace: namespace.trim() || undefined, + collection: collection.trim() || undefined, + description: description.trim() || undefined, + allow_override: true, + }; + await onSubmit(payload); + }; + + const clearFieldError = (field: string) => { + if (submitted) { + setErrors((prev) => { + const next = { ...prev }; + delete next[field]; + return next; + }); + } + }; + + return ( + + {error && ( + <> + +

{error}

+
+ + + )} + + {/* Identity (read-only name) */} + +

Identity

+
+ + + + + + + + + + + setDescription(e.target.value)} + placeholder="e.g. Training data for driver fraud model" + /> + + + + + + + + setFeatureServiceName( + selected.length > 0 ? selected[0].label : "", + ) + } + onCreateOption={(val) => setFeatureServiceName(val)} + placeholder="Select or type..." + isClearable + /> + + + + + + + {/* Organization */} + +

Organization (optional)

+
+ + + + + + setNamespace(e.target.value)} + placeholder="e.g. fraud" + /> + + + + + setCollection(e.target.value)} + placeholder="e.g. training" + /> + + + + + + + + {/* Storage */} + +

Storage Location

+
+ + + + + Only storage types matching your project's configured data sources are + shown. + + + + + + setStorageType(value)} + fullWidth + /> + + + + { + setStoragePath(e.target.value); + clearFieldError("storagePath"); + }} + isInvalid={!!errors.storagePath} + placeholder={currentStorageConfig.placeholder} + icon={storageType === "file" ? "document" : "storage"} + fullWidth + /> + + + {storageType === "spark" && ( + + Parquet, + }, + { + value: "avro", + inputDisplay: "Avro", + dropdownDisplay: Avro, + }, + { + value: "csv", + inputDisplay: "CSV", + dropdownDisplay: CSV, + }, + { + value: "json", + inputDisplay: "JSON", + dropdownDisplay: JSON, + }, + ]} + valueOfSelected={storageFileFormat} + onChange={setStorageFileFormat} + fullWidth + /> + + )} + + + + + {/* Schema */} + +

Schema

+
+ + + + { + setFeaturesInput([...featuresInput, { label: val }]); + }} + onChange={(selected) => setFeaturesInput(selected)} + placeholder="Search or type features..." + isClearable + fullWidth + /> + + + + { + setJoinKeysInput([...joinKeysInput, { label: val }]); + }} + onChange={(selected) => setJoinKeysInput(selected)} + placeholder="Search or type join keys..." + isClearable + fullWidth + /> + + + + setFullFeatureNames(e.target.checked)} + /> + + {/* Tags */} + + setTags(newTags)} + error={errors.tags} + /> +
+ ); +}; + +export default EditDatasetModal; diff --git a/ui/src/pages/saved-data-sets/Index.tsx b/ui/src/pages/saved-data-sets/Index.tsx index c6cc81f4146..1c587d83883 100644 --- a/ui/src/pages/saved-data-sets/Index.tsx +++ b/ui/src/pages/saved-data-sets/Index.tsx @@ -1,52 +1,623 @@ -import React, { useContext } from "react"; - -import { EuiPageTemplate, EuiLoadingSpinner } from "@elastic/eui"; +import React, { + useState, + useContext, + useCallback, + useMemo, + useEffect, +} from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPageTemplate, + EuiLoadingSpinner, + EuiButton, + EuiSpacer, + EuiConfirmModal, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFieldSearch, + EuiPanel, + EuiSelect, + EuiText, + EuiButtonGroup, + EuiBadge, + EuiAccordion, + EuiIcon, + EuiToolTip, +} from "@elastic/eui"; +import { useMutation, useQuery, useQueryClient } from "react-query"; import { DatasetIcon } from "../../graphics/DatasetIcon"; - -import useLoadRegistry from "../../queries/useLoadRegistry"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; +import DatasetsCardGrid from "./DatasetsCardGrid"; import DatasetsListingTable from "./DatasetsListingTable"; +import DatasetCatalogBrowser from "./DatasetCatalogBrowser"; import DatasetsIndexEmptyState from "./DatasetsIndexEmptyState"; +import AddToCatalogModal from "./AddToCatalogModal"; +import type { RegisterDatasetPayload } from "./RegisterDatasetModal"; +import ExportButton from "../../components/ExportButton"; +import useResourceQuery, { + savedDatasetListPath, +} from "../../queries/useResourceQuery"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; +import { restPost, restDelete } from "../../queries/restApiClient"; const useLoadSavedDataSets = () => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); + const { projectName } = useParams(); + return useResourceQuery({ + resourceType: "saved-datasets-list", + project: projectName, + restPath: savedDatasetListPath(projectName), + restSelect: (d) => d.savedDatasets, + }); +}; - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.savedDatasets; +const SORT_OPTIONS = [ + { value: "name_asc", text: "Name (A → Z)" }, + { value: "name_desc", text: "Name (Z → A)" }, + { value: "created_desc", text: "Newest first" }, + { value: "created_asc", text: "Oldest first" }, + { value: "features_desc", text: "Most features" }, +]; - return { - ...registryQuery, - data, - }; -}; +const VIEW_TOGGLE_BUTTONS = [ + { id: "catalog", label: "Catalog", iconType: "folderClosed" }, + { id: "cards", label: "Cards", iconType: "grid" }, + { id: "table", label: "Table", iconType: "list" }, +]; + +function getDatasetSortValue(dataset: any, key: string): any { + const spec = dataset.spec || dataset; + const meta = dataset.meta || {}; + if (key === "name") return (spec.name || "").toLowerCase(); + if (key === "created") + return meta.createdTimestamp || meta.created_timestamp || ""; + if (key === "features") return (spec.features || []).length; + return ""; +} const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadSavedDataSets(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadSavedDataSets(); + const [showRegisterModal, setShowRegisterModal] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState("created_desc"); + const [viewMode, setViewMode] = useState("catalog"); + const [namespaceFilter, setNamespaceFilter] = useState("all"); + const [submitError, setSubmitError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + const queryClient = useQueryClient(); + + useDocumentTitle(`Data Catalog | Feast`); + + const registerMutation = useMutation( + (payload: RegisterDatasetPayload) => + restPost(registryUrl, "/saved_datasets", payload, fetchOptions), + { + onSuccess: (_: any, variables: RegisterDatasetPayload) => { + setShowRegisterModal(false); + setSubmitError(null); + setSuccessMessage( + `Dataset "${variables.name}" registered successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + }, + onError: (err: Error) => { + setSubmitError(err.message); + }, + }, + ); + + const deleteMutation = useMutation( + (name: string) => + restDelete( + registryUrl, + `/saved_datasets/${encodeURIComponent(name)}?project=${encodeURIComponent(projectName || "")}`, + fetchOptions, + ), + { + onSuccess: () => { + setDeleteTarget(null); + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + }, + }, + ); + + // Poll active jobs + const { data: jobsData } = useQuery( + ["dataset-jobs", projectName], + async () => { + const response = await fetch( + `${registryUrl}/saved_datasets/jobs?project=${encodeURIComponent(projectName || "")}`, + fetchOptions, + ); + if (!response.ok) return { jobs: [] }; + return response.json(); + }, + { + enabled: !!registryUrl, + refetchInterval: 5000, + staleTime: 3000, + }, + ); + + const activeJobs = useMemo( + () => + (jobsData?.jobs || []).filter( + (j: any) => j.status === "pending" || j.status === "running", + ), + [jobsData], + ); + + const recentJobs = useMemo( + () => (jobsData?.jobs || []).slice(0, 10), + [jobsData], + ); + + // Toast for completed jobs + useEffect(() => { + const completedJobs = (jobsData?.jobs || []).filter( + (j: any) => j.status === "completed", + ); + if (completedJobs.length > 0) { + const latest = completedJobs[0]; + if (latest.completed_at) { + const completedTime = new Date(latest.completed_at).getTime(); + const now = Date.now(); + if (now - completedTime < 10000) { + setSuccessMessage( + `Dataset "${latest.dataset_name}" created successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + } + } + } + }, [jobsData, queryClient]); + + const handleRegisterSubmit = useCallback( + async (payload: RegisterDatasetPayload) => { + payload.project = projectName || ""; + await registerMutation.mutateAsync(payload); + }, + [projectName, registerMutation], + ); + + const handleDeleteConfirm = useCallback(() => { + if (deleteTarget) { + deleteMutation.mutate(deleteTarget); + } + }, [deleteTarget, deleteMutation]); + + // Compute summary stats + const stats = useMemo(() => { + if (!data) + return { + total: 0, + totalFeatures: 0, + storageTypes: new Set(), + namespaces: [] as string[], + }; + const totalFeatures = data.reduce( + (acc: number, ds: any) => acc + (ds.spec?.features?.length || 0), + 0, + ); + const storageTypes = new Set(); + const namespacesSet = new Set(); + data.forEach((ds: any) => { + const storage = ds.spec?.storage; + if (storage?.fileStorage) storageTypes.add("File"); + else if (storage?.bigqueryStorage) storageTypes.add("BigQuery"); + else if (storage?.snowflakeStorage) storageTypes.add("Snowflake"); + else if (storage?.redshiftStorage) storageTypes.add("Redshift"); + else if (storage?.sparkStorage) storageTypes.add("Spark"); + else if (storage?.trinoStorage) storageTypes.add("Trino"); + else if (storage?.athenaStorage) storageTypes.add("Athena"); + else if (storage?.customStorage) storageTypes.add("Custom"); + const ns = ds.spec?.namespace; + if (ns) namespacesSet.add(ns); + }); + const namespaces = Array.from(namespacesSet).sort(); + return { total: data.length, totalFeatures, storageTypes, namespaces }; + }, [data]); + + // Filter and sort + const processedData = useMemo(() => { + if (!data) return []; + let filtered = data; + + // Namespace filter + if (namespaceFilter !== "all") { + if (namespaceFilter === "_none") { + filtered = filtered.filter((ds: any) => !ds.spec?.namespace); + } else { + filtered = filtered.filter( + (ds: any) => ds.spec?.namespace === namespaceFilter, + ); + } + } + + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + filtered = filtered.filter((ds: any) => { + const name = (ds.spec?.name || "").toLowerCase(); + const tags = JSON.stringify(ds.spec?.tags || {}).toLowerCase(); + const features = (ds.spec?.features || []).join(" ").toLowerCase(); + const service = ( + ds.spec?.featureServiceName || + ds.spec?.feature_service_name || + "" + ).toLowerCase(); + const ns = (ds.spec?.namespace || "").toLowerCase(); + const col = (ds.spec?.collection || "").toLowerCase(); + const desc = (ds.spec?.description || "").toLowerCase(); + return ( + name.includes(q) || + tags.includes(q) || + features.includes(q) || + service.includes(q) || + ns.includes(q) || + col.includes(q) || + desc.includes(q) + ); + }); + } - useDocumentTitle(`Saved Datasets | Feast`); + const [key, order] = sortBy.split("_"); + filtered = [...filtered].sort((a, b) => { + const aVal = getDatasetSortValue(a, key); + const bVal = getDatasetSortValue(b, key); + if (typeof aVal === "number" && typeof bVal === "number") { + return order === "asc" ? aVal - bVal : bVal - aVal; + } + const cmp = String(aVal).localeCompare(String(bVal)); + return order === "asc" ? cmp : -cmp; + }); + + return filtered; + }, [data, searchQuery, sortBy, namespaceFilter]); + + const hasData = data && data.length > 0; return ( + {activeJobs.length > 0 && ( + + + + {activeJobs.length} active + + + + )} + + { + setSubmitError(null); + setShowRegisterModal(true); + }} + > + Add to Catalog + + + , + , + ]} /> + {/* Success toast */} + {successMessage && ( + <> + + + + )} + + {/* Active Jobs Panel */} + {recentJobs.length > 0 && ( + <> + + + + + + + Recent Activity + {activeJobs.length > 0 && ( + + {activeJobs.length} running + + )} + + + + } + paddingSize="s" + initialIsOpen={activeJobs.length > 0} + > + + {recentJobs.map((job: any) => ( + + + {job.status === "running" || job.status === "pending" ? ( + + ) : job.status === "completed" ? ( + + ) : ( + + )} + + + + {job.dataset_name} + + + + + {job.status} + + + + ))} + + + + + )} + + {/* Delete error */} + {deleteMutation.isError && ( + <> + +

{(deleteMutation.error as Error)?.message}

+
+ + + )} + {isLoading && ( -

- Loading -

+ + + + + + Loading datasets... + + + )} + + {isPermissionDenied && ( + +

You do not have permission to view saved datasets.

+
+ )} + {isError && !isPermissionDenied && ( + +

+ We encountered an error while loading datasets. Please check that + the registry server is running. +

+
+ )} + + {isSuccess && hasData && ( + <> + {/* View mode toggle — always visible */} + + + + + + {stats.total} datasets + {stats.namespaces.length > 0 && ( + <> + {" "} + across {stats.namespaces.length}{" "} + namespaces + + )} + + + + + + setViewMode(id)} + isIconOnly + buttonSize="compressed" + /> + + + + + + {/* Search + Namespace Filter + Sort — shared toolbar */} + + + setSearchQuery(e.target.value)} + isClearable + fullWidth + /> + + {stats.namespaces.length > 0 && ( + + ({ + value: ns, + text: ns, + })), + ]} + value={namespaceFilter} + onChange={(e) => setNamespaceFilter(e.target.value)} + compressed + prepend="Namespace" + /> + + )} + + setSortBy(e.target.value)} + compressed + prepend="Sort" + /> + + + + + + {/* Results count when filtering */} + {(searchQuery.trim() || namespaceFilter !== "all") && ( + <> + + Showing {processedData.length} of {data.length} datasets + {namespaceFilter !== "all" && namespaceFilter !== "_none" && ( + <> + {" "} + in namespace {namespaceFilter} + + )} + {namespaceFilter === "_none" && <> with no namespace} + + + + )} + + {/* Catalog (hierarchical) view */} + {viewMode === "catalog" && ( + setDeleteTarget(name)} + /> + )} + + {/* Flat views (cards / table) */} + {viewMode === "cards" && ( + setDeleteTarget(name)} + /> + )} + {viewMode === "table" && ( + + )} + + )} + + {isSuccess && !hasData && ( + { + setSubmitError(null); + setShowRegisterModal(true); + }} + /> )} - {isError &&

We encountered an error while loading.

} - {isSuccess && data && } - {isSuccess && !data && }
+ + {showRegisterModal && ( + setShowRegisterModal(false)} + onLinkSubmit={handleRegisterSubmit} + isLinkSubmitting={registerMutation.isLoading} + linkError={submitError} + /> + )} + + {deleteTarget && ( + setDeleteTarget(null)} + onConfirm={handleDeleteConfirm} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteMutation.isLoading} + > +

+ Are you sure you want to delete {deleteTarget}? +

+

+ + This removes the dataset metadata from the registry. The + underlying data at the storage location will not be deleted. + +

+
+ )}
); }; diff --git a/ui/src/pages/saved-data-sets/JobStatusPanel.tsx b/ui/src/pages/saved-data-sets/JobStatusPanel.tsx new file mode 100644 index 00000000000..bf6ed49fe06 --- /dev/null +++ b/ui/src/pages/saved-data-sets/JobStatusPanel.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useState, useContext, useCallback } from "react"; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiText, + EuiCallOut, + EuiButton, + EuiButtonEmpty, + EuiSpacer, + EuiProgress, +} from "@elastic/eui"; +import { useNavigate, useParams } from "react-router-dom"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; + +interface JobStatusPanelProps { + jobId: string; + datasetName: string; + onComplete?: () => void; + onClose: () => void; + onRetry?: () => void; +} + +interface JobStatus { + job_id: string; + status: "pending" | "running" | "completed" | "failed"; + dataset_name?: string; + error?: string; + created_at?: string; + completed_at?: string; +} + +const JobStatusPanel = ({ + jobId, + datasetName, + onComplete, + onClose, + onRetry, +}: JobStatusPanelProps) => { + const { projectName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + const navigate = useNavigate(); + + const [status, setStatus] = useState({ + job_id: jobId, + status: "pending", + dataset_name: datasetName, + }); + const [pollError, setPollError] = useState(null); + + const pollStatus = useCallback(async () => { + try { + const response = await fetch( + `${registryUrl}/saved_datasets/jobs/${encodeURIComponent(jobId)}`, + { method: "GET", ...fetchOptions }, + ); + if (!response.ok) { + const err = await response + .json() + .catch(() => ({ detail: "Unknown error" })); + throw new Error( + err.detail || `Status check failed: ${response.status}`, + ); + } + const data: JobStatus = await response.json(); + setStatus(data); + + if (data.status === "completed" && onComplete) { + onComplete(); + } + } catch (err: any) { + setPollError(err.message); + } + }, [jobId, registryUrl, fetchOptions, onComplete]); + + useEffect(() => { + const interval = setInterval(() => { + if (status.status === "pending" || status.status === "running") { + pollStatus(); + } + }, 3000); + + pollStatus(); + + return () => clearInterval(interval); + }, [pollStatus, status.status]); + + const isRunning = status.status === "pending" || status.status === "running"; + const isCompleted = status.status === "completed"; + const isFailed = status.status === "failed"; + + return ( + + {isRunning && ( + <> + + + + + + +

Creating dataset: {datasetName}

+

+ + Running feature retrieval and persisting results... + +

+
+
+
+ + + + + Job ID: {jobId} | Status: {status.status} + + + + Close (job continues in background) + + + )} + + {isCompleted && ( + <> + +

+ {datasetName} has been created and registered in + the catalog. +

+
+ + + + { + onClose(); + navigate(`/p/${projectName}/data-set/${datasetName}`); + }} + > + View Dataset + + + + Close + + + + )} + + {isFailed && ( + <> + +

+ {status.error || + "An unknown error occurred during dataset creation."} +

+
+ + + Job ID: {jobId} + + + + {onRetry && ( + + + Back to Form + + + )} + + Close + + + + )} + + {pollError && ( + <> + + +

{pollError}

+
+ + )} +
+ ); +}; + +export default JobStatusPanel; diff --git a/ui/src/pages/saved-data-sets/RegisterDatasetModal.tsx b/ui/src/pages/saved-data-sets/RegisterDatasetModal.tsx new file mode 100644 index 00000000000..e7543ddadf1 --- /dev/null +++ b/ui/src/pages/saved-data-sets/RegisterDatasetModal.tsx @@ -0,0 +1,702 @@ +import React, { useState, useMemo } from "react"; +import { + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiHorizontalRule, + EuiText, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiComboBox, + EuiComboBoxOptionOption, + EuiCheckbox, + EuiSuperSelect, + EuiSuperSelectOption, + EuiButton, + EuiButtonEmpty, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import FormModal from "../../components/forms/FormModal"; +import TagsEditor, { TagEntry } from "../../components/forms/TagsEditor"; +import useResourceQuery, { + featureServiceListPath, + featureViewListPath, + dataSourceListPath, +} from "../../queries/useResourceQuery"; + +export interface RegisterDatasetPayload { + name: string; + project: string; + features: string[]; + join_keys: string[]; + storage_path: string; + storage_type: string; + storage_file_format?: string; + tags: Record; + full_feature_names: boolean; + feature_service_name?: string; + namespace?: string; + collection?: string; + description?: string; +} + +interface RegisterDatasetModalProps { + onClose: () => void; + onSubmit: (data: RegisterDatasetPayload) => Promise; + isSubmitting: boolean; + error?: string | null; + embedded?: boolean; +} + +interface StorageTypeDefinition { + value: string; + label: string; + description: string; + placeholder: string; + helpText: string; + sourceTypeMatch: string[]; +} + +const ALL_STORAGE_TYPES: StorageTypeDefinition[] = [ + { + value: "file", + label: "File (Parquet / CSV)", + description: "Local or remote file path (S3, GCS, HDFS)", + placeholder: "s3://my-bucket/datasets/training_v1.parquet", + helpText: + "Path to the data file accessible by the Feast server (e.g. s3://bucket/path/data.parquet, gs://bucket/data.csv).", + sourceTypeMatch: ["BATCH_FILE"], + }, + { + value: "bigquery", + label: "BigQuery", + description: "Google BigQuery table reference", + placeholder: "project_id.dataset.table_name", + helpText: "Full BigQuery table reference: project:dataset.table", + sourceTypeMatch: ["BATCH_BIGQUERY"], + }, + { + value: "snowflake", + label: "Snowflake", + description: "Snowflake table reference", + placeholder: "database.schema.table_name", + helpText: "Snowflake table: database.schema.table", + sourceTypeMatch: ["BATCH_SNOWFLAKE"], + }, + { + value: "redshift", + label: "Redshift", + description: "Amazon Redshift table reference", + placeholder: "schema.table_name", + helpText: "Redshift table: schema.table", + sourceTypeMatch: ["BATCH_REDSHIFT"], + }, + { + value: "spark", + label: "Spark", + description: "Apache Spark table or path", + placeholder: "s3://bucket/path/ or catalog.database.table", + helpText: + "Spark path or catalog table reference (the data will be read via Spark).", + sourceTypeMatch: ["BATCH_SPARK"], + }, + { + value: "trino", + label: "Trino", + description: "Trino table reference", + placeholder: "catalog.schema.table", + helpText: "Trino table: catalog.schema.table", + sourceTypeMatch: ["BATCH_TRINO"], + }, + { + value: "athena", + label: "AWS Athena", + description: "AWS Athena table reference", + placeholder: "database.table_name", + helpText: "Athena table reference. Data is queried via Athena.", + sourceTypeMatch: ["BATCH_ATHENA"], + }, + { + value: "postgres", + label: "PostgreSQL", + description: "PostgreSQL table reference", + placeholder: "schema.table_name", + helpText: + "PostgreSQL table reference. Data is read via the PostgreSQL offline store.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "clickhouse", + label: "ClickHouse", + description: "ClickHouse table reference", + placeholder: "database.table_name", + helpText: + "ClickHouse table reference. Data is read via the ClickHouse offline store.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "couchbase", + label: "Couchbase Columnar", + description: "Couchbase Columnar collection reference", + placeholder: "database.scope.collection", + helpText: + "Couchbase Columnar reference in format: database.scope.collection", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "custom", + label: "Custom", + description: "Custom storage configuration", + placeholder: '{"class": "my.CustomStorage", "config": {}}', + helpText: "Serialized configuration for a custom storage implementation.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, +]; + +function detectDataSourceTypes(dataSources: any[]): Set { + const types = new Set(); + for (const ds of dataSources) { + const dsType = ds.spec?.type || ds.type; + if (dsType != null) { + const typeName = dataSourceTypeToName(dsType); + if (typeName) types.add(typeName); + } + if (ds.spec?.fileOptions || ds.fileOptions) types.add("BATCH_FILE"); + if (ds.spec?.bigqueryOptions || ds.bigqueryOptions) + types.add("BATCH_BIGQUERY"); + if (ds.spec?.snowflakeOptions || ds.snowflakeOptions) + types.add("BATCH_SNOWFLAKE"); + if (ds.spec?.redshiftOptions || ds.redshiftOptions) + types.add("BATCH_REDSHIFT"); + if (ds.spec?.sparkOptions || ds.sparkOptions) types.add("BATCH_SPARK"); + if (ds.spec?.trinoOptions || ds.trinoOptions) types.add("BATCH_TRINO"); + if (ds.spec?.athenaOptions || ds.athenaOptions) types.add("BATCH_ATHENA"); + if (ds.spec?.customOptions || ds.customOptions) types.add("CUSTOM_SOURCE"); + const classType = + ds.spec?.dataSourceClassType || ds.dataSourceClassType || ""; + if (classType.includes("postgres")) types.add("CUSTOM_SOURCE"); + if (classType.includes("clickhouse")) types.add("CUSTOM_SOURCE"); + if (classType.includes("couchbase")) types.add("CUSTOM_SOURCE"); + } + return types; +} + +function dataSourceTypeToName(typeNum: number | string): string | null { + const map: Record = { + "1": "BATCH_FILE", + "2": "BATCH_BIGQUERY", + "3": "BATCH_REDSHIFT", + "5": "BATCH_SNOWFLAKE", + "7": "BATCH_SPARK", + "8": "BATCH_TRINO", + "9": "BATCH_ATHENA", + "6": "STREAM_KAFKA", + "10": "STREAM_KINESIS", + "4": "REQUEST_SOURCE", + "12": "PUSH_SOURCE", + "11": "CUSTOM_SOURCE", + }; + return map[String(typeNum)] || null; +} + +const RegisterDatasetModal = ({ + onClose, + onSubmit, + isSubmitting, + error, + embedded = false, +}: RegisterDatasetModalProps) => { + const { projectName } = useParams(); + + const { data: featureServicesRaw } = useResourceQuery({ + resourceType: "register-modal-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices || [], + }); + + const { data: featureViewsRaw } = useResourceQuery({ + resourceType: "register-modal-fv", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: (d) => d.featureViews || [], + }); + + const { data: dataSourcesRaw } = useResourceQuery({ + resourceType: "register-modal-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources || [], + }); + + // Derive available storage types from project's data sources + const availableStorageOptions: EuiSuperSelectOption[] = + useMemo(() => { + if (!dataSourcesRaw || dataSourcesRaw.length === 0) { + // Fallback: show all storage types if no data sources loaded yet + return ALL_STORAGE_TYPES.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: ( + <> + {st.label} + +

{st.description}

+
+ + ), + })); + } + + const detectedTypes = detectDataSourceTypes(dataSourcesRaw); + + const matched = ALL_STORAGE_TYPES.filter((st) => + st.sourceTypeMatch.some((match) => detectedTypes.has(match)), + ); + + // Always include File as a fallback (datasets can be stored as standalone files) + const hasFile = matched.some((st) => st.value === "file"); + const result = hasFile ? matched : [ALL_STORAGE_TYPES[0], ...matched]; + + return result.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: ( + <> + {st.label} + +

{st.description}

+
+ + ), + })); + }, [dataSourcesRaw]); + + // Form state + const [name, setName] = useState(""); + const [namespace, setNamespace] = useState(""); + const [collection, setCollection] = useState(""); + const [description, setDescription] = useState(""); + const [storagePath, setStoragePath] = useState(""); + const [storageType, setStorageType] = useState("file"); + const [storageFileFormat, setStorageFileFormat] = useState("parquet"); + const [featuresInput, setFeaturesInput] = useState( + [], + ); + const [joinKeysInput, setJoinKeysInput] = useState( + [], + ); + const [tags, setTags] = useState([]); + const [featureServiceName, setFeatureServiceName] = useState(""); + const [fullFeatureNames, setFullFeatureNames] = useState(false); + const [errors, setErrors] = useState>({}); + const [submitted, setSubmitted] = useState(false); + + // Get current storage type config + const currentStorageConfig = + ALL_STORAGE_TYPES.find((st) => st.value === storageType) || + ALL_STORAGE_TYPES[0]; + + // Build suggestions from live data + const featureOptions: EuiComboBoxOptionOption[] = (featureViewsRaw || []) + .filter((fv: any) => fv.type !== "labelView") + .flatMap((fv: any) => { + const fvName = fv.spec?.name || ""; + const features = fv.spec?.features || []; + return features.map((f: any) => ({ + label: `${fvName}:${f.name || f}`, + })); + }); + + const joinKeyOptions: EuiComboBoxOptionOption[] = (() => { + const seen = new Set(); + (featureViewsRaw || []).forEach((fv: any) => { + const entities = fv.spec?.entities || []; + entities.forEach((e: string) => { + if (!seen.has(e)) seen.add(e); + }); + }); + return Array.from(seen).map((k) => ({ label: k })); + })(); + + const featureServiceOptions: EuiComboBoxOptionOption[] = ( + featureServicesRaw || [] + ).map((fs: any) => ({ + label: fs.spec?.name || fs.name || "", + })); + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!name.trim()) { + newErrors.name = "Dataset name is required."; + } else if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(name.trim())) { + newErrors.name = + "Must start with a letter or underscore, and contain only letters, numbers, underscores, and hyphens."; + } + + if (!storagePath.trim()) { + newErrors.storagePath = + "Storage path is required. Provide a file path or table reference."; + } else if (storageType === "file") { + if ( + !/^(s3|gs|gcs|hdfs|abfs|file):\/\/\S+$/.test(storagePath.trim()) && + !storagePath.trim().startsWith("/") + ) { + newErrors.storagePath = + "Should be a valid URI (s3://, gs://, hdfs://, file://) or absolute path."; + } + } + + const tagKeys = tags.map((t) => t.key).filter((k) => k.trim()); + if (new Set(tagKeys).size !== tagKeys.length) { + newErrors.tags = "Tag keys must be unique."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + setSubmitted(true); + if (!validate()) return; + + const tagsObj: Record = {}; + tags.forEach(({ key, value }) => { + if (key.trim() && value.trim()) tagsObj[key.trim()] = value.trim(); + }); + + const payload: RegisterDatasetPayload = { + name: name.trim(), + project: projectName || "", + features: featuresInput.map((o) => o.label), + join_keys: joinKeysInput.map((o) => o.label), + storage_path: storagePath.trim(), + storage_type: storageType, + storage_file_format: + storageType === "spark" ? storageFileFormat : undefined, + tags: tagsObj, + full_feature_names: fullFeatureNames, + feature_service_name: featureServiceName || undefined, + namespace: namespace.trim() || undefined, + collection: collection.trim() || undefined, + description: description.trim() || undefined, + }; + await onSubmit(payload); + }; + + const clearFieldError = (field: string) => { + if (submitted) { + setErrors((prev) => { + const next = { ...prev }; + delete next[field]; + return next; + }); + } + }; + + const formContent = ( + <> + {error && ( + <> + +

{error}

+
+ + + )} + + {/* Section: Identity */} + +

Identity

+
+ + + + + + { + setName(e.target.value); + clearFieldError("name"); + }} + isInvalid={!!errors.name} + placeholder="e.g. driver_training_v1" + autoFocus + /> + + + + + setDescription(e.target.value)} + placeholder="e.g. Training data for driver fraud model" + /> + + + + + + setFeatureServiceName( + selected.length > 0 ? selected[0].label : "", + ) + } + onCreateOption={(val) => setFeatureServiceName(val)} + placeholder="Select or type..." + isClearable + /> + + + + + + + {/* Section: Organization */} + +

Organization (optional)

+
+ + + + + Group datasets into namespaces and collections for hierarchical + organization. Leave empty to keep the dataset at the top level. + + + + + + + + setNamespace(e.target.value)} + placeholder="e.g. fraud" + /> + + + + + setCollection(e.target.value)} + placeholder="e.g. training" + /> + + + + + + + + {/* Section: Storage */} + +

Storage Location

+
+ + + + + Point to where the dataset data already exists. Only storage types + matching your project's configured data sources are shown. + + + + + + setStorageType(value)} + fullWidth + /> + + + + { + setStoragePath(e.target.value); + clearFieldError("storagePath"); + }} + isInvalid={!!errors.storagePath} + placeholder={currentStorageConfig.placeholder} + icon={storageType === "file" ? "document" : "storage"} + fullWidth + /> + + + {storageType === "spark" && ( + + Parquet, + }, + { + value: "avro", + inputDisplay: "Avro", + dropdownDisplay: Avro, + }, + { + value: "csv", + inputDisplay: "CSV", + dropdownDisplay: CSV, + }, + { + value: "json", + inputDisplay: "JSON", + dropdownDisplay: JSON, + }, + ]} + valueOfSelected={storageFileFormat} + onChange={setStorageFileFormat} + fullWidth + /> + + )} + + + + + {/* Section: Schema */} + +

Schema

+
+ + + + { + setFeaturesInput([...featuresInput, { label: val }]); + }} + onChange={(selected) => setFeaturesInput(selected)} + placeholder="Search or type features..." + isClearable + fullWidth + /> + + + + { + setJoinKeysInput([...joinKeysInput, { label: val }]); + }} + onChange={(selected) => setJoinKeysInput(selected)} + placeholder="Search or type join keys..." + isClearable + fullWidth + /> + + + + setFullFeatureNames(e.target.checked)} + /> + + {/* Section: Tags */} + + setTags(newTags)} + error={errors.tags} + /> + + ); + + if (embedded) { + return ( +
+ {formContent} + + + + Cancel + + + + Link Existing Dataset + + + +
+ ); + } + + return ( + + {formContent} + + ); +}; + +export default RegisterDatasetModal; +export type { RegisterDatasetModalProps }; diff --git a/ui/src/pages/saved-data-sets/useLoadDataset.ts b/ui/src/pages/saved-data-sets/useLoadDataset.ts index 40f8a8ebd48..c560f7f9eb6 100644 --- a/ui/src/pages/saved-data-sets/useLoadDataset.ts +++ b/ui/src/pages/saved-data-sets/useLoadDataset.ts @@ -1,22 +1,18 @@ -import { useContext } from "react"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import { useParams } from "react-router-dom"; +import useResourceQuery, { + savedDatasetDetailPath, +} from "../../queries/useResourceQuery"; -const useLoadEntity = (entityName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); +const useLoadDataset = (datasetName: string) => { + const { projectName } = useParams(); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.savedDatasets?.find( - (fv) => fv.spec?.name === entityName, - ); - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: `saved-dataset:${datasetName}`, + project: projectName, + restPath: savedDatasetDetailPath(datasetName, projectName || ""), + restSelect: (d) => d, + enabled: !!datasetName, + }); }; -export default useLoadEntity; +export default useLoadDataset; diff --git a/ui/src/parsers/mergedFVTypes.ts b/ui/src/parsers/mergedFVTypes.ts index 1c6b759be1e..0c65dea54f6 100644 --- a/ui/src/parsers/mergedFVTypes.ts +++ b/ui/src/parsers/mergedFVTypes.ts @@ -4,6 +4,7 @@ enum FEAST_FV_TYPES { regular = "regular", ondemand = "ondemand", stream = "stream", + label = "label", } interface regularFVInterface { @@ -27,7 +28,18 @@ interface SFVInterface { object: feast.core.IStreamFeatureView; } -type genericFVType = regularFVInterface | ODFVInterface | SFVInterface; +interface LabelViewInterface { + name: string; + type: FEAST_FV_TYPES.label; + features: feast.core.IFeatureSpecV2[]; + object: any; +} + +type genericFVType = + | regularFVInterface + | ODFVInterface + | SFVInterface + | LabelViewInterface; const mergedFVTypes = (objects: feast.core.Registry) => { const mergedFVMap: Record = {}; @@ -75,4 +87,10 @@ const mergedFVTypes = (objects: feast.core.Registry) => { export default mergedFVTypes; export { FEAST_FV_TYPES }; -export type { genericFVType, regularFVInterface, ODFVInterface, SFVInterface }; +export type { + genericFVType, + regularFVInterface, + ODFVInterface, + SFVInterface, + LabelViewInterface, +}; diff --git a/ui/src/parsers/parseEntityRelationships.ts b/ui/src/parsers/parseEntityRelationships.ts index 579374e30ff..b1a014044a0 100644 --- a/ui/src/parsers/parseEntityRelationships.ts +++ b/ui/src/parsers/parseEntityRelationships.ts @@ -14,12 +14,21 @@ interface EntityRelation { const parseEntityRelationships = (objects: feast.core.Registry) => { const links: EntityRelation[] = []; + const labelViewNames = new Set( + ((objects as any).labelViews || []).map((lv: any) => lv.spec?.name), + ); + objects.featureServices?.forEach((fs) => { - fs.spec?.features!.forEach((feature) => { + fs.spec?.features!.forEach((feature: any) => { + const viewName = feature?.featureViewName!; + const isLabelView = + feature?.viewType === "labelView" || labelViewNames.has(viewName); links.push({ source: { - type: FEAST_FCO_TYPES["featureView"], - name: feature?.featureViewName!, + type: isLabelView + ? FEAST_FCO_TYPES["labelView"] + : FEAST_FCO_TYPES["featureView"], + name: viewName, }, target: { type: FEAST_FCO_TYPES["featureService"], @@ -134,6 +143,58 @@ const parseEntityRelationships = (objects: feast.core.Registry) => { }); }); + (objects as any).labelViews?.forEach((lv: any) => { + lv.spec?.entities?.forEach((ent: string) => { + links.push({ + source: { + type: FEAST_FCO_TYPES["entity"], + name: ent, + }, + target: { + type: FEAST_FCO_TYPES["labelView"], + name: lv.spec?.name!, + }, + }); + }); + + if (lv.spec?.source?.name) { + links.push({ + source: { + type: FEAST_FCO_TYPES["dataSource"], + name: lv.spec.source.name, + }, + target: { + type: FEAST_FCO_TYPES["labelView"], + name: lv.spec?.name!, + }, + }); + } + if (lv.spec?.source?.batchSource?.name) { + links.push({ + source: { + type: FEAST_FCO_TYPES["dataSource"], + name: lv.spec.source.batchSource.name, + }, + target: { + type: FEAST_FCO_TYPES["labelView"], + name: lv.spec?.name!, + }, + }); + } + if (lv.spec?.batchSource?.name) { + links.push({ + source: { + type: FEAST_FCO_TYPES["dataSource"], + name: lv.spec.batchSource.name, + }, + target: { + type: FEAST_FCO_TYPES["labelView"], + name: lv.spec?.name!, + }, + }); + } + }); + return links; }; diff --git a/ui/src/parsers/types.ts b/ui/src/parsers/types.ts index 1e515f23f34..fc9f88d045f 100644 --- a/ui/src/parsers/types.ts +++ b/ui/src/parsers/types.ts @@ -3,6 +3,11 @@ enum FEAST_FCO_TYPES { entity = "entity", featureView = "featureView", featureService = "featureService", + labelView = "labelView", + mlflowRun = "mlflowRun", + mlflowModel = "mlflowModel", + openlineageJob = "openlineageJob", + openlineageDataset = "openlineageDataset", } export { FEAST_FCO_TYPES }; diff --git a/ui/src/queries/mutations/useDataSourceMutations.ts b/ui/src/queries/mutations/useDataSourceMutations.ts new file mode 100644 index 00000000000..a59313bf7a7 --- /dev/null +++ b/ui/src/queries/mutations/useDataSourceMutations.ts @@ -0,0 +1,105 @@ +import { useMutation, useQueryClient } from "react-query"; + +interface ApplyDataSourcePayload { + name: string; + project: string; + type?: number; + timestamp_field?: string; + created_timestamp_column?: string; + description?: string; + tags?: Record; + owner?: string; + file_options?: { uri: string }; + bigquery_options?: { table: string; query: string }; + snowflake_options?: { table: string; database: string; schema_: string }; + redshift_options?: { table: string; database: string; schema_: string }; + kafka_options?: { kafka_bootstrap_servers: string; topic: string }; + spark_options?: { table: string; path: string }; + custom_options?: { + configuration?: string; + class_name?: string; + config?: string; + }; + data_source_class_type?: string; +} + +interface DeleteDataSourcePayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const applyDataSource = async ( + payload: ApplyDataSourcePayload, +): Promise => { + const response = await fetch(`${API_BASE}/data_sources`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to apply data source: ${response.status}`, + ); + } + + return response.json(); +}; + +const deleteDataSource = async ( + payload: DeleteDataSourcePayload, +): Promise => { + const response = await fetch( + `${API_BASE}/data_sources/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + { method: "DELETE" }, + ); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to delete data source: ${response.status}`, + ); + } + + return response.json(); +}; + +const useApplyDataSource = () => { + const queryClient = useQueryClient(); + + return useMutation(applyDataSource, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["data-sources-rest"]); + queryClient.invalidateQueries(["data-source-rest"]); + }, + }); +}; + +const useDeleteDataSource = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteDataSource, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["data-sources-rest"]); + queryClient.invalidateQueries(["data-source-rest"]); + }, + }); +}; + +export { useApplyDataSource, useDeleteDataSource }; +export type { ApplyDataSourcePayload, DeleteDataSourcePayload }; diff --git a/ui/src/queries/mutations/useEntityMutations.ts b/ui/src/queries/mutations/useEntityMutations.ts new file mode 100644 index 00000000000..659c367558e --- /dev/null +++ b/ui/src/queries/mutations/useEntityMutations.ts @@ -0,0 +1,92 @@ +import { useMutation, useQueryClient } from "react-query"; + +interface ApplyEntityPayload { + name: string; + project: string; + join_key?: string; + value_type?: number; + description?: string; + tags?: Record; + owner?: string; +} + +interface DeleteEntityPayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const applyEntity = async ( + payload: ApplyEntityPayload, +): Promise => { + const response = await fetch(`${API_BASE}/entities`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to apply entity: ${response.status}`, + ); + } + + return response.json(); +}; + +const deleteEntity = async ( + payload: DeleteEntityPayload, +): Promise => { + const response = await fetch( + `${API_BASE}/entities/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + { method: "DELETE" }, + ); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to delete entity: ${response.status}`, + ); + } + + return response.json(); +}; + +const useApplyEntity = () => { + const queryClient = useQueryClient(); + + return useMutation(applyEntity, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["entities-rest"]); + queryClient.invalidateQueries(["entity-rest"]); + }, + }); +}; + +const useDeleteEntity = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteEntity, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["entities-rest"]); + queryClient.invalidateQueries(["entity-rest"]); + }, + }); +}; + +export { useApplyEntity, useDeleteEntity }; +export type { ApplyEntityPayload, DeleteEntityPayload }; diff --git a/ui/src/queries/mutations/useFeatureServiceMutations.ts b/ui/src/queries/mutations/useFeatureServiceMutations.ts new file mode 100644 index 00000000000..85bd3f4fd44 --- /dev/null +++ b/ui/src/queries/mutations/useFeatureServiceMutations.ts @@ -0,0 +1,100 @@ +import { useMutation, useQueryClient } from "react-query"; + +interface FeatureViewProjectionPayload { + feature_view_name: string; + feature_names?: string[]; +} + +interface ApplyFeatureServicePayload { + name: string; + project: string; + features: FeatureViewProjectionPayload[]; + description?: string; + tags?: Record; + owner?: string; +} + +interface DeleteFeatureServicePayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const applyFeatureService = async ( + payload: ApplyFeatureServicePayload, +): Promise => { + const response = await fetch(`${API_BASE}/feature_services`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to apply feature service: ${response.status}`, + ); + } + + return response.json(); +}; + +const deleteFeatureService = async ( + payload: DeleteFeatureServicePayload, +): Promise => { + const response = await fetch( + `${API_BASE}/feature_services/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + { method: "DELETE" }, + ); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to delete feature service: ${response.status}`, + ); + } + + return response.json(); +}; + +const useApplyFeatureService = () => { + const queryClient = useQueryClient(); + + return useMutation(applyFeatureService, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["feature-services-rest"]); + queryClient.invalidateQueries(["feature-service-rest"]); + }, + }); +}; + +const useDeleteFeatureService = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteFeatureService, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["feature-services-rest"]); + queryClient.invalidateQueries(["feature-service-rest"]); + }, + }); +}; + +export { useApplyFeatureService, useDeleteFeatureService }; +export type { + ApplyFeatureServicePayload, + DeleteFeatureServicePayload, + FeatureViewProjectionPayload, +}; diff --git a/ui/src/queries/mutations/useFeatureViewMutations.ts b/ui/src/queries/mutations/useFeatureViewMutations.ts new file mode 100644 index 00000000000..6ca31208093 --- /dev/null +++ b/ui/src/queries/mutations/useFeatureViewMutations.ts @@ -0,0 +1,101 @@ +import { useMutation, useQueryClient } from "react-query"; + +interface FeaturePayload { + name: string; + value_type: number; + description?: string; +} + +interface ApplyFeatureViewPayload { + name: string; + project: string; + entities?: string[]; + features?: FeaturePayload[]; + batch_source?: string; + ttl_seconds?: number; + online?: boolean; + description?: string; + tags?: Record; + owner?: string; +} + +interface DeleteFeatureViewPayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const applyFeatureView = async ( + payload: ApplyFeatureViewPayload, +): Promise => { + const response = await fetch(`${API_BASE}/feature_views`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to apply feature view: ${response.status}`, + ); + } + + return response.json(); +}; + +const deleteFeatureView = async ( + payload: DeleteFeatureViewPayload, +): Promise => { + const response = await fetch( + `${API_BASE}/feature_views/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + { method: "DELETE" }, + ); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to delete feature view: ${response.status}`, + ); + } + + return response.json(); +}; + +const useApplyFeatureView = () => { + const queryClient = useQueryClient(); + + return useMutation(applyFeatureView, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["feature-views-rest"]); + queryClient.invalidateQueries(["feature-view-rest"]); + }, + }); +}; + +const useDeleteFeatureView = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteFeatureView, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["feature-views-rest"]); + queryClient.invalidateQueries(["feature-view-rest"]); + }, + }); +}; + +export { useApplyFeatureView, useDeleteFeatureView }; +export type { ApplyFeatureViewPayload, DeleteFeatureViewPayload }; diff --git a/ui/src/queries/mutations/usePermissionMutations.ts b/ui/src/queries/mutations/usePermissionMutations.ts new file mode 100644 index 00000000000..aa3961c8ff9 --- /dev/null +++ b/ui/src/queries/mutations/usePermissionMutations.ts @@ -0,0 +1,74 @@ +import { useMutation, useQueryClient } from "react-query"; +import { restPost, restDelete } from "../restApiClient"; + +interface PolicyPayload { + role_based_policy?: { roles: string[] }; + group_based_policy?: { groups: string[] }; + namespace_based_policy?: { namespaces: string[] }; + combined_group_namespace_policy?: { + groups: string[]; + namespaces: string[]; + }; +} + +interface ApplyPermissionPayload { + name: string; + project: string; + types: string[]; + name_patterns: string[]; + actions: string[]; + policy: PolicyPayload; + tags?: Record; + required_tags?: Record; +} + +interface DeletePermissionPayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const useApplyPermission = () => { + const queryClient = useQueryClient(); + + return useMutation( + (payload: ApplyPermissionPayload) => + restPost(API_BASE, "/permissions", payload), + { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["permissions-rest"]); + queryClient.invalidateQueries(["registry-rest-bulk"]); + }, + }, + ); +}; + +const useDeletePermission = () => { + const queryClient = useQueryClient(); + + return useMutation( + (payload: DeletePermissionPayload) => + restDelete( + API_BASE, + `/permissions/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + ), + { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["permissions-rest"]); + queryClient.invalidateQueries(["registry-rest-bulk"]); + }, + }, + ); +}; + +export { useApplyPermission, useDeletePermission }; +export type { ApplyPermissionPayload, DeletePermissionPayload, PolicyPayload }; diff --git a/ui/src/queries/restApi.ts b/ui/src/queries/restApi.ts new file mode 100644 index 00000000000..f3734962a00 --- /dev/null +++ b/ui/src/queries/restApi.ts @@ -0,0 +1,19 @@ +const API_BASE = "/api/v1"; + +export async function fetchApi( + path: string, + params?: Record, +): Promise { + const url = new URL(`${API_BASE}${path}`, window.location.origin); + if (params) { + Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); + } + const res = await fetch(url.toString(), { + headers: { "Content-Type": "application/json" }, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({ detail: res.statusText })); + throw new Error(body.detail || `API error: ${res.status}`); + } + return res.json(); +} diff --git a/ui/src/queries/restApiClient.ts b/ui/src/queries/restApiClient.ts new file mode 100644 index 00000000000..c253020eed9 --- /dev/null +++ b/ui/src/queries/restApiClient.ts @@ -0,0 +1,93 @@ +import type { FetchOptions } from "../contexts/DataModeContext"; + +class RestApiError extends Error { + status: number; + constructor(message: string, status: number) { + super(message); + this.name = "RestApiError"; + this.status = status; + } +} + +const restFetch = async ( + baseUrl: string, + path: string, + fetchOptions?: FetchOptions, +): Promise => { + const url = `${baseUrl}${path}`; + const headers: Record = { + Accept: "application/json", + ...fetchOptions?.headers, + }; + + const res = await fetch(url, { + method: "GET", + headers, + credentials: fetchOptions?.credentials, + }); + + if (!res.ok) { + throw new RestApiError( + `REST API error: ${res.status} ${res.statusText}`, + res.status, + ); + } + + return res.json(); +}; + +const restPost = async ( + baseUrl: string, + path: string, + body: unknown, + fetchOptions?: FetchOptions, +): Promise => { + const url = `${baseUrl}${path}`; + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + ...fetchOptions?.headers, + }; + + const res = await fetch(url, { + method: "POST", + headers, + credentials: fetchOptions?.credentials, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const text = await res.text().catch(() => res.statusText); + throw new RestApiError(`REST API error: ${res.status} ${text}`, res.status); + } + + return res.json(); +}; + +const restDelete = async ( + baseUrl: string, + path: string, + fetchOptions?: FetchOptions, +): Promise => { + const url = `${baseUrl}${path}`; + const headers: Record = { + Accept: "application/json", + ...fetchOptions?.headers, + }; + + const res = await fetch(url, { + method: "DELETE", + headers, + credentials: fetchOptions?.credentials, + }); + + if (!res.ok) { + const text = await res.text().catch(() => res.statusText); + throw new RestApiError(`REST API error: ${res.status} ${text}`, res.status); + } + + return res.json(); +}; + +export default restFetch; +export { RestApiError, restPost, restDelete }; diff --git a/ui/src/queries/useLoadComputeEngine.ts b/ui/src/queries/useLoadComputeEngine.ts new file mode 100644 index 00000000000..dc9dfae73bd --- /dev/null +++ b/ui/src/queries/useLoadComputeEngine.ts @@ -0,0 +1,121 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../contexts/RegistryPathContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch, { RestApiError } from "./restApiClient"; + +export interface ComputeEngineConfig { + type: string; + [key: string]: any; +} + +export interface ComputeEngineInfo { + engineType: string; + engineClass: string; + config: ComputeEngineConfig; + featureViewCount: number; +} + +export interface FeatureViewEngineInfo { + name: string; + type: string; + online: boolean; + lastMaterialized?: string; + hasOverride: boolean; + overrides?: Record; + materializationIntervals: Array<{ + startTime?: string; + endTime?: string; + start_time?: string; + end_time?: string; + }>; +} + +const ENGINE_TYPE_TO_CLASS: Record = { + local: "LocalComputeEngine", + "spark.engine": "SparkComputeEngine", + "ray.engine": "RayComputeEngine", + "flink.engine": "FlinkComputeEngine", + "snowflake.engine": "SnowflakeComputeEngine", + lambda: "LambdaComputeEngine", + k8s: "KubernetesComputeEngine", +}; + +function extractFeatureViewInfos(featureViews: any[]): FeatureViewEngineInfo[] { + return featureViews.map((fv: any) => ({ + name: fv.name, + type: fv.type || "Batch", + online: fv.online ?? true, + lastMaterialized: fv.lastMaterialized, + hasOverride: fv.hasOverride ?? false, + overrides: fv.overrides, + materializationIntervals: fv.materializationIntervals || [], + })); +} + +export function useLoadComputeEngine(projectName?: string) { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const enginePath = + projectName && projectName !== "all" + ? `/compute_engines?project=${encodeURIComponent(projectName)}` + : "/compute_engines/all?limit=100"; + + const engineQuery = useQuery( + ["rest", "compute-engine", registryUrl, projectName || "all"], + () => restFetch(registryUrl, enginePath, fetchOptions), + { + enabled: !!registryUrl, + staleTime: 30_000, + retry: (failureCount, error) => { + if (error instanceof RestApiError && error.status === 403) return false; + return failureCount < 3; + }, + }, + ); + + const isPermissionDenied = + engineQuery.isError && + engineQuery.error instanceof RestApiError && + engineQuery.error.status === 403; + + let engineInfo: ComputeEngineInfo | null = null; + let featureViewInfos: FeatureViewEngineInfo[] = []; + + if (engineQuery.isSuccess && engineQuery.data) { + const engine = engineQuery.data.engine || engineQuery.data.engines?.[0]; + if (engine) { + engineInfo = { + engineType: engine.engineType || "local", + engineClass: + engine.engineClass || + ENGINE_TYPE_TO_CLASS[engine.engineType] || + "LocalComputeEngine", + config: engine.config || { type: engine.engineType || "local" }, + featureViewCount: engine.featureViewCount || 0, + }; + } + + const rawFvs = engineQuery.data.featureViews || []; + featureViewInfos = extractFeatureViewInfos(rawFvs); + } + + if (!engineInfo) { + engineInfo = { + engineType: "local", + engineClass: "LocalComputeEngine", + config: { type: "local" }, + featureViewCount: featureViewInfos.length, + }; + } + + return { + isLoading: engineQuery.isLoading, + isSuccess: engineQuery.isSuccess, + isError: engineQuery.isError, + isPermissionDenied, + engineInfo, + featureViewInfos, + }; +} diff --git a/ui/src/queries/useLoadDataSourcesREST.ts b/ui/src/queries/useLoadDataSourcesREST.ts new file mode 100644 index 00000000000..5d3890cc503 --- /dev/null +++ b/ui/src/queries/useLoadDataSourcesREST.ts @@ -0,0 +1,41 @@ +import { useQuery } from "react-query"; +import { fetchApi } from "./restApi"; + +interface DataSourceListResponse { + dataSources: any[]; + pagination: Record; + relationships?: Record; +} + +const useLoadDataSourcesREST = (project: string) => { + return useQuery( + ["data-sources-rest", project], + () => + fetchApi("/data_sources", { + project, + allow_cache: "false", + }), + { + enabled: !!project, + staleTime: 30000, + }, + ); +}; + +const useLoadDataSourceREST = (name: string, project: string) => { + return useQuery( + ["data-source-rest", name, project], + () => + fetchApi(`/data_sources/${encodeURIComponent(name)}`, { + project, + include_relationships: "true", + allow_cache: "false", + }), + { + enabled: !!name && !!project, + staleTime: 30000, + }, + ); +}; + +export { useLoadDataSourcesREST, useLoadDataSourceREST }; diff --git a/ui/src/queries/useLoadEntitiesREST.ts b/ui/src/queries/useLoadEntitiesREST.ts new file mode 100644 index 00000000000..7127de656ee --- /dev/null +++ b/ui/src/queries/useLoadEntitiesREST.ts @@ -0,0 +1,41 @@ +import { useQuery } from "react-query"; +import { fetchApi } from "./restApi"; + +interface EntityListResponse { + entities: any[]; + pagination: Record; + relationships?: Record; +} + +const useLoadEntitiesREST = (project: string) => { + return useQuery( + ["entities-rest", project], + () => + fetchApi("/entities", { + project, + allow_cache: "false", + }), + { + enabled: !!project, + staleTime: 30000, + }, + ); +}; + +const useLoadEntityREST = (name: string, project: string) => { + return useQuery( + ["entity-rest", name, project], + () => + fetchApi(`/entities/${encodeURIComponent(name)}`, { + project, + include_relationships: "true", + allow_cache: "false", + }), + { + enabled: !!name && !!project, + staleTime: 30000, + }, + ); +}; + +export { useLoadEntitiesREST, useLoadEntityREST }; diff --git a/ui/src/queries/useLoadFeatureModels.ts b/ui/src/queries/useLoadFeatureModels.ts new file mode 100644 index 00000000000..dc6f97843d0 --- /dev/null +++ b/ui/src/queries/useLoadFeatureModels.ts @@ -0,0 +1,37 @@ +import { useQuery } from "react-query"; + +export interface FeatureModelInfo { + model_name: string; + version: string; + stage: string; + mlflow_url: string; +} + +interface FeatureModelsResponse { + feature_models: Record; + error?: string; +} + +const useLoadFeatureModels = () => { + return useQuery( + "feature-models", + () => { + return fetch("/api/mlflow-feature-models") + .then((res) => { + if (!res.ok) { + return { feature_models: {} }; + } + return res.json(); + }) + .catch(() => { + return { feature_models: {} }; + }); + }, + { + staleTime: 60000, + retry: false, + }, + ); +}; + +export default useLoadFeatureModels; diff --git a/ui/src/queries/useLoadFeatureUsage.ts b/ui/src/queries/useLoadFeatureUsage.ts new file mode 100644 index 00000000000..c797abe88ce --- /dev/null +++ b/ui/src/queries/useLoadFeatureUsage.ts @@ -0,0 +1,35 @@ +import { useQuery } from "react-query"; + +interface FeatureUsageEntry { + run_count: number; + last_used: number | null; + models: string[]; +} + +interface FeatureUsageResponse { + feature_usage: Record; + mlflow_enabled?: boolean; + error?: string; +} + +const fetchFeatureUsage = async (): Promise => { + const response = await fetch("/api/mlflow-feature-usage"); + if (!response.ok) { + throw new Error(`Failed to fetch feature usage: ${response.statusText}`); + } + return response.json(); +}; + +const useLoadFeatureUsage = () => { + return useQuery( + "mlflowFeatureUsage", + fetchFeatureUsage, + { + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + }, + ); +}; + +export default useLoadFeatureUsage; +export type { FeatureUsageEntry, FeatureUsageResponse }; diff --git a/ui/src/queries/useLoadFeatureViewsREST.ts b/ui/src/queries/useLoadFeatureViewsREST.ts new file mode 100644 index 00000000000..0b67b960e11 --- /dev/null +++ b/ui/src/queries/useLoadFeatureViewsREST.ts @@ -0,0 +1,41 @@ +import { useQuery } from "react-query"; +import { fetchApi } from "./restApi"; + +interface FeatureViewListResponse { + featureViews: any[]; + pagination: Record; + relationships?: Record; +} + +const useLoadFeatureViewsREST = (project: string) => { + return useQuery( + ["feature-views-rest", project], + () => + fetchApi("/feature_views", { + project, + allow_cache: "false", + }), + { + enabled: !!project, + staleTime: 30000, + }, + ); +}; + +const useLoadFeatureViewREST = (name: string, project: string) => { + return useQuery( + ["feature-view-rest", name, project], + () => + fetchApi(`/feature_views/${encodeURIComponent(name)}`, { + project, + include_relationships: "true", + allow_cache: "false", + }), + { + enabled: !!name && !!project, + staleTime: 30000, + }, + ); +}; + +export { useLoadFeatureViewsREST, useLoadFeatureViewREST }; diff --git a/ui/src/queries/useLoadMlflowRuns.ts b/ui/src/queries/useLoadMlflowRuns.ts new file mode 100644 index 00000000000..041fd41137d --- /dev/null +++ b/ui/src/queries/useLoadMlflowRuns.ts @@ -0,0 +1,52 @@ +import { useQuery } from "react-query"; + +export interface RegisteredModelInfo { + model_name: string; + version: string; + stage: string; + mlflow_url: string; +} + +export interface MlflowRunData { + run_id: string; + run_name: string; + status: string; + start_time: number; + feature_service: string | null; + feature_views: string[]; + feature_refs: string[]; + retrieval_type: string | null; + entity_count: string | null; + mlflow_url: string; + registered_models: RegisteredModelInfo[]; +} + +interface MlflowRunsResponse { + runs: MlflowRunData[]; + mlflow_uri: string | null; + error?: string; +} + +const useLoadMlflowRuns = () => { + return useQuery( + "mlflow-runs", + () => { + return fetch("/api/mlflow-runs") + .then((res) => { + if (!res.ok) { + return { runs: [], mlflow_uri: null }; + } + return res.json(); + }) + .catch(() => { + return { runs: [], mlflow_uri: null }; + }); + }, + { + staleTime: 30000, + retry: false, + }, + ); +}; + +export default useLoadMlflowRuns; diff --git a/ui/src/queries/useLoadOpenLineageGraph.ts b/ui/src/queries/useLoadOpenLineageGraph.ts new file mode 100644 index 00000000000..25bd4e12389 --- /dev/null +++ b/ui/src/queries/useLoadOpenLineageGraph.ts @@ -0,0 +1,132 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../contexts/RegistryPathContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch from "./restApiClient"; + +export interface OpenLineageNode { + type: string; + namespace: string; + name: string; + producer?: string; + feast_object_type?: string; + feast_object_name?: string; + feast_project?: string; + schema?: any; + description?: string; + job_type?: string; + source_type?: string; + facets?: Record; +} + +export interface OpenLineageEdge { + source_type: string; + source_namespace: string; + source_name: string; + target_type: string; + target_namespace: string; + target_name: string; + edge_type?: string; + updated_at?: number; +} + +export interface OpenLineageSymlink { + dataset_namespace: string; + dataset_name: string; + linked_namespace: string; + linked_name: string; + link_type: string; +} + +export interface OpenLineageGraphData { + nodes: OpenLineageNode[]; + edges: OpenLineageEdge[]; + symlinks?: OpenLineageSymlink[]; +} + +export interface OpenLineageEvent { + event_id: string; + event_type: string; + event_time: number; + producer?: string; + job_namespace: string; + job_name: string; + run_id?: string; + event_json: string; + created_at: number; +} + +export interface RegistryRelationship { + source: { type: string; name: string }; + target: { type: string; name: string }; + type: string; + project?: string; +} + +export interface RegistryLineageData { + relationships: RegistryRelationship[]; + indirect_relationships: RegistryRelationship[]; +} + +const useLoadOpenLineageGraph = () => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + return useQuery( + ["openlineage-graph"], + () => + restFetch( + registryUrl, + "/lineage/openlineage/graph", + fetchOptions, + ), + { enabled: !!registryUrl }, + ); +}; + +const useLoadOpenLineageEvents = ( + namespace?: string, + jobName?: string, + limit: number = 100, +) => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const params = new URLSearchParams(); + if (namespace) params.set("namespace", namespace); + if (jobName) params.set("job_name", jobName); + params.set("limit", limit.toString()); + + return useQuery<{ events: OpenLineageEvent[]; total: number }>( + ["openlineage-events", namespace, jobName, limit], + () => + restFetch( + registryUrl, + `/lineage/openlineage/events?${params.toString()}`, + fetchOptions, + ), + { enabled: !!registryUrl }, + ); +}; + +const useLoadRegistryLineage = (project?: string) => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + return useQuery( + ["registry-lineage", project], + () => + restFetch( + registryUrl, + `/lineage/registry?project=${project}`, + fetchOptions, + ), + { enabled: !!registryUrl && !!project }, + ); +}; + +export { + useLoadOpenLineageGraph, + useLoadOpenLineageEvents, + useLoadRegistryLineage, +}; diff --git a/ui/src/queries/useLoadRegistry.ts b/ui/src/queries/useLoadRegistry.ts index e3f5ac87a1d..9e7cf2db2d5 100644 --- a/ui/src/queries/useLoadRegistry.ts +++ b/ui/src/queries/useLoadRegistry.ts @@ -4,18 +4,20 @@ import parseEntityRelationships, { EntityRelation, } from "../parsers/parseEntityRelationships"; import parseIndirectRelationships from "../parsers/parseIndirectRelationships"; -import { feast } from "../protos"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch from "./restApiClient"; +import type { FetchOptions } from "../contexts/DataModeContext"; interface FeatureStoreAllData { project: string; description?: string; - objects: feast.core.Registry; + objects: any; relationships: EntityRelation[]; mergedFVMap: Record; mergedFVList: genericFVType[]; indirectRelationships: EntityRelation[]; allFeatures: Feature[]; - permissions?: any[]; // Add permissions field + permissions?: any[]; } interface Feature { @@ -25,251 +27,224 @@ interface Feature { project?: string; } -const useLoadRegistry = (url: string, projectName?: string) => { - return useQuery( - `registry:${url}:${projectName || "all"}`, - () => { - return fetch(url, { - headers: { - "Content-Type": "application/json", - }, - }) - .then((res) => { - const contentType = res.headers.get("content-type"); - if (contentType && contentType.includes("application/json")) { - return res.json(); - } else { - return res.arrayBuffer(); - } - }) - .then((data) => { - let objects; - - if (data instanceof ArrayBuffer) { - objects = feast.core.Registry.decode(new Uint8Array(data)); - } else { - objects = data; - } - // const objects = FeastRegistrySchema.parse(json); - - if (!objects.featureViews) { - objects.featureViews = []; - } - - // Filter objects by project if projectName is provided - // Skip filtering if projectName is "all" (All Projects view) - // Only filter if we detect that the registry contains multiple projects - if (projectName && projectName !== "all") { - // Check if the registry actually has multiple projects - const projectsInRegistry = new Set(); - objects.featureViews?.forEach((fv: any) => { - if (fv?.spec?.project) projectsInRegistry.add(fv.spec.project); - }); - objects.entities?.forEach((entity: any) => { - if (entity?.spec?.project) - projectsInRegistry.add(entity.spec.project); - }); - - // Only apply filtering if there are actually multiple projects in the registry - // OR if the projectName matches one of the projects in the registry - const shouldFilter = - projectsInRegistry.size > 1 || - projectsInRegistry.has(projectName); - - if (shouldFilter && projectsInRegistry.has(projectName)) { - if (objects.featureViews) { - objects.featureViews = objects.featureViews.filter( - (fv: any) => fv?.spec?.project === projectName, - ); - } - if (objects.entities) { - objects.entities = objects.entities.filter( - (entity: any) => entity?.spec?.project === projectName, - ); - } - if (objects.dataSources) { - objects.dataSources = objects.dataSources.filter( - (ds: any) => ds?.project === projectName, - ); - } - if (objects.featureServices) { - objects.featureServices = objects.featureServices.filter( - (fs: any) => fs?.spec?.project === projectName, - ); - } - if (objects.onDemandFeatureViews) { - objects.onDemandFeatureViews = - objects.onDemandFeatureViews.filter( - (odfv: any) => odfv?.spec?.project === projectName, - ); - } - if (objects.streamFeatureViews) { - objects.streamFeatureViews = objects.streamFeatureViews.filter( - (sfv: any) => sfv?.spec?.project === projectName, - ); - } - if (objects.savedDatasets) { - objects.savedDatasets = objects.savedDatasets.filter( - (sd: any) => sd?.spec?.project === projectName, - ); - } - if (objects.validationReferences) { - objects.validationReferences = - objects.validationReferences.filter( - (vr: any) => vr?.project === projectName, - ); - } - if (objects.permissions) { - objects.permissions = objects.permissions.filter( - (perm: any) => - perm?.spec?.project === projectName || !perm?.spec?.project, - ); - } - } - } - - if ( - process.env.NODE_ENV === "test" && - objects.featureViews.length === 0 - ) { - try { - const fs = require("fs"); - const path = require("path"); - const { feast } = require("../protos"); - - const registry = fs.readFileSync( - path.resolve(__dirname, "../../public/registry.db"), - ); - const parsedRegistry = feast.core.Registry.decode(registry); - - if ( - parsedRegistry.featureViews && - parsedRegistry.featureViews.length > 0 - ) { - objects.featureViews = parsedRegistry.featureViews; - } - } catch (e) { - console.error("Error loading test registry:", e); - } - } - - const { mergedFVMap, mergedFVList } = mergedFVTypes(objects); - - const relationships = parseEntityRelationships(objects); +// --------------------------------------------------------------------------- +// Shared post-processing (used by the bulk REST fetch) +// --------------------------------------------------------------------------- + +const assembleFeatureStoreData = ( + objects: any, + projectName?: string, +): FeatureStoreAllData => { + const { mergedFVMap, mergedFVList } = mergedFVTypes(objects); + const relationships = parseEntityRelationships(objects); + const indirectRelationships = parseIndirectRelationships( + relationships, + objects, + ); - // Only contains Entity -> FS or DS -> FS relationships - const indirectRelationships = parseIndirectRelationships( - relationships, - objects, - ); + const allFeatures: Feature[] = + objects.featureViews?.flatMap( + (fv: any) => + fv?.spec?.features?.map((feature: any) => ({ + name: feature.name ?? "Unknown", + featureView: fv?.spec?.name || "Unknown FeatureView", + type: + feature.valueType != null + ? typeof feature.valueType === "number" + ? String(feature.valueType) + : feature.valueType + : "Unknown Type", + project: fv?.spec?.project || fv?.project, + })) || [], + ) || []; + + let resolvedProjectName: string = + projectName === "all" + ? "All Projects" + : projectName || + (objects.projects && + objects.projects.length > 0 && + objects.projects[0].spec && + objects.projects[0].spec.name + ? objects.projects[0].spec.name + : objects.project + ? objects.project + : "default"); + + let projectDescription: string | undefined; + if (projectName === "all") { + projectDescription = "View data across all projects"; + } else if (objects.projects && objects.projects.length > 0) { + const currentProject = objects.projects.find( + (p: any) => p?.spec?.name === resolvedProjectName, + ); + if (currentProject?.spec) { + projectDescription = currentProject.spec.description; + } + } + + return { + project: resolvedProjectName, + description: projectDescription, + objects, + mergedFVMap, + mergedFVList, + relationships, + indirectRelationships, + allFeatures, + permissions: objects.permissions || [], + }; +}; - // console.log({ - // objects, - // mergedFVMap, - // mergedFVList, - // relationships, - // indirectRelationships, - // }); - const allFeatures: Feature[] = - objects.featureViews?.flatMap( - (fv: any) => - fv?.spec?.features?.map((feature: any) => ({ - name: feature.name ?? "Unknown", - featureView: fv?.spec?.name || "Unknown FeatureView", - type: - feature.valueType != null - ? feast.types.ValueType.Enum[feature.valueType] - : "Unknown Type", - project: fv?.spec?.project, // Include project from parent feature view - })) || [], - ) || []; +// --------------------------------------------------------------------------- +// REST fetch strategy +// --------------------------------------------------------------------------- + +const permissionSafeFetch = async ( + apiBaseUrl: string, + path: string, + fallback: T, + fetchOptions?: FetchOptions, +): Promise => { + try { + return await restFetch(apiBaseUrl, path, fetchOptions); + } catch (err: any) { + if (err?.status === 403 || err?.status === 401) { + return fallback; + } + throw err; + } +}; - // Use the provided projectName parameter if available, otherwise try to determine from registry - let resolvedProjectName: string = - projectName === "all" - ? "All Projects" - : projectName || - (process.env.NODE_ENV === "test" - ? "credit_scoring_aws" - : objects.projects && - objects.projects.length > 0 && - objects.projects[0].spec && - objects.projects[0].spec.name - ? objects.projects[0].spec.name - : objects.project - ? objects.project - : "credit_scoring_aws"); +const fetchREST = async ( + apiBaseUrl: string, + projectName?: string, + fetchOptions?: FetchOptions, +): Promise => { + const projectParam = + projectName && projectName !== "all" + ? `?project=${encodeURIComponent(projectName)}` + : ""; + const useAllEndpoint = !projectParam; + + const emptyList = (key: string) => ({ [key]: [] }); + + const [ + entitiesResp, + featureViewsResp, + labelViewsResp, + featureServicesResp, + dataSourcesResp, + savedDatasetsResp, + projectsResp, + ] = await Promise.all([ + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/entities/all?include_relationships=true" + : `/entities${projectParam}&include_relationships=true`, + emptyList("entities"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/feature_views/all?include_relationships=true" + : `/feature_views${projectParam}&include_relationships=true`, + emptyList("featureViews"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/label_views/all?include_relationships=true" + : `/label_views${projectParam}&include_relationships=true`, + emptyList("featureViews"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/feature_services/all?include_relationships=true" + : `/feature_services${projectParam}&include_relationships=true`, + emptyList("featureServices"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/data_sources/all?include_relationships=true" + : `/data_sources${projectParam}&include_relationships=true`, + emptyList("dataSources"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/saved_datasets/all?include_relationships=true" + : `/saved_datasets${projectParam}&include_relationships=true`, + emptyList("savedDatasets"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + "/projects", + emptyList("projects"), + fetchOptions, + ), + ]); + + const entities = entitiesResp.entities || []; + const allFeatureViews = featureViewsResp.featureViews || []; + const labelViews: any[] = labelViewsResp.featureViews || []; + const featureServices = featureServicesResp.featureServices || []; + const dataSources = dataSourcesResp.dataSources || []; + const savedDatasets = savedDatasetsResp.savedDatasets || []; + const projects = projectsResp.projects || []; + + const featureViews: any[] = []; + const onDemandFeatureViews: any[] = []; + const streamFeatureViews: any[] = []; + + for (const fv of allFeatureViews) { + const fvType = fv.type; + if (fvType === "onDemandFeatureView") { + onDemandFeatureViews.push(fv); + } else if (fvType === "streamFeatureView") { + streamFeatureViews.push(fv); + } else { + featureViews.push(fv); + } + } + + const objects: any = { + entities, + featureViews, + onDemandFeatureViews, + streamFeatureViews, + labelViews, + featureServices, + dataSources, + savedDatasets, + projects, + }; + + return assembleFeatureStoreData(objects, projectName); +}; - let projectDescription = undefined; +// --------------------------------------------------------------------------- +// Public hook +// --------------------------------------------------------------------------- - // Find project description from the projects array - if (projectName === "all") { - projectDescription = "View data across all projects"; - } else if (objects.projects && objects.projects.length > 0) { - const currentProject = objects.projects.find( - (p: any) => p?.spec?.name === resolvedProjectName, - ); - if (currentProject?.spec) { - projectDescription = currentProject.spec.description; - } - } +const useLoadRegistry = (url: string, projectName?: string) => { + const { fetchOptions } = useDataMode(); - return { - project: resolvedProjectName, - description: projectDescription, - objects, - mergedFVMap, - mergedFVList, - relationships, - indirectRelationships, - allFeatures, - permissions: - objects.permissions && objects.permissions.length > 0 - ? objects.permissions - : [ - { - spec: { - name: "zipcode-features-reader", - types: [2], // FeatureView - name_patterns: ["zipcode_features"], - policy: { roles: ["analyst", "data_scientist"] }, - actions: [1, 4, 5], // DESCRIBE, READ_ONLINE, READ_OFFLINE - }, - }, - { - spec: { - name: "zipcode-source-writer", - types: [7], // FileSource - name_patterns: ["zipcode"], - policy: { roles: ["admin", "data_engineer"] }, - actions: [0, 2, 7], // CREATE, UPDATE, WRITE_OFFLINE - }, - }, - { - spec: { - name: "credit-score-v1-reader", - types: [6], // FeatureService - name_patterns: ["credit_score_v1"], - policy: { roles: ["model_user", "data_scientist"] }, - actions: [1, 4], // DESCRIBE, READ_ONLINE - }, - }, - { - spec: { - name: "risky-features-reader", - types: [2, 6], // FeatureView, FeatureService - name_patterns: [], - required_tags: { stage: "prod" }, - policy: { roles: ["trusted_analyst"] }, - actions: [5], // READ_OFFLINE - }, - }, - ], - }; - }); - }, + return useQuery( + ["registry-rest-bulk", url, projectName || "all"], + () => fetchREST(url, projectName, fetchOptions), { - staleTime: Infinity, // Given that we are reading from a registry dump, this seems reasonable for now. + staleTime: 30_000, + enabled: !!url, }, ); }; diff --git a/ui/src/queries/useLoadRunHistory.ts b/ui/src/queries/useLoadRunHistory.ts new file mode 100644 index 00000000000..eddc82a1be9 --- /dev/null +++ b/ui/src/queries/useLoadRunHistory.ts @@ -0,0 +1,66 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../contexts/RegistryPathContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch from "./restApiClient"; + +export interface RunSummary { + run_id: string; + job_namespace: string; + job_name: string; + state: string; + started_at: number | null; + ended_at: number | null; + updated_at: number; +} + +export interface RunIOEntry { + namespace: string; + name: string; + facets?: Record | null; +} + +export interface RunDetail extends RunSummary { + inputs: RunIOEntry[]; + outputs: RunIOEntry[]; + facets?: Record | null; +} + +const useRunHistory = (jobNamespace?: string, jobName?: string) => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const params = new URLSearchParams(); + if (jobNamespace) params.set("job_namespace", jobNamespace); + if (jobName) params.set("job_name", jobName); + params.set("limit", "50"); + + return useQuery<{ runs: RunSummary[]; total: number }>( + ["openlineage-runs", jobNamespace, jobName], + () => + restFetch( + registryUrl, + `/lineage/openlineage/runs?${params.toString()}`, + fetchOptions, + ), + { enabled: !!registryUrl && !!jobNamespace && !!jobName }, + ); +}; + +const useRunDetail = (runId?: string) => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + return useQuery( + ["openlineage-run-detail", runId], + () => + restFetch( + registryUrl, + `/lineage/openlineage/runs/${runId}`, + fetchOptions, + ), + { enabled: !!registryUrl && !!runId }, + ); +}; + +export { useRunHistory, useRunDetail }; diff --git a/ui/src/queries/useMonitoringApi.ts b/ui/src/queries/useMonitoringApi.ts new file mode 100644 index 00000000000..73a9b16e3fd --- /dev/null +++ b/ui/src/queries/useMonitoringApi.ts @@ -0,0 +1,318 @@ +import { useContext } from "react"; +import { useQuery, useMutation, useQueryClient } from "react-query"; +import MonitoringContext from "../contexts/MonitoringContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import type { FetchOptions } from "../contexts/DataModeContext"; + +interface FeatureMetric { + project_id: string; + feature_view_name: string; + feature_name: string; + metric_date: string; + granularity: string; + data_source_type: string; + computed_at: string; + is_baseline: boolean; + feature_type: string; + row_count: number; + null_count: number; + null_rate: number; + mean: number | null; + stddev: number | null; + min_val: number | null; + max_val: number | null; + p50: number | null; + p75: number | null; + p90: number | null; + p95: number | null; + p99: number | null; + histogram: NumericHistogram | CategoricalHistogram | null; +} + +interface NumericHistogram { + bins: number[]; + counts: number[]; + bin_width: number; +} + +interface CategoricalHistogram { + values: { value: string; count: number }[]; + other_count: number; + unique_count: number; +} + +interface FeatureViewMetric { + project_id: string; + feature_view_name: string; + metric_date: string; + granularity: string; + data_source_type: string; + computed_at: string; + is_baseline: boolean; + total_row_count: number; + total_features: number; + features_with_nulls: number; + avg_null_rate: number; + max_null_rate: number; +} + +interface FeatureServiceMetric { + project_id: string; + feature_service_name: string; + metric_date: string; + granularity: string; + data_source_type: string; + computed_at: string; + is_baseline: boolean; + total_feature_views: number; + total_features: number; + avg_null_rate: number; + max_null_rate: number; +} + +interface MonitoringFilters { + project: string; + feature_view_name?: string; + feature_name?: string; + feature_service_name?: string; + granularity?: string; + data_source_type?: string; + start_date?: string; + end_date?: string; + is_baseline?: boolean; +} + +const toQueryParams = ( + filters: MonitoringFilters, +): Record => { + return { + project: filters.project, + feature_view_name: filters.feature_view_name, + feature_name: filters.feature_name, + feature_service_name: filters.feature_service_name, + granularity: filters.granularity, + data_source_type: filters.data_source_type, + start_date: filters.start_date, + end_date: filters.end_date, + is_baseline: filters.is_baseline ? "true" : undefined, + }; +}; + +const buildQueryString = (params: Record) => { + const entries = Object.entries(params).filter( + ([, v]) => v !== undefined && v !== "", + ); + if (entries.length === 0) return ""; + return ( + "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(v!)}`).join("&") + ); +}; + +class MonitoringApiError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +const fetchMonitoring = async ( + baseUrl: string, + path: string, + params: Record, + fetchOptions?: FetchOptions, +): Promise => { + const qs = buildQueryString(params); + const res = await fetch(`${baseUrl}${path}${qs}`, { + method: "GET", + headers: { + Accept: "application/json", + ...fetchOptions?.headers, + }, + credentials: fetchOptions?.credentials, + }); + if (!res.ok) { + throw new MonitoringApiError( + res.status, + `Failed to fetch ${path}: ${res.status} ${res.statusText}`, + ); + } + const text = await res.text(); + const sanitized = text + .replace(/:\s*NaN/g, ": null") + .replace(/:\s*Infinity/g, ": null") + .replace(/:\s*-Infinity/g, ": null"); + return JSON.parse(sanitized); +}; + +const isServiceUnavailable = (error: unknown): boolean => + error instanceof MonitoringApiError && error.status === 503; + +const STALE_TIME = 30_000; + +const useFeatureMetrics = (filters: MonitoringFilters) => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + const path = filters.is_baseline + ? "/monitoring/metrics/baseline" + : "/monitoring/metrics/features"; + return useQuery( + ["monitoring-features", filters], + () => + fetchMonitoring( + apiBaseUrl, + path, + toQueryParams(filters), + fetchOptions, + ), + { staleTime: STALE_TIME, retry: 1 }, + ); +}; + +const aggregateToFeatureViewMetrics = ( + features: FeatureMetric[], +): FeatureViewMetric[] => { + const grouped = new Map(); + for (const f of features) { + const key = f.feature_view_name; + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key)!.push(f); + } + return Array.from(grouped.entries()).map(([fvName, feats]) => { + const nullRates = feats.map((f) => f.null_rate ?? 0); + const maxRowCount = Math.max(...feats.map((f) => f.row_count ?? 0)); + return { + project_id: feats[0].project_id, + feature_view_name: fvName, + metric_date: feats[0].metric_date, + granularity: feats[0].granularity, + data_source_type: feats[0].data_source_type, + computed_at: feats[0].computed_at, + is_baseline: feats[0].is_baseline, + total_row_count: maxRowCount, + total_features: feats.length, + features_with_nulls: feats.filter((f) => (f.null_count ?? 0) > 0).length, + avg_null_rate: + nullRates.length > 0 + ? nullRates.reduce((a, b) => a + b, 0) / nullRates.length + : 0, + max_null_rate: nullRates.length > 0 ? Math.max(...nullRates) : 0, + }; + }); +}; + +const useFeatureViewMetrics = (filters: MonitoringFilters) => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + const isBaseline = !!filters.is_baseline; + return useQuery( + ["monitoring-feature-views", filters], + async () => { + if (isBaseline) { + const features = await fetchMonitoring( + apiBaseUrl, + "/monitoring/metrics/baseline", + toQueryParams(filters), + fetchOptions, + ); + return aggregateToFeatureViewMetrics(features); + } + return fetchMonitoring( + apiBaseUrl, + "/monitoring/metrics/feature_views", + toQueryParams(filters), + fetchOptions, + ); + }, + { staleTime: STALE_TIME, retry: 1 }, + ); +}; + +const useFeatureServiceMetrics = (filters: MonitoringFilters) => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + return useQuery( + ["monitoring-feature-services", filters], + () => + fetchMonitoring( + apiBaseUrl, + "/monitoring/metrics/feature_services", + toQueryParams(filters), + fetchOptions, + ), + { staleTime: STALE_TIME, retry: 1 }, + ); +}; + +const useBaselineMetrics = ( + project: string, + featureViewName?: string, + featureName?: string, + dataSourceType?: string, +) => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + return useQuery( + ["monitoring-baseline", project, featureViewName, featureName], + () => + fetchMonitoring( + apiBaseUrl, + "/monitoring/metrics/baseline", + { + project, + feature_view_name: featureViewName, + feature_name: featureName, + data_source_type: dataSourceType, + }, + fetchOptions, + ), + { staleTime: STALE_TIME, retry: 1 }, + ); +}; + +const useComputeMetrics = () => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + const queryClient = useQueryClient(); + return useMutation( + async (body: { project: string; feature_view_name?: string }) => { + const res = await fetch(`${apiBaseUrl}/monitoring/auto_compute`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...fetchOptions?.headers, + }, + credentials: fetchOptions?.credentials, + body: JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`Failed to trigger compute: ${res.status}`); + } + return res.json(); + }, + { + onSuccess: () => { + queryClient.invalidateQueries("monitoring-features"); + queryClient.invalidateQueries("monitoring-feature-views"); + queryClient.invalidateQueries("monitoring-feature-services"); + }, + }, + ); +}; + +export { + isServiceUnavailable, + useFeatureMetrics, + useFeatureViewMetrics, + useFeatureServiceMetrics, + useBaselineMetrics, + useComputeMetrics, +}; +export type { + FeatureMetric, + FeatureViewMetric, + FeatureServiceMetric, + NumericHistogram, + CategoricalHistogram, + MonitoringFilters, +}; diff --git a/ui/src/queries/useResourceQuery.ts b/ui/src/queries/useResourceQuery.ts new file mode 100644 index 00000000000..b099d04fc4d --- /dev/null +++ b/ui/src/queries/useResourceQuery.ts @@ -0,0 +1,242 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../contexts/RegistryPathContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch from "./restApiClient"; +import { RestApiError } from "./restApiClient"; +import { FEAST_FV_TYPES, genericFVType } from "../parsers/mergedFVTypes"; + +interface ResourceQueryOptions { + resourceType: string; + project?: string; + restPath: string; + restSelect?: (data: any) => T | undefined; + enabled?: boolean; +} + +/** + * Generic hook for fetching a specific resource slice via REST API. + * + * Each caller fires its own lightweight endpoint request, and react-query + * deduplicates identical keys automatically. + */ +function useResourceQuery({ + resourceType, + project, + restPath, + restSelect, + enabled = true, +}: ResourceQueryOptions) { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const query = useQuery( + ["rest", resourceType, registryUrl, project || "all"], + () => restFetch(registryUrl, restPath, fetchOptions), + { + enabled: !!registryUrl && enabled, + staleTime: 30_000, + select: restSelect, + retry: (failureCount, error) => { + if (error instanceof RestApiError && error.status === 403) return false; + return failureCount < 3; + }, + }, + ); + + const isPermissionDenied = + query.isError && + query.error instanceof RestApiError && + query.error.status === 403; + + return { ...query, isPermissionDenied }; +} + +// --------------------------------------------------------------------------- +// REST endpoint path builders +// --------------------------------------------------------------------------- + +function entityListPath(project?: string): string { + if (project && project !== "all") { + return `/entities?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/entities/all?limit=100&include_relationships=true"; +} + +function entityDetailPath(name: string, project: string): string { + return `/entities/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function featureViewListPath(project?: string): string { + if (project && project !== "all") { + return `/feature_views?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/feature_views/all?limit=100&include_relationships=true"; +} + +function featureViewDetailPath(name: string, project: string): string { + return `/feature_views/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function featureServiceListPath(project?: string): string { + if (project && project !== "all") { + return `/feature_services?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/feature_services/all?limit=100&include_relationships=true"; +} + +function featureServiceDetailPath(name: string, project: string): string { + return `/feature_services/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function dataSourceListPath(project?: string): string { + if (project && project !== "all") { + return `/data_sources?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/data_sources/all?limit=100&include_relationships=true"; +} + +function dataSourceDetailPath(name: string, project: string): string { + return `/data_sources/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function savedDatasetListPath(project?: string): string { + if (project && project !== "all") { + return `/saved_datasets?project=${encodeURIComponent(project)}`; + } + return "/saved_datasets/all?limit=100"; +} + +function savedDatasetDetailPath(name: string, project: string): string { + return `/saved_datasets/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}`; +} + +function labelViewListPath(project?: string): string { + if (project && project !== "all") { + return `/label_views?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/label_views/all?limit=100&include_relationships=true"; +} + +function labelViewDetailPath(name: string, project: string): string { + return `/label_views/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function permissionListPath(project?: string): string { + if (project && project !== "all") { + return `/permissions?project=${encodeURIComponent(project)}`; + } + return `/permissions?project=default`; +} + +function featuresListPath(project?: string): string { + if (project && project !== "all") { + return `/features?project=${encodeURIComponent(project)}`; + } + return "/features/all?limit=100"; +} + +function featureDetailPath( + featureViewName: string, + featureName: string, + project: string, +): string { + return `/features/${encodeURIComponent(featureViewName)}/${encodeURIComponent(featureName)}?project=${encodeURIComponent(project)}`; +} + +// --------------------------------------------------------------------------- +// REST response → mergedFVList converter +// --------------------------------------------------------------------------- + +function restFeatureViewsToMergedList(resp: any): genericFVType[] { + const featureViews = resp?.featureViews || []; + return featureViews + .filter((fv: any) => fv.type !== "labelView") + .map((fv: any) => { + const fvType = fv.type; + if (fvType === "onDemandFeatureView") { + return { + name: fv.spec?.name, + type: FEAST_FV_TYPES.ondemand, + features: fv.spec?.features || [], + object: fv, + }; + } + if (fvType === "streamFeatureView") { + return { + name: fv.spec?.name, + type: FEAST_FV_TYPES.stream, + features: fv.spec?.features || [], + object: fv, + }; + } + return { + name: fv.spec?.name, + type: FEAST_FV_TYPES.regular, + features: fv.spec?.features || [], + object: fv, + }; + }); +} + +function restLabelViewsFromResponse(resp: any): any[] { + const featureViews = resp?.featureViews || []; + return featureViews.filter((fv: any) => fv.type === "labelView"); +} + +function restFeatureViewDetailToGeneric(resp: any): genericFVType | undefined { + if (!resp || !resp.spec) return undefined; + const fvType = resp.type; + if (fvType === "onDemandFeatureView") { + return { + name: resp.spec.name, + type: FEAST_FV_TYPES.ondemand, + features: resp.spec.features || [], + object: resp, + }; + } + if (fvType === "streamFeatureView") { + return { + name: resp.spec.name, + type: FEAST_FV_TYPES.stream, + features: resp.spec.features || [], + object: resp, + }; + } + if (fvType === "labelView") { + return { + name: resp.spec.name, + type: FEAST_FV_TYPES.label, + features: resp.spec.features || [], + object: resp, + }; + } + return { + name: resp.spec.name, + type: FEAST_FV_TYPES.regular, + features: resp.spec.features || [], + object: resp, + }; +} + +export default useResourceQuery; +export { + entityListPath, + entityDetailPath, + featureViewListPath, + featureViewDetailPath, + featureServiceListPath, + featureServiceDetailPath, + dataSourceListPath, + dataSourceDetailPath, + savedDatasetListPath, + savedDatasetDetailPath, + labelViewListPath, + labelViewDetailPath, + permissionListPath, + featuresListPath, + featureDetailPath, + restFeatureViewsToMergedList, + restFeatureViewDetailToGeneric, + restLabelViewsFromResponse, +}; diff --git a/ui/src/setupProxy.js b/ui/src/setupProxy.js new file mode 100644 index 00000000000..94762f63557 --- /dev/null +++ b/ui/src/setupProxy.js @@ -0,0 +1,332 @@ +const fs = require("fs"); +const path = require("path"); +const express = require("express"); +const { feast } = require("./protos"); + +const registryBuf = fs.readFileSync( + path.resolve(__dirname, "../public/registry.db"), +); +const parsedRegistry = feast.core.Registry.decode(registryBuf); +const projectsList = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "../public/projects-list.json")), +); + +const toJSON = (obj) => (obj && obj.toJSON ? obj.toJSON() : obj); + +const withType = (type) => (fv) => ({ + ...toJSON(fv), + type, +}); + +const state = { + entities: (parsedRegistry.entities || []).map(toJSON), + featureViews: (parsedRegistry.featureViews || []).map( + withType("featureView"), + ), + onDemandFeatureViews: (parsedRegistry.onDemandFeatureViews || []).map( + withType("onDemandFeatureView"), + ), + streamFeatureViews: (parsedRegistry.streamFeatureViews || []).map( + withType("streamFeatureView"), + ), + featureServices: (parsedRegistry.featureServices || []).map(toJSON), + dataSources: (parsedRegistry.dataSources || []).map(toJSON), + savedDatasets: (parsedRegistry.savedDatasets || []).map(toJSON), + projects: (parsedRegistry.projects || []).map(toJSON), +}; + +const allFeatureViews = () => [ + ...state.featureViews, + ...state.onDemandFeatureViews, + ...state.streamFeatureViews, +]; + +const objectProject = (obj) => obj?.spec?.project || obj?.project; + +const filterByProject = (items, project) => { + if (!project || project === "all") return items; + return items.filter((item) => objectProject(item) === project); +}; + +const allFeatures = (project) => + filterByProject(allFeatureViews(), project).flatMap((fv) => + (fv?.spec?.features || []).map((feature) => ({ + name: feature.name, + featureViewName: fv.spec?.name, + valueType: feature.valueType, + project: fv.spec?.project, + })), + ); + +const responseList = (res, key, items) => { + res.json({ + [key]: items, + pagination: {}, + relationships: {}, + }); +}; + +const findByName = (items, name) => + items.find((item) => item?.spec?.name === name || item?.name === name); + +const entityPayloadToResource = (payload) => ({ + spec: { + name: payload.name, + joinKey: payload.join_key || payload.name, + valueType: payload.value_type, + description: payload.description || "", + tags: payload.tags || {}, + owner: payload.owner || "", + project: payload.project, + }, + meta: {}, +}); + +const dataSourcePayloadToResource = (payload) => ({ + name: payload.name, + type: payload.type, + timestampField: payload.timestamp_field, + fieldMapping: payload.field_mapping || {}, + description: payload.description || "", + tags: payload.tags || {}, + owner: payload.owner || "", + project: payload.project, + fileOptions: payload.file_options, + bigqueryOptions: payload.bigquery_options, + snowflakeOptions: payload.snowflake_options, + redshiftOptions: payload.redshift_options, + kafkaOptions: payload.kafka_options, + sparkOptions: payload.spark_options, +}); + +const featureViewPayloadToResource = (payload) => ({ + spec: { + name: payload.name, + description: payload.description || "", + owner: payload.owner || "", + entities: payload.entities || [], + features: payload.features || [], + ttl: payload.ttl, + online: payload.online, + tags: payload.tags || {}, + project: payload.project, + batchSource: payload.batch_source + ? { name: payload.batch_source } + : undefined, + }, + meta: {}, + type: "featureView", +}); + +module.exports = function setupProxy(app) { + app.use("/api/v1", express.json()); + + app.get("/projects-list.json", (_req, res) => { + res.json({ + ...projectsList, + projects: projectsList.projects.map((project) => + project.id === "credit_scoring_aws" + ? { ...project, registryPath: "/api/v1" } + : project, + ), + }); + }); + + app.get("/api/v1/entities/all", (_req, res) => + responseList(res, "entities", state.entities), + ); + app.get("/api/v1/feature_views/all", (_req, res) => + responseList(res, "featureViews", allFeatureViews()), + ); + app.get("/api/v1/feature_services/all", (_req, res) => + responseList(res, "featureServices", state.featureServices), + ); + app.get("/api/v1/data_sources/all", (_req, res) => + responseList(res, "dataSources", state.dataSources), + ); + app.get("/api/v1/saved_datasets/all", (_req, res) => + responseList(res, "savedDatasets", state.savedDatasets), + ); + app.get("/api/v1/features/all", (_req, res) => + responseList(res, "features", allFeatures()), + ); + app.get("/api/v1/label_views/all", (_req, res) => + responseList(res, "featureViews", []), + ); + + app.get("/api/v1/entities", (req, res) => + responseList( + res, + "entities", + filterByProject(state.entities, req.query.project), + ), + ); + app.get("/api/v1/feature_views", (req, res) => + responseList( + res, + "featureViews", + filterByProject(allFeatureViews(), req.query.project), + ), + ); + app.get("/api/v1/feature_services", (req, res) => + responseList( + res, + "featureServices", + filterByProject(state.featureServices, req.query.project), + ), + ); + app.get("/api/v1/data_sources", (req, res) => + responseList( + res, + "dataSources", + filterByProject(state.dataSources, req.query.project), + ), + ); + app.get("/api/v1/saved_datasets", (req, res) => + responseList( + res, + "savedDatasets", + filterByProject(state.savedDatasets, req.query.project), + ), + ); + app.get("/api/v1/features", (req, res) => + responseList(res, "features", allFeatures(req.query.project)), + ); + app.get("/api/v1/label_views", (_req, res) => + responseList(res, "featureViews", []), + ); + app.get("/api/v1/labels", (_req, res) => responseList(res, "labels", [])); + app.get("/api/v1/projects", (_req, res) => + responseList(res, "projects", state.projects), + ); + app.get("/api/v1/permissions", (_req, res) => + responseList(res, "permissions", []), + ); + app.get("/api/v1/metrics/:type", (_req, res) => res.json({})); + + app.get("/api/v1/entities/:name", (req, res) => { + const entity = findByName(state.entities, req.params.name); + if (!entity) return res.status(404).json({ detail: "Not found" }); + return res.json(entity); + }); + app.get("/api/v1/feature_views/:name", (req, res) => { + const featureView = findByName(allFeatureViews(), req.params.name); + if (!featureView) return res.status(404).json({ detail: "Not found" }); + return res.json(featureView); + }); + app.get("/api/v1/feature_services/:name", (req, res) => { + const featureService = findByName(state.featureServices, req.params.name); + if (!featureService) return res.status(404).json({ detail: "Not found" }); + return res.json(featureService); + }); + app.get("/api/v1/data_sources/:name", (req, res) => { + const dataSource = findByName(state.dataSources, req.params.name); + if (!dataSource) return res.status(404).json({ detail: "Not found" }); + return res.json(dataSource); + }); + app.get("/api/v1/saved_datasets/:name", (req, res) => { + const savedDataset = findByName(state.savedDatasets, req.params.name); + if (!savedDataset) return res.status(404).json({ detail: "Not found" }); + return res.json(savedDataset); + }); + app.get("/api/v1/features/:fvName/:featureName", (req, res) => { + const featureView = findByName(allFeatureViews(), req.params.fvName); + const feature = featureView?.spec?.features?.find( + (f) => f.name === req.params.featureName, + ); + if (!feature) return res.status(404).json({ detail: "Not found" }); + return res.json({ + featureViewName: req.params.fvName, + featureName: req.params.featureName, + feature, + featureView, + }); + }); + + app.post("/api/v1/entities", (req, res) => { + const body = req.body || {}; + const existingIndex = state.entities.findIndex( + (entity) => entity?.spec?.name === body.name, + ); + const entity = entityPayloadToResource(body); + if (existingIndex >= 0) { + state.entities[existingIndex] = entity; + } else { + state.entities.push(entity); + } + res.json({ + name: body.name, + project: body.project, + status: "applied", + }); + }); + + app.post("/api/v1/data_sources", (req, res) => { + const body = req.body || {}; + const existingIndex = state.dataSources.findIndex( + (dataSource) => dataSource?.name === body.name, + ); + const dataSource = dataSourcePayloadToResource(body); + if (existingIndex >= 0) { + state.dataSources[existingIndex] = dataSource; + } else { + state.dataSources.push(dataSource); + } + res.json({ + name: body.name, + project: body.project, + status: "applied", + }); + }); + + app.post("/api/v1/feature_views", (req, res) => { + const body = req.body || {}; + const existingIndex = state.featureViews.findIndex( + (featureView) => featureView?.spec?.name === body.name, + ); + const featureView = featureViewPayloadToResource(body); + if (existingIndex >= 0) { + state.featureViews[existingIndex] = featureView; + } else { + state.featureViews.push(featureView); + } + res.json({ + name: body.name, + project: body.project, + status: "applied", + }); + }); + + app.delete("/api/v1/entities/:name", (req, res) => { + state.entities = state.entities.filter( + (entity) => entity?.spec?.name !== req.params.name, + ); + res.json({ + name: req.params.name, + project: req.query.project, + status: "deleted", + }); + }); + + app.delete("/api/v1/data_sources/:name", (req, res) => { + state.dataSources = state.dataSources.filter( + (dataSource) => dataSource?.name !== req.params.name, + ); + res.json({ + name: req.params.name, + project: req.query.project, + status: "deleted", + }); + }); + + app.delete("/api/v1/feature_views/:name", (req, res) => { + state.featureViews = state.featureViews.filter( + (featureView) => featureView?.spec?.name !== req.params.name, + ); + res.json({ + name: req.params.name, + project: req.query.project, + status: "deleted", + }); + }); +}; diff --git a/ui/src/utils/permissionUtils.ts b/ui/src/utils/permissionUtils.ts index 9caa162ec1c..2f1f8e06628 100644 --- a/ui/src/utils/permissionUtils.ts +++ b/ui/src/utils/permissionUtils.ts @@ -1,5 +1,15 @@ import { FEAST_FCO_TYPES } from "../parsers/types"; -import { feast } from "../protos"; + +/** + * Test if a regex pattern is potentially vulnerable to catastrophic backtracking. + * Rejects patterns with nested quantifiers like (a+)+ or (a*)* + */ +const isSafePattern = (pattern: string): boolean => { + if (pattern.length > 1000) return false; + // Reject nested quantifiers: a quantifier applied to a group containing a quantifier + if (/(\([^)]*[+*][^)]*\))[+*{]/.test(pattern)) return false; + return true; +}; /** * Get permissions for a specific entity @@ -43,6 +53,9 @@ export const getEntityPermissions = ( matchesName = true; // If no name patterns, matches all names } else { matchesName = permission.spec?.name_patterns?.some((pattern: string) => { + if (!pattern || !isSafePattern(pattern)) { + return pattern === entityName; + } try { const regex = new RegExp(pattern); return regex.test(entityName); diff --git a/ui/src/utils/timestamp.ts b/ui/src/utils/timestamp.ts index 390195f467b..5c85bc0de65 100644 --- a/ui/src/utils/timestamp.ts +++ b/ui/src/utils/timestamp.ts @@ -1,13 +1,20 @@ import Long from "long"; import { google } from "../protos"; -export function toDate(ts: google.protobuf.ITimestamp) { - var seconds: number; - if (ts.seconds instanceof Long) { - seconds = ts.seconds.low; - } else { - seconds = ts.seconds!; +export function toDate(ts: google.protobuf.ITimestamp | string | any): Date { + if (typeof ts === "string") { + return new Date(ts); } - return new Date(seconds * 1000); + if (ts && ts.seconds != null) { + var seconds: number; + if (ts.seconds instanceof Long) { + seconds = ts.seconds.low; + } else { + seconds = ts.seconds; + } + return new Date(seconds * 1000); + } + + return new Date(NaN); } diff --git a/ui/yarn.lock b/ui/yarn.lock index e0fa4f12a72..cb5589a81f9 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -4,12 +4,12 @@ "@adobe/css-tools@^4.4.0": version "4.4.4" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9" + resolved "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz" integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== "@apideck/better-ajv-errors@^0.3.1": version "0.3.6" - resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" + resolved "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz" integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== dependencies: json-schema "^0.4.0" @@ -18,7 +18,7 @@ "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.8.3": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: "@babel/helper-validator-identifier" "^7.27.1" @@ -27,12 +27,12 @@ "@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz" integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== "@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.21.3", "@babel/core@^7.23.9", "@babel/core@^7.25.8": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz" integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== dependencies: "@babel/code-frame" "^7.27.1" @@ -53,7 +53,7 @@ "@babel/eslint-parser@^7.16.3": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.28.5.tgz#0b8883a4a1c2cbed7b3cd9d7765d80e8f480b9ae" + resolved "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.5.tgz" integrity sha512-fcdRcWahONYo+JRnJg1/AekOacGvKx12Gu0qXJXFi2WBqQA1i7+O5PaxRB7kxE/Op94dExnCiiar6T09pvdHpA== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" @@ -62,7 +62,7 @@ "@babel/generator@^7.28.5", "@babel/generator@^7.7.2": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz" integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== dependencies: "@babel/parser" "^7.28.5" @@ -73,14 +73,14 @@ "@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz" integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== dependencies: "@babel/types" "^7.27.3" "@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz" integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== dependencies: "@babel/compat-data" "^7.27.2" @@ -91,7 +91,7 @@ "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3", "@babel/helper-create-class-features-plugin@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz" integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" @@ -104,7 +104,7 @@ "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz#7c1ddd64b2065c7f78034b25b43346a7e19ed997" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz" integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" @@ -113,7 +113,7 @@ "@babel/helper-define-polyfill-provider@^0.6.5": version "0.6.5" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz#742ccf1cb003c07b48859fc9fa2c1bbe40e5f753" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz" integrity sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg== dependencies: "@babel/helper-compilation-targets" "^7.27.2" @@ -124,12 +124,12 @@ "@babel/helper-globals@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== "@babel/helper-member-expression-to-functions@^7.27.1", "@babel/helper-member-expression-to-functions@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz" integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== dependencies: "@babel/traverse" "^7.28.5" @@ -137,7 +137,7 @@ "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz" integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== dependencies: "@babel/traverse" "^7.27.1" @@ -145,7 +145,7 @@ "@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz" integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== dependencies: "@babel/helper-module-imports" "^7.27.1" @@ -154,19 +154,19 @@ "@babel/helper-optimise-call-expression@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz" integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== dependencies: "@babel/types" "^7.27.1" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz" integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== "@babel/helper-remap-async-to-generator@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz" integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.1" @@ -175,7 +175,7 @@ "@babel/helper-replace-supers@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz" integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== dependencies: "@babel/helper-member-expression-to-functions" "^7.27.1" @@ -184,7 +184,7 @@ "@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz" integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== dependencies: "@babel/traverse" "^7.27.1" @@ -192,22 +192,22 @@ "@babel/helper-string-parser@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== "@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== "@babel/helper-validator-option@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== "@babel/helper-wrap-function@^7.27.1": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz#fe4872092bc1438ffd0ce579e6f699609f9d0a7a" + resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz" integrity sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g== dependencies: "@babel/template" "^7.27.2" @@ -216,7 +216,7 @@ "@babel/helpers@^7.28.4": version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz" integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== dependencies: "@babel/template" "^7.27.2" @@ -224,14 +224,14 @@ "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.15", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz" integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== dependencies: "@babel/types" "^7.28.5" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz#fbde57974707bbfa0376d34d425ff4fa6c732421" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz" integrity sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -239,21 +239,21 @@ "@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz" integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz" integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz" integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -262,7 +262,7 @@ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz#373f6e2de0016f73caf8f27004f61d167743742a" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz" integrity sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -270,7 +270,7 @@ "@babel/plugin-proposal-class-properties@^7.16.0": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz" integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== dependencies: "@babel/helper-create-class-features-plugin" "^7.18.6" @@ -278,7 +278,7 @@ "@babel/plugin-proposal-decorators@^7.16.4": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz#419c8acc31088e05a774344c021800f7ddc39bf0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz" integrity sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg== dependencies: "@babel/helper-create-class-features-plugin" "^7.27.1" @@ -287,7 +287,7 @@ "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz" integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== dependencies: "@babel/helper-plugin-utils" "^7.18.6" @@ -295,7 +295,7 @@ "@babel/plugin-proposal-numeric-separator@^7.16.0": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz" integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== dependencies: "@babel/helper-plugin-utils" "^7.18.6" @@ -303,7 +303,7 @@ "@babel/plugin-proposal-optional-chaining@^7.16.0": version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz" integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" @@ -312,7 +312,7 @@ "@babel/plugin-proposal-private-methods@^7.16.0": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz" integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== dependencies: "@babel/helper-create-class-features-plugin" "^7.18.6" @@ -320,12 +320,12 @@ "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== "@babel/plugin-proposal-private-property-in-object@^7.16.7", "@babel/plugin-proposal-private-property-in-object@^7.21.11": version "7.21.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz" integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" @@ -335,147 +335,147 @@ "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-class-static-block@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-decorators@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz#ee7dd9590aeebc05f9d4c8c0560007b05979a63d" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz" integrity sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-flow@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz#6c83cf0d7d635b716827284b7ecd5aead9237662" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz" integrity sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-import-assertions@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz" integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz" integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-jsx@^7.27.1", "@babel/plugin-syntax-jsx@^7.7.2": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz" integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-private-property-in-object@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.27.1", "@babel/plugin-syntax-typescript@^7.7.2": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz" integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz" integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.18.6" @@ -483,14 +483,14 @@ "@babel/plugin-transform-arrow-functions@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz" integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-async-generator-functions@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz#1276e6c7285ab2cd1eccb0bc7356b7a69ff842c2" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz" integrity sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -499,7 +499,7 @@ "@babel/plugin-transform-async-to-generator@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz" integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== dependencies: "@babel/helper-module-imports" "^7.27.1" @@ -508,21 +508,21 @@ "@babel/plugin-transform-block-scoped-functions@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz" integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-block-scoping@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz#e0d3af63bd8c80de2e567e690a54e84d85eb16f6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz" integrity sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-class-properties@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + resolved "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz" integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== dependencies: "@babel/helper-create-class-features-plugin" "^7.27.1" @@ -530,7 +530,7 @@ "@babel/plugin-transform-class-static-block@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz#d1b8e69b54c9993bc558203e1f49bfc979bfd852" + resolved "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz" integrity sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg== dependencies: "@babel/helper-create-class-features-plugin" "^7.28.3" @@ -538,7 +538,7 @@ "@babel/plugin-transform-classes@^7.28.4": version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz#75d66175486788c56728a73424d67cbc7473495c" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz" integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" @@ -550,7 +550,7 @@ "@babel/plugin-transform-computed-properties@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz" integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -558,7 +558,7 @@ "@babel/plugin-transform-destructuring@^7.28.0", "@babel/plugin-transform-destructuring@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz" integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -566,7 +566,7 @@ "@babel/plugin-transform-dotall-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz" integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -574,14 +574,14 @@ "@babel/plugin-transform-duplicate-keys@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz" integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz" integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -589,14 +589,14 @@ "@babel/plugin-transform-dynamic-import@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz" integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-explicit-resource-management@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz#45be6211b778dbf4b9d54c4e8a2b42fa72e09a1a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz" integrity sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -604,21 +604,21 @@ "@babel/plugin-transform-exponentiation-operator@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz#7cc90a8170e83532676cfa505278e147056e94fe" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz" integrity sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-export-namespace-from@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + resolved "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz" integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-flow-strip-types@^7.16.0": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz#5def3e1e7730f008d683144fb79b724f92c5cdf9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz" integrity sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -626,7 +626,7 @@ "@babel/plugin-transform-for-of@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz" integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -634,7 +634,7 @@ "@babel/plugin-transform-function-name@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz" integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== dependencies: "@babel/helper-compilation-targets" "^7.27.1" @@ -643,35 +643,35 @@ "@babel/plugin-transform-json-strings@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" + resolved "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz" integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-literals@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz" integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-logical-assignment-operators@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz#d028fd6db8c081dee4abebc812c2325e24a85b0e" + resolved "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz" integrity sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-member-expression-literals@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz" integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-modules-amd@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz" integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== dependencies: "@babel/helper-module-transforms" "^7.27.1" @@ -679,7 +679,7 @@ "@babel/plugin-transform-modules-commonjs@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz" integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== dependencies: "@babel/helper-module-transforms" "^7.27.1" @@ -687,7 +687,7 @@ "@babel/plugin-transform-modules-systemjs@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz#7439e592a92d7670dfcb95d0cbc04bd3e64801d2" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz" integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew== dependencies: "@babel/helper-module-transforms" "^7.28.3" @@ -697,7 +697,7 @@ "@babel/plugin-transform-modules-umd@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz" integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== dependencies: "@babel/helper-module-transforms" "^7.27.1" @@ -705,7 +705,7 @@ "@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz" integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -713,28 +713,28 @@ "@babel/plugin-transform-new-target@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz" integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + resolved "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz" integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-numeric-separator@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz" integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-object-rest-spread@^7.28.4": version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz#9ee1ceca80b3e6c4bac9247b2149e36958f7f98d" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz" integrity sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew== dependencies: "@babel/helper-compilation-targets" "^7.27.2" @@ -745,7 +745,7 @@ "@babel/plugin-transform-object-super@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz" integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -753,14 +753,14 @@ "@babel/plugin-transform-optional-catch-binding@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz" integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz#8238c785f9d5c1c515a90bf196efb50d075a4b26" + resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz" integrity sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -768,14 +768,14 @@ "@babel/plugin-transform-parameters@^7.27.7": version "7.27.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz" integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-private-methods@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + resolved "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz" integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== dependencies: "@babel/helper-create-class-features-plugin" "^7.27.1" @@ -783,7 +783,7 @@ "@babel/plugin-transform-private-property-in-object@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + resolved "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz" integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== dependencies: "@babel/helper-annotate-as-pure" "^7.27.1" @@ -792,35 +792,35 @@ "@babel/plugin-transform-property-literals@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz" integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-react-constant-elements@^7.21.3": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz#6c6b50424e749a6e48afd14cf7b92f98cb9383f9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz" integrity sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz" integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-react-jsx-development@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz#47ff95940e20a3a70e68ad3d4fcb657b647f6c98" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz" integrity sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q== dependencies: "@babel/plugin-transform-react-jsx" "^7.27.1" "@babel/plugin-transform-react-jsx@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz" integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== dependencies: "@babel/helper-annotate-as-pure" "^7.27.1" @@ -831,7 +831,7 @@ "@babel/plugin-transform-react-pure-annotations@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz#339f1ce355eae242e0649f232b1c68907c02e879" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz" integrity sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.1" @@ -839,14 +839,14 @@ "@babel/plugin-transform-regenerator@^7.28.4": version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz#9d3fa3bebb48ddd0091ce5729139cd99c67cea51" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz" integrity sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-regexp-modifiers@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz" integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -854,14 +854,14 @@ "@babel/plugin-transform-reserved-words@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz" integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-runtime@^7.16.4": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz#ae3e21fbefe2831ebac04dfa6b463691696afe17" + resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz" integrity sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w== dependencies: "@babel/helper-module-imports" "^7.27.1" @@ -873,14 +873,14 @@ "@babel/plugin-transform-shorthand-properties@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz" integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-spread@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz" integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -888,28 +888,28 @@ "@babel/plugin-transform-sticky-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz" integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-template-literals@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz" integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-typeof-symbol@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz" integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-typescript@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz#441c5f9a4a1315039516c6c612fc66d5f4594e72" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz" integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" @@ -920,14 +920,14 @@ "@babel/plugin-transform-unicode-escapes@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz" integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-unicode-property-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz" integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -935,7 +935,7 @@ "@babel/plugin-transform-unicode-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz" integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -943,7 +943,7 @@ "@babel/plugin-transform-unicode-sets-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz" integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -951,7 +951,7 @@ "@babel/preset-env@^7.11.0", "@babel/preset-env@^7.16.4", "@babel/preset-env@^7.20.2", "@babel/preset-env@^7.25.8": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.5.tgz#82dd159d1563f219a1ce94324b3071eb89e280b0" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz" integrity sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg== dependencies: "@babel/compat-data" "^7.28.5" @@ -1027,7 +1027,7 @@ "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz" integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -1036,7 +1036,7 @@ "@babel/preset-react@^7.16.0", "@babel/preset-react@^7.18.6", "@babel/preset-react@^7.25.7": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.28.5.tgz#6fcc0400fa79698433d653092c3919bb4b0878d9" + resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz" integrity sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -1048,7 +1048,7 @@ "@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.21.0": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz#540359efa3028236958466342967522fd8f2a60c" + resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz" integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -1059,12 +1059,12 @@ "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.8", "@babel/runtime@^7.24.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.9.2": version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== "@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.3.3": version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== dependencies: "@babel/code-frame" "^7.27.1" @@ -1073,7 +1073,7 @@ "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz" integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== dependencies: "@babel/code-frame" "^7.27.1" @@ -1086,7 +1086,7 @@ "@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz" integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== dependencies: "@babel/helper-string-parser" "^7.27.1" @@ -1094,22 +1094,22 @@ "@base2/pretty-print-object@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" + resolved "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz" integrity sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA== "@bcoe/v8-coverage@^0.2.3": version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@csstools/normalize.css@*": version "12.1.1" - resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.1.1.tgz#f0ad221b7280f3fc814689786fd9ee092776ef8f" + resolved "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz" integrity sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ== "@csstools/postcss-cascade-layers@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz#8a997edf97d34071dd2e37ea6022447dd9e795ad" + resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz" integrity sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA== dependencies: "@csstools/selector-specificity" "^2.0.2" @@ -1117,7 +1117,7 @@ "@csstools/postcss-color-function@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz#2bd36ab34f82d0497cfacdc9b18d34b5e6f64b6b" + resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz" integrity sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1125,21 +1125,21 @@ "@csstools/postcss-font-format-keywords@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz#677b34e9e88ae997a67283311657973150e8b16a" + resolved "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz" integrity sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-hwb-function@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz#ab54a9fce0ac102c754854769962f2422ae8aa8b" + resolved "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz" integrity sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-ic-unit@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz#28237d812a124d1a16a5acc5c3832b040b303e58" + resolved "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz" integrity sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1147,7 +1147,7 @@ "@csstools/postcss-is-pseudo-class@^2.0.7": version "2.0.7" - resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz#846ae6c0d5a1eaa878fce352c544f9c295509cd1" + resolved "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz" integrity sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA== dependencies: "@csstools/selector-specificity" "^2.0.0" @@ -1155,21 +1155,21 @@ "@csstools/postcss-nested-calc@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz#d7e9d1d0d3d15cf5ac891b16028af2a1044d0c26" + resolved "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz" integrity sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-normalize-display-values@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz#15da54a36e867b3ac5163ee12c1d7f82d4d612c3" + resolved "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz" integrity sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-oklab-function@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz#88cee0fbc8d6df27079ebd2fa016ee261eecf844" + resolved "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz" integrity sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1177,57 +1177,57 @@ "@csstools/postcss-progressive-custom-properties@^1.1.0", "@csstools/postcss-progressive-custom-properties@^1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz#542292558384361776b45c85226b9a3a34f276fa" + resolved "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz" integrity sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-stepped-value-functions@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz#f8772c3681cc2befed695e2b0b1d68e22f08c4f4" + resolved "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz" integrity sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-text-decoration-shorthand@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz#ea96cfbc87d921eca914d3ad29340d9bcc4c953f" + resolved "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz" integrity sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-trigonometric-functions@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz#94d3e4774c36d35dcdc88ce091336cb770d32756" + resolved "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz" integrity sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-unset-value@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz#c99bb70e2cdc7312948d1eb41df2412330b81f77" + resolved "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz" integrity sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g== "@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2": version "2.2.0" - resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016" + resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz" integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== "@elastic/datemath@^5.0.3": version "5.0.3" - resolved "https://registry.yarnpkg.com/@elastic/datemath/-/datemath-5.0.3.tgz#7baccdab672b9a3ecb7fe8387580670936b58573" + resolved "https://registry.npmjs.org/@elastic/datemath/-/datemath-5.0.3.tgz" integrity sha512-8Hbr1Uyjm5OcYBfEB60K7sCP6U3IXuWDaLaQmYv3UxgI4jqBWbakoemwWvsqPVUvnwEjuX6z7ghPZbefs8xiaA== dependencies: tslib "^1.9.3" "@elastic/eui-theme-borealis@1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@elastic/eui-theme-borealis/-/eui-theme-borealis-1.0.0.tgz#f85679d2d72dfc43a620241cbf4161d4e4e81841" + resolved "https://registry.npmjs.org/@elastic/eui-theme-borealis/-/eui-theme-borealis-1.0.0.tgz" integrity sha512-Zf3ZX5siUhF+TNOdP0FZ3PNEpVmfe3DDXFm5biAKFlGp4e5yrR1FKPYOzkOdJtPWlOoNaedawnALXNVjp1UH8w== "@elastic/eui@^95.12.0": version "95.12.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-95.12.0.tgz#862f2be8b72248a62b40704b9e62f2f5d7d43853" + resolved "https://registry.npmjs.org/@elastic/eui/-/eui-95.12.0.tgz" integrity sha512-SW4ru97FY2VitSqyCgURrM5OMk1W+Ww12b6S+VZN5ex50aNT296DfED/ByidlYaAoVihqjZuoB3HlQBBXydFpA== dependencies: "@hello-pangea/dnd" "^16.6.0" @@ -1266,7 +1266,7 @@ "@emotion/babel-plugin@^11.13.5": version "11.13.5" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz#eab8d65dbded74e0ecfd28dc218e75607c4e7bc0" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== dependencies: "@babel/helper-module-imports" "^7.16.7" @@ -1283,7 +1283,7 @@ "@emotion/cache@^11.13.5", "@emotion/cache@^11.14.0": version "11.14.0" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz" integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== dependencies: "@emotion/memoize" "^0.9.0" @@ -1294,7 +1294,7 @@ "@emotion/css@^11.13.0": version "11.13.5" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.13.5.tgz#db2d3be6780293640c082848e728a50544b9dfa4" + resolved "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz" integrity sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w== dependencies: "@emotion/babel-plugin" "^11.13.5" @@ -1305,29 +1305,29 @@ "@emotion/hash@^0.9.2": version "0.9.2" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz" integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== "@emotion/is-prop-valid@1.2.2": version "1.2.2" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz" integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== dependencies: "@emotion/memoize" "^0.8.1" "@emotion/memoize@^0.8.1": version "0.8.1" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== "@emotion/memoize@^0.9.0": version "0.9.0" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== "@emotion/react@^11.13.3": version "11.14.0" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.14.0.tgz#cfaae35ebc67dd9ef4ea2e9acc6cd29e157dd05d" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== dependencies: "@babel/runtime" "^7.18.3" @@ -1341,7 +1341,7 @@ "@emotion/serialize@^1.3.3": version "1.3.3" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.3.tgz#d291531005f17d704d0463a032fe679f376509e8" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz" integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== dependencies: "@emotion/hash" "^0.9.2" @@ -1352,49 +1352,49 @@ "@emotion/sheet@^1.4.0": version "1.4.0" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== "@emotion/unitless@0.8.1": version "0.8.1" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz" integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== "@emotion/unitless@^0.10.0": version "0.10.0" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz" integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== "@emotion/use-insertion-effect-with-fallbacks@^1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz#8a8cb77b590e09affb960f4ff1e9a89e532738bf" + resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz" integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== "@emotion/utils@^1.4.2": version "1.4.2" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.2.tgz#6df6c45881fcb1c412d6688a311a98b7f59c1b52" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz" integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== "@emotion/weak-memoize@^0.4.0": version "0.4.0" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== "@eslint-community/eslint-utils@^4.2.0": version "4.9.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz" integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: eslint-visitor-keys "^3.4.3" "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": version "4.12.2" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz" integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== "@eslint/eslintrc@^2.1.4": version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" @@ -1409,12 +1409,12 @@ "@eslint/js@8.57.1": version "8.57.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== "@happy-dom/jest-environment@^16.7.3": version "16.8.1" - resolved "https://registry.yarnpkg.com/@happy-dom/jest-environment/-/jest-environment-16.8.1.tgz#c2c49923d7aec45123361d7756615884fddb4756" + resolved "https://registry.npmjs.org/@happy-dom/jest-environment/-/jest-environment-16.8.1.tgz" integrity sha512-TNzYvCYyNySVM+an4fM5l4FIiRtBFiu7m5eyiXtIDWHkS3WLGvJ9yrRkzHtSEqDA7YOYdTIsEn5SQNQ7LCNn0Q== dependencies: "@jest/environment" "^29.4.0" @@ -1426,7 +1426,7 @@ "@hello-pangea/dnd@^16.6.0": version "16.6.0" - resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.6.0.tgz#7509639c7bd13f55e537b65a9dcfcd54e7c99ac7" + resolved "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.6.0.tgz" integrity sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ== dependencies: "@babel/runtime" "^7.24.1" @@ -1439,7 +1439,7 @@ "@humanwhocodes/config-array@^0.13.0": version "0.13.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz" integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: "@humanwhocodes/object-schema" "^2.0.3" @@ -1448,22 +1448,22 @@ "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/object-schema@^2.0.3": version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== "@inquirer/ansi@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-1.0.2.tgz#674a4c4d81ad460695cb2a1fc69d78cd187f337e" + resolved "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz" integrity sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ== "@inquirer/confirm@^5.0.0": version "5.1.21" - resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.21.tgz#610c4acd7797d94890a6e2dde2c98eb1e891dd12" + resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz" integrity sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ== dependencies: "@inquirer/core" "^10.3.2" @@ -1471,7 +1471,7 @@ "@inquirer/core@^10.3.2": version "10.3.2" - resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.3.2.tgz#535979ff3ff4fe1e7cc4f83e2320504c743b7e20" + resolved "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz" integrity sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A== dependencies: "@inquirer/ansi" "^1.0.2" @@ -1485,29 +1485,29 @@ "@inquirer/figures@^1.0.15": version "1.0.15" - resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.15.tgz#dbb49ed80df11df74268023b496ac5d9acd22b3a" + resolved "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz" integrity sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g== "@inquirer/type@^3.0.10": version "3.0.10" - resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.10.tgz#11ed564ec78432a200ea2601a212d24af8150d50" + resolved "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz" integrity sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA== "@isaacs/balanced-match@^4.0.1": version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + resolved "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz" integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== "@isaacs/brace-expansion@^5.0.0": version "5.0.0" - resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + resolved "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz" integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== dependencies: "@isaacs/balanced-match" "^4.0.1" "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== dependencies: camelcase "^5.3.1" @@ -1518,12 +1518,12 @@ "@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== "@jest/console@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + resolved "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz" integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== dependencies: "@jest/types" "^29.6.3" @@ -1535,7 +1535,7 @@ "@jest/core@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + resolved "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz" integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== dependencies: "@jest/console" "^29.7.0" @@ -1569,7 +1569,7 @@ "@jest/environment@^29.4.0", "@jest/environment@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz" integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== dependencies: "@jest/fake-timers" "^29.7.0" @@ -1579,14 +1579,14 @@ "@jest/expect-utils@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz" integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== dependencies: jest-get-type "^29.6.3" "@jest/expect@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz" integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== dependencies: expect "^29.7.0" @@ -1594,7 +1594,7 @@ "@jest/fake-timers@^29.4.0", "@jest/fake-timers@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz" integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== dependencies: "@jest/types" "^29.6.3" @@ -1606,7 +1606,7 @@ "@jest/globals@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz" integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== dependencies: "@jest/environment" "^29.7.0" @@ -1616,7 +1616,7 @@ "@jest/reporters@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz" integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== dependencies: "@bcoe/v8-coverage" "^0.2.3" @@ -1646,14 +1646,14 @@ "@jest/schemas@^29.6.3": version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== dependencies: "@sinclair/typebox" "^0.27.8" "@jest/source-map@^29.6.3": version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz" integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== dependencies: "@jridgewell/trace-mapping" "^0.3.18" @@ -1662,7 +1662,7 @@ "@jest/test-result@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz" integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== dependencies: "@jest/console" "^29.7.0" @@ -1672,7 +1672,7 @@ "@jest/test-sequencer@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz" integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== dependencies: "@jest/test-result" "^29.7.0" @@ -1682,7 +1682,7 @@ "@jest/transform@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz" integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== dependencies: "@babel/core" "^7.1.0" @@ -1703,7 +1703,7 @@ "@jest/transform@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== dependencies: "@babel/core" "^7.11.6" @@ -1724,7 +1724,7 @@ "@jest/types@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + resolved "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz" integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -1735,7 +1735,7 @@ "@jest/types@^29.4.0", "@jest/types@^29.6.3": version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== dependencies: "@jest/schemas" "^29.6.3" @@ -1747,7 +1747,7 @@ "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" @@ -1755,7 +1755,7 @@ "@jridgewell/remapping@^2.3.5": version "2.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz" integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== dependencies: "@jridgewell/gen-mapping" "^0.3.5" @@ -1763,12 +1763,12 @@ "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/source-map@^0.3.3": version "0.3.11" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz" integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== dependencies: "@jridgewell/gen-mapping" "^0.3.5" @@ -1776,12 +1776,12 @@ "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.5" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": version "0.3.31" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" @@ -1789,26 +1789,26 @@ "@jsdoc/salty@^0.2.1": version "0.2.9" - resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.9.tgz#4d8c147f7ca011532681ce86352a77a0178f1dec" + resolved "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz" integrity sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw== dependencies: lodash "^4.17.21" "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== "@mapbox/hast-util-table-cell-style@^0.2.0": version "0.2.1" - resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz#b8e92afdd38b668cf0762400de980073d2ade101" + resolved "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz" integrity sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw== dependencies: unist-util-visit "^1.4.1" "@mswjs/interceptors@^0.40.0": version "0.40.0" - resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.40.0.tgz#1b45f215ba8c2983ed133763ca03af92896083d6" + resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz" integrity sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ== dependencies: "@open-draft/deferred-promise" "^2.2.0" @@ -1820,14 +1820,14 @@ "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" - resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" + resolved "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz" integrity sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg== dependencies: eslint-scope "5.1.1" "@nodelib/fs.scandir@2.1.5": version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" @@ -1835,12 +1835,12 @@ "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" @@ -1848,12 +1848,12 @@ "@open-draft/deferred-promise@^2.2.0": version "2.2.0" - resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" + resolved "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz" integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA== "@open-draft/logger@^0.3.0": version "0.3.0" - resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954" + resolved "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz" integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ== dependencies: is-node-process "^1.2.0" @@ -1861,12 +1861,12 @@ "@open-draft/until@^2.0.0": version "2.1.0" - resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" + resolved "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.17" - resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz#8c2f34ca8651df74895422046e11ce5a120e7930" + resolved "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz" integrity sha512-tXDyE1/jzFsHXjhRZQ3hMl0IVhYe5qula43LDWIhVfjp9G/nT5OQY5AORVOrkEGAUltBJOfOWeETbmhm6kHhuQ== dependencies: ansi-html "^0.0.9" @@ -1879,27 +1879,27 @@ "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== "@protobufjs/base64@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== "@protobufjs/codegen@^2.0.4": version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz" integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== "@protobufjs/eventemitter@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== "@protobufjs/fetch@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== dependencies: "@protobufjs/aspromise" "^1.1.1" @@ -1907,32 +1907,32 @@ "@protobufjs/float@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== "@protobufjs/inquire@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz" integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== "@protobufjs/path@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz" integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== "@protobufjs/pool@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz" integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== "@protobufjs/utf8@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== "@reactflow/background@11.3.14": version "11.3.14" - resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.3.14.tgz#778ca30174f3de77fc321459ab3789e66e71a699" + resolved "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz" integrity sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA== dependencies: "@reactflow/core" "11.11.4" @@ -1941,7 +1941,7 @@ "@reactflow/controls@11.2.14": version "11.2.14" - resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.2.14.tgz#508ed2c40d23341b3b0919dd11e76fd49cf850c7" + resolved "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz" integrity sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw== dependencies: "@reactflow/core" "11.11.4" @@ -1950,7 +1950,7 @@ "@reactflow/core@11.11.4": version "11.11.4" - resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.11.4.tgz#89bd86d1862aa1416f3f49926cede7e8c2aab6a7" + resolved "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz" integrity sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q== dependencies: "@types/d3" "^7.4.0" @@ -1965,7 +1965,7 @@ "@reactflow/minimap@11.7.14": version "11.7.14" - resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.7.14.tgz#298d7a63cb1da06b2518c99744f716560c88ca73" + resolved "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz" integrity sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ== dependencies: "@reactflow/core" "11.11.4" @@ -1978,7 +1978,7 @@ "@reactflow/node-resizer@2.2.14": version "2.2.14" - resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz#1810c0ce51aeb936f179466a6660d1e02c7a77a8" + resolved "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz" integrity sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA== dependencies: "@reactflow/core" "11.11.4" @@ -1989,7 +1989,7 @@ "@reactflow/node-toolbar@1.3.14": version "1.3.14" - resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz#c6ffc76f82acacdce654f2160dc9852162d6e7c9" + resolved "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz" integrity sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ== dependencies: "@reactflow/core" "11.11.4" @@ -1998,12 +1998,12 @@ "@remix-run/router@1.23.1": version "1.23.1" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.1.tgz#0ce8857b024e24fc427585316383ad9d295b3a7f" + resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz" integrity sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ== "@rollup/plugin-babel@^5.2.0", "@rollup/plugin-babel@^5.3.1": version "5.3.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz" integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== dependencies: "@babel/helper-module-imports" "^7.10.4" @@ -2011,7 +2011,7 @@ "@rollup/plugin-commonjs@^21.0.2": version "21.1.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.1.0.tgz#45576d7b47609af2db87f55a6d4b46e44fc3a553" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.1.0.tgz" integrity sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2024,14 +2024,14 @@ "@rollup/plugin-json@^4.1.0": version "4.1.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" + resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz" integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== dependencies: "@rollup/pluginutils" "^3.0.8" "@rollup/plugin-node-resolve@^11.2.1": version "11.2.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz" integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2043,7 +2043,7 @@ "@rollup/plugin-node-resolve@^13.1.3": version "13.3.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz" integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2055,7 +2055,7 @@ "@rollup/plugin-replace@^2.4.1": version "2.4.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz" integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2063,7 +2063,7 @@ "@rollup/plugin-typescript@^8.3.1": version "8.5.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz#7ea11599a15b0a30fa7ea69ce3b791d41b862515" + resolved "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz" integrity sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2071,7 +2071,7 @@ "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0": version "3.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== dependencies: "@types/estree" "0.0.39" @@ -2080,7 +2080,7 @@ "@rollup/pluginutils@^5.1.3": version "5.3.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz#57ba1b0cbda8e7a3c597a4853c807b156e21a7b4" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz" integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q== dependencies: "@types/estree" "^1.0.0" @@ -2089,36 +2089,36 @@ "@rtsao/scc@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== "@rushstack/eslint-patch@^1.1.0": version "1.15.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz#8184bcb37791e6d3c3c13a9bfbe4af263f66665f" + resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz" integrity sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw== "@sinclair/typebox@^0.27.8": version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== "@sinonjs/commons@^3.0.0": version "3.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz" integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@^10.0.2": version "10.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz" integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== dependencies: "@sinonjs/commons" "^3.0.0" "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" - resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" + resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz" integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== dependencies: ejs "^3.1.6" @@ -2128,47 +2128,47 @@ "@svgr/babel-plugin-add-jsx-attribute@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz" integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== "@svgr/babel-plugin-remove-jsx-attribute@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz" integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== "@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz" integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== "@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz" integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== "@svgr/babel-plugin-svg-dynamic-title@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz" integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== "@svgr/babel-plugin-svg-em-dimensions@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz" integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== "@svgr/babel-plugin-transform-react-native-svg@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz" integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== "@svgr/babel-plugin-transform-svg-component@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz" integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== "@svgr/babel-preset@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz" integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== dependencies: "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" @@ -2182,7 +2182,7 @@ "@svgr/core@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz" integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== dependencies: "@babel/core" "^7.21.3" @@ -2193,7 +2193,7 @@ "@svgr/hast-util-to-babel-ast@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz" integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== dependencies: "@babel/types" "^7.21.3" @@ -2201,7 +2201,7 @@ "@svgr/plugin-jsx@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz" integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== dependencies: "@babel/core" "^7.21.3" @@ -2211,7 +2211,7 @@ "@svgr/plugin-svgo@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" + resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz" integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== dependencies: cosmiconfig "^8.1.3" @@ -2220,7 +2220,7 @@ "@svgr/webpack@^8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" + resolved "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz" integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== dependencies: "@babel/core" "^7.21.3" @@ -2234,7 +2234,7 @@ "@testing-library/dom@^10.4.0": version "10.4.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz" integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== dependencies: "@babel/code-frame" "^7.10.4" @@ -2248,7 +2248,7 @@ "@testing-library/jest-dom@^6.5.0": version "6.9.1" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2" + resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz" integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== dependencies: "@adobe/css-tools" "^4.4.0" @@ -2260,34 +2260,34 @@ "@testing-library/react@^16.0.1": version "16.3.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.0.tgz#3a85bb9bdebf180cd76dba16454e242564d598a6" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz" integrity sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw== dependencies: "@babel/runtime" "^7.12.5" "@testing-library/user-event@^14.5.2": version "14.6.1" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" + resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz" integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== "@tootallnate/once@2": version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== "@trysound/sax@0.2.0": version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== "@types/aria-query@^5.0.1": version "5.0.4" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz" integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== dependencies: "@babel/parser" "^7.20.7" @@ -2298,14 +2298,14 @@ "@types/babel__generator@*": version "7.27.0" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== dependencies: "@babel/parser" "^7.1.0" @@ -2313,14 +2313,14 @@ "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": version "7.28.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz" integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== dependencies: "@babel/types" "^7.28.2" "@types/body-parser@*": version "1.19.6" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz" integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== dependencies: "@types/connect" "*" @@ -2328,14 +2328,14 @@ "@types/bonjour@^3.5.9": version "3.5.13" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz" integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== dependencies: "@types/node" "*" "@types/connect-history-api-fallback@^1.3.5": version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz" integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== dependencies: "@types/express-serve-static-core" "*" @@ -2343,43 +2343,43 @@ "@types/connect@*": version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" "@types/d3-array@*": version "3.2.2" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c" + resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz" integrity sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw== "@types/d3-axis@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + resolved "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz" integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== dependencies: "@types/d3-selection" "*" "@types/d3-brush@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + resolved "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz" integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== dependencies: "@types/d3-selection" "*" "@types/d3-chord@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + resolved "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz" integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== "@types/d3-color@*": version "3.1.3" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== "@types/d3-contour@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + resolved "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz" integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== dependencies: "@types/d3-array" "*" @@ -2387,136 +2387,136 @@ "@types/d3-delaunay@*": version "6.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + resolved "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz" integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== "@types/d3-dispatch@*": version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz#ef004d8a128046cfce434d17182f834e44ef95b2" + resolved "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz" integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA== "@types/d3-drag@*", "@types/d3-drag@^3.0.1": version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + resolved "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz" integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== dependencies: "@types/d3-selection" "*" "@types/d3-dsv@*": version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + resolved "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz" integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== "@types/d3-ease@*": version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + resolved "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz" integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== "@types/d3-fetch@*": version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + resolved "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz" integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== dependencies: "@types/d3-dsv" "*" "@types/d3-force@*": version "3.0.10" - resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" + resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz" integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== "@types/d3-format@*": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + resolved "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz" integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== "@types/d3-geo@*": version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + resolved "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz" integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== dependencies: "@types/geojson" "*" "@types/d3-hierarchy@*": version "3.1.7" - resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + resolved "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz" integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== "@types/d3-interpolate@*": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== dependencies: "@types/d3-color" "*" "@types/d3-path@*": version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz" integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== "@types/d3-polygon@*": version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + resolved "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz" integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== "@types/d3-quadtree@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + resolved "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz" integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== "@types/d3-random@*": version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + resolved "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz" integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== "@types/d3-scale-chromatic@*": version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" + resolved "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz" integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== "@types/d3-scale@*": version "4.0.9" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" + resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz" integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== dependencies: "@types/d3-time" "*" "@types/d3-selection@*", "@types/d3-selection@^3.0.3": version "3.0.11" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz" integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== "@types/d3-shape@*": version "3.1.7" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz" integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== dependencies: "@types/d3-path" "*" "@types/d3-time-format@*": version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + resolved "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz" integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== "@types/d3-time@*": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz" integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== "@types/d3-timer@*": version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz" integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== "@types/d3-transition@*": version "3.0.9" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + resolved "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz" integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== dependencies: "@types/d3-selection" "*" "@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": version "3.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz" integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== dependencies: "@types/d3-interpolate" "*" @@ -2524,7 +2524,7 @@ "@types/d3@^7.4.0": version "7.4.3" - resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + resolved "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz" integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== dependencies: "@types/d3-array" "*" @@ -2560,46 +2560,38 @@ "@types/dagre@^0.7.52": version "0.7.53" - resolved "https://registry.yarnpkg.com/@types/dagre/-/dagre-0.7.53.tgz#4dab441bf31b6fb08af0b3e2a3f5ab0c0217a701" + resolved "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz" integrity sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ== "@types/eslint-scope@^3.7.7": version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz" integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== dependencies: "@types/eslint" "*" "@types/estree" "*" -"@types/eslint@*": - version "9.6.1" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" - integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/eslint@^7.29.0 || ^8.4.1": +"@types/eslint@*", "@types/eslint@^7.29.0 || ^8.4.1": version "8.56.12" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.12.tgz#1657c814ffeba4d2f84c0d4ba0f44ca7ea1ca53a" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz" integrity sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g== dependencies: "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" - integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== - -"@types/estree@0.0.39": +"@types/estree@*", "@types/estree@0.0.39": version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": +"@types/estree@^1.0.0", "@types/estree@^1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/express-serve-static-core@*": version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz#74f47555b3d804b54cb7030e6f9aa0c7485cfc5b" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz" integrity sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA== dependencies: "@types/node" "*" @@ -2609,7 +2601,7 @@ "@types/express-serve-static-core@^4.17.33": version "4.19.7" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz#f1d306dcc03b1aafbfb6b4fe684cce8a31cffc10" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz" integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg== dependencies: "@types/node" "*" @@ -2617,18 +2609,9 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@*": - version "5.0.5" - resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.5.tgz#3ba069177caa34ab96585ca23b3984d752300cdc" - integrity sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^5.0.0" - "@types/serve-static" "^1" - -"@types/express@^4.17.13": +"@types/express@*", "@types/express@^4.17.13": version "4.17.25" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz" integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== dependencies: "@types/body-parser" "*" @@ -2638,19 +2621,19 @@ "@types/fs-extra@^8.0.1": version "8.1.5" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.5.tgz#33aae2962d3b3ec9219b5aca2555ee00274f5927" + resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz" integrity sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ== dependencies: "@types/node" "*" "@types/geojson@*": version "7946.0.16" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz" integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== "@types/glob@^7.1.1": version "7.2.0" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + resolved "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz" integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== dependencies: "@types/minimatch" "*" @@ -2658,64 +2641,64 @@ "@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3": version "4.1.9" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz" integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== dependencies: "@types/node" "*" "@types/hast@^2.0.0": version "2.3.10" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + resolved "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz" integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== dependencies: "@types/unist" "^2" "@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.1": version "3.3.7" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c" + resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz" integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g== dependencies: hoist-non-react-statics "^3.3.0" "@types/html-minifier-terser@^6.0.0": version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" + resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== "@types/http-errors@*": version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz" integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== "@types/http-proxy@^1.17.8": version "1.17.17" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.17.tgz#d9e2c4571fe3507343cb210cd41790375e59a533" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz" integrity sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw== dependencies: "@types/node" "*" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== "@types/istanbul-lib-report@*": version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz" integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" "@types/jest@^27.0.1": version "27.5.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.2.tgz#ec49d29d926500ffb9fd22b84262e862049c026c" + resolved "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz" integrity sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA== dependencies: jest-matcher-utils "^27.0.0" @@ -2723,7 +2706,7 @@ "@types/jsdom@^20.0.0": version "20.0.1" - resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" + resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz" integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== dependencies: "@types/node" "*" @@ -2732,27 +2715,27 @@ "@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/json5@^0.0.29": version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/linkify-it@^5": version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz" integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== "@types/lodash@^4.14.202": version "4.17.20" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz" integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA== "@types/markdown-it@^14.1.1": version "14.1.2" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz" integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== dependencies: "@types/linkify-it" "^5" @@ -2760,106 +2743,92 @@ "@types/mdast@^3.0.0": version "3.0.15" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz" integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== dependencies: "@types/unist" "^2" "@types/mdurl@^2": version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz" integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== "@types/mime@^1": version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/minimatch@*", "@types/minimatch@^6.0.0": version "6.0.0" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-6.0.0.tgz#4d207b1cc941367bdcd195a3a781a7e4fc3b1e03" + resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-6.0.0.tgz" integrity sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA== dependencies: minimatch "*" "@types/node-forge@^1.3.0": version "1.3.14" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b" + resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz" integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw== dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=13.7.0": - version "24.10.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01" - integrity sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ== - dependencies: - undici-types "~7.16.0" - -"@types/node@^22.12.0": +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^22.12.0": version "22.19.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.1.tgz#1188f1ddc9f46b4cc3aec76749050b4e1f459b7b" + resolved "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz" integrity sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ== dependencies: undici-types "~6.21.0" "@types/numeral@^2.0.5": version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" + resolved "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.5.tgz" integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== "@types/parse-json@^4.0.0": version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== "@types/parse5@^5.0.0": version "5.0.3" - resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" + resolved "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== "@types/prismjs@*": version "1.26.5" - resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6" + resolved "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz" integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ== "@types/prop-types@*": version "15.7.15" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz" integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== "@types/qs@*": version "6.14.0" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz" integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== "@types/range-parser@*": version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/react-dom@^18.3.0": version "18.3.7" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz" integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ== "@types/react-window@^1.8.8": version "1.8.8" - resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + resolved "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz" integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== dependencies: "@types/react" "*" -"@types/react@*": - version "19.2.6" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.6.tgz#d27db1ff45012d53980f5589fda925278e1249ca" - integrity sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w== - dependencies: - csstype "^3.2.2" - -"@types/react@^18.3.11": +"@types/react@*", "@types/react@^18.3.11": version "18.3.27" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.27.tgz#74a3b590ea183983dc65a474dc17553ae1415c34" + resolved "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz" integrity sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w== dependencies: "@types/prop-types" "*" @@ -2867,38 +2836,38 @@ "@types/refractor@^3.4.0": version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.4.1.tgz#8b109804f77b3da8fad543d3f575fef1ece8835a" + resolved "https://registry.npmjs.org/@types/refractor/-/refractor-3.4.1.tgz" integrity sha512-wYuorIiCTSuvRT9srwt+taF6mH/ww+SyN2psM0sjef2qW+sS8GmshgDGTEDgWB1sTVGgYVE6EK7dBA2MxQxibg== dependencies: "@types/prismjs" "*" "@types/resolve@1.17.1": version "1.17.1" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz" integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== dependencies: "@types/node" "*" "@types/retry@0.12.0": version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== "@types/semver@^7.3.12": version "7.7.1" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz" integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA== "@types/send@*": version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" + resolved "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz" integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== dependencies: "@types/node" "*" "@types/send@<1": version "0.17.6" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.6.tgz#aeb5385be62ff58a52cd5459daa509ae91651d25" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz" integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== dependencies: "@types/mime" "^1" @@ -2906,14 +2875,14 @@ "@types/serve-index@^1.9.1": version "1.9.4" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz" integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== dependencies: "@types/express" "*" "@types/serve-static@^1", "@types/serve-static@^1.13.10": version "1.15.10" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz" integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== dependencies: "@types/http-errors" "*" @@ -2922,24 +2891,24 @@ "@types/sockjs@^0.3.33": version "0.3.36" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz" integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== dependencies: "@types/node" "*" "@types/stack-utils@^2.0.0": version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== "@types/statuses@^2.0.4": version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa" + resolved "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz" integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA== "@types/styled-components@^5.1.34": version "5.1.36" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.36.tgz#d63db8ad9005afc82f173012036c4c101dc93d57" + resolved "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.36.tgz" integrity sha512-pGMRNY5G2rNDKEv2DOiFYa7Ft1r0jrhmgBwHhOMzPTgCjO76bCot0/4uEfqj7K0Jf1KdQmDtAuaDk9EAs9foSw== dependencies: "@types/hoist-non-react-statics" "*" @@ -2948,58 +2917,58 @@ "@types/stylis@4.2.5": version "4.2.5" - resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df" + resolved "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz" integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== "@types/tough-cookie@*": version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz" integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== "@types/trusted-types@^2.0.2": version "2.0.7" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== "@types/unist@^2", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": version "2.0.11" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== "@types/use-sync-external-store@^0.0.3": version "0.0.3" - resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== "@types/ws@^8.5.5": version "8.18.1" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz" integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" "@types/yargs-parser@*": version "21.0.3" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== "@types/yargs@^16.0.0": version "16.0.11" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.11.tgz#de958fb62e77fc383fa6cd8066eabdd13da88f04" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz" integrity sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g== dependencies: "@types/yargs-parser" "*" "@types/yargs@^17.0.8": version "17.0.35" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz" integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== dependencies: "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.5.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz" integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== dependencies: "@eslint-community/regexpp" "^4.4.0" @@ -3015,14 +2984,14 @@ "@typescript-eslint/experimental-utils@^5.0.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz#14559bf73383a308026b427a4a6129bae2146741" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz" integrity sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw== dependencies: "@typescript-eslint/utils" "5.62.0" "@typescript-eslint/parser@^5.5.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz" integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== dependencies: "@typescript-eslint/scope-manager" "5.62.0" @@ -3032,7 +3001,7 @@ "@typescript-eslint/scope-manager@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz" integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== dependencies: "@typescript-eslint/types" "5.62.0" @@ -3040,7 +3009,7 @@ "@typescript-eslint/type-utils@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz" integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== dependencies: "@typescript-eslint/typescript-estree" "5.62.0" @@ -3050,12 +3019,12 @@ "@typescript-eslint/types@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz" integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== dependencies: "@typescript-eslint/types" "5.62.0" @@ -3068,7 +3037,7 @@ "@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.58.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz" integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" @@ -3082,7 +3051,7 @@ "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz" integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== dependencies: "@typescript-eslint/types" "5.62.0" @@ -3090,12 +3059,12 @@ "@ungap/structured-clone@^1.2.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz" integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== dependencies: "@webassemblyjs/helper-numbers" "1.13.2" @@ -3103,22 +3072,22 @@ "@webassemblyjs/floating-point-hex-parser@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz" integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== "@webassemblyjs/helper-api-error@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz" integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== "@webassemblyjs/helper-buffer@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz" integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== "@webassemblyjs/helper-numbers@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz" integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== dependencies: "@webassemblyjs/floating-point-hex-parser" "1.13.2" @@ -3127,12 +3096,12 @@ "@webassemblyjs/helper-wasm-bytecode@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz" integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== "@webassemblyjs/helper-wasm-section@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz" integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3142,26 +3111,26 @@ "@webassemblyjs/ieee754@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz" integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== dependencies: "@xtuc/ieee754" "^1.2.0" "@webassemblyjs/leb128@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz" integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== dependencies: "@xtuc/long" "4.2.2" "@webassemblyjs/utf8@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz" integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== "@webassemblyjs/wasm-edit@^1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz" integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3175,7 +3144,7 @@ "@webassemblyjs/wasm-gen@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz" integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3186,7 +3155,7 @@ "@webassemblyjs/wasm-opt@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz" integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3196,7 +3165,7 @@ "@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz" integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3208,7 +3177,7 @@ "@webassemblyjs/wast-printer@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz" integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3216,22 +3185,22 @@ "@xtuc/ieee754@^1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== "@xtuc/long@4.2.2": version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== abab@^2.0.5, abab@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: mime-types "~2.1.34" @@ -3239,7 +3208,7 @@ accepts@~1.3.4, accepts@~1.3.8: acorn-globals@^7.0.0: version "7.0.1" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz" integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== dependencies: acorn "^8.1.0" @@ -3247,34 +3216,34 @@ acorn-globals@^7.0.0: acorn-import-phases@^1.0.3: version "1.0.4" - resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" + resolved "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz" integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.0.2: version "8.3.4" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== dependencies: acorn "^8.11.0" acorn@^8.1.0, acorn@^8.11.0, acorn@^8.15.0, acorn@^8.8.1, acorn@^8.9.0: version "8.15.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== address@^1.0.1, address@^1.1.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz" integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== adjust-sourcemap-loader@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" + resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== dependencies: loader-utils "^2.0.0" @@ -3282,33 +3251,33 @@ adjust-sourcemap-loader@^4.0.0: agent-base@6: version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" ajv-formats@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== ajv-keywords@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== dependencies: fast-deep-equal "^3.1.3" ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -3318,7 +3287,7 @@ ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: fast-deep-equal "^3.1.3" @@ -3328,51 +3297,51 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: ansi-escapes@^4.2.1: version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: type-fest "^0.21.3" ansi-escapes@^6.0.0: version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz" integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== ansi-html-community@^0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== ansi-html@^0.0.9: version "0.0.9" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.9.tgz#6512d02342ae2cc68131952644a129cb734cd3f0" + resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz" integrity sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg== ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: version "6.2.2" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz" integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" ansi-styles@^5.0.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" @@ -3380,38 +3349,38 @@ anymatch@^3.0.3, anymatch@~3.1.2: argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" argparse@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== aria-hidden@^1.2.5: version "1.2.6" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" + resolved "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz" integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== dependencies: tslib "^2.0.0" -aria-query@5.3.0: +aria-query@5.3.0, aria-query@^5.0.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz" integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== dependencies: dequal "^2.0.3" -aria-query@^5.0.0, aria-query@^5.3.2: +aria-query@^5.3.2: version "5.3.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz" integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== dependencies: call-bound "^1.0.3" @@ -3419,12 +3388,12 @@ array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: array-flatten@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: version "3.1.9" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" + resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz" integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== dependencies: call-bind "^1.0.8" @@ -3438,12 +3407,12 @@ array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: array-union@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== array.prototype.findlast@^1.2.5: version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + resolved "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz" integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== dependencies: call-bind "^1.0.7" @@ -3455,7 +3424,7 @@ array.prototype.findlast@^1.2.5: array.prototype.findlastindex@^1.2.6: version "1.2.6" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" + resolved "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz" integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== dependencies: call-bind "^1.0.8" @@ -3468,7 +3437,7 @@ array.prototype.findlastindex@^1.2.6: array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz" integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== dependencies: call-bind "^1.0.8" @@ -3478,7 +3447,7 @@ array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: array.prototype.flatmap@^1.3.2, array.prototype.flatmap@^1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz" integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== dependencies: call-bind "^1.0.8" @@ -3488,7 +3457,7 @@ array.prototype.flatmap@^1.3.2, array.prototype.flatmap@^1.3.3: array.prototype.tosorted@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + resolved "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz" integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== dependencies: call-bind "^1.0.7" @@ -3499,7 +3468,7 @@ array.prototype.tosorted@^1.1.4: arraybuffer.prototype.slice@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + resolved "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz" integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== dependencies: array-buffer-byte-length "^1.0.1" @@ -3512,42 +3481,42 @@ arraybuffer.prototype.slice@^1.0.4: asap@~2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== ast-types-flow@^0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== async-function@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + resolved "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz" integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== async@^3.2.6: version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== at-least-node@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== attr-accept@^2.2.2: version "2.2.5" - resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e" + resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz" integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ== autoprefixer@^10.4.13: version "10.4.22" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.22.tgz#90b27ab55ec0cf0684210d1f056f7d65dac55f16" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz" integrity sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg== dependencies: browserslist "^4.27.0" @@ -3559,24 +3528,24 @@ autoprefixer@^10.4.13: available-typed-arrays@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== dependencies: possible-typed-array-names "^1.0.0" axe-core@^4.10.0: version "4.11.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.0.tgz#16f74d6482e343ff263d4f4503829e9ee91a86b6" + resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz" integrity sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ== axobject-query@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz" integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== babel-jest@^27.4.2: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz" integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== dependencies: "@jest/transform" "^27.5.1" @@ -3590,7 +3559,7 @@ babel-jest@^27.4.2: babel-jest@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== dependencies: "@jest/transform" "^29.7.0" @@ -3603,7 +3572,7 @@ babel-jest@^29.7.0: babel-loader@^8.2.3: version "8.4.1" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.4.1.tgz#6ccb75c66e62c3b144e1c5f2eaec5b8f6c08c675" + resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz" integrity sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA== dependencies: find-cache-dir "^3.3.1" @@ -3613,7 +3582,7 @@ babel-loader@^8.2.3: babel-plugin-istanbul@^6.1.1: version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -3624,7 +3593,7 @@ babel-plugin-istanbul@^6.1.1: babel-plugin-jest-hoist@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz" integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== dependencies: "@babel/template" "^7.3.3" @@ -3634,7 +3603,7 @@ babel-plugin-jest-hoist@^27.5.1: babel-plugin-jest-hoist@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz" integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== dependencies: "@babel/template" "^7.3.3" @@ -3644,7 +3613,7 @@ babel-plugin-jest-hoist@^29.6.3: babel-plugin-macros@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== dependencies: "@babel/runtime" "^7.12.5" @@ -3653,12 +3622,12 @@ babel-plugin-macros@^3.1.0: babel-plugin-named-asset-import@^0.3.8: version "0.3.8" - resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz#6b7fa43c59229685368683c28bc9734f24524cc2" + resolved "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz" integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q== babel-plugin-polyfill-corejs2@^0.4.14: version "0.4.14" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz#8101b82b769c568835611542488d463395c2ef8f" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz" integrity sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg== dependencies: "@babel/compat-data" "^7.27.7" @@ -3667,7 +3636,7 @@ babel-plugin-polyfill-corejs2@^0.4.14: babel-plugin-polyfill-corejs3@^0.13.0: version "0.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz" integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== dependencies: "@babel/helper-define-polyfill-provider" "^0.6.5" @@ -3675,19 +3644,19 @@ babel-plugin-polyfill-corejs3@^0.13.0: babel-plugin-polyfill-regenerator@^0.6.5: version "0.6.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz#32752e38ab6f6767b92650347bf26a31b16ae8c5" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz" integrity sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg== dependencies: "@babel/helper-define-polyfill-provider" "^0.6.5" babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" + resolved "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz" integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== babel-preset-current-node-syntax@^1.0.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz" integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" @@ -3708,7 +3677,7 @@ babel-preset-current-node-syntax@^1.0.0: babel-preset-jest@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz" integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== dependencies: babel-plugin-jest-hoist "^27.5.1" @@ -3716,7 +3685,7 @@ babel-preset-jest@^27.5.1: babel-preset-jest@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz" integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== dependencies: babel-plugin-jest-hoist "^29.6.3" @@ -3724,7 +3693,7 @@ babel-preset-jest@^29.6.3: babel-preset-react-app@^10.0.1: version "10.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-10.1.0.tgz#e367f223f6c27878e6cc28471d0d506a9ab9f96c" + resolved "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.1.0.tgz" integrity sha512-f9B1xMdnkCIqe+2dHrJsoQFRz7reChaAHE/65SdaykPklQqhme2WaC08oD3is77x9ff98/9EazAKFDZv5rFEQg== dependencies: "@babel/core" "^7.16.0" @@ -3747,27 +3716,27 @@ babel-preset-react-app@^10.0.1: bail@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" + resolved "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz" integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== baseline-browser-mapping@^2.8.25: version "2.8.29" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz#d8800b71399c783cb1bf2068c2bcc3b6cfd7892c" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz" integrity sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA== batch@0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== bfj@^7.0.2: version "7.1.0" - resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.1.0.tgz#c5177d522103f9040e1b12980fe8c38cf41d3f8b" + resolved "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz" integrity sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw== dependencies: bluebird "^3.7.2" @@ -3778,27 +3747,27 @@ bfj@^7.0.2: big-integer@^1.6.16: version "1.6.52" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz" integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== big.js@^5.2.2: version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bluebird@^3.7.2: version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== body-parser@1.20.3: version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" @@ -3816,7 +3785,7 @@ body-parser@1.20.3: bonjour-service@^1.0.11: version "1.3.0" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722" + resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz" integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== dependencies: fast-deep-equal "^3.1.3" @@ -3824,12 +3793,12 @@ bonjour-service@^1.0.11: boolbase@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@^1.1.7: version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" @@ -3837,21 +3806,21 @@ brace-expansion@^1.1.7: brace-expansion@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" braces@^3.0.3, braces@~3.0.2: version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: fill-range "^7.1.1" broadcast-channel@^3.4.1: version "3.7.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" + resolved "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz" integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== dependencies: "@babel/runtime" "^7.7.2" @@ -3865,7 +3834,7 @@ broadcast-channel@^3.4.1: browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.26.3, browserslist@^4.27.0, browserslist@^4.28.0: version "4.28.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz" integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ== dependencies: baseline-browser-mapping "^2.8.25" @@ -3876,29 +3845,29 @@ browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4 bser@2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" buffer-from@^1.0.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== builtin-modules@^3.1.0, builtin-modules@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== bytes@3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: es-errors "^1.3.0" @@ -3906,7 +3875,7 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply- call-bind@^1.0.7, call-bind@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== dependencies: call-bind-apply-helpers "^1.0.0" @@ -3916,7 +3885,7 @@ call-bind@^1.0.7, call-bind@^1.0.8: call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== dependencies: call-bind-apply-helpers "^1.0.2" @@ -3924,12 +3893,12 @@ call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camel-case@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz" integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== dependencies: pascal-case "^3.1.2" @@ -3937,22 +3906,22 @@ camel-case@^4.1.2: camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== camelcase@^6.2.0, camelcase@^6.2.1: version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== camelize@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz" integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== caniuse-api@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz" integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== dependencies: browserslist "^4.0.0" @@ -3962,29 +3931,29 @@ caniuse-api@^3.0.0: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001754: version "1.0.30001756" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz#fe80104631102f88e58cad8aa203a2c3e5ec9ebd" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz" integrity sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" - resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" + resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz" integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== catharsis@^0.9.0: version "0.9.0" - resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" + resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz" integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== dependencies: lodash "^4.17.15" ccount@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" + resolved "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -3992,47 +3961,47 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: chalk@^5.2.0: version "5.6.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + resolved "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz" integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== char-regex@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== char-regex@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.2.tgz#81385bb071af4df774bff8721d0ca15ef29ea0bb" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz" integrity sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg== character-entities-html4@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" + resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz" integrity sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g== character-entities-legacy@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" + resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz" integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== character-entities@^1.0.0: version "1.2.4" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" + resolved "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz" integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== character-reference-invalid@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" + resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== check-types@^11.2.3: version "11.2.3" - resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.3.tgz#1ffdf68faae4e941fce252840b1787b8edc93b71" + resolved "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz" integrity sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg== chokidar@^3.4.2, chokidar@^3.5.3: version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" @@ -4047,49 +4016,49 @@ chokidar@^3.4.2, chokidar@^3.5.3: chroma-js@^2.4.2: version "2.6.0" - resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.6.0.tgz#578743dd359698a75067a19fa5571dec54d0b70b" + resolved "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz" integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== chrome-trace-event@^1.0.2: version "1.0.4" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== ci-info@^3.2.0: version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cjs-module-lexer@^1.0.0: version "1.4.3" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz" integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== classcat@^5.0.3, classcat@^5.0.4: version "5.0.5" - resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77" + resolved "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz" integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w== classnames@^2.5.1: version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== clean-css@^5.2.2: version "5.3.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz" integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== dependencies: source-map "~0.6.0" cli-width@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz" integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== cliui@^8.0.1: version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== dependencies: string-width "^4.2.0" @@ -4098,93 +4067,93 @@ cliui@^8.0.1: co@^4.6.0: version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== collapse-white-space@^1.0.2: version "1.0.6" - resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" + resolved "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz" integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== collect-v8-coverage@^1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz#cc1f01eb8d02298cbc9a437c74c70ab4e5210b80" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz" integrity sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw== color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colord@^2.9.1: version "2.9.3" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== colorette@^1.1.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + resolved "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz" integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== colorette@^2.0.10: version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== combined-stream@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" comma-separated-tokens@^1.0.0: version "1.0.8" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" + resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== commander@^2.20.0: version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commander@^7.2.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== commander@^8.3.0: version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== common-tags@^1.8.0: version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz" integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== commondir@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== compressible@~2.0.18: version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== dependencies: mime-db ">= 1.43.0 < 2" compression@^1.7.4: version "1.8.1" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + resolved "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz" integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== dependencies: bytes "3.1.2" @@ -4197,81 +4166,81 @@ compression@^1.7.4: concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== confusing-browser-globals@^1.0.11: version "1.0.11" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" + resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== connect-history-api-fallback@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== content-disposition@0.5.4: version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: safe-buffer "5.2.1" content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== convert-source-map@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== cookie-signature@1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== cookie@0.7.1: version "0.7.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== cookie@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" + resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz" integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== core-js-compat@^3.43.0: version "3.47.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.47.0.tgz#698224bbdbb6f2e3f39decdda4147b161e3772a3" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz" integrity sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ== dependencies: browserslist "^4.28.0" core-js-pure@^3.23.3: version "3.47.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.47.0.tgz#1104df8a3b6eb9189fcc559b5a65b90f66e7e887" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz" integrity sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw== core-js@^3.19.2: version "3.47.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.47.0.tgz#436ef07650e191afeb84c24481b298bd60eb4a17" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz" integrity sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg== core-util-is@~1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== cosmiconfig@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz" integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== dependencies: "@types/parse-json" "^4.0.0" @@ -4282,7 +4251,7 @@ cosmiconfig@^6.0.0: cosmiconfig@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== dependencies: "@types/parse-json" "^4.0.0" @@ -4293,7 +4262,7 @@ cosmiconfig@^7.0.0: cosmiconfig@^8.1.3: version "8.3.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz" integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== dependencies: import-fresh "^3.3.0" @@ -4303,7 +4272,7 @@ cosmiconfig@^8.1.3: create-jest@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + resolved "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz" integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== dependencies: "@jest/types" "^29.6.3" @@ -4316,7 +4285,7 @@ create-jest@^29.7.0: cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" @@ -4325,43 +4294,43 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: crypto-random-string@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== css-blank-pseudo@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" + resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz" integrity sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ== dependencies: postcss-selector-parser "^6.0.9" css-box-model@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== dependencies: tiny-invariant "^1.0.6" css-color-keywords@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== css-declaration-sorter@^6.3.1: version "6.4.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz" integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== css-has-pseudo@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz#57f6be91ca242d5c9020ee3e51bbb5b89fc7af73" + resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz" integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== dependencies: postcss-selector-parser "^6.0.9" css-loader@^6.5.1: version "6.11.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" + resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz" integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== dependencies: icss-utils "^5.1.0" @@ -4375,7 +4344,7 @@ css-loader@^6.5.1: css-minimizer-webpack-plugin@^3.2.0: version "3.4.1" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz#ab78f781ced9181992fe7b6e4f3422e76429878f" + resolved "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz" integrity sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q== dependencies: cssnano "^5.0.6" @@ -4387,12 +4356,12 @@ css-minimizer-webpack-plugin@^3.2.0: css-prefers-color-scheme@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz#ca8a22e5992c10a5b9d315155e7caee625903349" + resolved "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz" integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== css-select@^4.1.3: version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + resolved "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz" integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== dependencies: boolbase "^1.0.0" @@ -4403,7 +4372,7 @@ css-select@^4.1.3: css-select@^5.1.0: version "5.2.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e" + resolved "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz" integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== dependencies: boolbase "^1.0.0" @@ -4414,7 +4383,7 @@ css-select@^5.1.0: css-to-react-native@3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz" integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== dependencies: camelize "^1.0.0" @@ -4423,7 +4392,7 @@ css-to-react-native@3.2.0: css-tree@^1.1.2, css-tree@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz" integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== dependencies: mdn-data "2.0.14" @@ -4431,7 +4400,7 @@ css-tree@^1.1.2, css-tree@^1.1.3: css-tree@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz" integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== dependencies: mdn-data "2.0.30" @@ -4439,7 +4408,7 @@ css-tree@^2.3.1: css-tree@~2.2.0: version "2.2.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz" integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== dependencies: mdn-data "2.0.28" @@ -4447,27 +4416,27 @@ css-tree@~2.2.0: css-what@^6.0.1, css-what@^6.1.0: version "6.2.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" + resolved "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz" integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== css.escape@^1.5.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== cssdb@^7.1.0: version "7.11.2" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.2.tgz#127a2f5b946ee653361a5af5333ea85a39df5ae5" + resolved "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz" integrity sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A== cssesc@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== cssnano-preset-default@^5.2.14: version "5.2.14" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz" integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== dependencies: css-declaration-sorter "^6.3.1" @@ -4502,12 +4471,12 @@ cssnano-preset-default@^5.2.14: cssnano-utils@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + resolved "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz" integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== cssnano@^5.0.6: version "5.1.15" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + resolved "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz" integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== dependencies: cssnano-preset-default "^5.2.14" @@ -4516,58 +4485,58 @@ cssnano@^5.0.6: csso@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + resolved "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz" integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== dependencies: css-tree "^1.1.2" csso@^5.0.5: version "5.0.5" - resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + resolved "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz" integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== dependencies: css-tree "~2.2.0" cssom@^0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz" integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== cssom@~0.3.6: version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== cssstyle@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz" integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== dependencies: cssom "~0.3.6" csstype@3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== csstype@^3.0.2, csstype@^3.2.2: version "3.2.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== "d3-color@1 - 3": version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== "d3-dispatch@1 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== "d3-drag@2 - 3", d3-drag@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== dependencies: d3-dispatch "1 - 3" @@ -4575,29 +4544,29 @@ csstype@^3.0.2, csstype@^3.2.2: "d3-ease@1 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz" integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== "d3-interpolate@1 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== dependencies: d3-color "1 - 3" "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== "d3-timer@1 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== "d3-transition@2 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz" integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== dependencies: d3-color "1 - 3" @@ -4608,7 +4577,7 @@ csstype@^3.0.2, csstype@^3.2.2: d3-zoom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz" integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== dependencies: d3-dispatch "1 - 3" @@ -4619,7 +4588,7 @@ d3-zoom@^3.0.0: dagre@^0.8.5: version "0.8.5" - resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" + resolved "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz" integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw== dependencies: graphlib "^2.1.8" @@ -4627,12 +4596,12 @@ dagre@^0.8.5: damerau-levenshtein@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== data-urls@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + resolved "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz" integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== dependencies: abab "^2.0.6" @@ -4641,7 +4610,7 @@ data-urls@^3.0.2: data-view-buffer@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz" integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== dependencies: call-bound "^1.0.3" @@ -4650,7 +4619,7 @@ data-view-buffer@^1.0.2: data-view-byte-length@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + resolved "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz" integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== dependencies: call-bound "^1.0.3" @@ -4659,7 +4628,7 @@ data-view-byte-length@^1.0.2: data-view-byte-offset@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + resolved "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz" integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== dependencies: call-bound "^1.0.2" @@ -4668,60 +4637,60 @@ data-view-byte-offset@^1.0.1: debug@2.6.9, debug@^2.6.0: version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1: version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" debug@^3.2.7: version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" decimal.js@^10.4.2: version "10.6.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz" integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== decode-uri-component@^0.2.2: version "0.2.2" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== dedent@^1.0.0: version "1.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.0.tgz#c1f9445335f0175a96587be245a282ff451446ca" + resolved "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz" integrity sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ== deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== deepmerge@^4.2.2, deepmerge@^4.3.1: version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== default-gateway@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== dependencies: execa "^5.0.0" define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: es-define-property "^1.0.0" @@ -4730,12 +4699,12 @@ define-data-property@^1.0.1, define-data-property@^1.1.4: define-lazy-prop@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== define-properties@^1.1.3, define-properties@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: define-data-property "^1.0.1" @@ -4744,47 +4713,47 @@ define-properties@^1.1.3, define-properties@^1.2.1: delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== depd@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== depd@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== dequal@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== destroy@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== detect-newline@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== detect-node-es@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + resolved "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== detect-node@^2.0.4, detect-node@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== detect-port-alt@^1.1.6: version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + resolved "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz" integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== dependencies: address "^1.0.1" @@ -4792,62 +4761,62 @@ detect-port-alt@^1.1.6: diff-sequences@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== diff-sequences@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== dir-glob@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: path-type "^4.0.0" dns-packet@^5.2.2: version "5.6.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz" integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== dependencies: "@leichtgewicht/ip-codec" "^2.0.1" doctrine@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== dependencies: esutils "^2.0.2" doctrine@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" dom-accessibility-api@^0.5.9: version "0.5.16" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== dom-accessibility-api@^0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz" integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== dom-converter@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== dependencies: utila "~0.4" dom-serializer@^1.0.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz" integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== dependencies: domelementtype "^2.0.1" @@ -4856,7 +4825,7 @@ dom-serializer@^1.0.1: dom-serializer@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz" integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== dependencies: domelementtype "^2.3.0" @@ -4865,33 +4834,33 @@ dom-serializer@^2.0.0: domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== domexception@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + resolved "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz" integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== dependencies: webidl-conversions "^7.0.0" domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== dependencies: domelementtype "^2.2.0" domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz" integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== dependencies: domelementtype "^2.3.0" domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== dependencies: dom-serializer "^1.0.1" @@ -4900,7 +4869,7 @@ domutils@^2.5.2, domutils@^2.8.0: domutils@^3.0.1: version "3.2.2" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + resolved "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz" integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== dependencies: dom-serializer "^2.0.0" @@ -4909,7 +4878,7 @@ domutils@^3.0.1: dot-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== dependencies: no-case "^3.0.4" @@ -4917,17 +4886,17 @@ dot-case@^3.0.4: dotenv-expand@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== dotenv@^10.0.0: version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: call-bind-apply-helpers "^1.0.1" @@ -4936,64 +4905,64 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1: duplexer@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== ee-first@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== ejs@^3.1.6: version "3.1.10" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz" integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" electron-to-chromium@^1.5.249: version "1.5.258" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.258.tgz#094b0280928b1bf967b202e4be5b335aa4754b69" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.258.tgz" integrity sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg== emittery@^0.13.1: version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== emojis-list@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== emoticon@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-3.2.0.tgz#c008ca7d7620fac742fe1bf4af8ff8fed154ae7f" + resolved "https://registry.npmjs.org/emoticon/-/emoticon-3.2.0.tgz" integrity sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg== encodeurl@~1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== encodeurl@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== enhanced-resolve@^5.17.3: version "5.18.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz" integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww== dependencies: graceful-fs "^4.2.4" @@ -5001,36 +4970,36 @@ enhanced-resolve@^5.17.3: entities@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== entities@^4.2.0, entities@^4.4.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== entities@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz" integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== error-ex@^1.3.1: version "1.3.4" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz" integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== dependencies: is-arrayish "^0.2.1" error-stack-parser@^2.0.6: version "2.1.4" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + resolved "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz" integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== dependencies: stackframe "^1.3.4" es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0: version "1.24.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz" integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg== dependencies: array-buffer-byte-length "^1.0.2" @@ -5090,17 +5059,17 @@ es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23 es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-iterator-helpers@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz#d1dd0f58129054c0ad922e6a9a1e65eef435fe75" + resolved "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz" integrity sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w== dependencies: call-bind "^1.0.8" @@ -5122,19 +5091,19 @@ es-iterator-helpers@^1.2.1: es-module-lexer@^1.2.1: version "1.7.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz" integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: es-errors "^1.3.0" @@ -5144,14 +5113,14 @@ es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" + resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz" integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== dependencies: hasown "^2.0.2" es-to-primitive@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz" integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== dependencies: is-callable "^1.2.7" @@ -5160,27 +5129,27 @@ es-to-primitive@^1.3.0: escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-html@~1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== escape-string-regexp@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== escodegen@^1.13.0, escodegen@^1.8.1: version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== dependencies: esprima "^4.0.1" @@ -5192,7 +5161,7 @@ escodegen@^1.13.0, escodegen@^1.8.1: escodegen@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz" integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== dependencies: esprima "^4.0.1" @@ -5203,7 +5172,7 @@ escodegen@^2.0.0: eslint-config-react-app@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz#73ba3929978001c5c86274c017ea57eb5fa644b4" + resolved "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz" integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA== dependencies: "@babel/core" "^7.16.0" @@ -5223,7 +5192,7 @@ eslint-config-react-app@^7.0.1: eslint-import-resolver-node@^0.3.9: version "0.3.9" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== dependencies: debug "^3.2.7" @@ -5232,14 +5201,14 @@ eslint-import-resolver-node@^0.3.9: eslint-module-utils@^2.12.1: version "2.12.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff" + resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz" integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== dependencies: debug "^3.2.7" eslint-plugin-flowtype@^8.0.3: version "8.0.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912" + resolved "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz" integrity sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ== dependencies: lodash "^4.17.21" @@ -5247,7 +5216,7 @@ eslint-plugin-flowtype@^8.0.3: eslint-plugin-import@^2.25.3: version "2.32.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz" integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== dependencies: "@rtsao/scc" "^1.1.0" @@ -5272,14 +5241,14 @@ eslint-plugin-import@^2.25.3: eslint-plugin-jest@^25.3.0: version "25.7.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz#ff4ac97520b53a96187bad9c9814e7d00de09a6a" + resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz" integrity sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ== dependencies: "@typescript-eslint/experimental-utils" "^5.0.0" eslint-plugin-jsx-a11y@^6.5.1: version "6.10.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz" integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== dependencies: aria-query "^5.3.2" @@ -5300,12 +5269,12 @@ eslint-plugin-jsx-a11y@^6.5.1: eslint-plugin-react-hooks@^4.3.0: version "4.6.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== eslint-plugin-react@^7.27.1: version "7.37.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz" integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== dependencies: array-includes "^3.1.8" @@ -5329,14 +5298,14 @@ eslint-plugin-react@^7.27.1: eslint-plugin-testing-library@^5.0.1: version "5.11.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz#5b46cdae96d4a78918711c0b4792f90088e62d20" + resolved "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz" integrity sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw== dependencies: "@typescript-eslint/utils" "^5.58.0" eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: esrecurse "^4.3.0" @@ -5344,7 +5313,7 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: eslint-scope@^7.2.2: version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" @@ -5352,17 +5321,17 @@ eslint-scope@^7.2.2: eslint-visitor-keys@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint-webpack-plugin@^3.1.1: version "3.2.0" - resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz#1978cdb9edc461e4b0195a20da950cf57988347c" + resolved "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz" integrity sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w== dependencies: "@types/eslint" "^7.29.0 || ^8.4.1" @@ -5373,7 +5342,7 @@ eslint-webpack-plugin@^3.1.1: eslint@^8.3.0: version "8.57.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" @@ -5417,7 +5386,7 @@ eslint@^8.3.0: espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: acorn "^8.9.0" @@ -5426,76 +5395,76 @@ espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: esprima@1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b" + resolved "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz" integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.4.2: version "1.6.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" esrecurse@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== estree-walker@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-0.2.1.tgz" integrity sha512-6/I1dwNKk0N9iGOU3ydzAAurz4NPo/ttxZNCqgIVbWFvWyzWBSNonRrJ5CpjDuyBfmM7ENN7WCzUi9aT/UPXXQ== estree-walker@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== estree-walker@^2.0.1, estree-walker@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== esutils@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== etag@~1.8.1: version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== eventemitter3@^4.0.0: version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.2.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== execa@^5.0.0: version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -5510,12 +5479,12 @@ execa@^5.0.0: exit@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== expect@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + resolved "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz" integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== dependencies: "@jest/expect-utils" "^29.7.0" @@ -5526,7 +5495,7 @@ expect@^29.7.0: express@^4.17.3: version "4.21.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" @@ -5563,17 +5532,17 @@ express@^4.17.3: extend@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.0.3, fast-glob@^3.2.9: version "3.3.3" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== dependencies: "@nodelib/fs.stat" "^2.0.2" @@ -5584,57 +5553,57 @@ fast-glob@^3.0.3, fast-glob@^3.2.9: fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: version "3.1.0" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz" integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastq@^1.6.0: version "1.19.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== dependencies: reusify "^1.0.4" fault@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" + resolved "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz" integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== dependencies: format "^0.2.0" faye-websocket@^0.11.3: version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== dependencies: websocket-driver ">=0.5.1" fb-watchman@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: bser "2.1.1" file-entry-cache@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: flat-cache "^3.0.4" file-loader@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== dependencies: loader-utils "^2.0.0" @@ -5642,38 +5611,38 @@ file-loader@^6.2.0: file-selector@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17" + resolved "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz" integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg== dependencies: tslib "^2.0.3" filelist@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz" integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== dependencies: minimatch "^5.0.1" filesize@^8.0.6: version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" + resolved "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz" integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== fill-range@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" filter-obj@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz" integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== finalhandler@1.3.1: version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" @@ -5686,7 +5655,7 @@ finalhandler@1.3.1: find-cache-dir@^3.3.1: version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== dependencies: commondir "^1.0.1" @@ -5695,19 +5664,19 @@ find-cache-dir@^3.3.1: find-root@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== find-up@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== dependencies: locate-path "^3.0.0" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" @@ -5715,7 +5684,7 @@ find-up@^4.0.0, find-up@^4.1.0: find-up@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -5723,7 +5692,7 @@ find-up@^5.0.0: flat-cache@^3.0.4: version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz" integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: flatted "^3.2.9" @@ -5732,31 +5701,31 @@ flat-cache@^3.0.4: flatted@^3.2.9: version "3.3.3" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== focus-lock@^1.3.6: version "1.3.6" - resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-1.3.6.tgz#955eec1e10591d56f679258edb94aedb11d691cd" + resolved "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.6.tgz" integrity sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg== dependencies: tslib "^2.0.3" follow-redirects@^1.0.0: version "1.15.11" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz" integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== dependencies: is-callable "^1.2.7" fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.3" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz" integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== dependencies: "@babel/code-frame" "^7.8.3" @@ -5775,7 +5744,7 @@ fork-ts-checker-webpack-plugin@^6.5.0: form-data@^4.0.0: version "4.0.5" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz" integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== dependencies: asynckit "^0.4.0" @@ -5786,27 +5755,27 @@ form-data@^4.0.0: format@^0.2.0: version "0.2.2" - resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz" integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== forwarded@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fraction.js@^5.3.4: version "5.3.4" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz" integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== fresh@0.5.2: version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs-extra@^10.0.0: version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: graceful-fs "^4.2.0" @@ -5815,7 +5784,7 @@ fs-extra@^10.0.0: fs-extra@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: graceful-fs "^4.2.0" @@ -5824,7 +5793,7 @@ fs-extra@^8.1.0: fs-extra@^9.0.0, fs-extra@^9.0.1: version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" @@ -5834,27 +5803,27 @@ fs-extra@^9.0.0, fs-extra@^9.0.1: fs-monkey@^1.0.4: version "1.1.0" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.1.0.tgz#632aa15a20e71828ed56b24303363fb1414e5997" + resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz" integrity sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw== fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: version "1.1.8" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz" integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== dependencies: call-bind "^1.0.8" @@ -5866,27 +5835,27 @@ function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: functions-have-names@^1.2.3: version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== generator-function@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2" + resolved "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz" integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-caller-file@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: call-bind-apply-helpers "^1.0.2" @@ -5902,22 +5871,22 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@ get-nonce@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + resolved "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== get-package-type@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== get-proto@^1.0.0, get-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== dependencies: dunder-proto "^1.0.1" @@ -5925,12 +5894,12 @@ get-proto@^1.0.0, get-proto@^1.0.1: get-stream@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== get-symbol-description@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz" integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== dependencies: call-bound "^1.0.3" @@ -5939,26 +5908,26 @@ get-symbol-description@^1.1.0: glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" glob-parent@^6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" glob-to-regexp@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -5970,7 +5939,7 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: glob@^8.0.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" @@ -5981,14 +5950,14 @@ glob@^8.0.0: global-modules@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== dependencies: global-prefix "^3.0.0" global-prefix@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz" integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== dependencies: ini "^1.3.5" @@ -5997,14 +5966,14 @@ global-prefix@^3.0.0: globals@^13.19.0: version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" globalthis@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: define-properties "^1.2.1" @@ -6012,7 +5981,7 @@ globalthis@^1.0.4: globby@10.0.1: version "10.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22" + resolved "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz" integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A== dependencies: "@types/glob" "^7.1.1" @@ -6026,7 +5995,7 @@ globby@10.0.1: globby@^11.0.4, globby@^11.1.0: version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" @@ -6038,46 +6007,46 @@ globby@^11.0.4, globby@^11.1.0: gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== graphemer@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== graphlib@^2.1.8: version "2.1.8" - resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" + resolved "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz" integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A== dependencies: lodash "^4.17.15" graphql@^16.8.1: version "16.12.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.12.0.tgz#28cc2462435b1ac3fdc6976d030cef83a0c13ac7" + resolved "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz" integrity sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ== gzip-size@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz" integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== dependencies: duplexer "^0.1.2" handle-thing@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== happy-dom@^16.8.1: version "16.8.1" - resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-16.8.1.tgz#43d7e998fd36aa6062acbdfa88262ef0bc0a105e" + resolved "https://registry.npmjs.org/happy-dom/-/happy-dom-16.8.1.tgz" integrity sha512-n0QrmT9lD81rbpKsyhnlz3DgnMZlaOkJPpgi746doA+HvaMC79bdWkwjrNnGJRvDrWTI8iOcJiVTJ5CdT/AZRw== dependencies: webidl-conversions "^7.0.0" @@ -6085,55 +6054,55 @@ happy-dom@^16.8.1: harmony-reflect@^1.4.6: version "1.6.2" - resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" + resolved "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz" integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== has-bigints@^1.0.2: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz" integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: es-define-property "^1.0.0" has-proto@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz" integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== dependencies: dunder-proto "^1.0.0" has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" hasown@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" hast-to-hyperscript@^9.0.0: version "9.0.1" - resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" + resolved "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz" integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA== dependencies: "@types/unist" "^2.0.3" @@ -6146,7 +6115,7 @@ hast-to-hyperscript@^9.0.0: hast-util-from-parse5@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a" + resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz" integrity sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA== dependencies: "@types/parse5" "^5.0.0" @@ -6158,17 +6127,17 @@ hast-util-from-parse5@^6.0.0: hast-util-is-element@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz#3b3ed5159a2707c6137b48637fbfe068e175a425" + resolved "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz" integrity sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ== hast-util-parse-selector@^2.0.0: version "2.2.5" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz" integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== hast-util-raw@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.1.0.tgz#e16a3c2642f65cc7c480c165400a40d604ab75d0" + resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-6.1.0.tgz" integrity sha512-5FoZLDHBpka20OlZZ4I/+RBw5piVQ8iI1doEvffQhx5CbCyTtP8UCq8Tw6NmTAMtXgsQxmhW7Ly8OdFre5/YMQ== dependencies: "@types/hast" "^2.0.0" @@ -6185,7 +6154,7 @@ hast-util-raw@^6.1.0: hast-util-to-html@^7.1.1: version "7.1.3" - resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz#9f339ca9bea71246e565fc79ff7dbfe98bb50f5e" + resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz" integrity sha512-yk2+1p3EJTEE9ZEUkgHsUSVhIpCsL/bvT8E5GzmWc+N1Po5gBw+0F8bo7dpxXR0nu0bQVxVZGX2lBGF21CmeDw== dependencies: ccount "^1.0.0" @@ -6201,7 +6170,7 @@ hast-util-to-html@^7.1.1: hast-util-to-parse5@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479" + resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz" integrity sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ== dependencies: hast-to-hyperscript "^9.0.0" @@ -6212,12 +6181,12 @@ hast-util-to-parse5@^6.0.0: hast-util-whitespace@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" + resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz" integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== hastscript@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz" integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w== dependencies: "@types/hast" "^2.0.0" @@ -6228,39 +6197,39 @@ hastscript@^6.0.0: he@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== headers-polyfill@^4.0.2: version "4.0.3" - resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07" + resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz" integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ== highlight.js@^10.4.1, highlight.js@~10.7.0: version "10.7.3" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== highlightjs-vue@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" + resolved "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz" integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" hoopy@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" + resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz" integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== hpack.js@^2.1.6: version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== dependencies: inherits "^2.0.1" @@ -6270,24 +6239,24 @@ hpack.js@^2.1.6: html-encoding-sniffer@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz" integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== dependencies: whatwg-encoding "^2.0.0" html-entities@^2.1.0, html-entities@^2.3.2: version "2.6.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8" + resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz" integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ== html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== html-minifier-terser@^6.0.2: version "6.1.0" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== dependencies: camel-case "^4.1.2" @@ -6300,12 +6269,12 @@ html-minifier-terser@^6.0.2: html-void-elements@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz" integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== html-webpack-plugin@^5.5.0: version "5.6.5" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz#d57defb83cabbf29bf56b2d4bf10b67b650066be" + resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz" integrity sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g== dependencies: "@types/html-minifier-terser" "^6.0.0" @@ -6316,7 +6285,7 @@ html-webpack-plugin@^5.5.0: htmlparser2@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== dependencies: domelementtype "^2.0.1" @@ -6326,12 +6295,12 @@ htmlparser2@^6.1.0: http-deceiver@^1.2.7: version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== http-errors@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: depd "2.0.0" @@ -6342,7 +6311,7 @@ http-errors@2.0.0: http-errors@~1.6.2: version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== dependencies: depd "~1.1.2" @@ -6352,12 +6321,12 @@ http-errors@~1.6.2: http-parser-js@>=0.5.1: version "0.5.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz" integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== http-proxy-agent@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== dependencies: "@tootallnate/once" "2" @@ -6366,7 +6335,7 @@ http-proxy-agent@^5.0.0: http-proxy-middleware@^2.0.3: version "2.0.9" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz" integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== dependencies: "@types/http-proxy" "^1.17.8" @@ -6377,7 +6346,7 @@ http-proxy-middleware@^2.0.3: http-proxy@^1.18.1: version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: eventemitter3 "^4.0.0" @@ -6386,7 +6355,7 @@ http-proxy@^1.18.1: https-proxy-agent@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -6394,53 +6363,53 @@ https-proxy-agent@^5.0.1: human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== iconv-lite@0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" iconv-lite@0.6.3, iconv-lite@^0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== idb@^7.0.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + resolved "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== identity-obj-proxy@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" + resolved "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz" integrity sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA== dependencies: harmony-reflect "^1.4.6" ignore@^5.1.1, ignore@^5.2.0: version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== immer@^9.0.7: version "9.0.21" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz" integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" @@ -6448,7 +6417,7 @@ import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: import-local@^3.0.2: version "3.2.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== dependencies: pkg-dir "^4.2.0" @@ -6456,17 +6425,17 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== indent-string@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" @@ -6474,32 +6443,32 @@ inflight@^1.0.4: inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== ini@^1.3.5: version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inline-style-parser@0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== inter-ui@^3.19.3: version "3.19.3" - resolved "https://registry.yarnpkg.com/inter-ui/-/inter-ui-3.19.3.tgz#cf4b4b6d30de8d5463e2462588654b325206488c" + resolved "https://registry.npmjs.org/inter-ui/-/inter-ui-3.19.3.tgz" integrity sha512-5FG9fjuYOXocIfjzcCBhICL5cpvwEetseL3FU6tP3d6Bn7g8wODhB+I9RNGRTizCT7CUG4GOK54OPxqq3msQgg== internal-slot@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz" integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== dependencies: es-errors "^1.3.0" @@ -6508,22 +6477,22 @@ internal-slot@^1.1.0: ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== ipaddr.js@^2.0.1: version "2.2.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== is-alphabetical@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" + resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== is-alphanumerical@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" + resolved "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz" integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== dependencies: is-alphabetical "^1.0.0" @@ -6531,7 +6500,7 @@ is-alphanumerical@^1.0.0: is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: version "3.0.5" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz" integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== dependencies: call-bind "^1.0.8" @@ -6540,12 +6509,12 @@ is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-async-function@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + resolved "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz" integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== dependencies: async-function "^1.0.0" @@ -6556,21 +6525,21 @@ is-async-function@^2.0.0: is-bigint@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz" integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== dependencies: has-bigints "^1.0.2" is-binary-path@~2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: binary-extensions "^2.0.0" is-boolean-object@^1.2.1: version "1.2.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz" integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== dependencies: call-bound "^1.0.3" @@ -6578,31 +6547,31 @@ is-boolean-object@^1.2.1: is-buffer@^2.0.0: version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== is-builtin-module@^3.1.0: version "3.2.1" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + resolved "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz" integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== dependencies: builtin-modules "^3.3.0" is-callable@^1.2.7: version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== is-core-module@^2.13.0, is-core-module@^2.16.1: version "2.16.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" is-data-view@^1.0.1, is-data-view@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + resolved "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz" integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== dependencies: call-bound "^1.0.2" @@ -6611,7 +6580,7 @@ is-data-view@^1.0.1, is-data-view@^1.0.2: is-date-object@^1.0.5, is-date-object@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz" integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== dependencies: call-bound "^1.0.2" @@ -6619,39 +6588,39 @@ is-date-object@^1.0.5, is-date-object@^1.1.0: is-decimal@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-finalizationregistry@^1.1.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + resolved "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz" integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== dependencies: call-bound "^1.0.3" is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-generator-fn@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-generator-function@^1.0.10: version "1.1.2" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.2.tgz#ae3b61e3d5ea4e4839b90bad22b02335051a17d5" + resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz" integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== dependencies: call-bound "^1.0.4" @@ -6662,39 +6631,39 @@ is-generator-function@^1.0.10: is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" is-hexadecimal@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== is-map@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== is-module@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== is-negative-zero@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== is-node-process@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" + resolved "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz" integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw== is-number-object@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz" integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== dependencies: call-bound "^1.0.3" @@ -6702,54 +6671,54 @@ is-number-object@^1.1.1: is-number@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-obj@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== is-path-inside@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== is-plain-obj@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== is-plain-object@5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== is-plain-object@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz" integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== is-potential-custom-element-name@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== is-reference@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz" integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== dependencies: "@types/estree" "*" is-regex@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz" integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== dependencies: call-bound "^1.0.2" @@ -6759,34 +6728,34 @@ is-regex@^1.2.1: is-regexp@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== is-root@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" + resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== is-set@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== is-shared-array-buffer@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz" integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== dependencies: call-bound "^1.0.3" is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== is-string@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz" integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== dependencies: call-bound "^1.0.3" @@ -6794,7 +6763,7 @@ is-string@^1.1.1: is-symbol@^1.0.4, is-symbol@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz" integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== dependencies: call-bound "^1.0.2" @@ -6803,31 +6772,31 @@ is-symbol@^1.0.4, is-symbol@^1.1.1: is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: version "1.1.15" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz" integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: which-typed-array "^1.1.16" is-typedarray@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== is-weakmap@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz" integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== is-weakref@^1.0.2, is-weakref@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz" integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== dependencies: call-bound "^1.0.3" is-weakset@^2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz" integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== dependencies: call-bound "^1.0.3" @@ -6835,44 +6804,44 @@ is-weakset@^2.0.3: is-whitespace-character@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" + resolved "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz" integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== is-word-character@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" + resolved "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz" integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== is-wsl@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" isarray@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== isarray@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz" integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== istanbul-lib-instrument@^5.0.4: version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== dependencies: "@babel/core" "^7.12.3" @@ -6883,7 +6852,7 @@ istanbul-lib-instrument@^5.0.4: istanbul-lib-instrument@^6.0.0: version "6.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz" integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== dependencies: "@babel/core" "^7.23.9" @@ -6894,7 +6863,7 @@ istanbul-lib-instrument@^6.0.0: istanbul-lib-report@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz" integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== dependencies: istanbul-lib-coverage "^3.0.0" @@ -6903,7 +6872,7 @@ istanbul-lib-report@^3.0.0: istanbul-lib-source-maps@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" @@ -6912,7 +6881,7 @@ istanbul-lib-source-maps@^4.0.0: istanbul-reports@^3.1.3: version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz" integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== dependencies: html-escaper "^2.0.0" @@ -6920,7 +6889,7 @@ istanbul-reports@^3.1.3: iterator.prototype@^1.1.4: version "1.1.5" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" + resolved "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz" integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== dependencies: define-data-property "^1.1.4" @@ -6932,7 +6901,7 @@ iterator.prototype@^1.1.4: jake@^10.8.5: version "10.9.4" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6" + resolved "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz" integrity sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA== dependencies: async "^3.2.6" @@ -6941,7 +6910,7 @@ jake@^10.8.5: jest-changed-files@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== dependencies: execa "^5.0.0" @@ -6950,7 +6919,7 @@ jest-changed-files@^29.7.0: jest-circus@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz" integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== dependencies: "@jest/environment" "^29.7.0" @@ -6976,7 +6945,7 @@ jest-circus@^29.7.0: jest-cli@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz" integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== dependencies: "@jest/core" "^29.7.0" @@ -6993,7 +6962,7 @@ jest-cli@^29.7.0: jest-config@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz" integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== dependencies: "@babel/core" "^7.11.6" @@ -7021,7 +6990,7 @@ jest-config@^29.7.0: jest-diff@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz" integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: chalk "^4.0.0" @@ -7031,7 +7000,7 @@ jest-diff@^27.5.1: jest-diff@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz" integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== dependencies: chalk "^4.0.0" @@ -7041,14 +7010,14 @@ jest-diff@^29.7.0: jest-docblock@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz" integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== dependencies: detect-newline "^3.0.0" jest-each@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz" integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== dependencies: "@jest/types" "^29.6.3" @@ -7059,7 +7028,7 @@ jest-each@^29.7.0: jest-environment-jsdom@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz#d206fa3551933c3fd519e5dfdb58a0f5139a837f" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz" integrity sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA== dependencies: "@jest/environment" "^29.7.0" @@ -7073,7 +7042,7 @@ jest-environment-jsdom@^29.7.0: jest-environment-node@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz" integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== dependencies: "@jest/environment" "^29.7.0" @@ -7085,17 +7054,17 @@ jest-environment-node@^29.7.0: jest-get-type@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz" integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== jest-get-type@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz" integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== jest-haste-map@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz" integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== dependencies: "@jest/types" "^27.5.1" @@ -7115,7 +7084,7 @@ jest-haste-map@^27.5.1: jest-haste-map@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz" integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== dependencies: "@jest/types" "^29.6.3" @@ -7134,7 +7103,7 @@ jest-haste-map@^29.7.0: jest-leak-detector@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz" integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== dependencies: jest-get-type "^29.6.3" @@ -7142,7 +7111,7 @@ jest-leak-detector@^29.7.0: jest-matcher-utils@^27.0.0: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz" integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== dependencies: chalk "^4.0.0" @@ -7152,7 +7121,7 @@ jest-matcher-utils@^27.0.0: jest-matcher-utils@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz" integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== dependencies: chalk "^4.0.0" @@ -7162,7 +7131,7 @@ jest-matcher-utils@^29.7.0: jest-message-util@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz" integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== dependencies: "@babel/code-frame" "^7.12.13" @@ -7177,7 +7146,7 @@ jest-message-util@^29.7.0: jest-mock@^29.4.0, jest-mock@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz" integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== dependencies: "@jest/types" "^29.6.3" @@ -7186,22 +7155,22 @@ jest-mock@^29.4.0, jest-mock@^29.7.0: jest-pnp-resolver@^1.2.2: version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== jest-regex-util@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz" integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== jest-regex-util@^29.0.0, jest-regex-util@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== jest-resolve-dependencies@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz" integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== dependencies: jest-regex-util "^29.6.3" @@ -7209,7 +7178,7 @@ jest-resolve-dependencies@^29.7.0: jest-resolve@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== dependencies: chalk "^4.0.0" @@ -7224,7 +7193,7 @@ jest-resolve@^29.7.0: jest-runner@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz" integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== dependencies: "@jest/console" "^29.7.0" @@ -7251,7 +7220,7 @@ jest-runner@^29.7.0: jest-runtime@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz" integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== dependencies: "@jest/environment" "^29.7.0" @@ -7279,7 +7248,7 @@ jest-runtime@^29.7.0: jest-serializer@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz" integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: "@types/node" "*" @@ -7287,7 +7256,7 @@ jest-serializer@^27.5.1: jest-snapshot@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz" integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== dependencies: "@babel/core" "^7.11.6" @@ -7313,7 +7282,7 @@ jest-snapshot@^29.7.0: jest-util@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz" integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: "@jest/types" "^27.5.1" @@ -7325,7 +7294,7 @@ jest-util@^27.5.1: jest-util@^29.4.0, jest-util@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== dependencies: "@jest/types" "^29.6.3" @@ -7337,7 +7306,7 @@ jest-util@^29.4.0, jest-util@^29.7.0: jest-validate@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz" integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== dependencies: "@jest/types" "^29.6.3" @@ -7349,7 +7318,7 @@ jest-validate@^29.7.0: jest-watch-typeahead@^2.2.2: version "2.2.2" - resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-2.2.2.tgz#5516d3cd006485caa5cfc9bd1de40f1f8b136abf" + resolved "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-2.2.2.tgz" integrity sha512-+QgOFW4o5Xlgd6jGS5X37i08tuuXNW8X0CV9WNFi+3n8ExCIP+E1melYhvYLjv5fE6D0yyzk74vsSO8I6GqtvQ== dependencies: ansi-escapes "^6.0.0" @@ -7362,7 +7331,7 @@ jest-watch-typeahead@^2.2.2: jest-watcher@^29.0.0, jest-watcher@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz" integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== dependencies: "@jest/test-result" "^29.7.0" @@ -7376,7 +7345,7 @@ jest-watcher@^29.0.0, jest-watcher@^29.7.0: jest-worker@^26.2.1: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== dependencies: "@types/node" "*" @@ -7385,7 +7354,7 @@ jest-worker@^26.2.1: jest-worker@^27.0.2, jest-worker@^27.4.5, jest-worker@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" @@ -7394,7 +7363,7 @@ jest-worker@^27.0.2, jest-worker@^27.4.5, jest-worker@^27.5.1: jest-worker@^28.0.2: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.3.tgz#7e3c4ce3fa23d1bb6accb169e7f396f98ed4bb98" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz" integrity sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g== dependencies: "@types/node" "*" @@ -7403,7 +7372,7 @@ jest-worker@^28.0.2: jest-worker@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== dependencies: "@types/node" "*" @@ -7413,7 +7382,7 @@ jest-worker@^29.7.0: jest@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== dependencies: "@jest/core" "^29.7.0" @@ -7423,17 +7392,17 @@ jest@^29.7.0: js-sha3@0.8.0: version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: version "3.14.2" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz" integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== dependencies: argparse "^1.0.7" @@ -7441,21 +7410,21 @@ js-yaml@^3.13.1: js-yaml@^4.1.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz" integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" js2xmlparser@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" + resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz" integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== dependencies: xmlcreate "^2.0.4" jsdoc@^4.0.0: version "4.0.5" - resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-4.0.5.tgz#fbed70e04a3abcf2143dad6b184947682bbc7315" + resolved "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz" integrity sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g== dependencies: "@babel/parser" "^7.20.15" @@ -7476,7 +7445,7 @@ jsdoc@^4.0.0: jsdom@^20.0.0: version "20.0.3" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz" integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== dependencies: abab "^2.0.6" @@ -7508,61 +7477,61 @@ jsdom@^20.0.0: jsesc@^3.0.2, jsesc@~3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== json-buffer@3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-schema-traverse@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== json-schema@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json5@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" json5@^2.1.2, json5@^2.2.0, json5@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" jsonfile@^6.0.1: version "6.2.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz" integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== dependencies: universalify "^2.0.0" @@ -7571,7 +7540,7 @@ jsonfile@^6.0.1: jsonpath@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" + resolved "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz" integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== dependencies: esprima "1.2.2" @@ -7580,12 +7549,12 @@ jsonpath@^1.1.1: jsonpointer@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" + resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz" integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== dependencies: array-includes "^3.1.6" @@ -7593,50 +7562,55 @@ jsonpointer@^5.0.0: object.assign "^4.1.4" object.values "^1.1.6" +keycloak-js@^26.2.4: + version "26.2.4" + resolved "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.4.tgz" + integrity sha512-PnXpR3ubETGOt0B/Qt2lxmPbkZr5bc3vlQsOqDoTPPQsZRp7JjhTKxlJ187uWh8qJhvBab6Gsjb06a8ayOPfuw== + keyv@^4.5.3: version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" kind-of@^6.0.2: version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== klaw@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" + resolved "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz" integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== dependencies: graceful-fs "^4.1.9" kleur@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== klona@^2.0.4, klona@^2.0.5: version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" + resolved "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== language-subtag-registry@^0.3.20: version "0.3.23" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz" integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== language-tags@^1.0.9: version "1.0.9" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz" integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: language-subtag-registry "^0.3.20" launch-editor@^2.6.0: version "2.12.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.12.0.tgz#cc740f4e0263a6b62ead2485f9896e545321f817" + resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz" integrity sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg== dependencies: picocolors "^1.1.1" @@ -7644,12 +7618,12 @@ launch-editor@^2.6.0: leven@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== levn@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" @@ -7657,7 +7631,7 @@ levn@^0.4.1: levn@~0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== dependencies: prelude-ls "~1.1.2" @@ -7665,29 +7639,29 @@ levn@~0.3.0: lilconfig@^2.0.3: version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== lines-and-columns@^1.1.6: version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== linkify-it@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz" integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== dependencies: uc.micro "^2.0.0" loader-runner@^4.3.1: version "4.3.1" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3" + resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz" integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q== loader-utils@^2.0.0, loader-utils@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== dependencies: big.js "^5.2.2" @@ -7696,12 +7670,12 @@ loader-utils@^2.0.0, loader-utils@^2.0.4: loader-utils@^3.2.0: version "3.3.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz" integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== locate-path@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== dependencies: p-locate "^3.0.0" @@ -7709,70 +7683,70 @@ locate-path@^3.0.0: locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" locate-path@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: p-locate "^5.0.0" lodash.debounce@^4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== lodash.memoize@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash.sortby@^4.7.0: version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== lodash.uniq@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== long@^5.0.0, long@^5.2.3: version "5.3.2" - resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + resolved "https://registry.npmjs.org/long/-/long-5.3.2.tgz" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== dependencies: js-tokens "^3.0.0 || ^4.0.0" lower-case@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== dependencies: tslib "^2.0.3" lowlight@^1.17.0: version "1.20.0" - resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz" integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== dependencies: fault "^1.0.0" @@ -7780,57 +7754,57 @@ lowlight@^1.17.0: lru-cache@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" lz-string@^1.5.0: version "1.5.0" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz" integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== dependencies: sourcemap-codec "^1.4.8" make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" make-dir@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz" integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== dependencies: semver "^7.5.3" makeerror@1.0.12: version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: tmpl "1.0.5" markdown-escapes@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" + resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== markdown-it-anchor@^8.6.7: version "8.6.7" - resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" + resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz" integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== markdown-it@^14.1.0: version "14.1.0" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz" integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== dependencies: argparse "^2.0.1" @@ -7842,32 +7816,32 @@ markdown-it@^14.1.0: marked@^4.0.10: version "4.3.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== match-sorter@^6.0.2: - version "6.4.0" - resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.4.0.tgz#ae9c166cb3c9efd337690b3160c0e28cb8377c13" - integrity sha512-d4664ahzdL1QTTvmK1iI0JsrxWeJ6gn33qkYtnPg3mcn+naBLtXSgSPOe+X2vUgtgGwaAk3eiaj7gwKjjMAq+Q== + version "6.3.4" + resolved "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz" + integrity sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg== dependencies: "@babel/runtime" "^7.23.8" remove-accents "0.5.0" math-intrinsics@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== mdast-util-definitions@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" + resolved "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz" integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ== dependencies: unist-util-visit "^2.0.0" mdast-util-to-hast@^10.2.0: version "10.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz#61875526a017d8857b71abc9333942700b2d3604" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz" integrity sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ== dependencies: "@types/mdast" "^3.0.0" @@ -7881,74 +7855,74 @@ mdast-util-to-hast@^10.2.0: mdn-data@2.0.14: version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== mdn-data@2.0.28: version "2.0.28" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz" integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== mdn-data@2.0.30: version "2.0.30" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== mdurl@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== mdurl@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz" integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== media-typer@0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== memfs@^3.1.2, memfs@^3.4.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" - integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + version "3.5.3" + resolved "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz" + integrity sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw== dependencies: fs-monkey "^1.0.4" "memoize-one@>=3.1.1 <6": version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== memoize-one@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== merge-descriptors@1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== methods@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" @@ -7956,44 +7930,39 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: microseconds@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" + resolved "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz" integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== -mime-db@1.52.0: +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -"mime-db@>= 1.43.0 < 2": - version "1.54.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" - integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== - mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" mime@1.6.0: version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== min-indent@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== mini-css-extract-plugin@^2.4.5: version "2.9.4" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz#cafa1a42f8c71357f49cd1566810d74ff1cb0200" + resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz" integrity sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ== dependencies: schema-utils "^4.0.0" @@ -8001,58 +7970,58 @@ mini-css-extract-plugin@^2.4.5: minimalistic-assert@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== minimatch@*, minimatch@^10.0.3: version "10.1.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz" integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== dependencies: "@isaacs/brace-expansion" "^5.0.0" minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" minimatch@^5.0.1: version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mkdirp@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== moment@^2.29.1: version "2.30.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== ms@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== msw@^2.7.0: version "2.12.2" - resolved "https://registry.yarnpkg.com/msw/-/msw-2.12.2.tgz#9e5c25ca5cffce6e9bd96c8ae1105096e81a82a2" + resolved "https://registry.npmjs.org/msw/-/msw-2.12.2.tgz" integrity sha512-Fsr8AR5Yu6C0thoWa1Z8qGBFQLDvLsWlAn/v3CNLiUizoRqBYArK3Ex3thXpMWRr1Li5/MKLOEZ5mLygUmWi1A== dependencies: "@inquirer/confirm" "^5.0.0" @@ -8076,7 +8045,7 @@ msw@^2.7.0: multicast-dns@^7.2.5: version "7.2.5" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz" integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== dependencies: dns-packet "^5.2.2" @@ -8084,49 +8053,49 @@ multicast-dns@^7.2.5: mute-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" + resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz" integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== nano-time@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + resolved "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz" integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== dependencies: big-integer "^1.6.16" nanoid@^3.3.11, nanoid@^3.3.7: version "3.3.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== natural-compare-lite@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== negotiator@0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== negotiator@~0.6.4: version "0.6.4" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz" integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== neo-async@^2.6.2: version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== no-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== dependencies: lower-case "^2.0.2" @@ -8134,58 +8103,58 @@ no-case@^3.0.4: node-emoji@^1.10.0: version "1.11.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz" integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== dependencies: lodash "^4.17.21" node-forge@^1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751" - integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg== + version "1.3.1" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-int64@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.27: version "2.0.27" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz" integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== normalize-range@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== normalize-url@^6.0.1: version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== npm-run-path@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" nth-check@^2.0.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz" integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== dependencies: boolbase "^1.0.0" numeral@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506" + resolved "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz" integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== nwsapi@2.2.13, nwsapi@^2.2.2: @@ -8195,22 +8164,22 @@ nwsapi@2.2.13, nwsapi@^2.2.2: object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.13.3, object-inspect@^1.13.4: version "1.13.4" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-keys@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.4, object.assign@^4.1.7: version "4.1.7" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz" integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== dependencies: call-bind "^1.0.8" @@ -8222,7 +8191,7 @@ object.assign@^4.1.4, object.assign@^4.1.7: object.entries@^1.1.9: version "1.1.9" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz" integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== dependencies: call-bind "^1.0.8" @@ -8232,7 +8201,7 @@ object.entries@^1.1.9: object.fromentries@^2.0.8: version "2.0.8" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz" integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: call-bind "^1.0.7" @@ -8242,7 +8211,7 @@ object.fromentries@^2.0.8: object.groupby@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + resolved "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz" integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== dependencies: call-bind "^1.0.7" @@ -8251,7 +8220,7 @@ object.groupby@^1.0.3: object.values@^1.1.6, object.values@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + resolved "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz" integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== dependencies: call-bind "^1.0.8" @@ -8261,43 +8230,43 @@ object.values@^1.1.6, object.values@^1.2.1: oblivious-set@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" + resolved "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz" integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== on-finished@2.4.1: version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: ee-first "1.1.1" on-headers@~1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz" integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" onetime@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" open@^8.0.9, open@^8.4.0: version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== dependencies: define-lazy-prop "^2.0.0" @@ -8306,7 +8275,7 @@ open@^8.0.9, open@^8.4.0: optionator@^0.8.1: version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: deep-is "~0.1.3" @@ -8318,7 +8287,7 @@ optionator@^0.8.1: optionator@^0.9.3: version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" @@ -8330,12 +8299,12 @@ optionator@^0.9.3: outvariant@^1.4.0, outvariant@^1.4.3: version "1.4.3" - resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" + resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz" integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== own-keys@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + resolved "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz" integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== dependencies: get-intrinsic "^1.2.6" @@ -8344,42 +8313,42 @@ own-keys@^1.0.1: p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" p-locate@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== dependencies: p-limit "^2.0.0" p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" p-locate@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: p-limit "^3.0.2" p-retry@^4.5.0: version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz" integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== dependencies: "@types/retry" "0.12.0" @@ -8387,12 +8356,12 @@ p-retry@^4.5.0: p-try@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== param-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== dependencies: dot-case "^3.0.4" @@ -8400,14 +8369,14 @@ param-case@^3.0.4: parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" parse-entities@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz" integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== dependencies: character-entities "^1.0.0" @@ -8419,7 +8388,7 @@ parse-entities@^2.0.0: parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" @@ -8429,24 +8398,24 @@ parse-json@^5.0.0, parse-json@^5.2.0: parse5@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== parse5@^7.0.0, parse5@^7.1.1: version "7.3.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz" integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== dependencies: entities "^6.0.0" parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== pascal-case@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== dependencies: no-case "^3.0.4" @@ -8454,103 +8423,103 @@ pascal-case@^3.1.2: path-exists@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-to-regexp@0.1.12: version "0.1.12" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-to-regexp@^6.3.0: version "6.3.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz" integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== path-type@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== performance-now@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== picocolors@1.1.1, picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== picomatch@^4.0.2: version "4.0.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== pirates@^4.0.4: version "4.0.7" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz" integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== dependencies: find-up "^4.0.0" pkg-up@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz" integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== dependencies: find-up "^3.0.0" possible-typed-array-names@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz" integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== postcss-attribute-case-insensitive@^5.0.2: version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" + resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz" integrity sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ== dependencies: postcss-selector-parser "^6.0.10" postcss-browser-comments@^4: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz#bcfc86134df5807f5d3c0eefa191d42136b5e72a" + resolved "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz" integrity sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg== postcss-calc@^8.2.3: version "8.2.4" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz" integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== dependencies: postcss-selector-parser "^6.0.9" @@ -8558,35 +8527,35 @@ postcss-calc@^8.2.3: postcss-clamp@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + resolved "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz" integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== dependencies: postcss-value-parser "^4.2.0" postcss-color-functional-notation@^4.2.4: version "4.2.4" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz#21a909e8d7454d3612d1659e471ce4696f28caec" + resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz" integrity sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg== dependencies: postcss-value-parser "^4.2.0" postcss-color-hex-alpha@^8.0.4: version "8.0.4" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz#c66e2980f2fbc1a63f5b079663340ce8b55f25a5" + resolved "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz" integrity sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ== dependencies: postcss-value-parser "^4.2.0" postcss-color-rebeccapurple@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz#63fdab91d878ebc4dd4b7c02619a0c3d6a56ced0" + resolved "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz" integrity sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg== dependencies: postcss-value-parser "^4.2.0" postcss-colormin@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz" integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== dependencies: browserslist "^4.21.4" @@ -8596,7 +8565,7 @@ postcss-colormin@^5.3.1: postcss-convert-values@^5.1.3: version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" + resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz" integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== dependencies: browserslist "^4.21.4" @@ -8604,55 +8573,55 @@ postcss-convert-values@^5.1.3: postcss-custom-media@^8.0.2: version "8.0.2" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz#c8f9637edf45fef761b014c024cee013f80529ea" + resolved "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz" integrity sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg== dependencies: postcss-value-parser "^4.2.0" postcss-custom-properties@^12.1.10: version "12.1.11" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz#d14bb9b3989ac4d40aaa0e110b43be67ac7845cf" + resolved "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz" integrity sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ== dependencies: postcss-value-parser "^4.2.0" postcss-custom-selectors@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz#1ab4684d65f30fed175520f82d223db0337239d9" + resolved "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz" integrity sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg== dependencies: postcss-selector-parser "^6.0.4" postcss-dir-pseudo-class@^6.0.5: version "6.0.5" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz#2bf31de5de76added44e0a25ecf60ae9f7c7c26c" + resolved "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz" integrity sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA== dependencies: postcss-selector-parser "^6.0.10" postcss-discard-comments@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" + resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz" integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== postcss-discard-duplicates@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz" integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== postcss-discard-empty@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz" integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== postcss-discard-overridden@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz" integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== postcss-double-position-gradients@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz#b96318fdb477be95997e86edd29c6e3557a49b91" + resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz" integrity sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -8660,55 +8629,55 @@ postcss-double-position-gradients@^3.1.2: postcss-env-function@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-4.0.6.tgz#7b2d24c812f540ed6eda4c81f6090416722a8e7a" + resolved "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz" integrity sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA== dependencies: postcss-value-parser "^4.2.0" postcss-flexbugs-fixes@^5.0.2: version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d" + resolved "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz" integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== postcss-focus-visible@^6.0.4: version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz#50c9ea9afa0ee657fb75635fabad25e18d76bf9e" + resolved "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz" integrity sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw== dependencies: postcss-selector-parser "^6.0.9" postcss-focus-within@^5.0.4: version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz#5b1d2ec603195f3344b716c0b75f61e44e8d2e20" + resolved "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz" integrity sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ== dependencies: postcss-selector-parser "^6.0.9" postcss-font-variant@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + resolved "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz" integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== postcss-gap-properties@^3.0.5: version "3.0.5" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz#f7e3cddcf73ee19e94ccf7cb77773f9560aa2fff" + resolved "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz" integrity sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg== postcss-image-set-function@^4.0.7: version "4.0.7" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz#08353bd756f1cbfb3b6e93182c7829879114481f" + resolved "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz" integrity sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw== dependencies: postcss-value-parser "^4.2.0" postcss-initial@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" + resolved "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz" integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== postcss-lab-function@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz#6fe4c015102ff7cd27d1bd5385582f67ebdbdc98" + resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz" integrity sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -8716,7 +8685,7 @@ postcss-lab-function@^4.2.1: postcss-loader@^6.2.1: version "6.2.1" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef" + resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz" integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q== dependencies: cosmiconfig "^7.0.0" @@ -8725,17 +8694,17 @@ postcss-loader@^6.2.1: postcss-logical@^5.0.4: version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-5.0.4.tgz#ec75b1ee54421acc04d5921576b7d8db6b0e6f73" + resolved "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz" integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== postcss-media-minmax@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz#7140bddec173e2d6d657edbd8554a55794e2a5b5" + resolved "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz" integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== postcss-merge-longhand@^5.1.7: version "5.1.7" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" + resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz" integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== dependencies: postcss-value-parser "^4.2.0" @@ -8743,7 +8712,7 @@ postcss-merge-longhand@^5.1.7: postcss-merge-rules@^5.1.4: version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz" integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== dependencies: browserslist "^4.21.4" @@ -8753,14 +8722,14 @@ postcss-merge-rules@^5.1.4: postcss-minify-font-values@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz" integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== dependencies: postcss-value-parser "^4.2.0" postcss-minify-gradients@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" + resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz" integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== dependencies: colord "^2.9.1" @@ -8769,7 +8738,7 @@ postcss-minify-gradients@^5.1.1: postcss-minify-params@^5.1.4: version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" + resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz" integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== dependencies: browserslist "^4.21.4" @@ -8778,19 +8747,19 @@ postcss-minify-params@^5.1.4: postcss-minify-selectors@^5.2.1: version "5.2.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" + resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz" integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== dependencies: postcss-selector-parser "^6.0.5" postcss-modules-extract-imports@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz" integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== postcss-modules-local-by-default@^4.0.5: version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + resolved "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz" integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== dependencies: icss-utils "^5.0.0" @@ -8799,21 +8768,21 @@ postcss-modules-local-by-default@^4.0.5: postcss-modules-scope@^3.2.0: version "3.2.1" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" + resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz" integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== dependencies: postcss-selector-parser "^7.0.0" postcss-modules-values@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + resolved "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== dependencies: icss-utils "^5.0.0" postcss-nesting@^10.2.0: version "10.2.0" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.2.0.tgz#0b12ce0db8edfd2d8ae0aaf86427370b898890be" + resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz" integrity sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA== dependencies: "@csstools/selector-specificity" "^2.0.0" @@ -8821,47 +8790,47 @@ postcss-nesting@^10.2.0: postcss-normalize-charset@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz" integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== postcss-normalize-display-values@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz" integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-positions@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" + resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz" integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-repeat-style@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" + resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz" integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-string@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz" integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-timing-functions@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz" integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-unicode@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" + resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz" integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== dependencies: browserslist "^4.21.4" @@ -8869,7 +8838,7 @@ postcss-normalize-unicode@^5.1.1: postcss-normalize-url@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz" integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== dependencies: normalize-url "^6.0.1" @@ -8877,14 +8846,14 @@ postcss-normalize-url@^5.1.0: postcss-normalize-whitespace@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz" integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== dependencies: postcss-value-parser "^4.2.0" postcss-normalize@^10.0.1: version "10.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize/-/postcss-normalize-10.0.1.tgz#464692676b52792a06b06880a176279216540dd7" + resolved "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz" integrity sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA== dependencies: "@csstools/normalize.css" "*" @@ -8893,12 +8862,12 @@ postcss-normalize@^10.0.1: postcss-opacity-percentage@^1.1.2: version "1.1.3" - resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz#5b89b35551a556e20c5d23eb5260fbfcf5245da6" + resolved "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz" integrity sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A== postcss-ordered-values@^5.1.3: version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" + resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz" integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== dependencies: cssnano-utils "^3.1.0" @@ -8906,26 +8875,26 @@ postcss-ordered-values@^5.1.3: postcss-overflow-shorthand@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz#7ed6486fec44b76f0eab15aa4866cda5d55d893e" + resolved "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz" integrity sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A== dependencies: postcss-value-parser "^4.2.0" postcss-page-break@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + resolved "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz" integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== postcss-place@^7.0.5: version "7.0.5" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-7.0.5.tgz#95dbf85fd9656a3a6e60e832b5809914236986c4" + resolved "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz" integrity sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g== dependencies: postcss-value-parser "^4.2.0" postcss-preset-env@^7.0.1: version "7.8.3" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz#2a50f5e612c3149cc7af75634e202a5b2ad4f1e2" + resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz" integrity sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag== dependencies: "@csstools/postcss-cascade-layers" "^1.1.1" @@ -8980,14 +8949,14 @@ postcss-preset-env@^7.0.1: postcss-pseudo-class-any-link@^7.1.6: version "7.1.6" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz#2693b221902da772c278def85a4d9a64b6e617ab" + resolved "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz" integrity sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w== dependencies: postcss-selector-parser "^6.0.10" postcss-reduce-initial@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz" integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== dependencies: browserslist "^4.21.4" @@ -8995,26 +8964,26 @@ postcss-reduce-initial@^5.1.2: postcss-reduce-transforms@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz" integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== dependencies: postcss-value-parser "^4.2.0" postcss-replace-overflow-wrap@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + resolved "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz" integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== postcss-selector-not@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz#8f0a709bf7d4b45222793fc34409be407537556d" + resolved "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz" integrity sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ== dependencies: postcss-selector-parser "^6.0.10" postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: version "6.1.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== dependencies: cssesc "^3.0.0" @@ -9022,7 +8991,7 @@ postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.4, postcss-selecto postcss-selector-parser@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz" integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA== dependencies: cssesc "^3.0.0" @@ -9030,7 +8999,7 @@ postcss-selector-parser@^7.0.0: postcss-svgo@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz" integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== dependencies: postcss-value-parser "^4.2.0" @@ -9038,19 +9007,19 @@ postcss-svgo@^5.1.0: postcss-unique-selectors@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz" integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== dependencies: postcss-selector-parser "^6.0.5" postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@8.4.49: version "8.4.49" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz" integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== dependencies: nanoid "^3.3.7" @@ -9059,7 +9028,7 @@ postcss@8.4.49: postcss@^8.2.14, postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.4: version "8.5.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: nanoid "^3.3.11" @@ -9068,27 +9037,27 @@ postcss@^8.2.14, postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.4: prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prelude-ls@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== prettier@^3.5.3: version "3.6.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz" integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== pretty-error@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz" integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== dependencies: lodash "^4.17.20" @@ -9096,7 +9065,7 @@ pretty-error@^4.0.0: pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== dependencies: ansi-regex "^5.0.1" @@ -9105,7 +9074,7 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: pretty-format@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== dependencies: "@jest/schemas" "^29.6.3" @@ -9114,29 +9083,29 @@ pretty-format@^29.7.0: prismjs@^1.30.0: version "1.30.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9" + resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz" integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw== prismjs@~1.27.0: version "1.27.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz" integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== process-nextick-args@~2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== promise@^8.1.0: version "8.3.0" - resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + resolved "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz" integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== dependencies: asap "~2.0.6" prompts@^2.0.1, prompts@^2.4.2: version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" @@ -9144,7 +9113,7 @@ prompts@^2.0.1, prompts@^2.4.2: prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: loose-envify "^1.4.0" @@ -9153,14 +9122,14 @@ prop-types@^15.6.2, prop-types@^15.8.1: property-information@^5.0.0, property-information@^5.3.0: version "5.6.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" + resolved "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz" integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== dependencies: xtend "^4.0.0" protobufjs-cli@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz#c58b8566784f0fa1aff11e8d875a31de999637fe" + resolved "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz" integrity sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ== dependencies: chalk "^4.0.0" @@ -9176,7 +9145,7 @@ protobufjs-cli@^1.1.3: protobufjs@^7.1.1: version "7.5.4" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz" integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== dependencies: "@protobufjs/aspromise" "^1.1.2" @@ -9194,7 +9163,7 @@ protobufjs@^7.1.1: proxy-addr@~2.0.7: version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: forwarded "0.2.0" @@ -9202,36 +9171,36 @@ proxy-addr@~2.0.7: psl@^1.1.33: version "1.15.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" + resolved "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz" integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== dependencies: punycode "^2.3.1" punycode.js@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz" integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== pure-rand@^6.0.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== qs@6.13.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: side-channel "^1.0.6" query-string@^7.1.1: version "7.1.3" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + resolved "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz" integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== dependencies: decode-uri-component "^0.2.2" @@ -9241,41 +9210,41 @@ query-string@^7.1.1: querystringify@^2.1.1: version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== queue-microtask@^1.2.2: version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== raf-schd@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== raf@^3.4.1: version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== dependencies: performance-now "^2.1.0" randombytes@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== raw-body@2.5.2: version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" @@ -9285,7 +9254,7 @@ raw-body@2.5.2: react-app-polyfill@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz#95221e0a9bd259e5ca6b177c7bb1cb6768f68fd7" + resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz" integrity sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w== dependencies: core-js "^3.19.2" @@ -9297,14 +9266,14 @@ react-app-polyfill@^3.0.0: react-clientside-effect@^1.2.7: version "1.2.8" - resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz#0b90a9d7b2a1823a3a10ed1ea3f651f7e0301cb7" + resolved "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz" integrity sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw== dependencies: "@babel/runtime" "^7.12.13" react-code-blocks@^0.1.6: version "0.1.6" - resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.1.6.tgz#ec64e7899223d3e910eb916465a66d95ce1ae1b2" + resolved "https://registry.npmjs.org/react-code-blocks/-/react-code-blocks-0.1.6.tgz" integrity sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg== dependencies: "@babel/runtime" "^7.10.4" @@ -9314,7 +9283,7 @@ react-code-blocks@^0.1.6: react-dev-utils@^12.0.1: version "12.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" + resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz" integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== dependencies: "@babel/code-frame" "^7.16.0" @@ -9344,7 +9313,7 @@ react-dev-utils@^12.0.1: react-dom@^18.3.1: version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== dependencies: loose-envify "^1.1.0" @@ -9352,7 +9321,7 @@ react-dom@^18.3.1: react-dropzone@^11.7.1: version "11.7.1" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356" + resolved "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.7.1.tgz" integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ== dependencies: attr-accept "^2.2.2" @@ -9361,7 +9330,7 @@ react-dropzone@^11.7.1: react-element-to-jsx-string@^15.0.0: version "15.0.0" - resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz#1cafd5b6ad41946ffc8755e254da3fc752a01ac6" + resolved "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz" integrity sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ== dependencies: "@base2/pretty-print-object" "1.0.1" @@ -9370,12 +9339,12 @@ react-element-to-jsx-string@^15.0.0: react-error-overlay@^6.0.11: version "6.1.0" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.1.0.tgz#22b86256beb1c5856f08a9a228adb8121dd985f2" + resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz" integrity sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ== react-focus-lock@^2.13.6: version "2.13.6" - resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.13.6.tgz#29751bf2e4e30f6248673cd87a347c74ff2af672" + resolved "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.13.6.tgz" integrity sha512-ehylFFWyYtBKXjAO9+3v8d0i+cnc1trGS0vlTGhzFW1vbFXVUTmR8s2tt/ZQG8x5hElg6rhENlLG1H3EZK0Llg== dependencies: "@babel/runtime" "^7.0.0" @@ -9387,7 +9356,7 @@ react-focus-lock@^2.13.6: react-focus-on@^3.9.1: version "3.10.0" - resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.10.0.tgz#60f6af03b59be5a0901f86cf9e24799c33e90327" + resolved "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.10.0.tgz" integrity sha512-r2yQchO6QfV5zB3J4Gj6cTYBoxD369vkt0oKj1NJLA5ChQzxjko6V/dqQ7nvmaUBm5pHC+pa8tzHT9jtsVRFMQ== dependencies: aria-hidden "^1.2.5" @@ -9399,27 +9368,27 @@ react-focus-on@^3.9.1: react-is@18.1.0: version "18.1.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz" integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-is@^18.0.0: version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== react-query@^3.39.3: version "3.39.3" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" + resolved "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz" integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== dependencies: "@babel/runtime" "^7.5.5" @@ -9428,7 +9397,7 @@ react-query@^3.39.3: react-redux@^8.1.3: version "8.1.3" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz" integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== dependencies: "@babel/runtime" "^7.12.1" @@ -9440,12 +9409,12 @@ react-redux@^8.1.3: react-refresh@^0.11.0: version "0.11.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.7: version "2.3.8" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" + resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz" integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== dependencies: react-style-singleton "^2.2.2" @@ -9453,7 +9422,7 @@ react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.7: react-remove-scroll@^2.6.3: version "2.7.1" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz#d2101d414f6d81d7d3bf033f3c1cb4785789f753" + resolved "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz" integrity sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA== dependencies: react-remove-scroll-bar "^2.3.7" @@ -9464,7 +9433,7 @@ react-remove-scroll@^2.6.3: react-router-dom@^6.28.0: version "6.30.2" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.30.2.tgz#ee8c161bce4890d34484b552f8510f9af0e22b01" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz" integrity sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q== dependencies: "@remix-run/router" "1.23.1" @@ -9472,14 +9441,14 @@ react-router-dom@^6.28.0: react-router@6.30.2: version "6.30.2" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.2.tgz#c78a3b40f7011f49a373b1df89492e7d4ec12359" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz" integrity sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA== dependencies: "@remix-run/router" "1.23.1" react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" + resolved "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz" integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== dependencies: get-nonce "^1.0.0" @@ -9487,7 +9456,7 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: react-syntax-highlighter@^15.5.0: version "15.6.6" - resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz#77417c81ebdc554300d0332800a2e1efe5b1190b" + resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz" integrity sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw== dependencies: "@babel/runtime" "^7.3.1" @@ -9499,12 +9468,12 @@ react-syntax-highlighter@^15.5.0: react-virtualized-auto-sizer@^1.0.24: version "1.0.26" - resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz#e9470ef6a778dc4f1d5fd76305fa2d8b610c357a" + resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz" integrity sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A== react-window@^1.8.10: version "1.8.11" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525" + resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz" integrity sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ== dependencies: "@babel/runtime" "^7.0.0" @@ -9512,14 +9481,14 @@ react-window@^1.8.10: react@^18.3.1: version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== dependencies: loose-envify "^1.1.0" reactflow@^11.11.4: version "11.11.4" - resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.4.tgz#e3593e313420542caed81aecbd73fb9bc6576653" + resolved "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz" integrity sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og== dependencies: "@reactflow/background" "11.3.14" @@ -9531,7 +9500,7 @@ reactflow@^11.11.4: readable-stream@^2.0.1: version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" @@ -9544,7 +9513,7 @@ readable-stream@^2.0.1: readable-stream@^3.0.6: version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" @@ -9553,21 +9522,21 @@ readable-stream@^3.0.6: readdirp@~3.6.0: version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" recursive-readdir@^2.2.2: version "2.2.3" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz" integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== dependencies: minimatch "^3.0.5" redent@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== dependencies: indent-string "^4.0.0" @@ -9575,14 +9544,14 @@ redent@^3.0.0: redux@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: "@babel/runtime" "^7.9.2" reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz" integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== dependencies: call-bind "^1.0.8" @@ -9596,7 +9565,7 @@ reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: refractor@^3.6.0: version "3.6.0" - resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" + resolved "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz" integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== dependencies: hastscript "^6.0.0" @@ -9605,29 +9574,29 @@ refractor@^3.6.0: regenerate-unicode-properties@^10.2.2: version "10.2.2" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz" integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== dependencies: regenerate "^1.4.2" regenerate@^1.4.2: version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== regenerator-runtime@^0.13.9: version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regex-parser@^2.2.11: version "2.3.1" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.3.1.tgz#ee3f70e50bdd81a221d505242cb9a9c275a2ad91" + resolved "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz" integrity sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ== regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: version "1.5.4" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz" integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== dependencies: call-bind "^1.0.8" @@ -9639,7 +9608,7 @@ regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: regexpu-core@^6.3.1: version "6.4.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz" integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== dependencies: regenerate "^1.4.2" @@ -9651,26 +9620,26 @@ regexpu-core@^6.3.1: regjsgen@^0.8.0: version "0.8.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz" integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== regjsparser@^0.13.0: version "0.13.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.0.tgz#01f8351335cf7898d43686bc74d2dd71c847ecc0" + resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz" integrity sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q== dependencies: jsesc "~3.1.0" rehype-raw@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-5.1.0.tgz#66d5e8d7188ada2d31bc137bc19a1000cf2c6b7e" + resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-5.1.0.tgz" integrity sha512-MDvHAb/5mUnif2R+0IPCYJU8WjHa9UzGtM/F4AVy5GixPlDZ1z3HacYy4xojDU+uBa+0X/3PIfyQI26/2ljJNA== dependencies: hast-util-raw "^6.1.0" rehype-react@^6.2.1: version "6.2.1" - resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a" + resolved "https://registry.npmjs.org/rehype-react/-/rehype-react-6.2.1.tgz" integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg== dependencies: "@mapbox/hast-util-table-cell-style" "^0.2.0" @@ -9678,26 +9647,26 @@ rehype-react@^6.2.1: rehype-stringify@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-8.0.0.tgz#9b6afb599bcf3165f10f93fc8548f9a03d2ec2ba" + resolved "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-8.0.0.tgz" integrity sha512-VkIs18G0pj2xklyllrPSvdShAV36Ff3yE5PUO9u36f6+2qJFnn22Z5gKwBOwgXviux4UC7K+/j13AnZfPICi/g== dependencies: hast-util-to-html "^7.1.1" relateurl@^0.2.7: version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + resolved "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz" integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== remark-breaks@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/remark-breaks/-/remark-breaks-2.0.2.tgz#55fdec6c7da84f659aa7fdb1aa95b632870cee8d" + resolved "https://registry.npmjs.org/remark-breaks/-/remark-breaks-2.0.2.tgz" integrity sha512-LsQnPPQ7Fzp9RTjj4IwdEmjPOr9bxe9zYKWhs9ZQOg9hMg8rOfeeqQ410cvVdIK87Famqza1CKRxNkepp2EvUA== dependencies: unist-util-visit "^2.0.0" remark-emoji@^2.1.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.2.0.tgz#1c702090a1525da5b80e15a8f963ef2c8236cac7" + resolved "https://registry.npmjs.org/remark-emoji/-/remark-emoji-2.2.0.tgz" integrity sha512-P3cj9s5ggsUvWw5fS2uzCHJMGuXYRb0NnZqYlNecewXt8QBU9n5vW3DUUKOhepS8F9CwdMx9B8a3i7pqFWAI5w== dependencies: emoticon "^3.2.0" @@ -9706,7 +9675,7 @@ remark-emoji@^2.1.0: remark-parse-no-trim@^8.0.4: version "8.0.4" - resolved "https://registry.yarnpkg.com/remark-parse-no-trim/-/remark-parse-no-trim-8.0.4.tgz#f5c9531644284071d4a57a49e19a42ad4e8040bd" + resolved "https://registry.npmjs.org/remark-parse-no-trim/-/remark-parse-no-trim-8.0.4.tgz" integrity sha512-WtqeHNTZ0LSdyemmY1/G6y9WoEFblTtgckfKF5/NUnri919/0/dEu8RCDfvXtJvu96soMvT+mLWWgYVUaiHoag== dependencies: ccount "^1.0.0" @@ -9727,19 +9696,19 @@ remark-parse-no-trim@^8.0.4: remark-rehype@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945" + resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-8.1.0.tgz" integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA== dependencies: mdast-util-to-hast "^10.2.0" remove-accents@0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" + resolved "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz" integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A== renderkid@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + resolved "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz" integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== dependencies: css-select "^4.1.3" @@ -9750,51 +9719,51 @@ renderkid@^3.0.0: repeat-string@^1.5.4: version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== requires-port@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== requizzle@^0.2.3: version "0.2.4" - resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" + resolved "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz" integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== dependencies: lodash "^4.17.21" resolve-cwd@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== dependencies: resolve-from "^5.0.0" resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-from@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-url-loader@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795" + resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz" integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg== dependencies: adjust-sourcemap-loader "^4.0.0" @@ -9805,12 +9774,12 @@ resolve-url-loader@^5.0.0: resolve.exports@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz" integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.10, resolve@^1.22.4: version "1.22.11" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz" integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== dependencies: is-core-module "^2.16.1" @@ -9819,7 +9788,7 @@ resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.10, resolve@^1. resolve@^2.0.0-next.5: version "2.0.0-next.5" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz" integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: is-core-module "^2.13.0" @@ -9828,29 +9797,29 @@ resolve@^2.0.0-next.5: retry@^0.13.1: version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== rettime@^0.7.0: version "0.7.0" - resolved "https://registry.yarnpkg.com/rettime/-/rettime-0.7.0.tgz#c040f1a65e396eaa4b8346dd96ed937edc79d96f" + resolved "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz" integrity sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw== reusify@^1.0.4: version "1.1.0" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" rollup-plugin-copy@^3.5.0: version "3.5.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz#7ffa2a7a8303e143876fa64fb5eed9022d304eeb" + resolved "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz" integrity sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA== dependencies: "@types/fs-extra" "^8.0.1" @@ -9861,28 +9830,28 @@ rollup-plugin-copy@^3.5.0: rollup-plugin-import-css@^3.0.2: version "3.5.8" - resolved "https://registry.yarnpkg.com/rollup-plugin-import-css/-/rollup-plugin-import-css-3.5.8.tgz#f1f7b61ae56c3e1edc9c71dcfde85e5464500cf8" + resolved "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.5.8.tgz" integrity sha512-a3YsZnwHz66mRHCKHjaPCSfWczczvS/HTkgDc+Eogn0mt/0JZXz0WjK0fzM5WwBpVtOqHB4/gHdmEY40ILsaVg== dependencies: "@rollup/pluginutils" "^5.1.3" rollup-plugin-svg@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-svg/-/rollup-plugin-svg-2.0.0.tgz#ce11b55e915d5b2190328c4e6632bd6b4fe12ee9" + resolved "https://registry.npmjs.org/rollup-plugin-svg/-/rollup-plugin-svg-2.0.0.tgz" integrity sha512-DmE7dSQHo1SC5L2uH2qul3Mjyd5oV6U1aVVkyvTLX/mUsRink7f1b1zaIm+32GEBA6EHu8H/JJi3DdWqM53ySQ== dependencies: rollup-pluginutils "^1.3.1" rollup-plugin-svgo@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-svgo/-/rollup-plugin-svgo-2.0.0.tgz#d182c145fd11f3f8a43de804e3f5b4b70d580a7a" + resolved "https://registry.npmjs.org/rollup-plugin-svgo/-/rollup-plugin-svgo-2.0.0.tgz" integrity sha512-0ryWbGY3sP62brw5p8md5W+1WUMrLUE8d437nGh9gQ+fZFFjlYbyIkctBrTvCm3bIdqN4gxbxg1Xxen1tteD+A== dependencies: svgo "2.8.0" rollup-plugin-terser@^7.0.0, rollup-plugin-terser@^7.0.2: version "7.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + resolved "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz" integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== dependencies: "@babel/code-frame" "^7.10.4" @@ -9892,7 +9861,7 @@ rollup-plugin-terser@^7.0.0, rollup-plugin-terser@^7.0.2: rollup-pluginutils@^1.3.1: version "1.5.2" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408" + resolved "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz" integrity sha512-SjdWWWO/CUoMpDy8RUbZ/pSpG68YHmhk5ROKNIoi2En9bJ8bTt3IhYi254RWiTclQmL7Awmrq+rZFOhZkJAHmQ== dependencies: estree-walker "^0.2.1" @@ -9900,21 +9869,21 @@ rollup-pluginutils@^1.3.1: rollup@^2.43.1, rollup@^2.68.0: version "2.79.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz" integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ== optionalDependencies: fsevents "~2.3.2" run-parallel@^1.1.9: version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" safe-array-concat@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz" integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== dependencies: call-bind "^1.0.8" @@ -9925,17 +9894,17 @@ safe-array-concat@^1.1.3: safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-push-apply@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + resolved "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz" integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== dependencies: es-errors "^1.3.0" @@ -9943,7 +9912,7 @@ safe-push-apply@^1.0.0: safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz" integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== dependencies: call-bound "^1.0.2" @@ -9952,17 +9921,17 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sanitize.css@*: version "13.0.0" - resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-13.0.0.tgz#2675553974b27964c75562ade3bd85d79879f173" + resolved "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz" integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA== sass-loader@^12.3.0: version "12.6.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb" + resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz" integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== dependencies: klona "^2.0.4" @@ -9970,21 +9939,21 @@ sass-loader@^12.3.0: saxes@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + resolved "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz" integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== dependencies: xmlchars "^2.2.0" scheduler@^0.23.2: version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz" integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: loose-envify "^1.1.0" schema-utils@2.7.0: version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== dependencies: "@types/json-schema" "^7.0.4" @@ -9993,7 +9962,7 @@ schema-utils@2.7.0: schema-utils@^2.6.5: version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== dependencies: "@types/json-schema" "^7.0.5" @@ -10002,7 +9971,7 @@ schema-utils@^2.6.5: schema-utils@^3.0.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== dependencies: "@types/json-schema" "^7.0.8" @@ -10011,7 +9980,7 @@ schema-utils@^3.0.0: schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.3: version "4.3.3" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz" integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== dependencies: "@types/json-schema" "^7.0.9" @@ -10021,12 +9990,12 @@ schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3 select-hose@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== selfsigned@^2.1.1: version "2.4.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz" integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== dependencies: "@types/node-forge" "^1.3.0" @@ -10034,17 +10003,17 @@ selfsigned@^2.1.1: semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.1.2, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4: version "7.7.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== send@0.19.0: version "0.19.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" @@ -10063,26 +10032,26 @@ send@0.19.0: serialize-javascript@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz" integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== dependencies: randombytes "^2.1.0" serialize-javascript@^6.0.0, serialize-javascript@^6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" serialize-query-params@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/serialize-query-params/-/serialize-query-params-2.0.2.tgz#598a3fb9e13f4ea1c1992fbd20231aa16b31db81" + resolved "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz" integrity sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q== serve-index@^1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== dependencies: accepts "~1.3.4" @@ -10095,7 +10064,7 @@ serve-index@^1.9.1: serve-static@1.16.2: version "1.16.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: encodeurl "~2.0.0" @@ -10105,7 +10074,7 @@ serve-static@1.16.2: set-function-length@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: define-data-property "^1.1.4" @@ -10117,7 +10086,7 @@ set-function-length@^1.2.2: set-function-name@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + resolved "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== dependencies: define-data-property "^1.1.4" @@ -10127,7 +10096,7 @@ set-function-name@^2.0.2: set-proto@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + resolved "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz" integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== dependencies: dunder-proto "^1.0.1" @@ -10136,39 +10105,39 @@ set-proto@^1.0.0: setprototypeof@1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== setprototypeof@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shallowequal@1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.7.3, shell-quote@^1.8.3: version "1.8.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz" integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== side-channel-list@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: es-errors "^1.3.0" @@ -10176,7 +10145,7 @@ side-channel-list@^1.0.0: side-channel-map@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + resolved "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz" integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== dependencies: call-bound "^1.0.2" @@ -10186,7 +10155,7 @@ side-channel-map@^1.0.1: side-channel-weakmap@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== dependencies: call-bound "^1.0.2" @@ -10197,7 +10166,7 @@ side-channel-weakmap@^1.0.2: side-channel@^1.0.6, side-channel@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== dependencies: es-errors "^1.3.0" @@ -10208,32 +10177,32 @@ side-channel@^1.0.6, side-channel@^1.1.0: signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signal-exit@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== sisteransi@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== slash@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== slash@^5.0.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" + resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz" integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== snake-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== dependencies: dot-case "^3.0.4" @@ -10241,7 +10210,7 @@ snake-case@^3.0.4: sockjs@^0.3.24: version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== dependencies: faye-websocket "^0.11.3" @@ -10250,17 +10219,17 @@ sockjs@^0.3.24: source-list-map@^2.0.0, source-list-map@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== source-map-js@^1.0.1, source-map-js@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== source-map-loader@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.2.tgz#af23192f9b344daa729f6772933194cc5fa54fee" + resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz" integrity sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg== dependencies: abab "^2.0.5" @@ -10269,7 +10238,7 @@ source-map-loader@^3.0.0: source-map-support@0.5.13: version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: buffer-from "^1.0.0" @@ -10277,7 +10246,7 @@ source-map-support@0.5.13: source-map-support@~0.5.20: version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" @@ -10285,39 +10254,39 @@ source-map-support@~0.5.20: source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@^0.5.7: version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== source-map@^0.7.3: version "0.7.6" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz" integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ== source-map@^0.8.0-beta.0: version "0.8.0-beta.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz" integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== dependencies: whatwg-url "^7.0.0" sourcemap-codec@^1.4.8: version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== space-separated-tokens@^1.0.0: version "1.1.5" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" + resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz" integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== spdy-transport@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== dependencies: debug "^4.1.0" @@ -10329,7 +10298,7 @@ spdy-transport@^3.0.0: spdy@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== dependencies: debug "^4.1.0" @@ -10340,61 +10309,61 @@ spdy@^4.0.2: split-on-first@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== stable@^0.1.8: version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== stack-utils@^2.0.3: version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: escape-string-regexp "^2.0.0" stackframe@^1.3.4: version "1.3.4" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz" integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== state-toggle@^1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" + resolved "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz" integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== static-eval@2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" + resolved "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz" integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== dependencies: escodegen "^1.8.1" statuses@2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== "statuses@>= 1.4.0 < 2": version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== statuses@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz" integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== stop-iteration-iterator@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz" integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== dependencies: es-errors "^1.3.0" @@ -10402,17 +10371,17 @@ stop-iteration-iterator@^1.1.0: strict-event-emitter@^0.5.1: version "0.5.1" - resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" + resolved "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz" integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ== strict-uri-encode@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== string-length@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: char-regex "^1.0.2" @@ -10420,7 +10389,7 @@ string-length@^4.0.1: string-length@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-5.0.1.tgz#3d647f497b6e8e8d41e422f7e0b23bc536c8381e" + resolved "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz" integrity sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow== dependencies: char-regex "^2.0.0" @@ -10428,12 +10397,12 @@ string-length@^5.0.1: string-natural-compare@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" + resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -10442,7 +10411,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: string.prototype.includes@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" + resolved "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz" integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== dependencies: call-bind "^1.0.7" @@ -10451,7 +10420,7 @@ string.prototype.includes@^2.0.1: string.prototype.matchall@^4.0.12, string.prototype.matchall@^4.0.6: version "4.0.12" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz" integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== dependencies: call-bind "^1.0.8" @@ -10470,7 +10439,7 @@ string.prototype.matchall@^4.0.12, string.prototype.matchall@^4.0.6: string.prototype.repeat@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + resolved "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz" integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== dependencies: define-properties "^1.1.3" @@ -10478,7 +10447,7 @@ string.prototype.repeat@^1.0.0: string.prototype.trim@^1.2.10: version "1.2.10" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz" integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== dependencies: call-bind "^1.0.8" @@ -10491,7 +10460,7 @@ string.prototype.trim@^1.2.10: string.prototype.trimend@^1.0.9: version "1.0.9" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz" integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== dependencies: call-bind "^1.0.8" @@ -10501,7 +10470,7 @@ string.prototype.trimend@^1.0.9: string.prototype.trimstart@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz" integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: call-bind "^1.0.7" @@ -10510,21 +10479,21 @@ string.prototype.trimstart@^1.0.8: string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" stringify-entities@^3.0.1: version "3.1.0" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.1.0.tgz#b8d3feac256d9ffcc9fa1fefdcf3ca70576ee903" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz" integrity sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg== dependencies: character-entities-html4 "^1.0.0" @@ -10533,7 +10502,7 @@ stringify-entities@^3.0.1: stringify-object@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + resolved "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz" integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== dependencies: get-own-enumerable-property-symbols "^3.0.0" @@ -10542,65 +10511,65 @@ stringify-object@^3.3.0: strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^7.0.1: version "7.1.2" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz" integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== dependencies: ansi-regex "^6.0.1" strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-bom@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== strip-comments@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + resolved "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz" integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-indent@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== dependencies: min-indent "^1.0.0" strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== style-loader@^3.3.1: version "3.3.4" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7" + resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz" integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== style-to-object@^0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" + resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz" integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== dependencies: inline-style-parser "0.1.1" styled-components@^6.1.0: version "6.1.19" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.19.tgz#9a41b4db79a3b7a2477daecabe8dd917235263d6" + resolved "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz" integrity sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA== dependencies: "@emotion/is-prop-valid" "1.2.2" @@ -10615,7 +10584,7 @@ styled-components@^6.1.0: stylehacks@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" + resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz" integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== dependencies: browserslist "^4.21.4" @@ -10623,41 +10592,41 @@ stylehacks@^5.1.1: stylis@4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== stylis@4.3.2: version "4.3.2" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz" integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" supports-color@^8.0.0: version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== svg-parser@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== svgo@2.8.0, svgo@^2.7.0: version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz" integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== dependencies: "@trysound/sax" "0.2.0" @@ -10670,7 +10639,7 @@ svgo@2.8.0, svgo@^2.7.0: svgo@^3.0.2: version "3.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8" + resolved "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz" integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw== dependencies: "@trysound/sax" "0.2.0" @@ -10683,32 +10652,32 @@ svgo@^3.0.2: symbol-tree@^3.2.4: version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== tabbable@^5.3.3: version "5.3.3" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" + resolved "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz" integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== tapable@^1.0.0: version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz" integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== temp-dir@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz" integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== tempy@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" + resolved "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz" integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== dependencies: is-stream "^2.0.0" @@ -10718,7 +10687,7 @@ tempy@^0.6.0: terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.11: version "5.3.14" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz" integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== dependencies: "@jridgewell/trace-mapping" "^0.3.25" @@ -10729,7 +10698,7 @@ terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.11: terser@^5.0.0, terser@^5.10.0, terser@^5.31.1: version "5.44.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c" + resolved "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz" integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw== dependencies: "@jridgewell/source-map" "^0.3.3" @@ -10739,7 +10708,7 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.31.1: test-exclude@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== dependencies: "@istanbuljs/schema" "^0.1.2" @@ -10748,61 +10717,61 @@ test-exclude@^6.0.0: text-diff@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/text-diff/-/text-diff-1.0.1.tgz#6c105905435e337857375c9d2f6ca63e453ff565" + resolved "https://registry.npmjs.org/text-diff/-/text-diff-1.0.1.tgz" integrity sha512-jAnlP3ggZk7FeLX1awaMR8Y2sMyil9P9FXvNjaIJIQBAom1zvpKGGH31htOVrUFp0vlyygmJJpNrbJ4rfjsxrA== text-table@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== thunky@^1.0.2: version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== tiny-invariant@^1.0.6: version "1.3.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== tldts-core@^7.0.18: version "7.0.18" - resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.0.18.tgz#78edfd38e8c35e20fb4d2cde63c759139e169d31" + resolved "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.18.tgz" integrity sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q== tldts@^7.0.5: version "7.0.18" - resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.0.18.tgz#72cac7a2bdb6bba78f8a09fdf7ef84843b09aa94" + resolved "https://registry.npmjs.org/tldts/-/tldts-7.0.18.tgz" integrity sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw== dependencies: tldts-core "^7.0.18" tmp@^0.2.1: version "0.2.5" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz" integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== tmpl@1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" toidentifier@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== tough-cookie@^4.1.2: version "4.1.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz" integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" @@ -10812,43 +10781,43 @@ tough-cookie@^4.1.2: tough-cookie@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.0.tgz#11e418b7864a2c0d874702bc8ce0f011261940e5" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz" integrity sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w== dependencies: tldts "^7.0.5" tr46@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz" integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== dependencies: punycode "^2.1.0" tr46@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + resolved "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz" integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== dependencies: punycode "^2.1.1" trim-trailing-lines@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" + resolved "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz" integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== trough@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" + resolved "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== tryer@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" + resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== tsconfig-paths@^3.15.0: version "3.15.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" @@ -10856,70 +10825,70 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2: +tslib@2.6.2, tslib@^2.6.0: version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.6.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tsutils@^3.21.0: version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" type-check@~0.3.2: version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== dependencies: prelude-ls "~1.1.2" type-detect@4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.16.0: version "0.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz" integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== type-fest@^0.20.2: version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== type-fest@^0.21.3: version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-fest@^4.26.1: version "4.41.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== type-is@~1.6.18: version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" @@ -10927,7 +10896,7 @@ type-is@~1.6.18: typed-array-buffer@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz" integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== dependencies: call-bound "^1.0.3" @@ -10936,7 +10905,7 @@ typed-array-buffer@^1.0.3: typed-array-byte-length@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + resolved "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz" integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== dependencies: call-bind "^1.0.8" @@ -10947,7 +10916,7 @@ typed-array-byte-length@^1.0.3: typed-array-byte-offset@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + resolved "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz" integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== dependencies: available-typed-arrays "^1.0.7" @@ -10960,7 +10929,7 @@ typed-array-byte-offset@^1.0.4: typed-array-length@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz" integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== dependencies: call-bind "^1.0.7" @@ -10972,29 +10941,29 @@ typed-array-length@^1.0.7: typedarray-to-buffer@^3.1.5: version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== dependencies: is-typedarray "^1.0.0" typescript@~5.7.2: version "5.7.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz" integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz" integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== uglify-js@^3.7.7: version "3.19.3" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== unbox-primitive@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz" integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== dependencies: call-bound "^1.0.3" @@ -11004,27 +10973,22 @@ unbox-primitive@^1.1.0: underscore@1.12.1: version "1.12.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== underscore@~1.13.2: version "1.13.7" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz" integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== undici-types@~6.21.0: version "6.21.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== -undici-types@~7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" - integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== - unherit@^1.0.4: version "1.1.3" - resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" + resolved "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz" integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ== dependencies: inherits "^2.0.0" @@ -11032,12 +10996,12 @@ unherit@^1.0.4: unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz" integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== dependencies: unicode-canonical-property-names-ecmascript "^2.0.0" @@ -11045,17 +11009,17 @@ unicode-match-property-ecmascript@^2.0.0: unicode-match-property-value-ecmascript@^2.2.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz" integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== unicode-property-aliases-ecmascript@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz#301d4f8a43d2b75c97adfad87c9dd5350c9475d1" + resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz" integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== unified@^9.2.2: version "9.2.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" + resolved "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz" integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== dependencies: bail "^1.0.0" @@ -11067,60 +11031,60 @@ unified@^9.2.2: unique-string@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz" integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== dependencies: crypto-random-string "^2.0.0" unist-builder@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" + resolved "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz" integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== unist-util-generated@^1.0.0: version "1.1.6" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" + resolved "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz" integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== unist-util-is@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz" integrity sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A== unist-util-is@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz" integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== unist-util-position@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" + resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz" integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== unist-util-remove-position@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc" + resolved "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz" integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA== dependencies: unist-util-visit "^2.0.0" unist-util-stringify-position@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz" integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== dependencies: "@types/unist" "^2.0.2" unist-util-visit-parents@^2.0.0: version "2.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz" integrity sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g== dependencies: unist-util-is "^3.0.0" unist-util-visit-parents@^3.0.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz" integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== dependencies: "@types/unist" "^2.0.0" @@ -11128,14 +11092,14 @@ unist-util-visit-parents@^3.0.0: unist-util-visit@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz" integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw== dependencies: unist-util-visit-parents "^2.0.0" unist-util-visit@^2.0.0, unist-util-visit@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz" integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== dependencies: "@types/unist" "^2.0.0" @@ -11144,22 +11108,22 @@ unist-util-visit@^2.0.0, unist-util-visit@^2.0.3: universalify@^0.1.0: version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unload@2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + resolved "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz" integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== dependencies: "@babel/runtime" "^7.6.2" @@ -11167,22 +11131,22 @@ unload@2.2.0: unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== until-async@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/until-async/-/until-async-3.0.2.tgz#447f1531fdd7bb2b4c7a98869bdb1a4c2a23865f" + resolved "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz" integrity sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw== upath@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== update-browserslist-db@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz#7802aa2ae91477f255b86e0e46dbc787a206ad4a" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz" integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A== dependencies: escalade "^3.2.0" @@ -11190,14 +11154,14 @@ update-browserslist-db@^1.1.4: uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" url-parse@^1.5.10, url-parse@^1.5.3: version "1.5.10" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== dependencies: querystringify "^2.1.1" @@ -11205,26 +11169,26 @@ url-parse@^1.5.10, url-parse@^1.5.3: use-callback-ref@^1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" + resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz" integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== dependencies: tslib "^2.0.0" use-memo-one@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz" integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== use-query-params@^2.2.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-2.2.1.tgz#c558ab70706f319112fbccabf6867b9f904e947d" + resolved "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz" integrity sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q== dependencies: serialize-query-params "^2.0.2" use-sidecar@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" + resolved "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz" integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== dependencies: detect-node-es "^1.1.0" @@ -11232,32 +11196,32 @@ use-sidecar@^1.1.3: use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.2: version "1.6.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== utila@~0.4: version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz" integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== utils-merge@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-to-istanbul@^9.0.1: version "9.3.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz" integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" @@ -11266,17 +11230,17 @@ v8-to-istanbul@^9.0.1: vary@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== vfile-location@^3.0.0, vfile-location@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c" + resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz" integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA== vfile-message@^2.0.0: version "2.0.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz" integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== dependencies: "@types/unist" "^2.0.0" @@ -11284,7 +11248,7 @@ vfile-message@^2.0.0: vfile@^4.0.0, vfile@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" + resolved "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz" integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== dependencies: "@types/unist" "^2.0.0" @@ -11294,21 +11258,21 @@ vfile@^4.0.0, vfile@^4.2.1: w3c-xmlserializer@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz" integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== dependencies: xml-name-validator "^4.0.0" walker@^1.0.7, walker@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: makeerror "1.0.12" watchpack@^2.4.4: version "2.4.4" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947" + resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz" integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA== dependencies: glob-to-regexp "^0.4.1" @@ -11316,29 +11280,29 @@ watchpack@^2.4.4: wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== dependencies: minimalistic-assert "^1.0.0" web-namespaces@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" + resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== webidl-conversions@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== webidl-conversions@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== webpack-dev-middleware@^5.3.4: version "5.3.4" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz" integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" @@ -11349,7 +11313,7 @@ webpack-dev-middleware@^5.3.4: webpack-dev-server@^4.6.0: version "4.15.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz" integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== dependencies: "@types/bonjour" "^3.5.9" @@ -11385,7 +11349,7 @@ webpack-dev-server@^4.6.0: webpack-manifest-plugin@^4.0.2: version "4.1.1" - resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz#10f8dbf4714ff93a215d5a45bcc416d80506f94f" + resolved "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz" integrity sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow== dependencies: tapable "^2.0.0" @@ -11393,7 +11357,7 @@ webpack-manifest-plugin@^4.0.2: webpack-sources@^1.4.3: version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== dependencies: source-list-map "^2.0.0" @@ -11401,7 +11365,7 @@ webpack-sources@^1.4.3: webpack-sources@^2.2.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz" integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== dependencies: source-list-map "^2.0.1" @@ -11409,12 +11373,12 @@ webpack-sources@^2.2.0: webpack-sources@^3.3.3: version "3.3.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz" integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== webpack@^5.64.4: version "5.103.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.103.0.tgz#17a7c5a5020d5a3a37c118d002eade5ee2c6f3da" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz" integrity sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw== dependencies: "@types/eslint-scope" "^3.7.7" @@ -11445,7 +11409,7 @@ webpack@^5.64.4: websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: http-parser-js ">=0.5.1" @@ -11454,29 +11418,29 @@ websocket-driver@>=0.5.1, websocket-driver@^0.7.4: websocket-extensions@>=0.1.1: version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-encoding@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz" integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== dependencies: iconv-lite "0.6.3" whatwg-fetch@^3.6.2: version "3.6.20" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz" integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== whatwg-mimetype@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== whatwg-url@^11.0.0: version "11.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz" integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== dependencies: tr46 "^3.0.0" @@ -11484,7 +11448,7 @@ whatwg-url@^11.0.0: whatwg-url@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz" integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== dependencies: lodash.sortby "^4.7.0" @@ -11493,7 +11457,7 @@ whatwg-url@^7.0.0: which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz" integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== dependencies: is-bigint "^1.1.0" @@ -11504,7 +11468,7 @@ which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: which-builtin-type@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + resolved "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz" integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== dependencies: call-bound "^1.0.2" @@ -11523,7 +11487,7 @@ which-builtin-type@^1.2.1: which-collection@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz" integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== dependencies: is-map "^2.0.3" @@ -11533,7 +11497,7 @@ which-collection@^1.0.2: which-typed-array@^1.1.16, which-typed-array@^1.1.19: version "1.1.19" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz" integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== dependencies: available-typed-arrays "^1.0.7" @@ -11546,42 +11510,42 @@ which-typed-array@^1.1.16, which-typed-array@^1.1.19: which@^1.3.1: version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -workbox-background-sync@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f" - integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg== +workbox-background-sync@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz" + integrity sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw== dependencies: idb "^7.0.1" - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-broadcast-update@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e" - integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ== +workbox-broadcast-update@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz" + integrity sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-build@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0" - integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw== +workbox-build@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz" + integrity sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ== dependencies: "@apideck/better-ajv-errors" "^0.3.1" "@babel/core" "^7.11.1" @@ -11605,136 +11569,136 @@ workbox-build@6.6.1: strip-comments "^2.0.1" tempy "^0.6.0" upath "^1.2.0" - workbox-background-sync "6.6.1" - workbox-broadcast-update "6.6.1" - workbox-cacheable-response "6.6.1" - workbox-core "6.6.1" - workbox-expiration "6.6.1" - workbox-google-analytics "6.6.1" - workbox-navigation-preload "6.6.1" - workbox-precaching "6.6.1" - workbox-range-requests "6.6.1" - workbox-recipes "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - workbox-streams "6.6.1" - workbox-sw "6.6.1" - workbox-window "6.6.1" - -workbox-cacheable-response@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9" - integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag== - dependencies: - workbox-core "6.6.1" - -workbox-core@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265" - integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw== - -workbox-expiration@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739" - integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A== + workbox-background-sync "6.6.0" + workbox-broadcast-update "6.6.0" + workbox-cacheable-response "6.6.0" + workbox-core "6.6.0" + workbox-expiration "6.6.0" + workbox-google-analytics "6.6.0" + workbox-navigation-preload "6.6.0" + workbox-precaching "6.6.0" + workbox-range-requests "6.6.0" + workbox-recipes "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" + workbox-streams "6.6.0" + workbox-sw "6.6.0" + workbox-window "6.6.0" + +workbox-cacheable-response@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz" + integrity sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw== + dependencies: + workbox-core "6.6.0" + +workbox-core@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz" + integrity sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ== + +workbox-expiration@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz" + integrity sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw== dependencies: idb "^7.0.1" - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-google-analytics@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d" - integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA== +workbox-google-analytics@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz" + integrity sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q== dependencies: - workbox-background-sync "6.6.1" - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" + workbox-background-sync "6.6.0" + workbox-core "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" -workbox-navigation-preload@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059" - integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA== +workbox-navigation-preload@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz" + integrity sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-precaching@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2" - integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A== +workbox-precaching@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz" + integrity sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw== dependencies: - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" + workbox-core "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" -workbox-range-requests@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39" - integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g== +workbox-range-requests@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz" + integrity sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-recipes@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae" - integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g== +workbox-recipes@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz" + integrity sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A== dependencies: - workbox-cacheable-response "6.6.1" - workbox-core "6.6.1" - workbox-expiration "6.6.1" - workbox-precaching "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" + workbox-cacheable-response "6.6.0" + workbox-core "6.6.0" + workbox-expiration "6.6.0" + workbox-precaching "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" -workbox-routing@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581" - integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg== +workbox-routing@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz" + integrity sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-strategies@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf" - integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw== +workbox-strategies@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz" + integrity sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-streams@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26" - integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q== +workbox-streams@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz" + integrity sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg== dependencies: - workbox-core "6.6.1" - workbox-routing "6.6.1" + workbox-core "6.6.0" + workbox-routing "6.6.0" -workbox-sw@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c" - integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ== +workbox-sw@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz" + integrity sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ== workbox-webpack-plugin@^6.4.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531" - integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA== + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz" + integrity sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A== dependencies: fast-json-stable-stringify "^2.1.0" pretty-bytes "^5.4.1" upath "^1.2.0" webpack-sources "^1.4.3" - workbox-build "6.6.1" + workbox-build "6.6.0" -workbox-window@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e" - integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ== +workbox-window@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz" + integrity sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw== dependencies: "@types/trusted-types" "^2.0.2" - workbox-core "6.6.1" + workbox-core "6.6.0" wrap-ansi@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" @@ -11743,7 +11707,7 @@ wrap-ansi@^6.2.0: wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -11752,12 +11716,12 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^3.0.0: version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== dependencies: imurmurhash "^0.1.4" @@ -11767,7 +11731,7 @@ write-file-atomic@^3.0.0: write-file-atomic@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== dependencies: imurmurhash "^0.1.4" @@ -11775,52 +11739,52 @@ write-file-atomic@^4.0.2: ws@^8.11.0, ws@^8.13.0: version "8.18.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== xml-name-validator@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== xmlchars@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== xmlcreate@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" + resolved "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz" integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== xtend@^4.0.0, xtend@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^5.0.5: version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yallist@^3.0.2: version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yargs-parser@^21.1.1: version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" @@ -11833,27 +11797,27 @@ yargs@^17.3.1, yargs@^17.7.2: yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== yoctocolors-cjs@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz#7e4964ea8ec422b7a40ac917d3a344cfd2304baa" + resolved "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz" integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== zod@^3.11.6: version "3.25.76" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== zustand@^4.4.1: version "4.5.7" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz" integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw== dependencies: use-sync-external-store "^1.2.2" zwitch@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" + resolved "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz" integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==