Skip to content

For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.

Manage resources with the Go client

Page as Markdown

Use the kgateway-client Go library to programmatically create and manage Solo Enterprise for kgateway resources from Go code.

About the kgateway-client

The kgateway-client library provides typed clients that mirror the Solo Enterprise for kgateway CRDs. You use this client alongside the Gateway API, the upstream kgateway, and the standard Kubernetes clients to manage the lifecycle of Solo Enterprise for kgateway and Kubernetes Gateway API resources with Go.

The following tables summarize the resources that you can manage with each client.

Kubernetes (k8s.io/client-go/kubernetes)

ResourceAPI group / versionClient method
Secretv1k8sClient.CoreV1().Secrets()
ConfigMapv1k8sClient.CoreV1().ConfigMaps()
Servicev1k8sClient.CoreV1().Services()
Deploymentapps/v1k8sClient.AppsV1().Deployments()
Namespacev1k8sClient.CoreV1().Namespaces()

Kubernetes Gateway API (sigs.k8s.io/gateway-api/pkg/client/clientset/versioned)

ResourceAPI group / versionClient method
Gatewaygateway.networking.k8s.io/v1gatewayClient.GatewayV1().Gateways()
HTTPRoutegateway.networking.k8s.io/v1gatewayClient.GatewayV1().HTTPRoutes()

kgateway OSS (github.com/kgateway-dev/kgateway/v2/pkg/client/clientset/versioned)

ResourceAPI group / versionClient method
TrafficPolicykgateway.solo.io/v1alpha1upstreamClient.GatewayKgateway().TrafficPolicies()

Solo Enterprise for kgateway (github.com/solo-io/kgateway-client/v2/clientset/versioned)

ResourceAPI group / versionClient method
EnterpriseKgatewayTrafficPolicyenterprisekgateway.solo.io/v1alpha1client.EnterprisekgatewayEnterprisekgateway().EnterpriseKgatewayTrafficPolicies()
EnterpriseKgatewayParametersenterprisekgateway.solo.io/v1alpha1client.EnterprisekgatewayEnterprisekgateway().EnterpriseKgatewayParameters()
AuthConfigextauth.solo.io/v1dynamic.NewForConfig() — the typed client.ExtauthV1().AuthConfigs() client exists but cannot be used due to a protobuf marshaling conflict
RateLimitConfigratelimit.solo.io/v1alpha1client.RatelimitV1alpha1().RateLimitConfigs()
WAFPolicywaf.solo.io/v1alpha1client.EnterprisekgatewayWaf().WAFPolicies()
EnterpriseListenerSetenterprisesolo.solo.io/v1alpha1client.EnterprisekgatewayEnterprisesolo().EnterpriseListenerSets()

Considerations when managing resource lifecycles

When you use Go to update a resource, make sure to always fetch the latest version of the resource before performing the update. Every Kubernetes object carries a resourceVersion field that must match the current version that is stored in etcd. If another controller or user modified the resource since your last Get, the versions do not match and the API server returns a 409 Conflict.

Concurrent write handling:

To handle concurrent writes, you can use a function, such as retry.RetryOnConflict that fetches the latest version of the object and retries the update when a 409 response is received.

retry.RetryOnConflict(retry.DefaultRetry, func() error {
    latest, err := policies.Get(ctx, "my-policy", metav1.GetOptions{})
    if err != nil {
        return err
    }
    // modify latest ...
    _, err = policies.Update(ctx, latest, metav1.UpdateOptions{})
    return err
})

k8serrors.IsAlreadyExists errors:

If your program can run more than once, such as in a retry loop or a CI pipeline, check for existing resources with the IsAlreadyExists function.

_, err = client.ExtauthV1().AuthConfigs("kgateway-system").Create(ctx, authConfig, metav1.CreateOptions{})
if k8serrors.IsAlreadyExists(err) {
    return nil
}
return err

About this guide

This guide walks you through how to set up the kgateway-client Go client and the corresponding Gateway API, the upstream kgateway, and the standard Kubernetes clients. Then, you explore how to use these clients to create different types of resources, including:

  • Gateway
  • HTTPRoute
  • EnterpriseKgatewayTrafficPolicy with different policies, including API key auth and local rate limiting
  • AuthConfig

You also explore how to use the test client that is built-in by default to verify your Go code without running it in a cluster.

