From 52618678c380d877aec5deb0323e1f71f1412784 Mon Sep 17 00:00:00 2001 From: dbbvitor Date: Thu, 24 Sep 2026 23:04:53 -0300 Subject: [PATCH 1/6] feat: Add dualStack server option to the Feast operator Add a DualStack field to ServerConfigs that binds server processes to the IPv6 wildcard address instead of 0.0.0.0, so they also accept IPv4 clients on dual-stack or IPv6-only clusters. Online and offline servers use the bracketed [::] form required by gunicorn and Arrow Flight; ui, lineage, and registry keep their existing behavior or use the bare :: form uvicorn expects. The registry server always binds dual-stack and ignores this setting. Signed-off-by: dbbvitor --- .secrets.baseline | 6 +- .../api/v1/featurestore_types.go | 5 ++ .../api/v1/zz_generated.deepcopy.go | 5 ++ .../feast-operator.clusterserviceversion.yaml | 2 +- .../manifests/feast.dev_featurestores.yaml | 40 ++++++++++++ .../crd/bases/feast.dev_featurestores.yaml | 40 ++++++++++++ infra/feast-operator/dist/install.yaml | 40 ++++++++++++ infra/feast-operator/docs/api/markdown/ref.md | 6 ++ .../controller/services/bind_host_test.go | 64 +++++++++++++++++++ .../controller/services/repo_config_test.go | 36 +++++++++++ .../internal/controller/services/services.go | 23 ++++++- .../controller/services/services_test.go | 19 ++++++ .../controller/services/services_types.go | 6 +- 13 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 infra/feast-operator/internal/controller/services/bind_host_test.go diff --git a/.secrets.baseline b/.secrets.baseline index 0e781c9ce67..a1440350d47 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -957,7 +957,7 @@ "filename": "infra/feast-operator/api/v1/featurestore_types.go", "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", "is_verified": false, - "line_number": 1075 + "line_number": 1080 } ], "infra/feast-operator/api/v1/zz_generated.deepcopy.go": [ @@ -980,7 +980,7 @@ "filename": "infra/feast-operator/api/v1/zz_generated.deepcopy.go", "hashed_secret": "c2028031c154bbe86fd69bef740855c74b927dcf", "is_verified": false, - "line_number": 1731 + "line_number": 1736 } ], "infra/feast-operator/api/v1alpha1/featurestore_types.go": [ @@ -1564,5 +1564,5 @@ } ] }, - "generated_at": "2026-09-15T09:12:05Z" + "generated_at": "2026-09-25T02:04:33Z" } diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index ef9b2f32356..ecf718767a7 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -897,6 +897,11 @@ type ServerConfigs struct { LogLevel *string `json:"logLevel,omitempty"` // Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. Metrics *bool `json:"metrics,omitempty"` + // DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, + // which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. + // The registry server always binds dual-stack and ignores this setting. + // +optional + DualStack *bool `json:"dualStack,omitempty"` // VolumeMounts defines the list of volumes that should be mounted into the feast container. // This allows attaching persistent storage, config files, secrets, or other resources // required by the Feast components. Ensure that each volume mount has a corresponding diff --git a/infra/feast-operator/api/v1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1/zz_generated.deepcopy.go index bd7fe316118..fcfd3378907 100644 --- a/infra/feast-operator/api/v1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1/zz_generated.deepcopy.go @@ -1629,6 +1629,11 @@ func (in *ServerConfigs) DeepCopyInto(out *ServerConfigs) { *out = new(bool) **out = **in } + if in.DualStack != nil { + in, out := &in.DualStack, &out.DualStack + *out = new(bool) + **out = **in + } if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]corev1.VolumeMount, len(*in)) diff --git a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml index d4795f3fb06..6f8f977d6fb 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml @@ -147,7 +147,7 @@ metadata: } ] capabilities: Basic Install - createdAt: "2026-08-21T03:30:58Z" + createdAt: "2026-09-25T01:00:43Z" operators.operatorframework.io/builder: operator-sdk-v1.41.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 name: feast-operator.v0.66.0 diff --git a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml index f2a879fbb33..98f5c18087c 100644 --- a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml +++ b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml @@ -1012,6 +1012,10 @@ spec: description: Server configuration (image, resources, env, TLS, etc.). properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -2409,6 +2413,10 @@ spec: server: description: Creates a remote offline server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -2960,6 +2968,10 @@ spec: server: description: Creates a feature server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -3621,6 +3633,10 @@ spec: server: description: Creates a registry server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -4990,6 +5006,10 @@ spec: ui: description: Creates a UI server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable present @@ -7883,6 +7903,10 @@ spec: description: Server configuration (image, resources, env, TLS, etc.). properties: + dualStack: + description: DualStack binds the server to the + IPv6 wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment @@ -9297,6 +9321,10 @@ spec: server: description: Creates a remote offline server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -9856,6 +9884,10 @@ spec: server: description: Creates a feature server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -10531,6 +10563,10 @@ spec: server: description: Creates a registry server container properties: + dualStack: + description: DualStack binds the server to the + IPv6 wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment @@ -11918,6 +11954,10 @@ spec: ui: description: Creates a UI server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable 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 5e1f21f69e6..fe06b9647ed 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -1012,6 +1012,10 @@ spec: description: Server configuration (image, resources, env, TLS, etc.). properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -2409,6 +2413,10 @@ spec: server: description: Creates a remote offline server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -2960,6 +2968,10 @@ spec: server: description: Creates a feature server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -3621,6 +3633,10 @@ spec: server: description: Creates a registry server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -4990,6 +5006,10 @@ spec: ui: description: Creates a UI server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable present @@ -7883,6 +7903,10 @@ spec: description: Server configuration (image, resources, env, TLS, etc.). properties: + dualStack: + description: DualStack binds the server to the + IPv6 wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment @@ -9297,6 +9321,10 @@ spec: server: description: Creates a remote offline server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -9856,6 +9884,10 @@ spec: server: description: Creates a feature server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -10531,6 +10563,10 @@ spec: server: description: Creates a registry server container properties: + dualStack: + description: DualStack binds the server to the + IPv6 wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment @@ -11918,6 +11954,10 @@ spec: ui: description: Creates a UI server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 7480b9423b5..bf9af96d265 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -1020,6 +1020,10 @@ spec: description: Server configuration (image, resources, env, TLS, etc.). properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -2417,6 +2421,10 @@ spec: server: description: Creates a remote offline server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -2968,6 +2976,10 @@ spec: server: description: Creates a feature server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -3629,6 +3641,10 @@ spec: server: description: Creates a registry server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -4998,6 +5014,10 @@ spec: ui: description: Creates a UI server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable present @@ -7891,6 +7911,10 @@ spec: description: Server configuration (image, resources, env, TLS, etc.). properties: + dualStack: + description: DualStack binds the server to the + IPv6 wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment @@ -9305,6 +9329,10 @@ spec: server: description: Creates a remote offline server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -9864,6 +9892,10 @@ spec: server: description: Creates a feature server container properties: + dualStack: + description: DualStack binds the server to the IPv6 + wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable @@ -10539,6 +10571,10 @@ spec: server: description: Creates a registry server container properties: + dualStack: + description: DualStack binds the server to the + IPv6 wildcard address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment @@ -11926,6 +11962,10 @@ spec: ui: description: Creates a UI server container properties: + dualStack: + description: DualStack binds the server to the IPv6 wildcard + address instead of 0.0.0. + type: boolean env: items: description: EnvVar represents an environment variable diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index a69d6c98d90..8424008906c 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -1023,6 +1023,9 @@ _Appears in:_ | `logLevel` _string_ | LogLevel sets the logging level for the server Allowed values: "debug", "info", "warning", "error", "critical". | | `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | +| `dualStack` _boolean_ | DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, +which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. +The registry server always binds dual-stack and ignores this setting. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the feast container. This allows attaching persistent storage, config files, secrets, or other resources required by the Feast components. Ensure that each volume mount has a corresponding @@ -1122,6 +1125,9 @@ _Appears in:_ | `logLevel` _string_ | LogLevel sets the logging level for the server Allowed values: "debug", "info", "warning", "error", "critical". | | `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | +| `dualStack` _boolean_ | DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, +which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. +The registry server always binds dual-stack and ignores this setting. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the feast container. This allows attaching persistent storage, config files, secrets, or other resources required by the Feast components. Ensure that each volume mount has a corresponding diff --git a/infra/feast-operator/internal/controller/services/bind_host_test.go b/infra/feast-operator/internal/controller/services/bind_host_test.go new file mode 100644 index 00000000000..bd2ca96a3d7 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/bind_host_test.go @@ -0,0 +1,64 @@ +/* +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 ( + "testing" + + "k8s.io/utils/ptr" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" +) + +func TestWithBindHost(t *testing.T) { + dual := &feastdevv1.ServerConfigs{DualStack: ptr.To(true)} + cases := []struct { + name string + feastType FeastServiceType + args []string + cfg *feastdevv1.ServerConfigs + want []string + }{ + {"unset keeps ipv4", OnlineFeastType, []string{"serve", "-h", hostAllIPv4}, &feastdevv1.ServerConfigs{}, []string{"serve", "-h", hostAllIPv4}}, + {"nil config keeps ipv4", UIFeastType, []string{"ui", "-h", hostAllIPv4}, nil, []string{"ui", "-h", hostAllIPv4}}, + {"false keeps ipv4", OfflineFeastType, []string{"serve_offline", "-h", hostAllIPv4}, &feastdevv1.ServerConfigs{DualStack: ptr.To(false)}, []string{"serve_offline", "-h", hostAllIPv4}}, + {"online bracketed", OnlineFeastType, []string{"serve", "--metrics", "-h", hostAllIPv4}, dual, []string{"serve", "--metrics", "-h", "[::]"}}, + {"offline bracketed", OfflineFeastType, []string{"serve_offline", "-h", hostAllIPv4}, dual, []string{"serve_offline", "-h", "[::]"}}, + {"ui bare", UIFeastType, []string{"ui", "-h", hostAllIPv4}, dual, []string{"ui", "-h", "::"}}, + {"lineage bare", LineageFeastType, []string{"serve_lineage", "-h", hostAllIPv4}, dual, []string{"serve_lineage", "-h", "::"}}, + {"registry has no host flag", RegistryFeastType, []string{"serve_registry"}, dual, []string{"serve_registry"}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + in := append([]string{}, tc.args...) + got := withBindHost(tc.feastType, in, tc.cfg) + if len(got) != len(tc.want) { + t.Fatalf("got %v, want %v", got, tc.want) + } + for i := range got { + if got[i] != tc.want[i] { + t.Fatalf("got %v, want %v", got, tc.want) + } + } + for i := range in { + if in[i] != tc.args[i] { + t.Fatalf("input mutated: %v", in) + } + } + }) + } +} 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 3d26929a46b..d80b655831f 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -23,9 +23,11 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8sscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -854,6 +856,40 @@ var _ = Describe("Repo Config", func() { Expect(*repoConfig.OpenLineage.Consumer.StandaloneServer).To(BeTrue()) }) + It("should bind the lineage deployment dual-stack when configured", func() { + featureStore := minimalFeatureStore() + remoteHost := "feast-banking-registry.feast.svc.cluster.local:443" + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + Hostname: &remoteHost, + }, + }, + } + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + Consumer: &feastdevv1.OpenLineageConsumerConfig{ + Enabled: true, + LineageServer: &feastdevv1.LineageServerConfig{ + Server: &feastdevv1.ServerConfigs{DualStack: ptr.To(true)}, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + featureStore.Status.ServiceHostnames.Registry = remoteHost + + feast := FeastServices{ + Handler: handler.FeastHandler{FeatureStore: featureStore, Scheme: k8sscheme.Scheme}, + } + deploy := &appsv1.Deployment{} + Expect(feast.setLineageDeployment(deploy)).To(Succeed()) + + Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.Containers[0].Command).To( + Equal([]string{feastCommand, "serve_lineage", "-h", hostAllIPv6})) + Expect(deploy.Spec.Template.Spec.Containers[0].Args).To(Equal([]string{"-p", "6580"})) + }) + It("should generate lineage repo config with feastRef remote registry", func() { featureStore := minimalFeatureStore() featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 43a6ea958d3..2dce8b2ac09 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -731,7 +731,7 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st } deploySettings := FeastServiceConstants[feastType] - deploySettings.Args = append([]string{}, deploySettings.Args...) + deploySettings.Args = withBindHost(feastType, deploySettings.Args, feast.getServerConfigs(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. @@ -793,6 +793,25 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st return feastCommand } +// withBindHost returns a copy of args with the "-h" host set to the IPv6 wildcard when dual-stack is enabled. +// gunicorn (online) and Arrow Flight (offline) need the bracketed form; uvicorn (ui, lineage) rejects it. +func withBindHost(feastType FeastServiceType, args []string, serverConfigs *feastdevv1.ServerConfigs) []string { + out := append([]string{}, args...) + if serverConfigs == nil || serverConfigs.DualStack == nil || !*serverConfigs.DualStack { + return out + } + host := hostAllIPv6 + if feastType == OnlineFeastType || feastType == OfflineFeastType { + host = hostAllIPv6Bracketed + } + for i := 0; i+1 < len(out); i++ { + if out[i] == "-h" { + out[i+1] = host + } + } + return out +} + func (feast *FeastServices) getDeploymentStrategy() appsv1.DeploymentStrategy { if feast.Handler.FeatureStore.Status.Applied.Services.DeploymentStrategy != nil { return *feast.Handler.FeatureStore.Status.Applied.Services.DeploymentStrategy @@ -1590,7 +1609,7 @@ func (feast *FeastServices) setLineageDeployment(deploy *appsv1.Deployment) erro container := corev1.Container{ Name: string(LineageFeastType), Image: image, - Command: append([]string{feastCommand}, svcConsts.Args...), + Command: append([]string{feastCommand}, withBindHost(LineageFeastType, svcConsts.Args, serverConfigs)...), Args: []string{"-p", fmt.Sprintf("%d", port)}, Ports: []corev1.ContainerPort{ { diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 981d25f86af..ad1f43ccb7e 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -588,6 +588,25 @@ var _ = Describe("Registry Service", func() { })) }) + It("should bind the online service dual-stack when configured", func() { + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{Metrics: ptr.To(true), DualStack: ptr.To(true)}, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) + deployment := feast.initFeastDeploy() + Expect(feast.setDeployment(deployment)).To(Succeed()) + + onlineContainer := GetOnlineContainer(*deployment) + Expect(onlineContainer).NotTo(BeNil()) + Expect(onlineContainer.Command).To(Equal([]string{feastCommand, "serve", "--metrics", "-h", "[::]", "-p", "6566"})) + }) + It("should handle empty NodeSelector gracefully", func() { // Set empty NodeSelector emptyNodeSelector := map[string]string{} diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index b431292cdfc..f1352d52030 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -132,6 +132,8 @@ const ( stringTrue = "true" stringFalse = "false" hostAllIPv4 = "0.0.0.0" + hostAllIPv6 = "::" + hostAllIPv6Bracketed = "[::]" tlsCertKey = "tls.crt" DefaultNs = "default" feastCommand = "feast" @@ -201,12 +203,12 @@ var ( TargetRestHttpsPort: 6573, }, UIFeastType: { - Args: []string{"ui", "-h", "0.0.0.0"}, + Args: []string{"ui", "-h", hostAllIPv4}, TargetHttpPort: 8888, TargetHttpsPort: 8443, }, LineageFeastType: { - Args: []string{"serve_lineage", "-h", "0.0.0.0"}, + Args: []string{"serve_lineage", "-h", hostAllIPv4}, TargetHttpPort: 6580, TargetHttpsPort: 6581, }, From 5e787656051ded480a766a79d2f41002dcb2cf38 Mon Sep 17 00:00:00 2001 From: dbbvitor Date: Thu, 24 Sep 2026 23:05:18 -0300 Subject: [PATCH 2/6] test: Cover -h boundary positions in withBindHost Mutation testing found survivors in withBindHost's loop bounds: an -h flag as the very last argument (no value to replace) and one as the first argument were both untested edge cases. Signed-off-by: dbbvitor --- .../internal/controller/services/bind_host_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infra/feast-operator/internal/controller/services/bind_host_test.go b/infra/feast-operator/internal/controller/services/bind_host_test.go index bd2ca96a3d7..0e28a59fc95 100644 --- a/infra/feast-operator/internal/controller/services/bind_host_test.go +++ b/infra/feast-operator/internal/controller/services/bind_host_test.go @@ -39,6 +39,8 @@ func TestWithBindHost(t *testing.T) { {"online bracketed", OnlineFeastType, []string{"serve", "--metrics", "-h", hostAllIPv4}, dual, []string{"serve", "--metrics", "-h", "[::]"}}, {"offline bracketed", OfflineFeastType, []string{"serve_offline", "-h", hostAllIPv4}, dual, []string{"serve_offline", "-h", "[::]"}}, {"ui bare", UIFeastType, []string{"ui", "-h", hostAllIPv4}, dual, []string{"ui", "-h", "::"}}, + {"-h as first arg is still replaced", LineageFeastType, []string{"-h", hostAllIPv4}, dual, []string{"-h", "::"}}, + {"-h with no following value does not panic or get touched", OnlineFeastType, []string{"serve", "-h"}, dual, []string{"serve", "-h"}}, {"lineage bare", LineageFeastType, []string{"serve_lineage", "-h", hostAllIPv4}, dual, []string{"serve_lineage", "-h", "::"}}, {"registry has no host flag", RegistryFeastType, []string{"serve_registry"}, dual, []string{"serve_registry"}}, } From 3871ccb812863d6899711f2a12835bf1562cdaea Mon Sep 17 00:00:00 2001 From: dbbvitor Date: Tue, 29 Sep 2026 23:03:50 -0300 Subject: [PATCH 3/6] fix: Let the registry opt out of its dual-stack REST bind The registry's REST server binds dual-stack by default in the SDK CLI. Render -h 0.0.0.0 only when the shared DualStack field is explicitly set to false, so nil/true keep today's dual-stack default instead of silently going IPv4-only. Signed-off-by: dbbvitor --- .../api/v1/featurestore_types.go | 2 +- infra/feast-operator/docs/api/markdown/ref.md | 4 +- .../services/registry_bind_host_test.go | 74 +++++++++++++++++++ .../internal/controller/services/services.go | 4 + 4 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 infra/feast-operator/internal/controller/services/registry_bind_host_test.go diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index ecf718767a7..da37a4e34f1 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -899,7 +899,7 @@ type ServerConfigs struct { Metrics *bool `json:"metrics,omitempty"` // DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, // which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. - // The registry server always binds dual-stack and ignores this setting. + // The registry's REST server binds dual-stack by default; set this to false to opt out to 0.0.0.0-only. // +optional DualStack *bool `json:"dualStack,omitempty"` // VolumeMounts defines the list of volumes that should be mounted into the feast container. diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index 8424008906c..30d68d7964e 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -1025,7 +1025,7 @@ Allowed values: "debug", "info", "warning", "error", "critical". | | `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | | `dualStack` _boolean_ | DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. -The registry server always binds dual-stack and ignores this setting. | +The registry's REST server binds dual-stack by default; set this to false to opt out to 0.0.0.0-only. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the feast container. This allows attaching persistent storage, config files, secrets, or other resources required by the Feast components. Ensure that each volume mount has a corresponding @@ -1127,7 +1127,7 @@ Allowed values: "debug", "info", "warning", "error", "critical". | | `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | | `dualStack` _boolean_ | DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. -The registry server always binds dual-stack and ignores this setting. | +The registry's REST server binds dual-stack by default; set this to false to opt out to 0.0.0.0-only. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the feast container. This allows attaching persistent storage, config files, secrets, or other resources required by the Feast components. Ensure that each volume mount has a corresponding diff --git a/infra/feast-operator/internal/controller/services/registry_bind_host_test.go b/infra/feast-operator/internal/controller/services/registry_bind_host_test.go new file mode 100644 index 00000000000..da4f8fdd132 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/registry_bind_host_test.go @@ -0,0 +1,74 @@ +/* +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 ( + "testing" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "k8s.io/utils/ptr" +) + +func registryFeastServices(restEnabled bool, dualStack *bool) *FeastServices { + fs := &feastdevv1.FeatureStore{} + fs.Status.Applied.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{DualStack: dualStack}, + GRPC: ptr.To(true), + RestAPI: ptr.To(restEnabled), + }, + }, + }, + } + return &FeastServices{Handler: handler.FeastHandler{FeatureStore: fs}} +} + +func TestRegistryContainerCommandHostFlag(t *testing.T) { + cases := []struct { + name string + restEnabled bool + dualStack *bool + wantHostArg bool + }{ + {"unset defaults to dual-stack, no -h", true, nil, false}, + {"explicit true stays dual-stack, no -h", true, ptr.To(true), false}, + {"explicit false opts out to 0.0.0.0", true, ptr.To(false), true}, + {"rest disabled never renders -h", false, ptr.To(false), false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + feast := registryFeastServices(tc.restEnabled, tc.dualStack) + args := feast.getContainerCommand(RegistryFeastType) + + gotHostArg := false + for i, a := range args { + if a == "-h" && i+1 < len(args) { + gotHostArg = true + if args[i+1] != hostAllIPv4 { + t.Fatalf("got -h %s, want %s", args[i+1], hostAllIPv4) + } + } + } + if gotHostArg != tc.wantHostArg { + t.Fatalf("got args %v, want -h present=%v", args, tc.wantHostArg) + } + }) + } +} diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 2dce8b2ac09..b6b893ebf3d 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -750,6 +750,10 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st if feast.isRegistryRestEnabled() { deploySettings.Args = append(deploySettings.Args, "--rest-api") deploySettings.Args = append(deploySettings.Args, "--rest-port", strconv.Itoa(int(getTargetRestPort(feastType, tls)))) + // Registry REST defaults to dual-stack; only render -h to opt out. + if ds := feast.getServerConfigs(feastType).DualStack; ds != nil && !*ds { + deploySettings.Args = append(deploySettings.Args, "-h", hostAllIPv4) + } } } From 3d5467fd81047cc9715b5e4ab4a064dd466f13bd Mon Sep 17 00:00:00 2001 From: dbbvitor Date: Tue, 29 Sep 2026 23:47:21 -0300 Subject: [PATCH 4/6] chore: Refresh secrets baseline timestamp to match master Only the generated_at metadata field diverged from master, which was enough for GitHub to report this PR as unmergeable and apparently skip queuing CI. No scan content changed. Signed-off-by: dbbvitor --- .secrets.baseline | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.secrets.baseline b/.secrets.baseline index a1440350d47..a526a7352ed 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1564,5 +1564,5 @@ } ] }, - "generated_at": "2026-09-25T02:04:33Z" + "generated_at": "2026-09-29T11:09:11Z" } From c2267df804f5b0d3dc0a345dcf75057e60a32a69 Mon Sep 17 00:00:00 2001 From: dbbvitor Date: Mon, 5 Oct 2026 16:45:50 -0300 Subject: [PATCH 5/6] fix(operator): Make default registry REST to IPv4 and dualStack opt-in Make unset or false dualStack render an explicit 0.0.0.0 host flag. Only dualStack=true selects the bare IPv6 wildcard, aligning registry REST defaults with other host-flag servers. Signed-off-by: dbbvitor --- .../api/v1/featurestore_types.go | 2 +- infra/feast-operator/docs/api/markdown/ref.md | 4 ++-- .../services/registry_bind_host_test.go | 21 ++++++++----------- .../internal/controller/services/services.go | 8 +++---- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index da37a4e34f1..04b78185a27 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -899,7 +899,7 @@ type ServerConfigs struct { Metrics *bool `json:"metrics,omitempty"` // DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, // which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. - // The registry's REST server binds dual-stack by default; set this to false to opt out to 0.0.0.0-only. + // Unset or false binds 0.0.0.0 (IPv4 only) for every server that takes a host flag, including the registry's REST server. // +optional DualStack *bool `json:"dualStack,omitempty"` // VolumeMounts defines the list of volumes that should be mounted into the feast container. diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index 30d68d7964e..c4b79a5b177 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -1025,7 +1025,7 @@ Allowed values: "debug", "info", "warning", "error", "critical". | | `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | | `dualStack` _boolean_ | DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. -The registry's REST server binds dual-stack by default; set this to false to opt out to 0.0.0.0-only. | +Unset or false binds 0.0.0.0 (IPv4 only) for every server that takes a host flag, including the registry's REST server. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the feast container. This allows attaching persistent storage, config files, secrets, or other resources required by the Feast components. Ensure that each volume mount has a corresponding @@ -1127,7 +1127,7 @@ Allowed values: "debug", "info", "warning", "error", "critical". | | `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | | `dualStack` _boolean_ | DualStack binds the server to the IPv6 wildcard address instead of 0.0.0.0, which serves both IPv6 and IPv4 clients on Linux. Enable it on IPv6-only or dual-stack clusters. -The registry's REST server binds dual-stack by default; set this to false to opt out to 0.0.0.0-only. | +Unset or false binds 0.0.0.0 (IPv4 only) for every server that takes a host flag, including the registry's REST server. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the feast container. This allows attaching persistent storage, config files, secrets, or other resources required by the Feast components. Ensure that each volume mount has a corresponding diff --git a/infra/feast-operator/internal/controller/services/registry_bind_host_test.go b/infra/feast-operator/internal/controller/services/registry_bind_host_test.go index da4f8fdd132..276dafefe32 100644 --- a/infra/feast-operator/internal/controller/services/registry_bind_host_test.go +++ b/infra/feast-operator/internal/controller/services/registry_bind_host_test.go @@ -45,29 +45,26 @@ func TestRegistryContainerCommandHostFlag(t *testing.T) { name string restEnabled bool dualStack *bool - wantHostArg bool + wantHost string // empty: no -h expected }{ - {"unset defaults to dual-stack, no -h", true, nil, false}, - {"explicit true stays dual-stack, no -h", true, ptr.To(true), false}, - {"explicit false opts out to 0.0.0.0", true, ptr.To(false), true}, - {"rest disabled never renders -h", false, ptr.To(false), false}, + {"unset keeps ipv4", true, nil, hostAllIPv4}, + {"explicit false keeps ipv4", true, ptr.To(false), hostAllIPv4}, + {"explicit true binds bare ipv6 wildcard", true, ptr.To(true), hostAllIPv6}, + {"rest disabled never renders -h", false, ptr.To(true), ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { feast := registryFeastServices(tc.restEnabled, tc.dualStack) args := feast.getContainerCommand(RegistryFeastType) - gotHostArg := false + gotHost := "" for i, a := range args { if a == "-h" && i+1 < len(args) { - gotHostArg = true - if args[i+1] != hostAllIPv4 { - t.Fatalf("got -h %s, want %s", args[i+1], hostAllIPv4) - } + gotHost = args[i+1] } } - if gotHostArg != tc.wantHostArg { - t.Fatalf("got args %v, want -h present=%v", args, tc.wantHostArg) + if gotHost != tc.wantHost { + t.Fatalf("got args %v, want -h %q", args, tc.wantHost) } }) } diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index b6b893ebf3d..a61bcd3d5bd 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -731,7 +731,7 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st } deploySettings := FeastServiceConstants[feastType] - deploySettings.Args = withBindHost(feastType, deploySettings.Args, feast.getServerConfigs(feastType)) + deploySettings.Args = append([]string{}, deploySettings.Args...) // 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. @@ -750,12 +750,10 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st if feast.isRegistryRestEnabled() { deploySettings.Args = append(deploySettings.Args, "--rest-api") deploySettings.Args = append(deploySettings.Args, "--rest-port", strconv.Itoa(int(getTargetRestPort(feastType, tls)))) - // Registry REST defaults to dual-stack; only render -h to opt out. - if ds := feast.getServerConfigs(feastType).DualStack; ds != nil && !*ds { - deploySettings.Args = append(deploySettings.Args, "-h", hostAllIPv4) - } + deploySettings.Args = append(deploySettings.Args, "-h", hostAllIPv4) } } + deploySettings.Args = withBindHost(feastType, deploySettings.Args, feast.getServerConfigs(feastType)) // Add worker configuration options for online store (feast serve) if feastType == OnlineFeastType { From a463b72f22146a78660d5702687024925076e1bf Mon Sep 17 00:00:00 2001 From: dbbvitor Date: Thu, 8 Oct 2026 10:14:49 -0300 Subject: [PATCH 6/6] feat: Bind the standalone MCP server dual-stack with dualStack Route the MCP --host through withBindHost, so mcpServer.dualStack renders :: instead of 0.0.0.0. The MCP server itself needs feast mcp --host :: support (#6977). Signed-off-by: dbbvitor --- docs/reference/feature-servers/mcp-server.md | 2 +- .../internal/controller/services/bind_host_test.go | 2 ++ .../internal/controller/services/services.go | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/reference/feature-servers/mcp-server.md b/docs/reference/feature-servers/mcp-server.md index 0de8538e1c2..04bbedd896d 100644 --- a/docs/reference/feature-servers/mcp-server.md +++ b/docs/reference/feature-servers/mcp-server.md @@ -218,7 +218,7 @@ See [sdk/python/feast/mcp/docker/README.md](https://github.com/feast-dev/feast/b ## Deploying with the Feast Operator -Setting `spec.services.mcpServer` adds a dedicated `feast mcp` container to the FeatureStore deployment, exposed on its own Service on port 8100. The operator sets `--host` and `--port` so that they match the generated Service. Every other setting comes from a `feast_mcp.yaml` supplied in a ConfigMap, which the operator mounts read-only at `/etc/feast/mcp`. +Setting `spec.services.mcpServer` adds a dedicated `feast mcp` container to the FeatureStore deployment, exposed on its own Service on port 8100. The operator sets `--host` and `--port` so that they match the generated Service. `--host` is `0.0.0.0` unless `spec.services.mcpServer.dualStack` is `true`, which sets it to `::` and needs an image with `feast mcp --host ::` support (#6977). Every other setting comes from a `feast_mcp.yaml` supplied in a ConfigMap, which the operator mounts read-only at `/etc/feast/mcp`. The ConfigMap must set `server.transport` to `http`, `streamable-http` or `sse`, because the default `stdio` cannot serve the Service. Without a ConfigMap, the operator passes `--transport http`, unless `FEAST_MCP_TRANSPORT` is set in the container `env`. diff --git a/infra/feast-operator/internal/controller/services/bind_host_test.go b/infra/feast-operator/internal/controller/services/bind_host_test.go index 0e28a59fc95..3b3301b3bdb 100644 --- a/infra/feast-operator/internal/controller/services/bind_host_test.go +++ b/infra/feast-operator/internal/controller/services/bind_host_test.go @@ -42,6 +42,8 @@ func TestWithBindHost(t *testing.T) { {"-h as first arg is still replaced", LineageFeastType, []string{"-h", hostAllIPv4}, dual, []string{"-h", "::"}}, {"-h with no following value does not panic or get touched", OnlineFeastType, []string{"serve", "-h"}, dual, []string{"serve", "-h"}}, {"lineage bare", LineageFeastType, []string{"serve_lineage", "-h", hostAllIPv4}, dual, []string{"serve_lineage", "-h", "::"}}, + {"mcp --host bare", McpServerFeastType, []string{"feast", "mcp", "--host", hostAllIPv4, "--port", "8100"}, dual, []string{"feast", "mcp", "--host", "::", "--port", "8100"}}, + {"mcp --host unset keeps ipv4", McpServerFeastType, []string{"feast", "mcp", "--host", hostAllIPv4, "--port", "8100"}, &feastdevv1.ServerConfigs{}, []string{"feast", "mcp", "--host", hostAllIPv4, "--port", "8100"}}, {"registry has no host flag", RegistryFeastType, []string{"serve_registry"}, dual, []string{"serve_registry"}}, } for _, tc := range cases { diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 418064db719..22693e906a5 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -815,8 +815,8 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st return feastCommand } -// withBindHost returns a copy of args with the "-h" host set to the IPv6 wildcard when dual-stack is enabled. -// gunicorn (online) and Arrow Flight (offline) need the bracketed form; uvicorn (ui, lineage) rejects it. +// withBindHost returns a copy of args with the "-h"/"--host" host set to the IPv6 wildcard when dual-stack is enabled. +// gunicorn (online) and Arrow Flight (offline) need the bracketed form; uvicorn (ui, lineage, mcp) rejects it. func withBindHost(feastType FeastServiceType, args []string, serverConfigs *feastdevv1.ServerConfigs) []string { out := append([]string{}, args...) if serverConfigs == nil || serverConfigs.DualStack == nil || !*serverConfigs.DualStack { @@ -827,7 +827,7 @@ func withBindHost(feastType FeastServiceType, args []string, serverConfigs *feas host = hostAllIPv6Bracketed } for i := 0; i+1 < len(out); i++ { - if out[i] == "-h" { + if out[i] == "-h" || out[i] == "--host" { out[i+1] = host } } @@ -851,7 +851,7 @@ func (feast *FeastServices) getMcpServerCommand() []string { } else if !feast.hasMcpServerTransportEnv() { cmd = append(cmd, "--transport", "http") } - return cmd + return withBindHost(McpServerFeastType, cmd, feast.getServerConfigs(McpServerFeastType)) } // hasMcpServerTransportEnv reports whether the user set the MCP transport through mcpServer.env.