diff --git a/api/v1/inferencepool_types.go b/api/v1/inferencepool_types.go index 98f45d5bb..f55f297c3 100644 --- a/api/v1/inferencepool_types.go +++ b/api/v1/inferencepool_types.go @@ -51,15 +51,16 @@ type InferencePoolList struct { // InferencePoolSpec defines the desired state of InferencePool type InferencePoolSpec struct { - // Selector defines a map of labels to watch model server Pods - // that should be included in the InferencePool. - // In some cases, implementations may translate this field to a Service selector, so this matches the simple - // map used for Service selectors instead of the full Kubernetes LabelSelector type. - // If specified, it will be applied to match the model server pods in the same namespace as the InferencePool. - // Cross namesoace selector is not supported. + // Selector determines which Pods are members of this inference pool. + // It matches Pods by their labels only within the same namespace; cross-namespace + // selection is not supported. + // + // The structure of this LabelSelector is intentionally simple to be compatible + // with Kubernetes Service selectors, as some implementations may translate + // this configuration into a Service resource. // // +kubebuilder:validation:Required - Selector map[LabelKey]LabelValue `json:"selector"` + Selector LabelSelector `json:"selector"` // TargetPortNumber defines the port number to access the selected model server Pods. // The number must be in the range 1 to 65535. diff --git a/api/v1/shared_types.go b/api/v1/shared_types.go index 800291b33..04319acd3 100644 --- a/api/v1/shared_types.go +++ b/api/v1/shared_types.go @@ -127,3 +127,13 @@ type LabelKey string // +kubebuilder:validation:MaxLength=63 // +kubebuilder:validation:Pattern=`^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$` type LabelValue string + +// LabelSelector defines a query for resources based on their labels. +// This simplified version uses only the matchLabels field. +type LabelSelector struct { + // matchLabels contains a set of required {key,value} pairs. + // An object must match every label in this map to be selected. + // The matching logic is an AND operation on all entries. + // +optional + MatchLabels map[LabelKey]LabelValue `json:"matchLabels,omitempty" protobuf:"bytes,1,rep,name=matchLabels"` +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index adf19acbe..c16d36b28 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -174,13 +174,7 @@ func (in *InferencePoolList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InferencePoolSpec) DeepCopyInto(out *InferencePoolSpec) { *out = *in - if in.Selector != nil { - in, out := &in.Selector, &out.Selector - *out = make(map[LabelKey]LabelValue, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } + in.Selector.DeepCopyInto(&out.Selector) in.EndpointPickerConfig.DeepCopyInto(&out.EndpointPickerConfig) } @@ -216,6 +210,28 @@ func (in *InferencePoolStatus) DeepCopy() *InferencePoolStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LabelSelector) DeepCopyInto(out *LabelSelector) { + *out = *in + if in.MatchLabels != nil { + in, out := &in.MatchLabels, &out.MatchLabels + *out = make(map[LabelKey]LabelValue, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LabelSelector. +func (in *LabelSelector) DeepCopy() *LabelSelector { + if in == nil { + return nil + } + out := new(LabelSelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ParentGatewayReference) DeepCopyInto(out *ParentGatewayReference) { *out = *in diff --git a/apix/v1alpha2/inferencepool_conversion.go b/apix/v1alpha2/inferencepool_conversion.go new file mode 100644 index 000000000..742008cb7 --- /dev/null +++ b/apix/v1alpha2/inferencepool_conversion.go @@ -0,0 +1,143 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 v1alpha2 + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + runtime "k8s.io/apimachinery/pkg/runtime" + v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" +) + +// ConvertTo converts this InferencePool (v1alpha2) to the v1 version. +func (src *InferencePool) ConvertTo() (*v1.InferencePool, error) { + if src == nil { + return nil, nil + } + + v1EndPointPickerConfig, err := convertEndpointPickerConfigToV1(&src.Spec.EndpointPickerConfig) + if err != nil { + return nil, err + } + v1Status, err := converStatusToV1(src.Status) + if err != nil { + return nil, err + } + dst := &v1.InferencePool{ + TypeMeta: src.TypeMeta, + ObjectMeta: src.ObjectMeta, + Spec: v1.InferencePoolSpec{ + TargetPortNumber: src.Spec.TargetPortNumber, + EndpointPickerConfig: *v1EndPointPickerConfig, + }, + Status: *v1Status, + } + if src.Spec.Selector != nil { + dst.Spec.Selector.MatchLabels = make(map[v1.LabelKey]v1.LabelValue, len(src.Spec.Selector)) + for k, v := range src.Spec.Selector { + dst.Spec.Selector.MatchLabels[v1.LabelKey(k)] = v1.LabelValue(v) + } + } + return dst, nil +} + +// ConvertFrom converts from the v1 version to this version (v1alpha2). +func ConvertFrom(src *v1.InferencePool) (*InferencePool, error) { + if src == nil { + return nil, nil + } + + endPointPickerConfig, err := convertEndpointPickerConfigFromV1(&src.Spec.EndpointPickerConfig) + if err != nil { + return nil, err + } + status, err := converStatusFromV1(src.Status) + if err != nil { + return nil, err + } + dst := &InferencePool{ + TypeMeta: metav1.TypeMeta{ + Kind: "InferencePool", + APIVersion: "inference.networking.x-k8s.io/v1alpha2", + }, + ObjectMeta: src.ObjectMeta, + Spec: InferencePoolSpec{ + TargetPortNumber: src.Spec.TargetPortNumber, + EndpointPickerConfig: *endPointPickerConfig, + }, + Status: *status, + } + + if src.Spec.Selector.MatchLabels != nil { + dst.Spec.Selector = make(map[LabelKey]LabelValue, len(src.Spec.Selector.MatchLabels)) + for k, v := range src.Spec.Selector.MatchLabels { + dst.Spec.Selector[LabelKey(k)] = LabelValue(v) + } + } + + return dst, nil +} + +func converStatusToV1(src InferencePoolStatus) (*v1.InferencePoolStatus, error) { + u, err := toUnstructured(&src) + if err != nil { + return nil, err + } + return convert[v1.InferencePoolStatus](u) +} + +func converStatusFromV1(src v1.InferencePoolStatus) (*InferencePoolStatus, error) { + u, err := toUnstructured(&src) + if err != nil { + return nil, err + } + return convert[InferencePoolStatus](u) +} + +func convertEndpointPickerConfigToV1(src *EndpointPickerConfig) (*v1.EndpointPickerConfig, error) { + u, err := toUnstructured(&src) + if err != nil { + return nil, err + } + return convert[v1.EndpointPickerConfig](u) +} + +func convertEndpointPickerConfigFromV1(src *v1.EndpointPickerConfig) (*EndpointPickerConfig, error) { + u, err := toUnstructured(&src) + if err != nil { + return nil, err + } + return convert[EndpointPickerConfig](u) +} + +func toUnstructured(obj any) (*unstructured.Unstructured, error) { + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + return &unstructured.Unstructured{Object: u}, nil +} + +func convert[T any](u *unstructured.Unstructured) (*T, error) { + var res T + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &res); err != nil { + return nil, fmt.Errorf("error converting unstructured to T: %v", err) + } + return &res, nil +} diff --git a/apix/v1alpha2/inferencepool_conversion_test.go b/apix/v1alpha2/inferencepool_conversion_test.go new file mode 100644 index 000000000..fe181ef0f --- /dev/null +++ b/apix/v1alpha2/inferencepool_conversion_test.go @@ -0,0 +1,270 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 v1alpha2 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" +) + +var ( + group = Group("my-group") + kind = Kind("MyKind") + failureMode = ExtensionFailureMode("Deny") + portNumber = PortNumber(9000) + timestamp = metav1.Unix(0, 0) + + v1Group = v1.Group("my-group") + v1Kind = v1.Kind("MyKind") + v1FailureMode = v1.ExtensionFailureMode("Deny") + v1PortNumber = v1.PortNumber(9000) +) + +func TestInferencePoolConvertTo(t *testing.T) { + tests := []struct { + name string + src *InferencePool + want *v1.InferencePool + wantErr bool + }{ + { + name: "full conversion from v1alpha2 to v1 including status", + src: &InferencePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pool", + Namespace: "test-ns", + }, + Spec: InferencePoolSpec{ + Selector: map[LabelKey]LabelValue{ + "app": "my-model-server", + }, + TargetPortNumber: 8080, + EndpointPickerConfig: EndpointPickerConfig{ + ExtensionRef: &Extension{ + ExtensionReference: ExtensionReference{ + Group: &group, + Kind: &kind, + Name: "my-epp-service", + PortNumber: &portNumber, + }, + ExtensionConnection: ExtensionConnection{ + FailureMode: &failureMode, + }, + }, + }, + }, + Status: InferencePoolStatus{ + Parents: []PoolStatus{ + { + GatewayRef: ParentGatewayReference{Name: "my-gateway"}, + Conditions: []metav1.Condition{ + { + Type: string(InferencePoolConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(InferencePoolReasonAccepted), + LastTransitionTime: timestamp, + }, + }, + }, + }, + }, + }, + want: &v1.InferencePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pool", + Namespace: "test-ns", + }, + Spec: v1.InferencePoolSpec{ + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "app": "my-model-server", + }, + }, + TargetPortNumber: 8080, + EndpointPickerConfig: v1.EndpointPickerConfig{ + ExtensionRef: &v1.Extension{ + ExtensionReference: v1.ExtensionReference{ + Group: &v1Group, + Kind: &v1Kind, + Name: "my-epp-service", + PortNumber: &v1PortNumber, + }, + ExtensionConnection: v1.ExtensionConnection{ + FailureMode: &v1FailureMode, + }, + }, + }, + }, + Status: v1.InferencePoolStatus{ + Parents: []v1.PoolStatus{ + { + GatewayRef: v1.ParentGatewayReference{Name: "my-gateway"}, + Conditions: []metav1.Condition{ + { + Type: string(v1.InferencePoolConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(v1.InferencePoolReasonAccepted), + LastTransitionTime: timestamp, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "nil source should return nil and no error", + src: nil, + want: nil, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.src.ConvertTo() + if (err != nil) != tt.wantErr { + t.Fatalf("ConvertTo() error = %v, wantErr %v", err, tt.wantErr) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertTo() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestInferencePoolConvertFrom(t *testing.T) { + tests := []struct { + name string + src *v1.InferencePool + want *InferencePool + wantErr bool + }{ + { + name: "full conversion from v1 to v1alpha2 including status", + src: &v1.InferencePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pool", + Namespace: "test-ns", + }, + Spec: v1.InferencePoolSpec{ + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "app": "my-model-server", + }, + }, + TargetPortNumber: 8080, + EndpointPickerConfig: v1.EndpointPickerConfig{ + ExtensionRef: &v1.Extension{ + ExtensionReference: v1.ExtensionReference{ + Group: &v1Group, + Kind: &v1Kind, + Name: "my-epp-service", + PortNumber: &v1PortNumber, + }, + ExtensionConnection: v1.ExtensionConnection{ + FailureMode: &v1FailureMode, + }, + }, + }, + }, + Status: v1.InferencePoolStatus{ + Parents: []v1.PoolStatus{ + { + GatewayRef: v1.ParentGatewayReference{Name: "my-gateway"}, + Conditions: []metav1.Condition{ + { + Type: string(v1.InferencePoolConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(v1.InferencePoolReasonAccepted), + LastTransitionTime: timestamp, + }, + }, + }, + }, + }, + }, + want: &InferencePool{ + TypeMeta: metav1.TypeMeta{ + Kind: "InferencePool", + APIVersion: "inference.networking.x-k8s.io/v1alpha2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pool", + Namespace: "test-ns", + }, + Spec: InferencePoolSpec{ + Selector: map[LabelKey]LabelValue{ + "app": "my-model-server", + }, + TargetPortNumber: 8080, + EndpointPickerConfig: EndpointPickerConfig{ + ExtensionRef: &Extension{ + ExtensionReference: ExtensionReference{ + Group: &group, + Kind: &kind, + Name: "my-epp-service", + PortNumber: &portNumber, + }, + ExtensionConnection: ExtensionConnection{ + FailureMode: &failureMode, + }, + }, + }, + }, + Status: InferencePoolStatus{ + Parents: []PoolStatus{ + { + GatewayRef: ParentGatewayReference{Name: "my-gateway"}, + Conditions: []metav1.Condition{ + { + Type: string(InferencePoolConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(InferencePoolReasonAccepted), + LastTransitionTime: timestamp, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "nil source should return nil and no error", + src: nil, + want: nil, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertFrom(tt.src) + if (err != nil) != tt.wantErr { + t.Fatalf("ConvertFrom() error = %v, wantErr %v", err, tt.wantErr) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("ConvertFrom() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/apix/v1alpha2/zz_generated.deepcopy.go b/apix/v1alpha2/zz_generated.deepcopy.go index 697c443c4..cb73c542f 100644 --- a/apix/v1alpha2/zz_generated.deepcopy.go +++ b/apix/v1alpha2/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1alpha2 import ( "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/client-go/applyconfiguration/api/v1/inferencepoolspec.go b/client-go/applyconfiguration/api/v1/inferencepoolspec.go index d9c08b238..dcd7d7a66 100644 --- a/client-go/applyconfiguration/api/v1/inferencepoolspec.go +++ b/client-go/applyconfiguration/api/v1/inferencepoolspec.go @@ -18,15 +18,11 @@ limitations under the License. package v1 -import ( - apiv1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" -) - // InferencePoolSpecApplyConfiguration represents a declarative configuration of the InferencePoolSpec type for use // with apply. type InferencePoolSpecApplyConfiguration struct { - Selector map[apiv1.LabelKey]apiv1.LabelValue `json:"selector,omitempty"` - TargetPortNumber *int32 `json:"targetPortNumber,omitempty"` + Selector *LabelSelectorApplyConfiguration `json:"selector,omitempty"` + TargetPortNumber *int32 `json:"targetPortNumber,omitempty"` EndpointPickerConfigApplyConfiguration `json:",inline"` } @@ -36,17 +32,11 @@ func InferencePoolSpec() *InferencePoolSpecApplyConfiguration { return &InferencePoolSpecApplyConfiguration{} } -// WithSelector puts the entries into the Selector field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, the entries provided by each call will be put on the Selector field, -// overwriting an existing map entries in Selector field with the same key. -func (b *InferencePoolSpecApplyConfiguration) WithSelector(entries map[apiv1.LabelKey]apiv1.LabelValue) *InferencePoolSpecApplyConfiguration { - if b.Selector == nil && len(entries) > 0 { - b.Selector = make(map[apiv1.LabelKey]apiv1.LabelValue, len(entries)) - } - for k, v := range entries { - b.Selector[k] = v - } +// WithSelector sets the Selector field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Selector field is set to the value of the last call. +func (b *InferencePoolSpecApplyConfiguration) WithSelector(value *LabelSelectorApplyConfiguration) *InferencePoolSpecApplyConfiguration { + b.Selector = value return b } diff --git a/client-go/applyconfiguration/api/v1/labelselector.go b/client-go/applyconfiguration/api/v1/labelselector.go new file mode 100644 index 000000000..70953db1e --- /dev/null +++ b/client-go/applyconfiguration/api/v1/labelselector.go @@ -0,0 +1,49 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +import ( + apiv1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" +) + +// LabelSelectorApplyConfiguration represents a declarative configuration of the LabelSelector type for use +// with apply. +type LabelSelectorApplyConfiguration struct { + MatchLabels map[apiv1.LabelKey]apiv1.LabelValue `json:"matchLabels,omitempty"` +} + +// LabelSelectorApplyConfiguration constructs a declarative configuration of the LabelSelector type for use with +// apply. +func LabelSelector() *LabelSelectorApplyConfiguration { + return &LabelSelectorApplyConfiguration{} +} + +// WithMatchLabels puts the entries into the MatchLabels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the MatchLabels field, +// overwriting an existing map entries in MatchLabels field with the same key. +func (b *LabelSelectorApplyConfiguration) WithMatchLabels(entries map[apiv1.LabelKey]apiv1.LabelValue) *LabelSelectorApplyConfiguration { + if b.MatchLabels == nil && len(entries) > 0 { + b.MatchLabels = make(map[apiv1.LabelKey]apiv1.LabelValue, len(entries)) + } + for k, v := range entries { + b.MatchLabels[k] = v + } + return b +} diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go index 46895d8f0..f83f28762 100644 --- a/client-go/applyconfiguration/utils.go +++ b/client-go/applyconfiguration/utils.go @@ -48,6 +48,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1.InferencePoolSpecApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("InferencePoolStatus"): return &apiv1.InferencePoolStatusApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("LabelSelector"): + return &apiv1.LabelSelectorApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ParentGatewayReference"): return &apiv1.ParentGatewayReferenceApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("PoolStatus"): diff --git a/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml b/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml index 78e3a9c7a..c2618eee7 100644 --- a/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml +++ b/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml @@ -96,30 +96,38 @@ spec: - name type: object selector: - additionalProperties: - description: |- - LabelValue is the value of a label. This is used for validation - of maps. This matches the Kubernetes label validation rules: - * must be 63 characters or less (can be empty), - * unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]), - * could contain dashes (-), underscores (_), dots (.), and alphanumerics between. + description: |- + Selector determines which Pods are members of this inference pool. + It matches Pods by their labels only within the same namespace; cross-namespace + selection is not supported. + + The structure of this LabelSelector is intentionally simple to be compatible + with Kubernetes Service selectors, as some implementations may translate + this configuration into a Service resource. + properties: + matchLabels: + additionalProperties: + description: |- + LabelValue is the value of a label. This is used for validation + of maps. This matches the Kubernetes label validation rules: + * must be 63 characters or less (can be empty), + * unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]), + * could contain dashes (-), underscores (_), dots (.), and alphanumerics between. - Valid values include: + Valid values include: - * MyValue - * my.name - * 123-my-value - maxLength: 63 - minLength: 0 - pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ - type: string - description: |- - Selector defines a map of labels to watch model server Pods - that should be included in the InferencePool. - In some cases, implementations may translate this field to a Service selector, so this matches the simple - map used for Service selectors instead of the full Kubernetes LabelSelector type. - If specified, it will be applied to match the model server pods in the same namespace as the InferencePool. - Cross namesoace selector is not supported. + * MyValue + * my.name + * 123-my-value + maxLength: 63 + minLength: 0 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + description: |- + matchLabels contains a set of required {key,value} pairs. + An object must match every label in this map to be selected. + The matching logic is an AND operation on all entries. + type: object type: object targetPortNumber: description: |- diff --git a/config/manifests/inferencepool-resources.yaml b/config/manifests/inferencepool-resources.yaml index a1eb3e039..f5dbcf45b 100644 --- a/config/manifests/inferencepool-resources.yaml +++ b/config/manifests/inferencepool-resources.yaml @@ -10,7 +10,8 @@ metadata: spec: targetPortNumber: 8000 selector: - app: vllm-llama3-8b-instruct + matchLabels: + app: vllm-llama3-8b-instruct extensionRef: name: vllm-llama3-8b-instruct-epp --- diff --git a/conformance/resources/base.yaml b/conformance/resources/base.yaml index dda72fab9..c69e8bf01 100644 --- a/conformance/resources/base.yaml +++ b/conformance/resources/base.yaml @@ -153,7 +153,8 @@ metadata: namespace: gateway-conformance-app-backend spec: selector: - app: primary-inference-model-server + matchLabels: + app: primary-inference-model-server targetPortNumber: 3000 extensionRef: name: primary-endpoint-picker-svc diff --git a/pkg/common/convert.go b/pkg/common/convert.go deleted file mode 100644 index bb6715598..000000000 --- a/pkg/common/convert.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2025 The Kubernetes Authors. - -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 common defines structs for referring to fully qualified k8s resources. -package common - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - - v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" - "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2" -) - -func ToUnstructured(obj any) (*unstructured.Unstructured, error) { - u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) - if err != nil { - return nil, err - } - return &unstructured.Unstructured{Object: u}, nil -} - -var ToInferencePool = convert[v1.InferencePool] - -var ToXInferencePool = convert[v1alpha2.InferencePool] - -func convert[T any](u *unstructured.Unstructured) (*T, error) { - var res T - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &res); err != nil { - return nil, fmt.Errorf("error converting unstructured to T: %v", err) - } - return &res, nil -} diff --git a/pkg/epp/controller/inferencepool_reconciler.go b/pkg/epp/controller/inferencepool_reconciler.go index 1d3c0aa9e..986abe371 100644 --- a/pkg/epp/controller/inferencepool_reconciler.go +++ b/pkg/epp/controller/inferencepool_reconciler.go @@ -21,7 +21,6 @@ import ( "fmt" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -88,14 +87,8 @@ func (c *InferencePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques // If it's already a v1 object, just use it. v1infPool = pool case *v1alpha2.InferencePool: - // If it's a v1alpha2 object, convert it to v1. - var uns *unstructured.Unstructured - uns, err := common.ToUnstructured(pool) - if err != nil { - logger.Error(err, "Failed to convert inferencePool to unstructured") - return ctrl.Result{}, err - } - v1infPool, err = common.ToInferencePool(uns) + var err error + v1infPool, err = pool.ConvertTo() if err != nil { logger.Error(err, "Failed to convert unstructured to inferencePool") return ctrl.Result{}, err diff --git a/pkg/epp/controller/inferencepool_reconciler_test.go b/pkg/epp/controller/inferencepool_reconciler_test.go index a29d531b9..78a58efba 100644 --- a/pkg/epp/controller/inferencepool_reconciler_test.go +++ b/pkg/epp/controller/inferencepool_reconciler_test.go @@ -127,7 +127,9 @@ func TestInferencePoolReconciler(t *testing.T) { if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil { t.Errorf("Unexpected pool get error: %v", err) } - newPool1.Spec.Selector = map[v1.LabelKey]v1.LabelValue{"app": "vllm_v2"} + newPool1.Spec.Selector = v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{"app": "vllm_v2"}, + } if err := fakeClient.Update(ctx, newPool1, &client.UpdateOptions{}); err != nil { t.Errorf("Unexpected pool update error: %v", err) } @@ -319,11 +321,8 @@ func xDiffStore(t *testing.T, datastore datastore.Datastore, params xDiffStorePa if gotPool == nil && params.wantPool == nil { return "" } - uns, err := common.ToUnstructured(gotPool) - if err != nil { - t.Fatalf("failed to convert XInferencePool to Unstructured: %v", err) - } - gotXPool, err := common.ToXInferencePool(uns) + + gotXPool, err := v1alpha2.ConvertFrom(gotPool) if err != nil { t.Fatalf("failed to convert unstructured to InferencePool: %v", err) } diff --git a/pkg/epp/controller/pod_reconciler_test.go b/pkg/epp/controller/pod_reconciler_test.go index 0f6202177..90fd84b7f 100644 --- a/pkg/epp/controller/pod_reconciler_test.go +++ b/pkg/epp/controller/pod_reconciler_test.go @@ -62,8 +62,10 @@ func TestPodReconciler(t *testing.T) { pool: &v1.InferencePool{ Spec: v1.InferencePoolSpec{ TargetPortNumber: int32(8000), - Selector: map[v1.LabelKey]v1.LabelValue{ - "some-key": "some-val", + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "some-key": "some-val", + }, }, }, }, @@ -78,8 +80,10 @@ func TestPodReconciler(t *testing.T) { pool: &v1.InferencePool{ Spec: v1.InferencePoolSpec{ TargetPortNumber: int32(8000), - Selector: map[v1.LabelKey]v1.LabelValue{ - "some-key": "some-val", + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "some-key": "some-val", + }, }, }, }, @@ -94,8 +98,10 @@ func TestPodReconciler(t *testing.T) { pool: &v1.InferencePool{ Spec: v1.InferencePoolSpec{ TargetPortNumber: int32(8000), - Selector: map[v1.LabelKey]v1.LabelValue{ - "some-key": "some-val", + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "some-key": "some-val", + }, }, }, }, @@ -111,8 +117,10 @@ func TestPodReconciler(t *testing.T) { pool: &v1.InferencePool{ Spec: v1.InferencePoolSpec{ TargetPortNumber: int32(8000), - Selector: map[v1.LabelKey]v1.LabelValue{ - "some-key": "some-val", + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "some-key": "some-val", + }, }, }, }, @@ -125,8 +133,10 @@ func TestPodReconciler(t *testing.T) { pool: &v1.InferencePool{ Spec: v1.InferencePoolSpec{ TargetPortNumber: int32(8000), - Selector: map[v1.LabelKey]v1.LabelValue{ - "some-key": "some-val", + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "some-key": "some-val", + }, }, }, }, @@ -140,8 +150,10 @@ func TestPodReconciler(t *testing.T) { pool: &v1.InferencePool{ Spec: v1.InferencePoolSpec{ TargetPortNumber: int32(8000), - Selector: map[v1.LabelKey]v1.LabelValue{ - "some-key": "some-val", + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "some-key": "some-val", + }, }, }, }, @@ -156,8 +168,10 @@ func TestPodReconciler(t *testing.T) { pool: &v1.InferencePool{ Spec: v1.InferencePoolSpec{ TargetPortNumber: int32(8000), - Selector: map[v1.LabelKey]v1.LabelValue{ - "some-key": "some-val", + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "some-key": "some-val", + }, }, }, }, diff --git a/pkg/epp/datastore/datastore.go b/pkg/epp/datastore/datastore.go index eefc1df67..6106fda87 100644 --- a/pkg/epp/datastore/datastore.go +++ b/pkg/epp/datastore/datastore.go @@ -157,7 +157,7 @@ func (ds *datastore) PoolLabelsMatch(podLabels map[string]string) bool { if ds.pool == nil { return false } - poolSelector := selectorFromInferencePoolSelector(ds.pool.Spec.Selector) + poolSelector := selectorFromInferencePoolSelector(ds.pool.Spec.Selector.MatchLabels) podSet := labels.Set(podLabels) return poolSelector.Matches(podSet) } @@ -288,7 +288,7 @@ func (ds *datastore) podResyncAll(ctx context.Context, reader client.Reader) err logger := log.FromContext(ctx) podList := &corev1.PodList{} if err := reader.List(ctx, podList, &client.ListOptions{ - LabelSelector: selectorFromInferencePoolSelector(ds.pool.Spec.Selector), + LabelSelector: selectorFromInferencePoolSelector(ds.pool.Spec.Selector.MatchLabels), Namespace: ds.pool.Namespace, }); err != nil { return fmt.Errorf("failed to list pods - %w", err) diff --git a/pkg/epp/requestcontrol/director_test.go b/pkg/epp/requestcontrol/director_test.go index b0e5aa173..5796fd0ca 100644 --- a/pkg/epp/requestcontrol/director_test.go +++ b/pkg/epp/requestcontrol/director_test.go @@ -103,8 +103,10 @@ func TestDirector_HandleRequest(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "test-pool", Namespace: "default"}, Spec: v1.InferencePoolSpec{ TargetPortNumber: int32(8000), - Selector: map[v1.LabelKey]v1.LabelValue{ - "app": "inference", + Selector: v1.LabelSelector{ + MatchLabels: map[v1.LabelKey]v1.LabelValue{ + "app": "inference", + }, }, }, } diff --git a/pkg/epp/util/testing/wrappers.go b/pkg/epp/util/testing/wrappers.go index 84a6015e5..9095dc5e2 100644 --- a/pkg/epp/util/testing/wrappers.go +++ b/pkg/epp/util/testing/wrappers.go @@ -195,7 +195,9 @@ func (m *InferencePoolWrapper) Selector(selector map[string]string) *InferencePo for k, v := range selector { s[v1.LabelKey(k)] = v1.LabelValue(v) } - m.Spec.Selector = s + m.Spec.Selector = v1.LabelSelector{ + MatchLabels: s, + } return m }