Before you begin

  1. Follow the Get started guide to install Solo Enterprise for kgateway.

  2. Follow the Sample app guide to create a gateway proxy with an HTTP listener and deploy the httpbin sample app.

  3. Get the external address of the gateway and save it in an environment variable.

    export INGRESS_GW_ADDRESS=$(kubectl get svc -n kgateway-system http -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
    echo $INGRESS_GW_ADDRESS  
    kubectl port-forward deployment/http -n kgateway-system 8080:8080

  1. Install Go 1.25.7 or later.

Step 1: Install the Go client library

Add the Solo Enterprise for kgateway Go client and its upstream dependencies to your Go module.

  1. Create a directory and use this directory to initialize a Go module. This command creates a go.mod file in your directory.

    mkdir kgateway-go && cd kgateway-go
    go mod init kgateway-go

    Example output:

    go: creating new go.mod: module kgateway-go
    
  2. Install the kgateway-client Go client.

    go get github.com/solo-io/kgateway-client/v2@latest
  3. Install the upstream kgateway, Kubernetes Gateway API, and Kubernetes clients.

    go get github.com/kgateway-dev/kgateway/v2@latest
    go get sigs.k8s.io/gateway-api@latest
    go get k8s.io/client-go@latest
  4. Resolve and record all transitive dependencies in the go.sum file. Without this step, Go cannot find the checksum entries it needs to build your program.

    go mod tidy
  5. Verify that your go.mod file includes the dependency.

    grep -E "kgateway|gateway-api|client-go" go.mod

    Example output:

    module kgateway-go
     github.com/solo-io/kgateway-client/v2 v2.1.2
     k8s.io/client-go v0.36.0
     github.com/kgateway-dev/kgateway/v2 v2.2.4 // indirect
     sigs.k8s.io/gateway-api v1.5.1 // indirect
    

    The library provides the following clients that you typically use together:

    ClientsetImport pathWhat it manages
    Gateway APIsigs.k8s.io/gateway-api/pkg/client/clientset/versionedGateway, HTTPRoute, and other standard Gateway API resources
    Upstream kgatewaygithub.com/kgateway-dev/kgateway/v2/pkg/client/clientset/versionedTrafficPolicy, GatewayExtension, and other upstream OSS resources
    Solo Enterprise kgatewaygithub.com/solo-io/kgateway-client/v2/clientset/versionedEnterpriseKgatewayTrafficPolicy, AuthConfig, RateLimitConfig, and other enterprise resources
    Kubernetes clientk8s.io/client-goNot a resource clientset — provides foundational utilities used throughout this guide: rest.Config (cluster connection), clientcmd (kubeconfig loading), homedir, and retry.RetryOnConflict

Step 2: Connect to your cluster

Create a Go program that connects to your cluster and lists EnterpriseKgatewayTrafficPolicy resources.

  1. Create a main.go file.

    In Go, the main.go file is the entry point of your program. The main.go program in this example connects to your cluster and lists existing EnterpriseKgatewayTrafficPolicy resources. The example assumes the Go program runs outside the cluster, such as on your local machine or a CI runner, and connects to the cluster by using a kubeconfig file (typically ~/.kube/config). This setup is the most common one for development and automation workflows.

    cat <<'EOF' > main.go
    package main
    
    import (
    	"context"
    	"flag"
    	"fmt"
    	"path/filepath"
    
    	enterpriseclientset "github.com/solo-io/kgateway-client/v2/clientset/versioned"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/client-go/tools/clientcmd"
    	"k8s.io/client-go/util/homedir"
    )
    
    func main() {
    	var kubeconfig *string
    	if home := homedir.HomeDir(); home != "" {
    		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "path to kubeconfig")
    	} else {
    		kubeconfig = flag.String("kubeconfig", "", "path to kubeconfig")
    	}
    	flag.Parse()
    
    	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
    	if err != nil {
    		panic(err)
    	}
    
    	client, err := enterpriseclientset.NewForConfig(config)
    	if err != nil {
    		panic(err)
    	}
    
    	ctx := context.Background()
    	list, err := client.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("").
    		List(ctx, metav1.ListOptions{})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Printf("Found %d EnterpriseKgatewayTrafficPolicies\n", len(list.Items))
    }
    EOF
    In-cluster usage: If the Go program runs inside a Kubernetes pod, replace the kubeconfig setup with rest.InClusterConfig(). The pod’s ServiceAccount must have RBAC permissions for the resources it manages. For more information, see the Kubernetes RBAC docs.
  2. Run the program to verify that you can connect to your cluster and list Solo Enterprise for kgateway resources.

    go run .

    Example output:

    Found 0 EnterpriseKgatewayTrafficPolicies
    

Step 3: Create a Gateway and HTTPRoute

