Skip to content

Commit 3a4abf5

Browse files
ShumzZzZzjyejare
authored andcommitted
feat: Add packaged feature repository support to Feast Operator
Add spec.feastProjectDir.packaged for repositories distributed in feature server images, including staged and direct-use lifecycle modes, canonical path validation, generated CRD artifacts, documentation, and tests. Integrate the init image override introduced by feast-dev#6598 with this precedence: services.initImage, packaged.image, RELATED_IMAGE_FEATURE_SERVER, then DefaultImage. Signed-off-by: Shumin <shumin.zheng@outlook.com>
1 parent 11ebea4 commit 3a4abf5

21 files changed

Lines changed: 1056 additions & 94 deletions

‎.secrets.baseline‎

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/how-to-guides/feast-operator/01-project-provisioning.md‎

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
The operator needs a Feast feature repository (a directory containing `feature_store.yaml`
44
and Python feature-view definitions) to work from. `spec.feastProjectDir` controls how that
5-
directory is created inside the pods. Exactly one of `git` or `init` must be set.
5+
directory is created inside the pods. When `feastProjectDir` is specified, exactly one of
6+
`git`, `init`, or `packaged` must be set.
67

78
---
89

@@ -89,7 +90,7 @@ feastProjectDir:
8990
### Full `git` field reference
9091

9192
| Field | Type | Description |
92-
|-------|------|-------------|
93+
| ------- | ------ | ------------- |
9394
| `url` | string | Repository URL (HTTPS or SSH) |
9495
| `ref` | string | Branch, tag, or commit SHA. Defaults to the remote HEAD |
9596
| `featureRepoPath` | string | Relative path within the repo to the feature repository directory. Default: `feature_repo` |
@@ -151,9 +152,90 @@ feastProjectDir:
151152

152153
---
153154

155+
## Option C — Use a repository packaged in an image (`feastProjectDir.packaged`)
156+
157+
Use `packaged` when the feature repository is built into a feature-server image. This is
158+
useful in air-gapped environments and in release workflows where feature definitions and
159+
their Python dependencies are promoted together as an immutable image.
160+
161+
```yaml
162+
apiVersion: feast.dev/v1
163+
kind: FeatureStore
164+
metadata:
165+
name: packaged-feature-store
166+
spec:
167+
feastProject: credit_scoring
168+
feastProjectDir:
169+
packaged:
170+
image: registry.example.com/feature-server@sha256:0123456789abcdef
171+
featureRepoPath: /opt/feast/feature_repo
172+
```
173+
174+
The repository can be added to a Feast feature-server image with a Dockerfile such as:
175+
176+
```dockerfile
177+
FROM quay.io/feastdev/feature-server:latest
178+
COPY feature_repo/ /opt/feast/feature_repo/
179+
```
180+
181+
`featureRepoPath` must be a canonical absolute, non-root path: do not use `.`, `..`,
182+
repeated separators, or a trailing separator. Put it outside operator-mounted locations
183+
such as `/feast-data`; a volume mounted there would hide files baked into the image. When
184+
init containers are enabled, the packaged path also must not equal, contain, or be
185+
contained by the staged repository path.
186+
187+
With init containers enabled (the default), each Pod starts in this order:
188+
189+
1. `feast-init` replaces the operator-managed staged repository with a fresh copy of the
190+
repository from `packaged.featureRepoPath`. With the default storage configuration,
191+
for example, it copies `/opt/feast/feature_repo` from the image to
192+
`/feast-data/<feastProject>/feature_repo`.
193+
2. In the staged copy only, `feast-init` replaces `feature_store.yaml` (if exists in the
194+
baked image) with the configuration generated from the FeatureStore resource. The file
195+
baked into the image is not modified. The Python feature definitions come from the
196+
packaged repository, while the FeatureStore resource remains authoritative for runtime
197+
configuration.
198+
3. When `services.runFeastApplyOnInit` is omitted or `true` (the default), `feast-apply`
199+
runs `feast apply` from the staged repository using the packaged image. Setting it to
200+
`false` skips only this step; repository staging still occurs.
201+
4. The Feast service containers start with the staged repository as their working
202+
directory.
203+
204+
The repository baked into the image is therefore the source artifact, while the staged
205+
repository is the runtime copy used by `feast apply` and the Feast services.
206+
207+
For a baked repository whose own `feature_store.yaml` must remain authoritative, disable
208+
init containers:
209+
210+
```yaml
211+
services:
212+
disableInitContainers: true
213+
```
214+
215+
In that mode, Feast service containers use `featureRepoPath` directly and neither staging
216+
nor `feast apply` runs during pod initialization. The Operator does not update the registry,
217+
so `feast apply` must be handled separately—for example, by CI/CD or a separately managed
218+
Kubernetes Job or CronJob—whenever the packaged feature definitions change.
219+
220+
The packaged `image` is optional. When set, it is the default for repository initialization,
221+
`feast apply`, and Feast services. `services.initImage` takes precedence for the
222+
`feast-init` and `feast-apply` init containers, while an explicit image on an individual
223+
service takes precedence for that service. When the packaged image is omitted, the operator
224+
uses `RELATED_IMAGE_FEATURE_SERVER` or its built-in feature-server image fallback.
225+
226+
### Full `packaged` field reference
227+
228+
| Field | Type | Required | Description |
229+
|-------|------|----------|-------------|
230+
| `featureRepoPath` | string | yes | Canonical absolute, non-root path to the feature repository in the image; it must not overlap the staged repository path |
231+
| `image` | string | no | Image containing the repository; defaults to the operator feature-server image |
232+
233+
---
234+
154235
## `feast apply` on startup
155236

