Skip to content

Commit 941ac2d

Browse files
salonichf5ciarams87
authored andcommitted
Update inference extension conformance tests (#4089)
Enable conformance tests for Inference Extension Co-author: Ciara Stacker <[email protected]>
1 parent 7910bd2 commit 941ac2d

File tree

27 files changed

+441
-88
lines changed

27 files changed

+441
-88
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ jobs:
443443
build-os: ${{ matrix.build-os }}
444444
production-release: ${{ inputs.is_production_release == true && (inputs.dry_run == false || inputs.dry_run == null) }}
445445
release_version: ${{ inputs.release_version }}
446+
enable-inference-extension: true
446447
secrets: inherit
447448
permissions:
448449
contents: write

.github/workflows/conformance.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ on:
1616
enable-experimental:
1717
required: true
1818
type: boolean
19+
enable-inference-extension:
20+
required: true
21+
type: boolean
1922
production-release:
2023
required: false
2124
type: boolean
@@ -32,6 +35,7 @@ defaults:
3235
env:
3336
PLUS_USAGE_ENDPOINT: ${{ secrets.JWT_PLUS_REPORTING_ENDPOINT }}
3437
ENABLE_EXPERIMENTAL: ${{ inputs.enable-experimental }}
38+
ENABLE_INFERENCE_EXTENSION: ${{ inputs.enable-inference-extension }}
3539

3640
permissions:
3741
contents: read
@@ -194,3 +198,24 @@ jobs:
194198
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
195199
run: gh release upload ${{ github.ref_name }} conformance-profile.yaml --clobber
196200
working-directory: ./tests
201+
202+
- name: Run inference conformance tests
203+
run: |
204+
make run-inference-conformance-tests CONFORMANCE_TAG=${{ github.sha }} NGF_VERSION=${{ github.ref_name }} CLUSTER_NAME=${{ github.run_id }}
205+
core_result=$(cat conformance-profile-inference.yaml | yq '.profiles[0].core.result')
206+
if [ "${core_result}" == "failure" ] ]; then echo "Inference Conformance test failed, see above for details." && exit 2; fi
207+
working-directory: ./tests
208+
209+
- name: Upload profile to GitHub
210+
if: ${{ inputs.enable-experimental }} # add experimental flag to filter result upload
211+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
212+
with:
213+
name: conformance-profile-inference-${{ inputs.image }}-${{ inputs.k8s-version }}-${{ steps.ngf-meta.outputs.version }}-${{ github.run_id }}
214+
path: ./tests/conformance-profile-inference.yaml
215+
216+
- name: Upload profile to release
217+
if: ${{ inputs.production-release && inputs.enable-experimental }}
218+
env:
219+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
220+
run: gh release upload ${{ github.ref_name }} conformance-profile-inference.yaml --clobber
221+
working-directory: ./tests

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
cover.html
1414
cmd-cover.html
1515
conformance-profile.yaml
16+
conformance-profile-inference.yaml
1617

1718
# Dependency directories (remove the comment below to include it)
1819
# vendor/

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ TELEMETRY_ENDPOINT=# if empty, NGF will report telemetry in its logs at debug le
1515
TELEMETRY_ENDPOINT_INSECURE = false
1616

1717
ENABLE_EXPERIMENTAL ?= false
18+
ENABLE_INFERENCE_EXTENSION ?= false
1819

1920
# go build flags - should not be overridden by the user
2021
GO_LINKER_FlAGS_VARS = -X main.version=${VERSION} -X main.telemetryReportPeriod=${TELEMETRY_REPORT_PERIOD} -X main.telemetryEndpoint=${TELEMETRY_ENDPOINT} -X main.telemetryEndpointInsecure=${TELEMETRY_ENDPOINT_INSECURE}
@@ -237,10 +238,16 @@ install-ngf-local-build-with-plus: check-for-plus-usage-endpoint build-images-wi
237238