Create Gateway API and an HTTPRoute for the httpbin sample app.

  1. Create a gateway.go file with the createGateway function. In Go, all .go files in the same directory share the same package, so this function is automatically accessible from main.go. When you call this function, an HTTP Gateway with the name http-go is created in the kgateway-system namespace.

    cat <<'EOF' > gateway.go
    package main
    
    import (
    	"context"
    
    	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/client-go/rest"
    	gwv1 "sigs.k8s.io/gateway-api/apis/v1"
    	gatewayclientset "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
    )
    
    func createGateway(ctx context.Context, config *rest.Config, namespace string) error {
    	client, err := gatewayclientset.NewForConfig(config)
    	if err != nil {
    		return err
    	}
    
    	gw := &gwv1.Gateway{
    		TypeMeta: metav1.TypeMeta{
    			APIVersion: gwv1.GroupVersion.String(),
    			Kind:       "Gateway",
    		},
    		ObjectMeta: metav1.ObjectMeta{
    			Name:      "http-go",
    			Namespace: namespace,
    		},
    		Spec: gwv1.GatewaySpec{
    			GatewayClassName: "enterprise-kgateway",
    			Listeners: []gwv1.Listener{
    				{
    					Name:     "http",
    					Protocol: gwv1.HTTPProtocolType,
    					Port:     8080,
    					AllowedRoutes: &gwv1.AllowedRoutes{
    						Namespaces: &gwv1.RouteNamespaces{
    							From: ptr(gwv1.NamespacesFromAll),
    						},
    					},
    				},
    			},
    		},
    	}
    
    	_, err = client.GatewayV1().Gateways(namespace).Create(ctx, gw, metav1.CreateOptions{})
    	if k8serrors.IsAlreadyExists(err) {
    		return nil
    	}
    	return err
    }
    
    func ptr[T any](v T) *T { return &v }
    EOF
  2. Create an httproute.go file with the createHTTPRoute function. When called, the function creates an HTTPRoute resource with the name httpbin-go in the httpbin namespace. The route accepts requests for the api.example.com hostname on the /anything path prefix and forwards them to the httpbin service on port 8000.

    cat <<'EOF' > httproute.go
    package main
    
    import (
    	"context"
    
    	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/client-go/rest"
    	gwv1 "sigs.k8s.io/gateway-api/apis/v1"
    	gatewayclientset "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
    )
    
    func createHTTPRoute(ctx context.Context, config *rest.Config, namespace string) error {
    	client, err := gatewayclientset.NewForConfig(config)
    	if err != nil {
    		return err
    	}
    
    	pathPrefix := gwv1.PathMatchPathPrefix
    	pathValue := "/anything"
    	port := gwv1.PortNumber(8000)
    
    	route := &gwv1.HTTPRoute{
    		TypeMeta: metav1.TypeMeta{
    			APIVersion: gwv1.GroupVersion.String(),
    			Kind:       "HTTPRoute",
    		},
    		ObjectMeta: metav1.ObjectMeta{
    			Name:      "httpbin-go",
    			Namespace: namespace,
    		},
    		Spec: gwv1.HTTPRouteSpec{
    			CommonRouteSpec: gwv1.CommonRouteSpec{
    				ParentRefs: []gwv1.ParentReference{
    					{
    						Name:      "http-go",
    						Namespace: ptr(gwv1.Namespace("kgateway-system")),
    					},
    				},
    			},
    			Hostnames: []gwv1.Hostname{"api.example.com"},
    			Rules: []gwv1.HTTPRouteRule{
    				{
    					Matches: []gwv1.HTTPRouteMatch{
    						{
    							Path: &gwv1.HTTPPathMatch{
    								Type:  &pathPrefix,
    								Value: &pathValue,
    							},
    						},
    					},
    					BackendRefs: []gwv1.HTTPBackendRef{
    						{
    							BackendRef: gwv1.BackendRef{
    								BackendObjectReference: gwv1.BackendObjectReference{
    									Name: "httpbin",
    									Port: &port,
    								},
    							},
    						},
    					},
    				},
    			},
    		},
    	}
    
    	_, err = client.GatewayV1().HTTPRoutes(namespace).Create(ctx, route, metav1.CreateOptions{})
    	if k8serrors.IsAlreadyExists(err) {
    		return nil
    	}
    	return err
    }
    EOF
  3. Update the main.go file to call the createGateway and createHTTPRoute functions.

    cat <<'EOF' > main.go
    package main
    
    import (
    	"context"
    	"flag"
    	"fmt"
    	"path/filepath"
    
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/client-go/tools/clientcmd"
    	"k8s.io/client-go/util/homedir"
    
    	enterpriseclientset "github.com/solo-io/kgateway-client/v2/clientset/versioned"
    )
    
    func main() {
    	var kubeconfig *string
    	if home := homedir.HomeDir(); home != "" {
    		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "path to kubeconfig")
    	} else {
    		kubeconfig = flag.String("kubeconfig", "", "path to kubeconfig")
    	}
    	flag.Parse()
    
    	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
    	if err != nil {
    		panic(err)
    	}
    
    	client, err := enterpriseclientset.NewForConfig(config)
    	if err != nil {
    		panic(err)
    	}
    
    	ctx := context.Background()
    
    	if err := createGateway(ctx, config, "kgateway-system"); err != nil {
    		panic(err)
    	}
    	if err := createHTTPRoute(ctx, config, "httpbin"); err != nil {
    		panic(err)
    	}
    	fmt.Println("Gateway and HTTPRoute created successfully")
    
    	list, err := client.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("").
    		List(ctx, metav1.ListOptions{})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Printf("Found %d EnterpriseKgatewayTrafficPolicies\n", len(list.Items))
    }
    EOF
  4. Run the program to create the resources. Use . to compile all .go files in the current directory, not just main.go.

    go run .

    Example output:

    Gateway and HTTPRoute created successfully
    Found 0 EnterpriseKgatewayTrafficPolicies
    
  5. Verify that the resources were created in the cluster.

    kubectl get gateway -n kgateway-system && kubectl get httproute -n httpbin

    Example output:

    NAME                                       CLASS                  ADDRESS   PROGRAMMED   AGE
    gateway.gateway.networking.k8s.io/http-go  enterprise-kgateway             True         10s
    
    NAME                                             HOSTNAMES             AGE
    httproute.gateway.networking.k8s.io/httpbin-go   ["api.example.com"]  10s
    
  6. Get the external address of the http-go gateway and save it in an environment variable.

    export INGRESS_GW_ADDRESS=$(kubectl get svc -n kgateway-system http-go -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
    echo $INGRESS_GW_ADDRESS
    kubectl port-forward deployment/http-go -n kgateway-system 8080:8080
  7. Send a request to the httpbin app to verify the gateway and HTTPRoute are routing traffic correctly.

    curl -i http://$INGRESS_GW_ADDRESS:8080/anything -H "host: api.example.com"
    curl -i localhost:8080/anything -H "host: api.example.com"

    Example output:

    HTTP/1.1 200 OK
    access-control-allow-credentials: true
    access-control-allow-origin: *
    content-type: application/json
    date: Mon, 05 May 2025 10:00:00 GMT
    server: envoy

Step 4: Configure API key auth

In this example, you set up API key auth for the httpbin app. This setup requires three resources: a Kubernetes Secret that stores the API key, an AuthConfig that defines the API key authentication method, and an EnterpriseKgatewayTrafficPolicy that attaches the AuthConfig to the httpbin route.

Although kgateway-client exposes a typed client.ExtauthV1().AuthConfigs() client, the AuthConfigSpec type uses a custom protobuf-based JSON marshaler (jsonpb.Marshaler) that conflicts with the google.golang.org/protobuf version that is pulled in by k8s.io/client-go. Passing the typed struct to the Kubernetes API server causes a runtime panic. For AuthConfig, use the dynamic client (k8s.io/client-go/dynamic) instead. It encodes the resource as a plain map[string]interface{}, which bypasses the protobuf marshaler entirely.

  1. Create an extauth.go file with the following three functions.

    • createAPIKeySecret(): Creates the Kubernetes Secret for your API key by using the standard kubernetes client. The example uses a pre-defined API key that must be used to successfully authenticate.
    • createAuthConfig(): Creates the AuthConfig resource by using the dynamic client.
    • createExtAuthPolicy(): Creates the EnterpriseKgatewayTrafficPolicy by using the kgateway-client.
    cat <<'EOF' > extauth.go
    package main
    
    import (
    	"context"
    
    	upstreamkgateway "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/kgateway"
    	upstreamshared "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/shared"
    	enterprisev1alpha1 "github.com/solo-io/kgateway-client/v2/api/v1alpha1/enterprisekgateway"
    	enterpriseshared "github.com/solo-io/kgateway-client/v2/api/v1alpha1/shared"
    	enterpriseclientset "github.com/solo-io/kgateway-client/v2/clientset/versioned"
    	corev1 "k8s.io/api/core/v1"
    	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    	"k8s.io/apimachinery/pkg/runtime/schema"
    	"k8s.io/client-go/dynamic"
    	"k8s.io/client-go/kubernetes"
    	"k8s.io/client-go/rest"
    	gwv1 "sigs.k8s.io/gateway-api/apis/v1"
    )
    
    func createAPIKeySecret(ctx context.Context, config *rest.Config) error {
    	client, err := kubernetes.NewForConfig(config)
    	if err != nil {
    		return err
    	}
    
    	secret := &corev1.Secret{
    		ObjectMeta: metav1.ObjectMeta{
    			Name:      "my-apikey",
    			Namespace: "kgateway-system",
    			Labels:    map[string]string{"team": "infrastructure"},
    		},
    		Type: "extauth.solo.io/apikey",
    		StringData: map[string]string{
    			"api-key": "N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy",
    		},
    	}
    
    	_, err = client.CoreV1().Secrets("kgateway-system").Create(ctx, secret, metav1.CreateOptions{})
    	if k8serrors.IsAlreadyExists(err) {
    		return nil
    	}
    	return err
    }
    
    func createAuthConfig(ctx context.Context, config *rest.Config) error {
    	client, err := dynamic.NewForConfig(config)
    	if err != nil {
    		return err
    	}
    
    	gvr := schema.GroupVersionResource{
    		Group:    "extauth.solo.io",
    		Version:  "v1",
    		Resource: "authconfigs",
    	}
    
    	authConfig := &unstructured.Unstructured{
    		Object: map[string]interface{}{
    			"apiVersion": "extauth.solo.io/v1",
    			"kind":       "AuthConfig",
    			"metadata": map[string]interface{}{
    				"name":      "apikey-auth",
    				"namespace": "kgateway-system",
    			},
    			"spec": map[string]interface{}{
    				"configs": []interface{}{
    					map[string]interface{}{
    						"apiKeyAuth": map[string]interface{}{
    							"headerName": "api-key",
    							"labelSelector": map[string]interface{}{
    								"team": "infrastructure",
    							},
    						},
    					},
    				},
    			},
    		},
    	}
    
    	_, err = client.Resource(gvr).Namespace("kgateway-system").Create(ctx, authConfig, metav1.CreateOptions{})
    	if k8serrors.IsAlreadyExists(err) {
    		return nil
    	}
    	return err
    }
    
    func createExtAuthPolicy(ctx context.Context, config *rest.Config) error {
    	client, err := enterpriseclientset.NewForConfig(config)
    	if err != nil {
    		return err
    	}
    
    	ns := gwv1.Namespace("kgateway-system")
    
    	policy := &enterprisev1alpha1.EnterpriseKgatewayTrafficPolicy{
    		TypeMeta: metav1.TypeMeta{
    			APIVersion: enterprisev1alpha1.SchemeGroupVersion.String(),
    			Kind:       "EnterpriseKgatewayTrafficPolicy",
    		},
    		ObjectMeta: metav1.ObjectMeta{
    			Name:      "extauth-policy",
    			Namespace: "kgateway-system",
    		},
    		Spec: enterprisev1alpha1.EnterpriseKgatewayTrafficPolicySpec{
    			TrafficPolicySpec: upstreamkgateway.TrafficPolicySpec{
    				TargetRefs: []upstreamshared.LocalPolicyTargetReferenceWithSectionName{
    					{
    						LocalPolicyTargetReference: upstreamshared.LocalPolicyTargetReference{
    							Group: "gateway.networking.k8s.io",
    							Kind:  "Gateway",
    							Name:  "http-go",
    						},
    					},
    				},
    			},
    			EntExtAuth: &enterprisev1alpha1.EntExtAuth{
    				AuthConfigRef: &enterpriseshared.AuthConfigRef{
    					Name:      "apikey-auth",
    					Namespace: &ns,
    				},
    			},
    		},
    	}
    
    	_, err = client.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("kgateway-system").
    		Create(ctx, policy, metav1.CreateOptions{})
    	if k8serrors.IsAlreadyExists(err) {
    		return nil
    	}
    	return err
    }
    EOF
  2. Update the main.go file to call the three extauth functions in order.

    cat <<'EOF' > main.go
    package main
    
    import (
    	"context"
    	"flag"
    	"fmt"
    	"path/filepath"
    
    	enterpriseclientset "github.com/solo-io/kgateway-client/v2/clientset/versioned"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/client-go/tools/clientcmd"
    	"k8s.io/client-go/util/homedir"
    )
    
    func main() {
    	var kubeconfig *string
    	if home := homedir.HomeDir(); home != "" {
    		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "path to kubeconfig")
    	} else {
    		kubeconfig = flag.String("kubeconfig", "", "path to kubeconfig")
    	}
    	flag.Parse()
    
    	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
    	if err != nil {
    		panic(err)
    	}
    
    	client, err := enterpriseclientset.NewForConfig(config)
    	if err != nil {
    		panic(err)
    	}
    
    	ctx := context.Background()
    
    	if err := createGateway(ctx, config, "kgateway-system"); err != nil {
    		panic(err)
    	}
    	if err := createHTTPRoute(ctx, config, "httpbin"); err != nil {
    		panic(err)
    	}
    	fmt.Println("Gateway and HTTPRoute created successfully")
    
    	list, err := client.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("").
    		List(ctx, metav1.ListOptions{})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Printf("Found %d EnterpriseKgatewayTrafficPolicies\n", len(list.Items))
    
    	if err := createAPIKeySecret(ctx, config); err != nil {
    		panic(err)
    	}
    	fmt.Println("API key secret created")
    
    	if err := createAuthConfig(ctx, config); err != nil {
    		panic(err)
    	}
    	fmt.Println("AuthConfig created")
    
    	if err := createExtAuthPolicy(ctx, config); err != nil {
    		panic(err)
    	}
    	fmt.Println("ExtAuth policy created")
    }
    EOF
  3. Run the program and verify the output.

    go run .

    Example output:

    Gateway and HTTPRoute created successfully
    Found 0 EnterpriseKgatewayTrafficPolicies
    API key secret created
    AuthConfig created
    ExtAuth policy created
    
  4. Verify that the policy is applied to the gateway.

    kubectl get enterprisekgatewaytrafficpolicy extauth-policy -n kgateway-system -o yaml
  5. Send a request to the httpbin app without an API key. Verify that the request is denied with a 401 Unauthorized response.

    curl -i http://$INGRESS_GW_ADDRESS:8080/anything -H "host: api.example.com"
    curl -i localhost:8080/anything -H "host: api.example.com"

    Example output:

    HTTP/1.1 401 Unauthorized
    date: Mon, 05 May 2025 10:00:00 GMT
    server: envoy
    content-length: 0
  6. Send a request with the valid API key in the api-key header. Verify that the request succeeds with a 200 OK response.

    curl -i http://$INGRESS_GW_ADDRESS:8080/anything -H "host: api.example.com" \
    -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    curl -i localhost:8080/anything -H "host: api.example.com" \
    -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"

    Example output:

    HTTP/1.1 200 OK
    access-control-allow-credentials: true
    access-control-allow-origin: *
    date: Mon, 05 May 2025 10:00:01 GMT
    content-length: 0
    server: envoy

