Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* @LiorLieberman @rikatz @robscott @Stevenjin8 @youngnick
/pkg/i2gw/emitters/envoygateway/ @kkk777-7
/pkg/i2gw/emitters/envoygateway/ @kkk777-7
/pkg/i2gw/emitters/agentgateway/ @howardjohn @npolshakova @markuskobler @danehans @puertomontt
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious: Why two separate lines?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My editor adds a newline at end of file. Happy to minimize the diff if you'd prefer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tell me you use vim without telling me you use vim. I like the new line

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ To contribute a new provider support - please read [CONTRIBUTING.md](CONTRIBUTIN

### Supported Emitters
* [standard](https://gateway-api.sigs.k8s.io/) (default)
* [agentgateway](https://agentgateway.dev/)
* [envoy-gateway](https://gateway.envoyproxy.io/)
* [gce](https://docs.cloud.google.com/kubernetes-engine/docs/concepts/gateway-api)
* [kgateway](https://kgateway.dev/)
Expand Down
1 change: 1 addition & 0 deletions cmd/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
_ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/openapi3"

// Call init for emitters
_ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitters/agentgateway"
_ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitters/envoygateway"
_ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitters/gce"
_ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitters/kgateway"
Expand Down
5 changes: 5 additions & 0 deletions e2e/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ func deployGatewayImplementation(
r = framework.GlobalResourceManager.Acquire(implementation.EnvoyGatewayName, func() (framework.CleanupFunc, error) {
return implementation.DeployEnvoyGateway(ctx, t, k8sClient, apiextClient, gwClient, kubeconfig, ns, skipCleanup)
})
case implementation.AgentgatewayName:
ns := fmt.Sprintf("%s-agentgateway-system", framework.E2EPrefix)
r = framework.GlobalResourceManager.Acquire(implementation.AgentgatewayName, func() (framework.CleanupFunc, error) {
return implementation.DeployAgentgateway(ctx, t, k8sClient, gwClient, kubeconfig, ns, skipCleanup)
})
default:
t.Fatalf("Unknown gateway implementation: %s", gwImpl)
}
Expand Down
116 changes: 116 additions & 0 deletions e2e/implementation/agentgateway.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
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.
*/

package implementation

import (
"context"
"fmt"
"log"
"time"

"github.com/kubernetes-sigs/ingress2gateway/e2e/framework"
"helm.sh/helm/v4/pkg/cli"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
gwclientset "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
)

const (
AgentgatewayName = "agentgateway"
agentgatewayVersion = "v1.0.0"
agentgatewayChart = "oci://cr.agentgateway.dev/charts/agentgateway"
agentgatewayCRDsChart = "oci://cr.agentgateway.dev/charts/agentgateway-crds"
agentgatewayReleaseName = "agentgateway"
agentgatewayCRDsRelease = "agentgateway-crds"
)

func DeployAgentgateway(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this helper for any tests? If so - I'm not seeing any new test cases in this PR. If not, I wonder if we should add a helper we aren't using.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Given that the agentgateway controller was split out from kgateway I initially just duplicated the kgateway e2e test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be an opportunity to implement the first emitter test - see category 4 in https://github.com/kubernetes-sigs/ingress2gateway/blob/main/e2e/README.md.

WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on adding a quick test. Just a sanity check that it works to help me sleep at night

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I thought I had correctly wired up the tests. It should be used now

ctx context.Context,
l framework.Logger,
client *kubernetes.Clientset,
gwClient *gwclientset.Clientset,
kubeconfigPath string,
namespace string,
skipCleanup bool,
) (func(), error) {
l.Logf("Deploying agentgateway %s", agentgatewayVersion)

settings := cli.New()
settings.KubeConfig = kubeconfigPath

// Install CRDs first to avoid races creating extension resources.
if err := framework.InstallChart(
ctx,
l,
settings,
"",
agentgatewayCRDsRelease,
agentgatewayCRDsChart,
agentgatewayVersion,
namespace,
true,
false,
nil,
); err != nil {
return nil, fmt.Errorf("installing agentgateway CRDs chart: %w", err)
}

if err := framework.InstallChart(
ctx,
l,
settings,
"",
agentgatewayReleaseName,
agentgatewayChart,
agentgatewayVersion,
namespace,
false,
false,
nil,
); err != nil {
return nil, fmt.Errorf("installing agentgateway chart: %w", err)
}

//nolint:contextcheck // Intentional background context in cleanup function
return func() {
if skipCleanup {
log.Printf("Skipping cleanup of agentgateway")
return
}

cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

log.Printf("Cleaning up agentgateway")

log.Printf("Deleting GatewayClass %s", AgentgatewayName)
if err := gwClient.GatewayV1().GatewayClasses().Delete(cleanupCtx, AgentgatewayName, metav1.DeleteOptions{}); err != nil {
log.Printf("Deleting GatewayClass: %v", err)
}

if err := framework.UninstallChart(cleanupCtx, settings, agentgatewayReleaseName, namespace); err != nil {
log.Printf("Uninstalling agentgateway chart: %v", err)
}
if err := framework.UninstallChart(cleanupCtx, settings, agentgatewayCRDsRelease, namespace); err != nil {
log.Printf("Uninstalling agentgateway CRDs chart: %v", err)
}

if err := framework.DeleteNamespaceAndWait(cleanupCtx, client, namespace); err != nil {
log.Printf("Deleting namespace: %v", err)
}
}, nil
}
1 change: 1 addition & 0 deletions e2e/implementation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func TestImplementations(t *testing.T) {
{name: implementation.KongName},
{name: implementation.KgatewayName},
{name: implementation.EnvoyGatewayName},
{name: implementation.AgentgatewayName},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a smoke test for agentgateway as an implementation. This does not test any emitter features. Given that this PR is titled "Implement agentgateway emitter" I just wanted to make sure this is what you intended.

Copy link
Copy Markdown
Contributor Author

@markuskobler markuskobler Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are planning to add more functionality now that agentgateway has released v1.0 and is no longer part of kgateway, which is what a previous PR was attempting to add agentgateway emitter support against.

The main reason I avoided adding anything beyond logging a warning for unsupported functionality was the number of changes the current go.mod would pull in. I hope to get the controller go.mod split from the api later this week or next.

I can make this a much bigger change if you wanted however?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the full context here and have no strong opinions. Just wanted to ensure you're aware what the test you've added does and doesn't do 🙂 I'll leave it up to you.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I think it's a good idea to have agentgateway in the implementation smoke tests.

}

for _, impl := range implementations {
Expand Down
78 changes: 78 additions & 0 deletions pkg/i2gw/emitters/agentgateway/agentgateway.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
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.
*/

package agentgateway_emitter

import (
"fmt"

"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
emitterir "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitter_intermediate"
"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitters/utils"
"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/notifications"
"k8s.io/apimachinery/pkg/util/validation/field"
)

const (
emitterName = "agentgateway"
)

func init() {
i2gw.EmitterConstructorByName[emitterName] = NewEmitter
}

type Emitter struct {
notify notifications.NotifyFunc
}

func NewEmitter(conf *i2gw.EmitterConf) i2gw.Emitter {
return &Emitter{
notify: conf.Report.Notifier(emitterName),
}
}

func (e *Emitter) Emit(ir emitterir.EmitterIR) (gr i2gw.GatewayResources, errs field.ErrorList) {
for ns, gw := range ir.Gateways {
gw.Spec.GatewayClassName = emitterName
ir.Gateways[ns] = gw
}
gr, errs = utils.ToGatewayResources(ir)
if len(errs) != 0 {
return
}

for nn, rc := range ir.HTTPRoutes {
applyBodySize(&rc, e.notify)
ir.HTTPRoutes[nn] = rc
}

utils.LogUnparsedErrors(ir, e.notify)
return gr, nil
}

func applyBodySize(
rc *emitterir.HTTPRouteContext,
notify notifications.NotifyFunc,
) {
for idx := range rc.BodySizeByRuleIdx {
notify(
notifications.WarningNotification,
fmt.Sprintf("Body size limit is not supported for HTTPRoute targets in AgentgatewayPolicy; ignoring%s", formatRuleInfo(rc, idx)),
&rc.HTTPRoute,
)
}
rc.BodySizeByRuleIdx = nil
}
154 changes: 154 additions & 0 deletions pkg/i2gw/emitters/agentgateway/agentgateway_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
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.
*/

package agentgateway_emitter

import (
"testing"

emitterir "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/emitter_intermediate"
"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/notifications"
"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

func TestEmit_Gateway(t *testing.T) {
e := &Emitter{notify: notifications.NoopNotify}
nn := types.NamespacedName{Namespace: "default", Name: "test-gateway"}

gr, errs := e.Emit(emitterir.EmitterIR{
Gateways: map[types.NamespacedName]emitterir.GatewayContext{
nn: {
Gateway: gatewayv1.Gateway{
Spec: gatewayv1.GatewaySpec{
Listeners: []gatewayv1.Listener{{
Name: "http",
Port: 80,
Protocol: gatewayv1.HTTPProtocolType,
Hostname: common.PtrTo(gatewayv1.Hostname("example.com")),
}},
},
},
},
},
})
if len(errs) != 0 {
t.Fatalf("unexpected errors: %v", errs)
}

if gw, ok := gr.Gateways[nn]; !ok {
t.Fatalf("missing gateway %s", nn)
} else if gw.Spec.GatewayClassName != emitterName {
t.Errorf("unexpected GatewayClassName %q", gw.Spec.GatewayClassName)
}
}

func TestEmit_BodySize(t *testing.T) {
nn := types.NamespacedName{Namespace: "default", Name: "test-http-route"}

testHTTPRoute := gatewayv1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "test-http-route"},
Spec: gatewayv1.HTTPRouteSpec{
CommonRouteSpec: gatewayv1.CommonRouteSpec{
ParentRefs: []gatewayv1.ParentReference{{
Name: gatewayv1.ObjectName("test-gateway"),
}},
},
Hostnames: []gatewayv1.Hostname{"example.com"},
Rules: []gatewayv1.HTTPRouteRule{
{
Matches: []gatewayv1.HTTPRouteMatch{{
Path: &gatewayv1.HTTPPathMatch{
Type: common.PtrTo(gatewayv1.PathMatchPathPrefix),
Value: common.PtrTo("/"),
},
}},
BackendRefs: []gatewayv1.HTTPBackendRef{{
BackendRef: gatewayv1.BackendRef{
BackendObjectReference: gatewayv1.BackendObjectReference{
Name: gatewayv1.ObjectName("test-service"),
Port: common.PtrTo(gatewayv1.PortNumber(80)),
},
},
}},
},
},
},
}

// Limits cannot be expressed on HTTPRoute targets in AgentgatewayPolicy because spec.frontend is Gateway-only
testCases := []struct {
name string
ir emitterir.EmitterIR
wantWarnings int
}{
{
name: "single rule body size emits warning and no policy",
ir: emitterir.EmitterIR{
HTTPRoutes: map[types.NamespacedName]emitterir.HTTPRouteContext{
nn: {
HTTPRoute: testHTTPRoute,
BodySizeByRuleIdx: map[int]*emitterir.BodySize{
0: {BufferSize: common.PtrTo(resource.MustParse("1Mi"))},
},
},
},
},
wantWarnings: 1,
},
{
name: "multiple rules each emit a warning",
ir: emitterir.EmitterIR{
HTTPRoutes: map[types.NamespacedName]emitterir.HTTPRouteContext{
nn: {
HTTPRoute: testHTTPRoute,
BodySizeByRuleIdx: map[int]*emitterir.BodySize{
0: {BufferSize: common.PtrTo(resource.MustParse("1Mi"))},
routeRuleAllIndex: {BufferSize: common.PtrTo(resource.MustParse("4Mi"))},
},
},
},
},
wantWarnings: 2,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var warnings int
notify := func(level notifications.MessageType, _ string, _ ...client.Object) {
if level == notifications.WarningNotification {
warnings++
}
}
e := &Emitter{notify: notify}
got, errs := e.Emit(tc.ir)
if len(errs) != 0 {
t.Fatalf("unexpected errors: %v", errs)
}
if len(got.GatewayExtensions) != 0 {
t.Errorf("want 0 GatewayExtensions, got %d", len(got.GatewayExtensions))
}
if warnings != tc.wantWarnings {
t.Errorf("want %d warnings, got %d", tc.wantWarnings, warnings)
}
})
}
}
Loading
Loading