Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@
"filename": "infra/feast-operator/api/v1/featurestore_types.go",
"hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c",
"is_verified": false,
"line_number": 946
"line_number": 958
}
],
"infra/feast-operator/api/v1/zz_generated.deepcopy.go": [
Expand All @@ -966,21 +966,21 @@
"filename": "infra/feast-operator/api/v1/zz_generated.deepcopy.go",
"hashed_secret": "f914fc9324de1bec1ad13dec94a8ea2ddb41fc87",
"is_verified": false,
"line_number": 822
"line_number": 842
},
{
"type": "Secret Keyword",
"filename": "infra/feast-operator/api/v1/zz_generated.deepcopy.go",
"hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c",
"is_verified": false,
"line_number": 883
"line_number": 903
},
{
"type": "Secret Keyword",
"filename": "infra/feast-operator/api/v1/zz_generated.deepcopy.go",
"hashed_secret": "c2028031c154bbe86fd69bef740855c74b927dcf",
"is_verified": false,
"line_number": 1575
"line_number": 1595
}
],
"infra/feast-operator/api/v1alpha1/featurestore_types.go": [
Expand All @@ -989,7 +989,7 @@
"filename": "infra/feast-operator/api/v1alpha1/featurestore_types.go",
"hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c",
"is_verified": false,
"line_number": 651
"line_number": 663
}
],
"infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go": [
Expand All @@ -998,21 +998,21 @@
"filename": "infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go",
"hashed_secret": "f914fc9324de1bec1ad13dec94a8ea2ddb41fc87",
"is_verified": false,
"line_number": 595
"line_number": 615
},
{
"type": "Secret Keyword",
"filename": "infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go",
"hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c",
"is_verified": false,
"line_number": 1103
"line_number": 1123
},
{
"type": "Secret Keyword",
"filename": "infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go",
"hashed_secret": "c2028031c154bbe86fd69bef740855c74b927dcf",
"is_verified": false,
"line_number": 1108
"line_number": 1128
}
],
"infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml": [
Expand Down Expand Up @@ -1181,7 +1181,7 @@
"filename": "infra/feast-operator/internal/controller/services/services.go",
"hashed_secret": "36dc326eb15c7bdd8d91a6b87905bcea20b637d1",
"is_verified": false,
"line_number": 183
"line_number": 184
}
],
"infra/feast-operator/internal/controller/services/tls_test.go": [
Expand Down Expand Up @@ -1564,5 +1564,5 @@
}
]
},
"generated_at": "2026-07-30T16:22:49Z"
"generated_at": "2026-07-31T05:29:18Z"
}
94 changes: 88 additions & 6 deletions docs/how-to-guides/feast-operator/01-project-provisioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

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

---

Expand Down Expand Up @@ -89,7 +90,7 @@ feastProjectDir:
### Full `git` field reference

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

---

## Option C — Use a repository packaged in an image (`feastProjectDir.packaged`)

Use `packaged` when the feature repository is built into a feature-server image. This is
useful in air-gapped environments and in release workflows where feature definitions and
their Python dependencies are promoted together as an immutable image.

```yaml
apiVersion: feast.dev/v1
kind: FeatureStore
metadata:
name: packaged-feature-store
spec:
feastProject: credit_scoring
feastProjectDir:
packaged:
image: registry.example.com/feature-server@sha256:0123456789abcdef
featureRepoPath: /opt/feast/feature_repo
```

The repository can be added to a Feast feature-server image with a Dockerfile such as:

```dockerfile
FROM quay.io/feastdev/feature-server:latest
COPY feature_repo/ /opt/feast/feature_repo/
```

`featureRepoPath` must be a canonical absolute, non-root path: do not use `.`, `..`,
repeated separators, or a trailing separator. Put it outside operator-mounted locations
such as `/feast-data`; a volume mounted there would hide files baked into the image. When
init containers are enabled, the packaged path also must not equal, contain, or be
contained by the staged repository path.

With init containers enabled (the default), each Pod starts in this order:

1. `feast-init` replaces the operator-managed staged repository with a fresh copy of the
repository from `packaged.featureRepoPath`. With the default storage configuration,
for example, it copies `/opt/feast/feature_repo` from the image to
`/feast-data/<feastProject>/feature_repo`.
2. In the staged copy only, `feast-init` replaces `feature_store.yaml` (if exists in the
baked image) with the configuration generated from the FeatureStore resource. The file
baked into the image is not modified. The Python feature definitions come from the
packaged repository, while the FeatureStore resource remains authoritative for runtime
configuration.
3. When `services.runFeastApplyOnInit` is omitted or `true` (the default), `feast-apply`
runs `feast apply` from the staged repository using the packaged image. Setting it to
`false` skips only this step; repository staging still occurs.
4. The Feast service containers start with the staged repository as their working
directory.

The repository baked into the image is therefore the source artifact, while the staged
repository is the runtime copy used by `feast apply` and the Feast services.

For a baked repository whose own `feature_store.yaml` must remain authoritative, disable
init containers:

```yaml
services:
disableInitContainers: true
```

In that mode, Feast service containers use `featureRepoPath` directly and neither staging
nor `feast apply` runs during pod initialization. The Operator does not update the registry,
so `feast apply` must be handled separately—for example, by CI/CD or a separately managed
Kubernetes Job or CronJob—whenever the packaged feature definitions change.