Step 5: Configure local rate limiting

In this step, you configure local rate limiting for the httpbin app. Local rate limiting is enforced by the gateway proxy itself and does not require an external rate limiting service.

The createLocalRateLimitPolicy function in this examples creates an EnterpriseKgatewayTrafficPolicy with the following rate limiting settings:

  • maxTokens: 1: A maximum of 1 token is available at any given time.
  • tokensPerFill: 1: 1 token is added per fill interval.
  • fillInterval: 1m: The bucket refills once per minute. Combined with tokensPerFill, this allows exactly 1 request per minute. Requests that arrive when the bucket is empty receive a 429 Too Many Requests response.
  1. Create a ratelimit.go file with the createLocalRateLimitPolicy function.

    cat <<'EOF' > ratelimit.go
    package main
    
    import (
    	"context"
    	"time"
    
    	upstreamkgateway "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/kgateway"
    	upstreamshared "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/shared"
    	enterprisev1alpha1 "github.com/solo-io/kgateway-client/v2/api/v1alpha1/enterprisekgateway"
    	enterpriseclientset "github.com/solo-io/kgateway-client/v2/clientset/versioned"
    	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/client-go/rest"
    )
    
    func createLocalRateLimitPolicy(ctx context.Context, config *rest.Config) error {
    	client, _ := enterpriseclientset.NewForConfig(config)
    
    	maxTokens := int32(1)
    	tokensPerFill := int32(1)
    	fillInterval := metav1.Duration{Duration: 1 * time.Minute}
    
    	policy := &enterprisev1alpha1.EnterpriseKgatewayTrafficPolicy{
    		TypeMeta: metav1.TypeMeta{
    			APIVersion: enterprisev1alpha1.SchemeGroupVersion.String(),
    			Kind:       "EnterpriseKgatewayTrafficPolicy",
    		},
    		ObjectMeta: metav1.ObjectMeta{
    			Name:      "local-ratelimit",
    			Namespace: "kgateway-system",
    		},
    		Spec: enterprisev1alpha1.EnterpriseKgatewayTrafficPolicySpec{
    			TrafficPolicySpec: upstreamkgateway.TrafficPolicySpec{
    				TargetRefs: []upstreamshared.LocalPolicyTargetReferenceWithSectionName{
    					{
    						LocalPolicyTargetReference: upstreamshared.LocalPolicyTargetReference{
    							Group: "gateway.networking.k8s.io",
    							Kind:  "Gateway",
    							Name:  "http-go",
    						},
    					},
    				},
    				RateLimit: &upstreamkgateway.RateLimit{
    					Local: &upstreamkgateway.LocalRateLimitPolicy{
    						TokenBucket: &upstreamkgateway.TokenBucket{
    							MaxTokens:     maxTokens,
    							TokensPerFill: &tokensPerFill,
    							FillInterval:  fillInterval,
    						},
    					},
    				},
    			},
    		},
    	}
    
    	_, err := client.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("kgateway-system").
    		Create(ctx, policy, metav1.CreateOptions{})
    	if k8serrors.IsAlreadyExists(err) {
    		return nil
    	}
    	return err
    }
    EOF
  2. Update main.go to call the createLocalRateLimitPolicy function.

    cat <<'EOF' > main.go
    package main
    
    import (
    	"context"
    	"flag"
    	"fmt"
    	"path/filepath"
    
    	enterpriseclientset "github.com/solo-io/kgateway-client/v2/clientset/versioned"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/client-go/tools/clientcmd"
    	"k8s.io/client-go/util/homedir"
    )
    
    func main() {
    	var kubeconfig *string
    	if home := homedir.HomeDir(); home != "" {
    		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "path to kubeconfig")
    	} else {
    		kubeconfig = flag.String("kubeconfig", "", "path to kubeconfig")
    	}
    	flag.Parse()
    
    	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
    	if err != nil {
    		panic(err)
    	}
    
    	client, err := enterpriseclientset.NewForConfig(config)
    	if err != nil {
    		panic(err)
    	}
    
    	ctx := context.Background()
    
    	if err := createGateway(ctx, config, "kgateway-system"); err != nil {
    		panic(err)
    	}
    	if err := createHTTPRoute(ctx, config, "httpbin"); err != nil {
    		panic(err)
    	}
    	fmt.Println("Gateway and HTTPRoute created successfully")
    
    	list, err := client.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("").
    		List(ctx, metav1.ListOptions{})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Printf("Found %d EnterpriseKgatewayTrafficPolicies\n", len(list.Items))
    
    	if err := createAPIKeySecret(ctx, config); err != nil {
    		panic(err)
    	}
    	fmt.Println("API key secret created")
    
    	if err := createAuthConfig(ctx, config); err != nil {
    		panic(err)
    	}
    	fmt.Println("AuthConfig created")
    
    	if err := createExtAuthPolicy(ctx, config); err != nil {
    		panic(err)
    	}
    	fmt.Println("ExtAuth policy created")
    
    	if err := createLocalRateLimitPolicy(ctx, config); err != nil {
    		panic(err)
    	}
    	fmt.Println("Rate limit policy created")
    }
    EOF
  3. Run the program.

    go run .

    Example output:

    Gateway and HTTPRoute created successfully
    Found 0 EnterpriseKgatewayTrafficPolicies
    API key secret created
    AuthConfig created
    ExtAuth policy created
    Rate limit policy created
    
  4. Verify the rate limit policy.

    kubectl get enterprisekgatewaytrafficpolicy local-ratelimit -n kgateway-system -o yaml
  5. Test rate limiting by sending 3 requests in quick succession. The first request consumes the single token and returns a 200 OK response. The remaining requests find the bucket empty and return 429 Too Many Requests.

    for i in $(seq 1 3); do
      curl -s -o /dev/null -w "%{http_code}\n" http://$INGRESS_GW_ADDRESS:8080/anything \
        -H "host: api.example.com" \
        -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    done
    for i in $(seq 1 3); do
      curl -s -o /dev/null -w "%{http_code}\n" localhost:8080/anything \
        -H "host: api.example.com" \
        -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    done

    Example output:

    200
    429
    429

