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
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,19 @@ vet: ;$(info $(M)...Begin to run go vet against code.) @ ## Run go vet against
test: vet;$(info $(M)...Begin to run integration tests.) @ ## Run integration tests.
go test -race -cover ./pkg/... ./cmd/...

# Run e2e tests
# Run kgateway emitter e2e tests
.PHONY: test-e2e-kgtw
test-e2e-kgtw: vet;$(info $(M)...Begin to run kgateway emitter e2e tests.) @
go test -v -count=1 ./test/e2e/emitters/kgateway/...

# Run agentgateway emitter e2e tests
.PHONY: test-e2e-agtw
test-e2e-agtw: vet;$(info $(M)...Begin to run agentgateway emitter e2e tests.) @
go test -v -count=1 ./test/e2e/emitters/agentgateway/...

# Run all e2e tests
.PHONY: test-e2e
test-e2e: vet;$(info $(M)...Begin to run e2e tests.) @ ## Run e2e tests.
go test -v -count=1 ./test/e2e/...
test-e2e: vet test-e2e-kgtw test-e2e-agtw;$(info $(M)...Begin to run all e2e tests.) @

# Run integration and e2e tests
.PHONY: test-all
Expand Down
123 changes: 123 additions & 0 deletions pkg/i2gw/emitters/agentgateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Agentgateway Emitter

The Agentgateway Emitter supports generating **Gateway API** resources plus **agentgateway**-specific extensions
from Ingress manifests using:

- **Provider**: `ingress-nginx`

**Note:** All other providers will be ignored by the emitter.

## What it outputs

- Standard **Gateway API** objects (Gateways, HTTPRoutes, etc.)
- Agentgateway extension objects emitted as unstructured resources, e.g. `AgentgatewayPolicy`.

The emitter also ensures that any generated Gateway resources use:

- `spec.gatewayClassName: agentgateway`

## Development Workflow

The typical development workflow for adding an Ingress NGINX feature to the Agentgateway emitter is:

