diff --git a/infra/feast-operator/cmd/main.go b/infra/feast-operator/cmd/main.go index 4be1777ac72..f565aa0be8c 100644 --- a/infra/feast-operator/cmd/main.go +++ b/infra/feast-operator/cmd/main.go @@ -25,11 +25,18 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + 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" @@ -59,6 +66,29 @@ func init() { // +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 @@ -145,6 +175,7 @@ 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{ DisableFor: []client.Object{ 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..1a56d98af3b 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,16 @@ +- op: test + path: "/spec/template/spec/containers/0/env/1/name" + value: RELATED_IMAGE_FEATURE_SERVER - op: replace - path: "/spec/template/spec/containers/0/env/0" + path: "/spec/template/spec/containers/0/env/1" value: name: RELATED_IMAGE_FEATURE_SERVER value: ${FS_IMG} +- op: test + path: "/spec/template/spec/containers/0/env/2/name" + value: RELATED_IMAGE_CRON_JOB - op: replace - path: "/spec/template/spec/containers/0/env/1" + path: "/spec/template/spec/containers/0/env/2" value: 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 6de980383f0..372906b1097 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,16 @@ +- op: test + path: "/spec/template/spec/containers/0/env/1/name" + value: RELATED_IMAGE_FEATURE_SERVER - op: replace - path: "/spec/template/spec/containers/0/env/0" + path: "/spec/template/spec/containers/0/env/1" value: name: RELATED_IMAGE_FEATURE_SERVER value: quay.io/feastdev/feature-server:0.62.0 +- op: test + path: "/spec/template/spec/containers/0/env/2/name" + value: RELATED_IMAGE_CRON_JOB - op: replace - path: "/spec/template/spec/containers/0/env/1" + path: "/spec/template/spec/containers/0/env/2" value: name: RELATED_IMAGE_CRON_JOB value: quay.io/openshift/origin-cli:4.17 diff --git a/infra/feast-operator/config/manager/manager.yaml b/infra/feast-operator/config/manager/manager.yaml index 2fddf4725ba..4213787e075 100644 --- a/infra/feast-operator/config/manager/manager.yaml +++ b/infra/feast-operator/config/manager/manager.yaml @@ -73,6 +73,8 @@ spec: drop: - "ALL" env: + - name: GOMEMLIMIT + value: "230MiB" - name: RELATED_IMAGE_FEATURE_SERVER value: feast:latest - name: RELATED_IMAGE_CRON_JOB diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 558279cd396..b481185505b 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -20650,6 +20650,8 @@ spec: command: - /manager env: + - name: GOMEMLIMIT + value: 230MiB - name: RELATED_IMAGE_FEATURE_SERVER value: quay.io/feastdev/feature-server:0.62.0 - name: RELATED_IMAGE_CRON_JOB diff --git a/infra/feast-operator/internal/controller/authz/authz.go b/infra/feast-operator/internal/controller/authz/authz.go index e9811c1c789..2007298ef84 100644 --- a/infra/feast-operator/internal/controller/authz/authz.go +++ b/infra/feast-operator/internal/controller/authz/authz.go @@ -331,6 +331,7 @@ 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, } } diff --git a/infra/feast-operator/internal/controller/services/namespace_registry.go b/infra/feast-operator/internal/controller/services/namespace_registry.go index c796f4ca6b6..6d83f70f1a0 100644 --- a/infra/feast-operator/internal/controller/services/namespace_registry.go +++ b/infra/feast-operator/internal/controller/services/namespace_registry.go @@ -179,6 +179,7 @@ func (feast *FeastServices) setNamespaceRegistryRoleBinding(rb *rbacv1.RoleBindi ObjectMeta: metav1.ObjectMeta{ Name: roleName, Namespace: rb.Namespace, + Labels: feast.getLabels(), }, } role.Rules = desiredRules @@ -205,6 +206,7 @@ func (feast *FeastServices) setNamespaceRegistryRoleBinding(rb *rbacv1.RoleBindi } } + rb.Labels = feast.getLabels() rb.RoleRef = rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "Role", diff --git a/infra/feast-operator/internal/controller/services/scaling.go b/infra/feast-operator/internal/controller/services/scaling.go index ef1dd1f91d8..b02dc1eee07 100644 --- a/infra/feast-operator/internal/controller/services/scaling.go +++ b/infra/feast-operator/internal/controller/services/scaling.go @@ -231,7 +231,7 @@ func (feast *FeastServices) buildPDBApplyConfig() *pdbac.PodDisruptionBudgetAppl WithBlockOwnerDeletion(true), ). WithSpec(pdbac.PodDisruptionBudgetSpec(). - WithSelector(metaac.LabelSelector().WithMatchLabels(feast.getLabels())), + WithSelector(metaac.LabelSelector().WithMatchLabels(feast.getSelectorLabels())), ) if pdbConfig.MinAvailable != nil { @@ -249,8 +249,7 @@ 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/services.go b/infra/feast-operator/internal/controller/services/services.go index d8ff12a9348..b0cf2ba42f6 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -384,10 +384,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) @@ -408,9 +411,10 @@ 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{ @@ -818,7 +822,7 @@ 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{ { @@ -868,6 +872,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, @@ -976,7 +981,7 @@ func (feast *FeastServices) applyTopologySpread(podSpec *corev1.PodSpec) { MaxSkew: 1, TopologyKey: "topology.kubernetes.io/zone", WhenUnsatisfiable: corev1.ScheduleAnyway, - LabelSelector: metav1.SetAsLabelSelector(feast.getLabels()), + LabelSelector: metav1.SetAsLabelSelector(feast.getSelectorLabels()), }} } @@ -999,10 +1004,10 @@ func (feast *FeastServices) applyAffinity(podSpec *corev1.PodSpec) { Weight: 100, PodAffinityTerm: corev1.PodAffinityTerm{ TopologyKey: "kubernetes.io/hostname", - LabelSelector: metav1.SetAsLabelSelector(feast.getLabels()), - }, - }}, - }, + LabelSelector: metav1.SetAsLabelSelector(feast.getSelectorLabels()), + }, + }}, + }, } } @@ -1060,12 +1065,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 + ":" @@ -1438,10 +1455,10 @@ func IsDeploymentAvailable(conditions []appsv1.DeploymentCondition) bool { // 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{} - labels := feast.getLabels() + selectorLabels := feast.getSelectorLabels() if err := feast.Handler.Client.List(feast.Handler.Context, &podList, client.InNamespace(deploy.Namespace), - client.MatchingLabels(labels), + client.MatchingLabels(selectorLabels), ); err != nil { return "" } diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index 5b4479698f3..e5939b6c212 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -103,6 +103,11 @@ const ( OidcMissingSecretError string = "missing OIDC secret: %s" ) +const ( + ManagedByLabelKey = "app.kubernetes.io/managed-by" + ManagedByLabelValue = "feast-operator" +) + var ( DefaultImage = "quay.io/feastdev/feature-server:" + feastversion.FeastVersion DefaultCronJobImage = "quay.io/openshift/origin-cli:4.17"