Step 6: Write unit tests with the fake client

The library ships with a fake clientset that stores objects in memory. You can use this clientset in unit tests to verify your policy logic without applying the resources in a cluster. The tests assert on the actual policy fields, so a misconfigured token bucket or wrong targetRef causes the test to fail.

  1. Create a policy_test.go file that defines your test functions. This example file implements the following test functions:

    • TestLocalRateLimitPolicy()

      • Verify the token bucket is stored with MaxTokens: 1, TokensPerFill: 1, and FillInterval: 1m.
      • Verify that the targetRef points to the http-go Gateway.
    • TestExtAuthPolicy():

      • Verify that the targetRef points to the http-go Gateway.
      • Verify that EntExtAuth.AuthConfigRef.Name is apikey-auth.
      • Verify that EntExtAuth.AuthConfigRef.Namespace is kgateway-system.
    cat <<'EOF' > policy_test.go
    package main_test
    
    import (
    	"context"
    	"testing"
    	"time"
    
    	upstreamkgateway "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/kgateway"
    	upstreamshared "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/shared"
    	enterprisev1alpha1 "github.com/solo-io/kgateway-client/v2/api/v1alpha1/enterprisekgateway"
    	enterpriseshared "github.com/solo-io/kgateway-client/v2/api/v1alpha1/shared"
    	fakeclientset "github.com/solo-io/kgateway-client/v2/clientset/versioned/fake"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	gwv1 "sigs.k8s.io/gateway-api/apis/v1"
    )
    
    func TestLocalRateLimitPolicy(t *testing.T) {
    	fakeClient := fakeclientset.NewSimpleClientset()
    	ctx := context.Background()
    
    	maxTokens := int32(1)
    	tokensPerFill := int32(1)
    	fillInterval := metav1.Duration{Duration: 1 * time.Minute}
    
    	policy := &enterprisev1alpha1.EnterpriseKgatewayTrafficPolicy{
    		ObjectMeta: metav1.ObjectMeta{
    			Name:      "local-ratelimit",
    			Namespace: "kgateway-system",
    		},
    		Spec: enterprisev1alpha1.EnterpriseKgatewayTrafficPolicySpec{
    			TrafficPolicySpec: upstreamkgateway.TrafficPolicySpec{
    				TargetRefs: []upstreamshared.LocalPolicyTargetReferenceWithSectionName{
    					{
    						LocalPolicyTargetReference: upstreamshared.LocalPolicyTargetReference{
    							Group: "gateway.networking.k8s.io",
    							Kind:  "Gateway",
    							Name:  "http-go",
    						},
    					},
    				},
    				RateLimit: &upstreamkgateway.RateLimit{
    					Local: &upstreamkgateway.LocalRateLimitPolicy{
    						TokenBucket: &upstreamkgateway.TokenBucket{
    							MaxTokens:     maxTokens,
    							TokensPerFill: &tokensPerFill,
    							FillInterval:  fillInterval,
    						},
    					},
    				},
    			},
    		},
    	}
    
    	_, err := fakeClient.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("kgateway-system").
    		Create(ctx, policy, metav1.CreateOptions{})
    	if err != nil {
    		t.Fatalf("Create() error = %v", err)
    	}
    
    	got, err := fakeClient.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("kgateway-system").
    		Get(ctx, "local-ratelimit", metav1.GetOptions{})
    	if err != nil {
    		t.Fatalf("Get() error = %v", err)
    	}
    
    	tb := got.Spec.TrafficPolicySpec.RateLimit.Local.TokenBucket
    	if tb.MaxTokens != 1 {
    		t.Errorf("expected MaxTokens 1, got %d", tb.MaxTokens)
    	}
    	if *tb.TokensPerFill != 1 {
    		t.Errorf("expected TokensPerFill 1, got %d", *tb.TokensPerFill)
    	}
    	if tb.FillInterval.Duration != time.Minute {
    		t.Errorf("expected FillInterval 1m, got %s", tb.FillInterval.Duration)
    	}
    
    	refs := got.Spec.TrafficPolicySpec.TargetRefs
    	if len(refs) != 1 || string(refs[0].Name) != "http-go" || string(refs[0].Kind) != "Gateway" {
    		t.Errorf("unexpected targetRef: %+v", refs)
    	}
    }
    
    func TestExtAuthPolicy(t *testing.T) {
    	fakeClient := fakeclientset.NewSimpleClientset()
    	ctx := context.Background()
    
    	ns := gwv1.Namespace("kgateway-system")
    
    	policy := &enterprisev1alpha1.EnterpriseKgatewayTrafficPolicy{
    		ObjectMeta: metav1.ObjectMeta{
    			Name:      "extauth-policy",
    			Namespace: "kgateway-system",
    		},
    		Spec: enterprisev1alpha1.EnterpriseKgatewayTrafficPolicySpec{
    			TrafficPolicySpec: upstreamkgateway.TrafficPolicySpec{
    				TargetRefs: []upstreamshared.LocalPolicyTargetReferenceWithSectionName{
    					{
    						LocalPolicyTargetReference: upstreamshared.LocalPolicyTargetReference{
    							Group: "gateway.networking.k8s.io",
    							Kind:  "Gateway",
    							Name:  "http-go",
    						},
    					},
    				},
    			},
    			EntExtAuth: &enterprisev1alpha1.EntExtAuth{
    				AuthConfigRef: &enterpriseshared.AuthConfigRef{
    					Name:      "apikey-auth",
    					Namespace: &ns,
    				},
    			},
    		},
    	}
    
    	_, err := fakeClient.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("kgateway-system").
    		Create(ctx, policy, metav1.CreateOptions{})
    	if err != nil {
    		t.Fatalf("Create() error = %v", err)
    	}
    
    	got, err := fakeClient.EnterprisekgatewayEnterprisekgateway().
    		EnterpriseKgatewayTrafficPolicies("kgateway-system").
    		Get(ctx, "extauth-policy", metav1.GetOptions{})
    	if err != nil {
    		t.Fatalf("Get() error = %v", err)
    	}
    
    	refs := got.Spec.TrafficPolicySpec.TargetRefs
    	if len(refs) != 1 || string(refs[0].Name) != "http-go" || string(refs[0].Kind) != "Gateway" {
    		t.Errorf("unexpected targetRef: %+v", refs)
    	}
    
    	authRef := got.Spec.EntExtAuth.AuthConfigRef
    	if authRef.Name != "apikey-auth" {
    		t.Errorf("expected AuthConfigRef.Name apikey-auth, got %s", authRef.Name)
    	}
    	if authRef.Namespace == nil || string(*authRef.Namespace) != "kgateway-system" {
    		t.Errorf("expected AuthConfigRef.Namespace kgateway-system, got %v", authRef.Namespace)
    	}
    }
    EOF
  2. Run the tests.

    go test ./...

    Example output:

    ok  	kgateway-go	0.003s
    

    ok means all tests passed. kgateway-go is the module name from your go.mod file, and 0.003s is the total run time. Because the fake clientset stores objects in memory, no cluster connection is made and the tests complete in milliseconds.

    If a test fails, Go prints the test name, the file and line number of the failing assertion, and the actual value that was found. For example, if MaxTokens was accidentally set to 100 instead of 1, you see a result similar to the following error:

    --- FAIL: TestLocalRateLimitPolicy (0.00s)
        policy_test.go:65: expected MaxTokens 1, got 100
    FAIL
    FAIL	kgateway-go	0.003s
    

Cleanup

You can optionally remove the resources that you set up as part of this guide.
kubectl delete enterprisekgatewaytrafficpolicy --all -n kgateway-system
kubectl delete enterprisekgatewaytrafficpolicy --all -n httpbin
kubectl delete authconfig oauth-oidc -n kgateway-system --ignore-not-found
kubectl delete ratelimitconfig global-ratelimit -n kgateway-system --ignore-not-found
kubectl delete httproute httpbin-go -n httpbin --ignore-not-found
kubectl delete gateway http-go -n kgateway-system --ignore-not-found
Was this page helpful?