1. Use [this issue](https://github.com/kubernetes-sigs/ingress2gateway/issues/232) to prioritize the list of Ingress NGINX features unless
the business provides requirements. When this list is complete, refer to [this doc](https://docs.google.com/document/d/12ejNTb45hASGYvUjd3t9mfNwuMTX3KBM68ALM4jRFBY/edit?usp=sharing) for additional features. **Note:** Several of the features from the above list have already been implemented, so review the
current supported features before adding more.
2. If a feature cannot map to an existing agentgateway API, open an Agentgateway issue describing what’s needed.
3. Extend the ingress-nginx IR/generic Policy IR as needed so the provider can represent the feature in a structured way.
4. Add a feature-specific function to the ingress-nginx provider (`pkg/i2gw/providers/ingressnginx`), e.g.
`rateLimitFeature()`, that parses the Ingress NGINX annotation(s) and records them as generic Policies in the
provider IR.
5. Update the Agentgateway Emitter (`pkg/i2gw/emitters/agentgateway/emitter.go`) to consume the IR and emit
agentgateway-specific resources.
6. Add/extend integration and e2e tests to cover the new behavior.
7. Update the list of supported annotations with the feature you added.
8. Submit a PR to merge your changes upstream. [This branch](https://github.com/danehans/ingress2gateway/tree/impl_emitter_nginx_feat) is the **current** upstream, but [k8s-sigs](https://github.com/kubernetes-sigs/ingress2gateway) or [solo](https://github.com/solo-io/ingress2gateway) repos should be used before releasing.

## Testing

Run the tool with a test input manifest:

```bash
go run . print \
--providers=ingress-nginx \
--emitter=agentgateway \
--input-file ./pkg/i2gw/emitters/agentgateway/testing/testdata/<FEATURE>.yaml
```

The command should generate Gateway API resources plus agentgateway extension resources (when applicable).

## Supported Annotations

### Traffic Behavior

#### Local Rate Limiting

The agentgateway emitter currently supports projecting local rate limiting via:

- `nginx.ingress.kubernetes.io/limit-rps`
- `nginx.ingress.kubernetes.io/limit-rpm`
- `nginx.ingress.kubernetes.io/limit-burst-multiplier`

These are mapped into an `AgentgatewayPolicy` using agentgateway’s `LocalRateLimit` model:

- `limit-rps` → `LocalRateLimit{ requests: <limit>, unit: Seconds }`
- `limit-rpm` → `LocalRateLimit{ requests: <limit>, unit: Minutes }`
- `limit-burst-multiplier` (when > 1) → `LocalRateLimit{ burst: limit * multiplier }`

**Notes:**

- Burst multiplier defaults to `1` if unset/zero.
- Unknown/unsupported units are ignored.

## AgentgatewayPolicy Projection

Rate limit annotations are converted into `AgentgatewayPolicy` resources.

### Naming

Policies are created **per source Ingress name**:

- `metadata.name: <ingress-name>`
- `metadata.namespace: <route-namespace>`

### Attachment Semantics

If a policy covers all backends of the generated HTTPRoute, the policy is attached using `spec.targetRefs`
to the HTTPRoute.

If a policy only covers some (rule, backendRef) pairs, the emitter **returns an error** and does not emit
+agentgateway resources for that Ingress.

Conceptually:

- **Full coverage** → `AgentgatewayPolicy.spec.targetRefs[]` references the HTTPRoute
- **Partial coverage** → **error** (agentgateway does not support attaching `AgentgatewayPolicy` via per-backend
`HTTPRoute` `ExtensionRef` filters)

#### Why?

Agentgateway does not support `HTTPRoute` `backendRefs[].filters[].type: ExtensionRef` for attaching policies.
Attempting to generate per-backend `ExtensionRef` filters results in `HTTPRoute` status failures (e.g.
`ResolvedRefs=False` with an `IncompatibleFilters` error). To avoid emitting manifests that will be rejected or
non-functional at runtime, the emitter fails fast during generation when only partial attachment is possible.

#### Workarounds

- Split the source Ingress into separate Ingress resources so each generated HTTPRoute can be fully covered by a policy.
- Adjust annotations so the policy applies uniformly to all paths/backends of the resulting HTTPRoute.

## Deterministic Output

For stable golden tests, agentgateway extension objects are sorted (Kind, Namespace, Name) before being appended
to the output extensions list.

## Limitations

- Only the **ingress-nginx provider** is currently supported by the Agentgateway emitter.
- Regex path matching is not currently implemented for agentgateway output.

## Future Work

The code defines GVKs for additional agentgateway extension types (e.g. `AgentgatewayBackend`), but they are not
yet emitted by the current implementation.
49 changes: 24 additions & 25 deletions pkg/i2gw/emitters/agentgateway/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ limitations under the License.
package agentgateway

import (
"fmt"
"sort"

"github.com/kgateway-dev/ingress2gateway/pkg/i2gw"
emitterir "github.com/kgateway-dev/ingress2gateway/pkg/i2gw/emitter_intermediate"
"github.com/kgateway-dev/ingress2gateway/pkg/i2gw/emitters/utils"

agentgatewayv1alpha1 "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway"
"github.com/kgateway-dev/kgateway/v2/api/v1alpha1/shared"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -76,7 +78,9 @@ func (e *Emitter) Emit(ir emitterir.EmitterIR) (i2gw.GatewayResources, field.Err
}
sort.Strings(policyNames)

for polSourceIngressName, pol := range ingx.Policies {
for _, polSourceIngressName := range policyNames {
pol := ingx.Policies[polSourceIngressName]

// Normalize (rule, backend) coverage to unique pairs to avoid
// generating duplicate filters on the same backendRef.
coverage := uniquePolicyIndices(pol.RuleBackendSources)
Expand All @@ -86,8 +90,10 @@ func (e *Emitter) Emit(ir emitterir.EmitterIR) (i2gw.GatewayResources, field.Err
// Set targetRefs for the policy
agp := agentgatewayPolicies[polSourceIngressName]
if agp != nil {
// If this policy covers all route backends, attach via targetRefs.
// Otherwise, attach via ExtensionRef filters on the covered backendRefs.
// AgentgatewayPolicy is attached via policy attachment (targetRefs/targetSelectors).
// Agentgateway does *not* support attaching a policy to individual backendRefs using
// HTTPRouteFilter{Type: ExtensionRef}. If we can't apply the policy to the full route,
// we must surface a conversion error.
if len(coverage) == numRules(httpRouteContext.HTTPRoute) {
// Full coverage via targetRefs.
agp.Spec.TargetRefs = []shared.LocalPolicyTargetReferenceWithSectionName{{
Expand All @@ -98,28 +104,21 @@ func (e *Emitter) Emit(ir emitterir.EmitterIR) (i2gw.GatewayResources, field.Err
},
}}
} else {
// Partial coverage via ExtensionRef filters on backendRefs.
for _, idx := range coverage {
if idx.Rule >= len(httpRouteContext.Spec.Rules) {
continue
}
rule := &httpRouteContext.Spec.Rules[idx.Rule]
if idx.Backend >= len(rule.BackendRefs) {
continue
}

rule.BackendRefs[idx.Backend].Filters = append(
rule.BackendRefs[idx.Backend].Filters,
gatewayv1.HTTPRouteFilter{
Type: gatewayv1.HTTPRouteFilterExtensionRef,
ExtensionRef: &gatewayv1.LocalObjectReference{
Group: gatewayv1.Group(AgentgatewayPolicyGVK.Group),
Kind: gatewayv1.Kind(AgentgatewayPolicyGVK.Kind),
Name: gatewayv1.ObjectName(agp.Name),
},
},
)
}
total := numRules(httpRouteContext.HTTPRoute)
errs = append(errs, field.Invalid(
field.NewPath("HTTPRoute").
Key(fmt.Sprintf("%s/%s", httpRouteKey.Namespace, httpRouteKey.Name)).
Child("spec", "rules"),
agp.Name,
fmt.Sprintf(
"agentgateway does not support HTTPRoute backendRef ExtensionRef filters; "+
"policy %q only applies to a subset of backendRefs (%d/%d). Split the Ingress "+
"into separate resources or make the policy apply to all paths/backends.",
agp.Name, len(coverage), total,
),
))
// Avoid emitting an invalid policy (ExactlyOneOf targetRefs/targetSelectors).
delete(agentgatewayPolicies, polSourceIngressName)
}
}
}
Expand Down
1 change: 0 additions & 1 deletion pkg/i2gw/emitters/agentgateway/emitter_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,3 @@ func TestAgentgatewayIngressNginxIntegration_RateLimit(t *testing.T) {
})
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ metadata:
spec:
ingressClassName: nginx
rules:
- host: ratelimit.example.org
- host: ratelimit-rps.example.org
http:
paths:
- backend:
Expand All @@ -29,7 +29,7 @@ metadata:
spec:
ingressClassName: nginx
rules:
- host: ratelimit.example.org
- host: ratelimit-rpm.example.org
http:
paths:
- backend:
Expand Down Expand Up @@ -61,4 +61,3 @@ spec:
number: 80
path: /
pathType: Prefix

Loading
Loading