238239
.PHONY: helm-install-local
239240
helm-install-local: install-gateway-crds ## Helm install NGF on configured kind cluster with local images. To build, load, and install with helm run make install-ngf-local-build.
240-
helm install nginx-gateway $(CHART_DIR) --set nginx.image.repository=$(NGINX_PREFIX) --create-namespace --wait --set nginxGateway.image.pullPolicy=$(PULL_POLICY) --set nginx.service.type=$(NGINX_SERVICE_TYPE) --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=$(PULL_POLICY) --set nginxGateway.gwAPIExperimentalFeatures.enable=$(ENABLE_EXPERIMENTAL) -n nginx-gateway $(HELM_PARAMETERS)
241+
@if [ "$(ENABLE_INFERENCE_EXTENSION)" = "true" ]; then \
242+
$(MAKE) install-inference-crds; \
243+
fi
244+
helm install nginx-gateway $(CHART_DIR) --set nginx.image.repository=$(NGINX_PREFIX) --create-namespace --wait --set nginxGateway.image.pullPolicy=Never --set nginx.service.type=NodePort --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never --set nginxGateway.gwAPIExperimentalFeatures.enable=$(ENABLE_EXPERIMENTAL) -n nginx-gateway $(HELM_PARAMETERS)
241245

242246
.PHONY: helm-install-local-with-plus
243247
helm-install-local-with-plus: check-for-plus-usage-endpoint install-gateway-crds ## Helm install NGF with NGINX Plus on configured kind cluster with local images. To build, load, and install with helm run make install-ngf-local-build-with-plus.
248+
@if [ "$(ENABLE_INFERENCE_EXTENSION)" = "true" ]; then \
249+
$(MAKE) install-inference-crds; \
250+
fi
244251
kubectl create namespace nginx-gateway || true
245252
kubectl -n nginx-gateway create secret generic nplus-license --from-file $(PLUS_LICENSE_FILE) || true
246253
helm install nginx-gateway $(CHART_DIR) --set nginx.image.repository=$(NGINX_PLUS_PREFIX) --wait --set nginxGateway.image.pullPolicy=$(PULL_POLICY) --set nginx.service.type=$(NGINX_SERVICE_TYPE) --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=$(PULL_POLICY) --set nginxGateway.gwAPIExperimentalFeatures.enable=$(ENABLE_EXPERIMENTAL) -n nginx-gateway --set nginx.plus=true --set nginx.usage.endpoint=$(PLUS_USAGE_ENDPOINT) $(HELM_PARAMETERS)

cmd/gateway/endpoint_picker.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package main
22