156-
By default, when the init container completes (git clone or `feast init`), the operator runs
237+
By default, when repository initialization completes (git clone, `feast init`, or packaged
238+
repository staging), the operator runs
157239
`feast apply` before starting the servers. This registers all feature definitions with the
158240
registry.
159241

@@ -175,9 +257,8 @@ services:
175257

176258
## When `feastProjectDir` is omitted
177259

178-
If neither `git` nor `init` is set, the operator mounts an empty directory. In this case
179-
you must supply a `feature_store.yaml` through another mechanism (e.g. a ConfigMap volume
180-
mount via `services.volumes` + `volumeMounts`).
260+
If `feastProjectDir` is not set, the operator defaults to `feastProjectDir.init: {}` and
261+
creates a local template repository.
181262

182263
---
183264

@@ -188,3 +269,4 @@ mount via `services.volumes` + `volumeMounts`).
188269
- [Sample: private git repo with token](https://github.com/feast-dev/feast/blob/stable/infra/feast-operator/config/samples/v1_featurestore_git_token.yaml)
189270
- [Sample: monorepo with featureRepoPath](https://github.com/feast-dev/feast/blob/stable/infra/feast-operator/config/samples/v1_featurestore_git_repopath.yaml)
190271
- [Sample: feast init](https://github.com/feast-dev/feast/blob/stable/infra/feast-operator/config/samples/v1_featurestore_init.yaml)
272+
- [Sample: packaged feature repository](https://github.com/feast-dev/feast/blob/stable/infra/feast-operator/config/samples/v1_featurestore_packaged.yaml)

‎docs/how-to-guides/feast-operator/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ look for store-specific YAML options in the Feast SDK docs.
2323

2424
| # | Guide | Topic |
2525
|---|-------|-------|
26-
| 1 | [Project Provisioning](01-project-provisioning.md) | `feastProjectDir`: cloning a git repo vs `feast init` templates |
26+
| 1 | [Project Provisioning](01-project-provisioning.md) | `feastProjectDir`: git clone, `feast init`, or a repository packaged in an image |
2727
| 2 | [Persistence](02-persistence.md) | File (path + PVC) vs DB store for offline/online/registry; Secret format |
2828
| 3 | [Serving & Observability](03-serving-and-observability.md) | Feature server workers, log level, Prometheus metrics, offline push batching, MCP |
2929
| 4 | [Registry Topology](04-registry-topology.md) | Local vs remote registry, cross-namespace `feastRef`, remote TLS |

‎docs/how-to-guides/production-deployment-topologies.md‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,12 +1066,15 @@ Production environments in regulated industries (finance, government, defense) o
10661066

10671067
### Default init container behavior
10681068

1069-
When `feastProjectDir` is set on the FeatureStore CR, the operator creates up to two init containers:
1069+
When `feastProjectDir` is set on the FeatureStore CR, the operator creates up to two init containers unless `services.disableInitContainers` is `true`:
10701070

1071-
1. **`feast-init`** — bootstraps the feature repository by running either `git clone` (if `feastProjectDir.git` is set) or `feast init` (if `feastProjectDir.init` is set), then writes the generated `feature_store.yaml` into the repo directory.
1071+
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.
10721072
2. **`feast-apply`** — runs `feast apply` to register feature definitions in the registry. Controlled by `runFeastApplyOnInit` (defaults to `true`). Skipped when `disableInitContainers` is `true`.
10731073

1074-
In air-gapped environments, `git clone` will fail because the cluster cannot reach external Git repositories. The solution is to **pre-bake** the feature repository into a custom container image and disable the init containers entirely.
1074+
In air-gapped environments, use `feastProjectDir.packaged` to identify a feature repository baked into an image. The operator supports two lifecycle modes:
1075+
1076+
* Keep init containers enabled to refresh shared storage from the image, generate configuration from the FeatureStore CR, and optionally run `feast apply`.
1077+
* Set `services.disableInitContainers: true` to run directly from the baked path and treat its `feature_store.yaml` as authoritative.
10751078

10761079
### Air-gapped deployment workflow
10771080

@@ -1086,12 +1089,12 @@ graph TD
10861089
end
10871090
10881091
subgraph InternalRegistry["Internal Container Registry"]
1089-
Mirror["registry.internal.example.com<br/>/feast/feature-server:v0.61"]
1092+
Mirror["registry.internal.example.com<br/>/feast/feature-server:release"]
10901093
end
10911094
10921095
subgraph AirGappedCluster["Air-Gapped Kubernetes Cluster"]
10931096
SA["ServiceAccount<br/>(imagePullSecrets)"]
1094-
CR["FeatureStore CR<br/>disableInitContainers: true<br/>image: registry.internal..."]
1097+
CR["FeatureStore CR<br/>feastProjectDir.packaged<br/>disableInitContainers: true"]
10951098
Deploy["Feast Deployment<br/>(no init containers)"]
10961099
SA --> Deploy
10971100
CR --> Deploy
@@ -1105,8 +1108,8 @@ graph TD
11051108

11061109
1. **Build a custom container image** that bundles the feature repository and all Python dependencies into the Feast base image.
11071110
2. **Push** the image to your internal container registry.
1108-
3. **Set `services.disableInitContainers: true`** on the FeatureStore CR to skip `git clone` / `feast init` and `feast apply`.
1109-
4. **Override the image** on each service using the per-service `image` field.
1111+
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.
1112+
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.
11101113
5. **Set `imagePullPolicy: IfNotPresent`** (or `Never` if images are pre-loaded on nodes).
11111114
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.
11121115

@@ -1119,6 +1122,10 @@ metadata:
11191122
name: airgap-production
11201123
spec:
11211124
feastProject: my_project
1125+
feastProjectDir:
1126+
packaged:
1127+
image: registry.internal.example.com/feast/feature-server:release
1128+
featureRepoPath: /opt/feast/feature_repo
11221129
services:
11231130
disableInitContainers: true
11241131
onlineStore:
@@ -1128,7 +1135,6 @@ spec:
11281135
secretRef:
11291136
name: feast-online-store
11301137
server:
1131-
image: registry.internal.example.com/feast/feature-server:v0.61
11321138
imagePullPolicy: IfNotPresent
11331139
resources:
11341140
requests:
@@ -1145,10 +1151,14 @@ spec:
11451151
secretRef:
11461152
name: feast-registry-store
11471153
server:
1148-
image: registry.internal.example.com/feast/feature-server:v0.61
11491154
imagePullPolicy: IfNotPresent
11501155
```
11511156

1157+
The packaged image is the default for every Feast service and for the `feast-init` and
1158+
`feast-apply` init containers. A per-service `image` still takes precedence for that
1159+
service, and `services.initImage` takes precedence for both init containers. Remove
1160+
`disableInitContainers: true` to use operator-managed staging and startup apply instead.
1161+
11521162
{% hint style="info" %}
11531163
**Pre-populating the registry:** With init containers disabled, `feast apply` does not run on pod startup. You can populate the registry by:
11541164

‎infra/feast-operator/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This is a K8s Operator that can be used to deploy and manage **Feast**, an open
77

88
| Guide | Topic |
99
|-------|-------|
10-
| [1 — Project Provisioning](https://docs.feast.dev/how-to-guides/feast-operator/01-project-provisioning) | `feastProjectDir`: git clone vs `feast init` templates |
10+
| [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 |
1111
| [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 |
1212
| [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 |
1313
| [4 — Registry Topology](https://docs.feast.dev/how-to-guides/feast-operator/04-registry-topology) | Local, remote, cross-namespace `feastRef` |

‎infra/feast-operator/api/v1/featurestore_types.go‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,22 @@ type FeatureStoreSpec struct {
175175
}
176176

177177
// FeastProjectDir defines how to create the feast project directory.
178-
// +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init)].exists_one(c, c)",message="One selection required between init or git."
178+
// +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."
179179
type FeastProjectDir struct {
180-
Git *GitCloneOptions `json:"git,omitempty"`
181-
Init *FeastInitOptions `json:"init,omitempty"`
180+
Git *GitCloneOptions `json:"git,omitempty"`
181+
Init *FeastInitOptions `json:"init,omitempty"`
182+
Packaged *FeastPackagedOptions `json:"packaged,omitempty"`
183+
}
184+
185+
// FeastPackagedOptions describes a feature repository packaged in a feature server image.
186+
// +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."
187+
type FeastPackagedOptions struct {
188+
// Image containing the packaged feature repository. When set, this image is used by the
189+
// repository initialization and feast apply containers and as the default service image.
190+
// When omitted, the operator's configured feature server image is used.
191+
Image string `json:"image,omitempty"`
192+
// FeatureRepoPath is the canonical absolute path to the feature repository in the image.
193+
FeatureRepoPath string `json:"featureRepoPath"`
182194
}
183195

184196
// GitCloneOptions describes how a clone should be performed.
@@ -408,7 +420,7 @@ type FeatureStoreServices struct {
408420
// Disable the 'feast repo initialization' initContainer
409421
DisableInitContainers bool `json:"disableInitContainers,omitempty"`
410422
// InitImage overrides the image for init containers (feast-init, feast-apply).
411-
// Resolution order: InitImage → RELATED_IMAGE_FEATURE_SERVER → DefaultImage.
423+
// Resolution order: InitImage → FeastProjectDir.Packaged.Image → RELATED_IMAGE_FEATURE_SERVER → DefaultImage.
412424
// +optional
413425
InitImage *string `json:"initImage,omitempty"`
414426
// Runs feast apply on pod start to populate the registry. Defaults to true. Ignored when DisableInitContainers is true.

‎infra/feast-operator/api/v1/zz_generated.deepcopy.go‎

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)