Skip to content

Commit 64a37d1

Browse files
feat(conformance): Add initial InferencePool tests and shared Gateway setup (#772)
* Add inferencepool_lifecycle test. * Resolve setup issues and enable InferencePool test * correct Lint error Multiplication of durations * Fix missing containerPort, is missing * change gateway name from "gateway-conformance-app" to "conformance-gateway" * clarify why K8s types are needed. * Update conformance/conformance.go Co-authored-by: Lior Lieberman <[email protected]> * Update conformance/conformance.go Co-authored-by: Lior Lieberman <[email protected]> * remove for loop when adding SupportedFeatures * remove exessive logging * Update conformance/conformance.go Co-authored-by: Lior Lieberman <[email protected]> * move excess debug logs behind debug flag. * remove CONFORMANCE.GO prefix from logs. * change the pull logic and use default value from GatewayMustHaveAddress * fix mt.Sprintf can be replaced with string concatenation * add a function for logDebug * factor out ensureGatewayAvailableAndReady * removed todo comment in helper.go * remove CONFORMANCE.GO from log * error messages, should not be capitalized or end with punctuation --------- Co-authored-by: Lior Lieberman <[email protected]>
1 parent 2dce3ea commit 64a37d1

File tree

5 files changed

+326
-60
lines changed

5 files changed

+326
-60
lines changed

conformance/conformance.go

Lines changed: 126 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,38 @@ limitations under the License.
1919
package conformance
2020

2121
import (
22+
"context"
23+
"errors"
2224
"fmt"
2325
"io/fs"
2426
"os"
2527
"testing"
28+
"time"
2629

2730
"github.com/stretchr/testify/require"
2831
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
32+
apierrors "k8s.io/apimachinery/pkg/api/errors"
33+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/types"
35+
"k8s.io/apimachinery/pkg/util/wait"
2936
clientset "k8s.io/client-go/kubernetes"
37+
clientsetscheme "k8s.io/client-go/kubernetes/scheme"
3038

3139
// Import runtime package for scheme creation
3240
"k8s.io/apimachinery/pkg/runtime"
3341
"k8s.io/apimachinery/pkg/util/sets"
3442
"sigs.k8s.io/controller-runtime/pkg/client"
35-
"sigs.k8s.io/controller-runtime/pkg/client/config"
43+
k8sconfig "sigs.k8s.io/controller-runtime/pkg/client/config"
3644
"sigs.k8s.io/yaml"
3745

3846
// Import necessary types and utilities from the core Gateway API conformance suite.
39-
// Assumes sigs.k8s.io/gateway-api is a dependency in the go.mod.
4047
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" // Import core Gateway API types
4148
confapis "sigs.k8s.io/gateway-api/conformance/apis/v1" // Report struct definition
4249
confconfig "sigs.k8s.io/gateway-api/conformance/utils/config"
4350
confflags "sigs.k8s.io/gateway-api/conformance/utils/flags"
51+
apikubernetes "sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
4452
confsuite "sigs.k8s.io/gateway-api/conformance/utils/suite"
45-
"sigs.k8s.io/gateway-api/pkg/features" // Using core features definitions if applicable
53+
"sigs.k8s.io/gateway-api/pkg/features"
4654

4755
// Import the test definitions package to access the ConformanceTests slice
4856
"sigs.k8s.io/gateway-api-inference-extension/conformance/tests"
@@ -58,48 +66,59 @@ import (
5866
inferencev1alpha2 "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
5967
)
6068

69+
// Constants for the shared Gateway
70+
const (
71+
SharedGatewayName = "conformance-gateway" // Name of the Gateway in manifests.yaml
72+
SharedGatewayNamespace = "gateway-conformance-infra" // Namespace of the Gateway
73+
)
74+
6175
// GatewayLayerProfileName defines the name for the conformance profile that tests
6276
// the Gateway API layer aspects of the Inference Extension (e.g., InferencePool, InferenceModel CRDs).
6377
// Future profiles will cover EPP and ModelServer layers.
6478
const GatewayLayerProfileName confsuite.ConformanceProfileName = "Gateway"
6579

66-
var InferenceCoreFeatures = sets.New[features.FeatureName]() // Placeholder - Populate with actual features specific to this profile or manage features per profile
80+
// InferenceCoreFeatures defines the core features that implementations
81+
// of the "Gateway" profile for the Inference Extension MUST support.
82+
var InferenceCoreFeatures = sets.New(
83+
features.SupportGateway, // This is needed to ensure manifest gets applied during setup.
84+
)
6785

68-
// GatewayLayerProfile defines the conformance profile for the Gateway API layer
69-
// of the Inference Extension.
70-
// In future iterations, we will add constants and ConformanceProfile structs for
71-
// EPPProfileName ("EPP") and ModelServerProfileName ("ModelServer")
72-
// to cover their respective conformance layers.
7386
var GatewayLayerProfile = confsuite.ConformanceProfile{
7487
Name: GatewayLayerProfileName,
7588
CoreFeatures: InferenceCoreFeatures,
7689
}
7790

91+
// logDebugf conditionally logs a debug message if debug mode is enabled.
92+
func logDebugf(t *testing.T, debug bool, format string, args ...any) {
93+
if debug {
94+
t.Helper()
95+
t.Logf(format, args...)
96+
}
97+
}
98+
7899
// DefaultOptions parses command line flags and sets up the suite options.
79100
// Adapted from the core Gateway API conformance suite.
80101
func DefaultOptions(t *testing.T) confsuite.ConformanceOptions {
81102
t.Helper()
82103

83-
cfg, err := config.GetConfig()
104+
cfg, err := k8sconfig.GetConfig()
84105
require.NoError(t, err, "error loading Kubernetes config")
85106

86-
// Initialize client options. The scheme must include Gateway API types
87-
// and the Inference Extension types.
88-
clientOptions := client.Options{}
89-
scheme := clientOptions.Scheme
90-
if scheme == nil {
91-
// If default options don't provide a scheme, create one using runtime.NewScheme().
92-
scheme = runtime.NewScheme()
93-
clientOptions.Scheme = scheme
94-
}
107+
scheme := runtime.NewScheme()
108+
109+
t.Log("Registering API types with scheme...")
110+
// Register core K8s types (like v1.Secret for certs) to scheme, needed by client to create/manage these resources.
111+
require.NoError(t, clientsetscheme.AddToScheme(scheme), "failed to add core Kubernetes types to scheme")
112+
// Add Gateway API types
113+
require.NoError(t, gatewayv1.Install(scheme), "failed to install gatewayv1 types into scheme")
114+
// Add APIExtensions types (for CRDs)
115+
require.NoError(t, apiextensionsv1.AddToScheme(scheme), "failed to add apiextensionsv1 types to scheme")
95116

96-
// Register necessary API Types
97-
require.NoError(t, gatewayv1.Install(scheme)) // Add core Gateway API types
98-
// Add the Inference Extension API types to the scheme using the correct import alias
99-
require.NoError(t, inferencev1alpha2.Install(scheme))
100-
require.NoError(t, apiextensionsv1.AddToScheme(scheme)) // Needed for CRD checks
117+
// Register Inference Extension API types
118+
t.Logf("Attempting to install inferencev1alpha2 types into scheme from package: %s", inferencev1alpha2.GroupName)
119+
require.NoError(t, inferencev1alpha2.Install(scheme), "failed to install inferencev1alpha2 types into scheme")
101120

102-
// Create the Kubernetes clients
121+
clientOptions := client.Options{Scheme: scheme}
103122
c, err := client.New(cfg, clientOptions)
104123
require.NoError(t, err, "error initializing Kubernetes client")
105124
cs, err := clientset.NewForConfig(cfg)
@@ -124,23 +143,26 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions {
124143
inferenceExtensionVersion := "v0.3.0"
125144
_ = inferenceExtensionVersion // Avoid unused variable error until implemented
126145

127-
// Create ConformanceOptions
146+
baseManifestsValue := "resources/manifests/manifests.yaml"
147+
128148
opts := confsuite.ConformanceOptions{
129149
Client: c,
150+
ClientOptions: clientOptions,
130151
Clientset: cs,
131152
RestConfig: cfg,
132153
GatewayClassName: *confflags.GatewayClassName,
154+
BaseManifests: baseManifestsValue,
133155
Debug: *confflags.ShowDebug,
134156
CleanupBaseResources: *confflags.CleanupBaseResources,
135-
SupportedFeatures: sets.New[features.FeatureName](), // Initialize empty, will be populated below
157+
SupportedFeatures: sets.New[features.FeatureName](),
136158
TimeoutConfig: confconfig.DefaultTimeoutConfig(),
137159
SkipTests: skipTests,
138160
ExemptFeatures: exemptFeatures,
139161
RunTest: *confflags.RunTest,
140162
Mode: *confflags.Mode,
141163
Implementation: implementation,
142164
ConformanceProfiles: conformanceProfiles,
143-
ManifestFS: []fs.FS{&Manifests}, // Assumes embed.go defines `Manifests`
165+
ManifestFS: []fs.FS{&Manifests},
144166
ReportOutputPath: *confflags.ReportOutput,
145167
SkipProvisionalTests: *confflags.SkipProvisionalTests,
146168
// TODO: Add the inference extension specific fields to ConformanceOptions struct if needed,
@@ -152,16 +174,20 @@ func DefaultOptions(t *testing.T) confsuite.ConformanceOptions {
152174
// Populate SupportedFeatures based on the GatewayLayerProfile.
153175
// Since all features are mandatory for this profile, add all defined core features.
154176
if opts.ConformanceProfiles.Has(GatewayLayerProfileName) {
155-
for feature := range GatewayLayerProfile.CoreFeatures {
156-
opts.SupportedFeatures.Insert(feature)
177+
logDebugf(t, opts.Debug, "Populating SupportedFeatures with GatewayLayerProfile.CoreFeatures: %v", GatewayLayerProfile.CoreFeatures.UnsortedList())
178+
if GatewayLayerProfile.CoreFeatures.Len() > 0 {
179+
opts.SupportedFeatures = opts.SupportedFeatures.Insert(GatewayLayerProfile.CoreFeatures.UnsortedList()...)
157180
}
158181
}
159182

160183
// Remove any features explicitly exempted via flags.
161-
for feature := range opts.ExemptFeatures {
162-
opts.SupportedFeatures.Delete(feature)
184+
if opts.ExemptFeatures.Len() > 0 {
185+
logDebugf(t, opts.Debug, "Removing ExemptFeatures from SupportedFeatures: %v", opts.ExemptFeatures.UnsortedList())
186+
opts.SupportedFeatures = opts.SupportedFeatures.Delete(opts.ExemptFeatures.UnsortedList()...)
163187
}
164188

189+
logDebugf(t, opts.Debug, "Final opts.SupportedFeatures: %v", opts.SupportedFeatures.UnsortedList())
190+
165191
return opts
166192
}
167193

@@ -172,7 +198,9 @@ func RunConformance(t *testing.T) {
172198

173199
// RunConformanceWithOptions runs the Inference Extension conformance tests with specific options.
174200
func RunConformanceWithOptions(t *testing.T, opts confsuite.ConformanceOptions) {
201+
t.Helper()
175202
t.Logf("Running Inference Extension conformance tests with GatewayClass %s", opts.GatewayClassName)
203+
logDebugf(t, opts.Debug, "RunConformanceWithOptions: BaseManifests path being used by opts: %q", opts.BaseManifests)
176204

177205
// Register the GatewayLayerProfile with the suite runner.
178206
// In the future, other profiles (EPP, ModelServer) will also be registered here,
@@ -183,13 +211,13 @@ func RunConformanceWithOptions(t *testing.T, opts confsuite.ConformanceOptions)
183211
cSuite, err := confsuite.NewConformanceTestSuite(opts)
184212
require.NoError(t, err, "error initializing conformance suite")
185213

186-
t.Log("Setting up Inference Extension conformance tests")
187-
// Setup requires the list of tests, which is populated by the init() functions
188-
// triggered by the blank imports at the top of this file.
189214
cSuite.Setup(t, tests.ConformanceTests)
190215

191-
t.Log("Running Inference Extension conformance tests")
192-
// Run the tests.
216+
sharedGwNN := types.NamespacedName{Name: SharedGatewayName, Namespace: SharedGatewayNamespace}
217+
218+
// Validate Gateway setup.
219+
ensureGatewayAvailableAndReady(t, cSuite.Client, opts, sharedGwNN)
220+
t.Log("Running Inference Extension conformance tests against all registered tests")
193221
err = cSuite.Run(t, tests.ConformanceTests)
194222
require.NoError(t, err, "error running conformance tests")
195223

@@ -209,6 +237,67 @@ func RunConformanceWithOptions(t *testing.T, opts confsuite.ConformanceOptions)
209237
}
210238
}
211239

240+
// ensureGatewayAvailableAndReady polls for the specified Gateway to exist and become ready
241+
// with an address and programmed condition.
242+
func ensureGatewayAvailableAndReady(t *testing.T, k8sClient client.Client, opts confsuite.ConformanceOptions, gatewayNN types.NamespacedName) {
243+
t.Helper()
244+
245+
t.Logf("Attempting to fetch Gateway %s/%s.", gatewayNN.Namespace, gatewayNN.Name)
246+
gw := &gatewayv1.Gateway{} // This gw instance will be populated by the poll function
247+
248+
// Define polling interval
249+
// TODO: Make this configurable using a local TimeoutConfig (from ConformanceOptions perhaps)
250+
pollingInterval := 5 * time.Second
251+
// Use the GatewayMustHaveAddress timeout from the suite's TimeoutConfig for the Gateway object to appear
252+
waitForGatewayCreationTimeout := opts.TimeoutConfig.GatewayMustHaveAddress
253+
254+
logDebugf(t, opts.Debug, "Waiting up to %v for Gateway object %s/%s to appear after manifest application...", waitForGatewayCreationTimeout, gatewayNN.Namespace, gatewayNN.Name)
255+
256+
ctx := context.TODO()
257+
pollErr := wait.PollUntilContextTimeout(ctx, pollingInterval, waitForGatewayCreationTimeout, true, func(pollCtx context.Context) (bool, error) {
258+
fetchErr := k8sClient.Get(pollCtx, gatewayNN, gw)
259+
if fetchErr == nil {
260+
t.Logf("Successfully fetched Gateway %s/%s. Spec.GatewayClassName: %s",
261+
gw.Namespace, gw.Name, gw.Spec.GatewayClassName)
262+
return true, nil
263+
}
264+
if apierrors.IsNotFound(fetchErr) {
265+
logDebugf(t, opts.Debug, "Gateway %s/%s not found, still waiting...", gatewayNN.Namespace, gatewayNN.Name)
266+
return false, nil // Not found, continue polling
267+
}
268+
// For any other error, stop polling and return this error
269+
t.Logf("Error fetching Gateway %s/%s: %v. Halting polling for this attempt.", gatewayNN.Namespace, gatewayNN.Name, fetchErr)
270+
return false, fetchErr
271+
})
272+
273+
// Check if polling timed out or an error occurred during polling
274+
if pollErr != nil {
275+
var failureMessage string
276+
if errors.Is(pollErr, context.DeadlineExceeded) {
277+
failureMessage = fmt.Sprintf("Timed out after %v waiting for Gateway object %s/%s to appear in the API server.",
278+
waitForGatewayCreationTimeout, gatewayNN.Namespace, gatewayNN.Name)
279+
} else {
280+
failureMessage = fmt.Sprintf("Error while waiting for Gateway object %s/%s to appear: %v.",
281+
gatewayNN.Namespace, gatewayNN.Name, pollErr)
282+
}
283+
finalMessage := failureMessage + " The Gateway object should have been created by the base manifest application."
284+
require.FailNow(t, finalMessage) // Use FailNow to stop if the Gateway isn't found.
285+
}
286+
287+
logDebugf(t, opts.Debug, "Waiting for shared Gateway %s/%s to be ready", gatewayNN.Namespace, gatewayNN.Name)
288+
apikubernetes.GatewayMustHaveCondition(t, k8sClient, opts.TimeoutConfig, gatewayNN, metav1.Condition{
289+
Type: string(gatewayv1.GatewayConditionAccepted),
290+
Status: metav1.ConditionTrue,
291+
})
292+
apikubernetes.GatewayMustHaveCondition(t, k8sClient, opts.TimeoutConfig, gatewayNN, metav1.Condition{
293+
Type: string(gatewayv1.GatewayConditionProgrammed),
294+
Status: metav1.ConditionTrue,
295+
})
296+
_, err := apikubernetes.WaitForGatewayAddress(t, k8sClient, opts.TimeoutConfig, apikubernetes.NewGatewayRef(gatewayNN))
297+
require.NoErrorf(t, err, "shared gateway %s/%s did not get an address", gatewayNN.Namespace, gatewayNN.Name)
298+
t.Logf("Shared Gateway %s/%s is ready.", gatewayNN.Namespace, gatewayNN.Name)
299+
}
300+
212301
// writeReport writes the generated conformance report to the specified output file or logs it.
213302
// Adapted from the core Gateway API suite.
214303
func writeReport(logf func(string, ...any), report confapis.ConformanceReport, output string) error {

conformance/embed.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ import "embed"
2121
// Manifests embeds the contents of the conformance/resources directory making
2222
// the YAML files within them available to the test suite at runtime.
2323
//
24-
//go:embed resources/* tests/*
24+
//go:embed resources tests/*
2525
var Manifests embed.FS

conformance/resources/manifests/manifests.yaml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,23 @@ metadata:
2222
labels:
2323
gateway-conformance: backend
2424

25+
---
26+
# Namespace for simple web server backends. This is expected by
27+
# the upstream conformance suite's Setup method.
28+
apiVersion: v1
29+
kind: Namespace
30+
metadata:
31+
name: gateway-conformance-web-backend
32+
labels:
33+
gateway-conformance: web-backend
34+
2535
---
2636
# A basic Gateway resource that allows HTTPRoutes from the same namespace.
2737
# Tests can use this as a parent reference for routes that target InferencePools.
28-
# Using a simple echo server instead of an actual model server to simplify the test
29-
# execution, this design may need to be revised based on the test case needs.
30-
apiVersion: gateway.networking.k8s.io/v1 # Using v1 as per latest Gateway API standard
38+
apiVersion: gateway.networking.k8s.io/v1
3139
kind: Gateway
3240
metadata:
33-
name: same-namespace
41+
name: conformance-gateway
3442
namespace: gateway-conformance-infra
3543
spec:
3644
# The conformance suite runner will replace this placeholder
@@ -42,7 +50,7 @@ spec:
4250
protocol: HTTP
4351
allowedRoutes:
4452
namespaces:
45-
from: Same # Restrict to same namespace initially for simplicity
53+
from: All
4654
kinds:
4755
# Allows HTTPRoutes to attach, which can then reference InferencePools.
4856
- group: gateway.networking.k8s.io

0 commit comments

Comments
 (0)