33
import (
4+
"crypto/tls"
45
"errors"
56
"fmt"
67
"io"
78
"net"
89
"net/http"
10+
"strings"
911
"time"
1012

1113
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
1214
extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
1315
"github.com/go-logr/logr"
1416
"google.golang.org/grpc"
17+
"google.golang.org/grpc/credentials"
1518
"google.golang.org/grpc/credentials/insecure"
1619
eppMetadata "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
1720

@@ -34,7 +37,19 @@ func endpointPickerServer(handler http.Handler) error {
3437
// realExtProcClientFactory returns a factory that creates a new gRPC connection and client per request.
3538
func realExtProcClientFactory() extProcClientFactory {
3639
return func(target string) (extprocv3.ExternalProcessorClient, func() error, error) {
37-
conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
40+
var opts []grpc.DialOption
41+
enableTLS := true
42+
insecureSkipVerify := true
43+
44+
if !enableTLS {
45+
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
46+
} else {
47+
creds := credentials.NewTLS(&tls.Config{
48+
InsecureSkipVerify: insecureSkipVerify, //nolint:gosec
49+
})
50+
opts = append(opts, grpc.WithTransportCredentials(creds))
51+
}
52+
conn, err := grpc.NewClient(target, opts...)
3853
if err != nil {
3954
return nil, nil, err
4055
}
@@ -148,8 +163,15 @@ func buildHeaderRequest(r *http.Request) *extprocv3.ProcessingRequest {
148163

149164
for key, values := range r.Header {
150165
for _, value := range values {
166+
// Normalize header keys to lowercase for case-insensitive matching.
167+
// This addresses the mismatch between Go's default HTTP header normalization (Title-Case)
168+
// and EPP's expectation of lowercase header keys. Additionally, HTTP/2 — which gRPC uses —
169+
// requires all header field names to be lowercase as specified in RFC 7540, Section 8.1.2:
170+
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2
171+
normalizedKey := strings.ToLower(key)
172+
151173
headerMap.Headers = append(headerMap.Headers, &corev3.HeaderValue{
152-
Key: key,
174+
Key: normalizedKey,
153175
Value: value,
154176
})
155177
}

internal/controller/nginx/conf/nginx-plus.conf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ events {
1212
http {
1313
include /etc/nginx/conf.d/*.conf;
1414
include /etc/nginx/mime.types;
15-
js_import /usr/lib/nginx/modules/njs/httpmatches.js;
16-
js_import /usr/lib/nginx/modules/njs/epp.js;
15+
js_import modules/njs/httpmatches.js;
16+
js_import modules/njs/epp.js;
1717

1818
default_type application/octet-stream;
1919

internal/controller/nginx/conf/nginx.conf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ events {
1212
http {
1313
include /etc/nginx/conf.d/*.conf;
1414
include /etc/nginx/mime.types;
15-
js_import /usr/lib/nginx/modules/njs/httpmatches.js;
16-
js_import /usr/lib/nginx/modules/njs/epp.js;
15+
js_import modules/njs/httpmatches.js;
16+
js_import modules/njs/epp.js;
1717

1818
default_type application/octet-stream;
1919

internal/controller/nginx/config/maps.go

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -186,37 +186,57 @@ func createAddHeadersMap(name string) shared.Map {
186186
// buildInferenceMaps creates maps for InferencePool Backends.
187187
func buildInferenceMaps(groups []dataplane.BackendGroup) []shared.Map {
188188
inferenceMaps := make([]shared.Map, 0, len(groups))
189+
189190
for _, group := range groups {
190191
for _, backend := range group.Backends {
191-
if backend.EndpointPickerConfig != nil {
192-
var defaultResult string
193-
switch backend.EndpointPickerConfig.FailureMode {
194-
// in FailClose mode, if the EPP is unavailable or returns an error,
195-
// we return an invalid backend to ensure the request fails
196-
case inference.EndpointPickerFailClose:
197-
defaultResult = invalidBackendRef
198-
// in FailOpen mode, if the EPP is unavailable or returns an error,
199-
// we fall back to the upstream
200-
case inference.EndpointPickerFailOpen:
201-
defaultResult = backend.UpstreamName
202-
}
203-
params := []shared.MapParameter{
204-
{
205-
Value: "~.+",
206-
Result: "$inference_workload_endpoint",
207-
},
208-
{
209-
Value: "default",
210-
Result: defaultResult,
211-
},
212-
}
213-
backendVarName := strings.ReplaceAll(backend.UpstreamName, "-", "_")
214-
inferenceMaps = append(inferenceMaps, shared.Map{
215-
Source: "$inference_workload_endpoint",
216-
Variable: fmt.Sprintf("$inference_backend_%s", backendVarName),
217-
Parameters: params,
218-
})
192+
if backend.EndpointPickerConfig == nil || backend.EndpointPickerConfig.EndpointPickerRef == nil {
193+
continue
194+
}
195+
196+
// Decide what the map must return when the picker didn’t set a value.
197+
var defaultResult string
198+
switch backend.EndpointPickerConfig.EndpointPickerRef.FailureMode {
199+
// in FailClose mode, if the EPP is unavailable or returns an error,
200+
// we return an invalid backend to ensure the request fails
201+
case inference.EndpointPickerFailClose:
202+
defaultResult = invalidBackendRef
203+
204+
// in FailOpen mode, if the EPP is unavailable or returns an error,
205+
// we fall back to the upstream
206+
case inference.EndpointPickerFailOpen:
207+
defaultResult = backend.UpstreamName
219208
}
209+
210+
// Build the ordered parameter list.
211+
params := make([]shared.MapParameter, 0, 3)
212+
213+
// no endpoint picked by EPP go to inference pool directly
214+
params = append(params, shared.MapParameter{
215+
Value: `""`,
216+
Result: backend.UpstreamName,
217+
})
218+
219+
// endpoint picked by the EPP is stored in $inference_workload_endpoint.
220+
params = append(params, shared.MapParameter{
221+
Value: `~.+`,
222+
Result: `$inference_workload_endpoint`,
223+
})
224+
225+
// this is set based on EPP failure mode,
226+
// if EPP is failOpen, we set the default to the inference pool upstream,
227+
// if EPP is failClose, we set the default to invalidBackendRef.
228+
params = append(params, shared.MapParameter{
229+
Value: "default",
230+
Result: defaultResult,
231+
})
232+
233+
backendVarName := strings.ReplaceAll(backend.UpstreamName, "-", "_")
234+
235+
inferenceMaps = append(inferenceMaps, shared.Map{
236+
Source: `$inference_workload_endpoint`,
237+
Variable: fmt.Sprintf("$inference_backend_%s", backendVarName),
238+
Parameters: params,
239+
})
220240
}
221241
}
222242
return inferenceMaps

internal/controller/nginx/config/maps_test.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ func TestExecuteMaps(t *testing.T) {
7373
Backends: []dataplane.Backend{
7474
{
7575
UpstreamName: "upstream1",
76-
EndpointPickerConfig: &inference.EndpointPickerRef{
77-
FailureMode: inference.EndpointPickerFailClose,
76+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
77+
NsName: "default",
78+
EndpointPickerRef: &inference.EndpointPickerRef{
79+
FailureMode: inference.EndpointPickerFailClose,
80+
},
7881
},
7982
},
8083
},
@@ -400,14 +403,20 @@ func TestBuildInferenceMaps(t *testing.T) {
400403
Backends: []dataplane.Backend{
401404
{
402405
UpstreamName: "upstream1",
403-
EndpointPickerConfig: &inference.EndpointPickerRef{
404-
FailureMode: inference.EndpointPickerFailClose,
406+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
407+
NsName: "default",
408+
EndpointPickerRef: &inference.EndpointPickerRef{
409+
FailureMode: inference.EndpointPickerFailClose,
410+
},
405411
},
406412
},
407413
{
408414
UpstreamName: "upstream2",
409-
EndpointPickerConfig: &inference.EndpointPickerRef{
410-
FailureMode: inference.EndpointPickerFailOpen,
415+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
416+
NsName: "default",
417+
EndpointPickerRef: &inference.EndpointPickerRef{
418+
FailureMode: inference.EndpointPickerFailOpen,
419+
},
411420
},
412421
},
413422
{
@@ -421,6 +430,22 @@ func TestBuildInferenceMaps(t *testing.T) {
421430
g.Expect(maps).To(HaveLen(2))
422431
g.Expect(maps[0].Source).To(Equal("$inference_workload_endpoint"))
423432
g.Expect(maps[0].Variable).To(Equal("$inference_backend_upstream1"))
424-
g.Expect(maps[0].Parameters[1].Result).To(Equal("invalid-backend-ref"))
425-
g.Expect(maps[1].Parameters[1].Result).To(Equal("upstream2"))
433+
g.Expect(maps[0].Parameters).To(HaveLen(3))
434+
g.Expect(maps[0].Parameters[0].Value).To(Equal("\"\""))
435+
g.Expect(maps[0].Parameters[0].Result).To(Equal("upstream1"))
436+
g.Expect(maps[0].Parameters[1].Value).To(Equal("~.+"))
437+
g.Expect(maps[0].Parameters[1].Result).To(Equal("$inference_workload_endpoint"))
438+
g.Expect(maps[0].Parameters[2].Value).To(Equal("default"))
439+
g.Expect(maps[0].Parameters[2].Result).To(Equal("invalid-backend-ref"))
440+
441+
// Check the second map
442+
g.Expect(maps[1].Source).To(Equal("$inference_workload_endpoint"))
443+
g.Expect(maps[1].Variable).To(Equal("$inference_backend_upstream2"))
444+
g.Expect(maps[1].Parameters).To(HaveLen(3))
445+
g.Expect(maps[1].Parameters[0].Value).To(Equal("\"\""))
446+
g.Expect(maps[1].Parameters[0].Result).To(Equal("upstream2"))
447+
g.Expect(maps[1].Parameters[1].Value).To(Equal("~.+"))
448+
g.Expect(maps[1].Parameters[1].Result).To(Equal("$inference_workload_endpoint"))
449+
g.Expect(maps[1].Parameters[2].Value).To(Equal("default"))
450+
g.Expect(maps[1].Parameters[2].Result).To(Equal("upstream2"))
426451
}

internal/controller/nginx/config/servers.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -452,13 +452,18 @@ func createInternalLocationsForRule(
452452
intLocation, match = initializeInternalMatchLocationWithInference(pathRuleIdx, matchRuleIdx, r.Match)
453453
intInfLocation := initializeInternalInferenceRedirectLocation(pathRuleIdx, matchRuleIdx)
454454
for _, b := range r.BackendGroup.Backends {
455-
if b.EndpointPickerConfig != nil {
455+
if b.EndpointPickerConfig != nil && b.EndpointPickerConfig.EndpointPickerRef != nil {
456+
eppRef := b.EndpointPickerConfig.EndpointPickerRef
456457
var portNum int
457-
if b.EndpointPickerConfig.Port != nil {
458-
portNum = int(b.EndpointPickerConfig.Port.Number)
458+
if eppRef.Port != nil {
459+
portNum = int(eppRef.Port.Number)
459460
}
460461
intInfLocation.EPPInternalPath = intLocation.Path
461-
intInfLocation.EPPHost = string(b.EndpointPickerConfig.Name)
462+
if b.EndpointPickerConfig.NsName != "" {
463+
intInfLocation.EPPHost = string(eppRef.Name) + "." + b.EndpointPickerConfig.NsName
464+
} else {
465+
intInfLocation.EPPHost = string(eppRef.Name)
466+
}
462467
intInfLocation.EPPPort = portNum
463468
}
464469
}
@@ -506,14 +511,19 @@ func createInferenceLocationsForRule(
506511
mirrorPercentage,
507512
)
508513
for _, b := range r.BackendGroup.Backends {
509-
if b.EndpointPickerConfig != nil {
514+
if b.EndpointPickerConfig != nil && b.EndpointPickerConfig.EndpointPickerRef != nil {
510515
for i := range extLocations {
516+
eppRef := b.EndpointPickerConfig.EndpointPickerRef
511517
var portNum int
512-
if b.EndpointPickerConfig.Port != nil {
513-
portNum = int(b.EndpointPickerConfig.Port.Number)
518+
if eppRef.Port != nil {
519+
portNum = int(eppRef.Port.Number)
514520
}
515521
extLocations[i].EPPInternalPath = intLocation.Path
516-
extLocations[i].EPPHost = string(b.EndpointPickerConfig.Name)
522+
if b.EndpointPickerConfig.NsName != "" {
523+
extLocations[i].EPPHost = string(eppRef.Name) + "." + b.EndpointPickerConfig.NsName
524+
} else {
525+
extLocations[i].EPPHost = string(eppRef.Name)
526+
}
517527
extLocations[i].EPPPort = portNum
518528
}
519529
}

0 commit comments

Comments
 (0)