The packaged `image` is optional. When set, it is the default for repository initialization,
`feast apply`, and Feast services. `services.initImage` takes precedence for the
`feast-init` and `feast-apply` init containers, while an explicit image on an individual
service takes precedence for that service. When the packaged image is omitted, the operator
uses `RELATED_IMAGE_FEATURE_SERVER` or its built-in feature-server image fallback.

### Full `packaged` field reference

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `featureRepoPath` | string | yes | Canonical absolute, non-root path to the feature repository in the image; it must not overlap the staged repository path |
| `image` | string | no | Image containing the repository; defaults to the operator feature-server image |

---

## `feast apply` on startup

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

Expand All @@ -175,9 +257,8 @@ services:

## When `feastProjectDir` is omitted

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

---

Expand All @@ -188,3 +269,4 @@ mount via `services.volumes` + `volumeMounts`).
- [Sample: private git repo with token](https://github.com/feast-dev/feast/blob/stable/infra/feast-operator/config/samples/v1_featurestore_git_token.yaml)
- [Sample: monorepo with featureRepoPath](https://github.com/feast-dev/feast/blob/stable/infra/feast-operator/config/samples/v1_featurestore_git_repopath.yaml)
- [Sample: feast init](https://github.com/feast-dev/feast/blob/stable/infra/feast-operator/config/samples/v1_featurestore_init.yaml)
- [Sample: packaged feature repository](https://github.com/feast-dev/feast/blob/stable/infra/feast-operator/config/samples/v1_featurestore_packaged.yaml)
2 changes: 1 addition & 1 deletion docs/how-to-guides/feast-operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ look for store-specific YAML options in the Feast SDK docs.

| # | Guide | Topic |
|---|-------|-------|
| 1 | [Project Provisioning](01-project-provisioning.md) | `feastProjectDir`: cloning a git repo vs `feast init` templates |
| 1 | [Project Provisioning](01-project-provisioning.md) | `feastProjectDir`: git clone, `feast init`, or a repository packaged in an image |
| 2 | [Persistence](02-persistence.md) | File (path + PVC) vs DB store for offline/online/registry; Secret format |
| 3 | [Serving & Observability](03-serving-and-observability.md) | Feature server workers, log level, Prometheus metrics, offline push batching, MCP |
| 4 | [Registry Topology](04-registry-topology.md) | Local vs remote registry, cross-namespace `feastRef`, remote TLS |
Expand Down
28 changes: 19 additions & 9 deletions docs/how-to-guides/production-deployment-topologies.md
Original file line number Diff line number Diff line change
Expand Up @@ -1066,12 +1066,15 @@ Production environments in regulated industries (finance, government, defense) o

### Default init container behavior

When `feastProjectDir` is set on the FeatureStore CR, the operator creates up to two init containers:
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 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.
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, `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.
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

Expand All @@ -1086,12 +1089,12 @@ graph TD
end

subgraph InternalRegistry["Internal Container Registry"]
Mirror["registry.internal.example.com<br/>/feast/feature-server:v0.61"]
Mirror["registry.internal.example.com<br/>/feast/feature-server:release"]
end

subgraph AirGappedCluster["Air-Gapped Kubernetes Cluster"]
SA["ServiceAccount<br/>(imagePullSecrets)"]
CR["FeatureStore CR<br/>disableInitContainers: true<br/>image: registry.internal..."]
CR["FeatureStore CR<br/>feastProjectDir.packaged<br/>disableInitContainers: true"]
Deploy["Feast Deployment<br/>(no init containers)"]
SA --> Deploy
CR --> Deploy
Expand All @@ -1105,8 +1108,8 @@ graph TD

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. **Set `services.disableInitContainers: true`** on the FeatureStore CR to skip `git clone` / `feast init` and `feast apply`.
4. **Override the image** on each service using the per-service `image` field.
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.

Expand All @@ -1119,6 +1122,10 @@ 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:
Expand All @@ -1128,7 +1135,6 @@ spec:
secretRef:
name: feast-online-store
server:
image: registry.internal.example.com/feast/feature-server:v0.61
imagePullPolicy: IfNotPresent
resources:
requests:
Expand All @@ -1145,10 +1151,14 @@ spec:
secretRef:
name: feast-registry-store
server:
image: registry.internal.example.com/feast/feature-server:v0.61
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:

Expand Down
2 changes: 1 addition & 1 deletion infra/feast-operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This is a K8s Operator that can be used to deploy and manage **Feast**, an open

| Guide | Topic |
|-------|-------|
| [1 — Project Provisioning](https://docs.feast.dev/how-to-guides/feast-operator/01-project-provisioning) | `feastProjectDir`: git clone vs `feast init` templates |
| [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` |
Expand Down
20 changes: 16 additions & 4 deletions infra/feast-operator/api/v1/featurestore_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,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.
Expand Down Expand Up @@ -408,7 +420,7 @@ type FeatureStoreServices struct {
// 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 → RELATED_IMAGE_FEATURE_SERVER → DefaultImage.
// 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.
Expand Down
20 changes: 20 additions & 0 deletions infra/feast-operator/api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading