Skip to content
Merged
147 changes: 122 additions & 25 deletions internal/provider/kubernetes/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@
}
}

func isTransientError(err error) bool {
return kerrors.IsServerTimeout(err) ||
kerrors.IsTimeout(err) ||
kerrors.IsTooManyRequests(err) ||
kerrors.IsServiceUnavailable(err) ||
kerrors.IsStoreReadError(err) ||
kerrors.IsInternalError(err) ||
kerrors.IsUnexpectedServerError(err)
}
Comment thread
zhaohuabing marked this conversation as resolved.

// Reconcile handles reconciling all resources in a single call. Any resource event should enqueue the
// same reconcile.Request containing the gateway controller name. This allows multiple resource updates to
// be handled by a single call to Reconcile. The reconcile.Request DOES NOT map to a specific resource.
Expand Down Expand Up @@ -222,6 +232,11 @@
// This should run before processGateways and processBackendRefs
if managedGC.Spec.ParametersRef != nil && managedGC.DeletionTimestamp == nil {
if err := r.processGatewayClassParamsRef(ctx, managedGC, gwcResourceMapping, gwcResource); err != nil {
if isTransientError(err) {
Comment thread
zhaohuabing marked this conversation as resolved.
Outdated
r.log.Error(err, "transient error processing GatewayClass parametersRef", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 238 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L235-L238

Added lines #L235 - L238 were not covered by tests

r.log.Error(err, fmt.Sprintf("failed processGatewayClassParamsRef for gatewayClass %s, skipping it", managedGC.Name))
msg := fmt.Sprintf("%s: %v", status.MsgGatewayClassInvalidParams, err)
gc := status.SetGatewayClassAccepted(
Expand All @@ -233,6 +248,32 @@
continue
}
}

// process envoy gateway secret refs
if err := r.processEnvoyProxySecretRef(ctx, gwcResource); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing TLS SecretRef for EnvoyProxy", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 257 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L254-L257

Added lines #L254 - L257 were not covered by tests

r.log.Error(err, fmt.Sprintf("failed process TLS SecretRef for EnvoyProxy for gatewayClass %s, skipping it", managedGC.Name))
gc := status.SetGatewayClassAccepted(
managedGC.DeepCopy(),
false,
string(gwapiv1.GatewayClassReasonAccepted),
fmt.Sprintf("%s: %v", status.MsgGatewayClassInvalidParams, err))
r.resources.GatewayClassStatuses.Store(utils.NamespacedName(gc), &gc.Status)
continue

Check warning on line 266 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L259-L266

Added lines #L259 - L266 were not covered by tests

@zhaohuabing zhaohuabing Jul 2, 2025

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.

Skip the processing GatewayClass if failed to process its parameter - should we continue processing the resources for an invalid GatewayClass and publish them?

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.

What's the downside? Will any of these resources actually translate to Infra/XDS, or would they only be updated with a status that reflect problems (if they have any) ?

@zhaohuabing zhaohuabing Jul 3, 2025

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.

Will any of these resources actually translate to Infra/XDS

Yes, all resources of this GatewayClass will be translated into Infra/XDS if we continue processing the failed GatewayClass, but the resulting configuration are incorrect - for example, the EnvoyProxy resource may not work as expected, including client cert, additional sidecars, custom bootstrap, etc.

they only be updated with a status that reflect problems (if they have any) ?

The previous change took this approach, but as this comment suggested, it's a big design decision and it may be risky to cherry-pick it into v1.4 branch. So I reverted it and track it at #6448 . We can discuss this in the next EG meeting and do it in the main branch.

This PR skips processing for the failed GatewayClass. As a result, the Envoy fleet associated with all Gateways under that GatewayClass will be deleted because the Gateway API runner deleted infra IRs not in the published resources.

If we consider that to be a higher risk than deploying a potentially misconfigured Envoy fleet, we could alternatively choose to continue processing and publishing the resources for the failed GatewayClass. cc @guydc @arkodg

Update: this PR continues processing failed GatewayClass and publish its resources - this may publish Infra IR and xDS IR with misconfiguration, but avoids taking down the whole Gateway with a single user error from an EnvoyProxy resource.

}

// GatewayClass is valid so far, mark it as accepted.
gc := status.SetGatewayClassAccepted(
managedGC.DeepCopy(),
true,
string(gwapiv1.GatewayClassReasonAccepted),
status.MsgValidGatewayClass)
r.resources.GatewayClassStatuses.Store(utils.NamespacedName(gc), &gc.Status)

// it's safe here to append gwcResource to gwcResources
gwcResources = append(gwcResources, gwcResource)
// process global resources
Expand All @@ -243,21 +284,31 @@

// Add all Gateways, their associated Routes, and referenced resources to the resourceTree
if err = r.processGateways(ctx, managedGC, gwcResourceMapping, gwcResource); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing gateways", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 290 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L287-L290

Added lines #L287 - L290 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed processGateways for gatewayClass %s, skipping it", managedGC.Name))
continue

@zhaohuabing zhaohuabing Jul 2, 2025

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.

If one of the resources fail, continue processing remaining resources and publish them as-is. This avoid skipping valid resources and publishing partial resources.

}

if r.eppCRDExists {
// Add all EnvoyPatchPolicies to the resourceTree
if err = r.processEnvoyPatchPolicies(ctx, gwcResource, gwcResourceMapping); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing EnvoyPatchPolicies", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}
r.log.Error(err, fmt.Sprintf("failed processEnvoyPatchPolicies for gatewayClass %s, skipping it", managedGC.Name))
continue
}
}

if r.ctpCRDExists {
// Add all ClientTrafficPolicies and their referenced resources to the resourceTree
if err = r.processClientTrafficPolicies(ctx, gwcResource, gwcResourceMapping); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing ClientTrafficPolicies", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 311 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L308-L311

Added lines #L308 - L311 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed processClientTrafficPolicies for gatewayClass %s, skipping it", managedGC.Name))
continue
Comment thread
zhaohuabing marked this conversation as resolved.
Outdated
}
Expand All @@ -266,6 +317,10 @@
if r.btpCRDExists {
// Add all BackendTrafficPolicies to the resourceTree
if err = r.processBackendTrafficPolicies(ctx, gwcResource, gwcResourceMapping); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing BackendTrafficPolicies", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 323 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L320-L323

Added lines #L320 - L323 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed processBackendTrafficPolicies for gatewayClass %s, skipping it", managedGC.Name))
continue
}
Expand All @@ -274,49 +329,75 @@
if r.spCRDExists {
// Add all SecurityPolicies and their referenced resources to the resourceTree
if err = r.processSecurityPolicies(ctx, gwcResource, gwcResourceMapping); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing SecurityPolicies", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 335 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L332-L335

Added lines #L332 - L335 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed processSecurityPolicies for gatewayClass %s, skipping it", managedGC.Name))
continue
}
}

if r.bTLSPolicyCRDExists {
// Add all BackendTLSPolies to the resourceTree
if err = r.processBackendTLSPolicies(ctx, gwcResource, gwcResourceMapping); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing BackendTLSPolicies", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 346 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L343-L346

Added lines #L343 - L346 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed processBackendTLSPolicies for gatewayClass %s, skipping it", managedGC.Name))
continue
}
}

if r.eepCRDExists {
// Add all EnvoyExtensionPolicies and their referenced resources to the resourceTree
if err = r.processEnvoyExtensionPolicies(ctx, gwcResource, gwcResourceMapping); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing EnvoyExtensionPolicies", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 357 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L354-L357

Added lines #L354 - L357 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed processEnvoyExtensionPolicies for gatewayClass %s, skipping it", managedGC.Name))
continue
}
}

if err = r.processExtensionServerPolicies(ctx, gwcResource); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing ExtensionServerPolicies", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 366 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L363-L366

Added lines #L363 - L366 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed processExtensionServerPolicies for gatewayClass %s, skipping it", managedGC.Name))
continue
}

if r.backendCRDExists {
if err = r.processBackends(ctx, gwcResource); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing Backends", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 375 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L372-L375

Added lines #L372 - L375 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed processBackends for gatewayClass %s, skipping it", managedGC.Name))
continue
}
}

// Add the referenced services, ServiceImports, and EndpointSlices in
// the collected BackendRefs to the resourceTree.
// BackendRefs are referred by various Route objects and the ExtAuth in SecurityPolicies.
r.processBackendRefs(ctx, gwcResource, gwcResourceMapping)
if err = r.processBackendRefs(ctx, gwcResource, gwcResourceMapping); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error processing BackendRefs", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 387 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L384-L387

Added lines #L384 - L387 were not covered by tests

r.log.Error(err, fmt.Sprintf("failed processBackendRefs for gatewayClass %s, skipping it", managedGC.Name))

Check warning on line 389 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L389

Added line #L389 was not covered by tests
}

// For this particular Gateway, and all associated objects, check whether the
// namespace exists. Add to the resourceTree.
for ns := range gwcResourceMapping.allAssociatedNamespaces {
namespace, err := r.getNamespace(ctx, ns)
if err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error getting namespace", "namespace", ns, "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 400 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L397-L400

Added lines #L397 - L400 were not covered by tests
r.log.Error(err, "unable to find the namespace")
if kerrors.IsNotFound(err) {
continue
Expand All @@ -335,30 +416,27 @@
}
}

// process envoy gateway secret refs
r.processEnvoyProxySecretRef(ctx, gwcResource)
gc := status.SetGatewayClassAccepted(
managedGC.DeepCopy(),
true,
string(gwapiv1.GatewayClassReasonAccepted),
status.MsgValidGatewayClass)
r.resources.GatewayClassStatuses.Store(utils.NamespacedName(gc), &gc.Status)

if len(gwcResource.Gateways) == 0 {
r.log.Info("No gateways found for accepted gatewayClass")

// If needed, remove the finalizer from the accepted GatewayClass.
if err := r.removeFinalizer(ctx, managedGC); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error removing finalizer from gatewayClass", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 427 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L425-L427

Added lines #L425 - L427 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed to remove finalizer from gatewayClass %s",
managedGC.Name))
continue
}
} else {
// finalize the accepted GatewayClass.
if err := r.addFinalizer(ctx, managedGC); err != nil {
if isTransientError(err) {
r.log.Error(err, "transient error adding finalizer to gatewayClass", "gatewayClass", managedGC.Name)
return reconcile.Result{}, err
}

Check warning on line 437 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L434-L437

Added lines #L434 - L437 were not covered by tests
r.log.Error(err, fmt.Sprintf("failed adding finalizer to gatewayClass %s",
managedGC.Name))
continue
}
}
}
Expand All @@ -373,9 +451,9 @@
return reconcile.Result{}, nil
}

func (r *gatewayAPIReconciler) processEnvoyProxySecretRef(ctx context.Context, gwcResource *resource.Resources) {
func (r *gatewayAPIReconciler) processEnvoyProxySecretRef(ctx context.Context, gwcResource *resource.Resources) error {
if gwcResource.EnvoyProxyForGatewayClass == nil || gwcResource.EnvoyProxyForGatewayClass.Spec.BackendTLS == nil || gwcResource.EnvoyProxyForGatewayClass.Spec.BackendTLS.ClientCertificateRef == nil {
return
return nil
}
certRef := gwcResource.EnvoyProxyForGatewayClass.Spec.BackendTLS.ClientCertificateRef
if refsSecret(certRef) {
Expand All @@ -387,11 +465,10 @@
gwcResource.EnvoyProxyForGatewayClass.Namespace,
resource.KindEnvoyProxy,
*certRef); err != nil {
r.log.Error(err,
"failed to process TLS SecretRef for EnvoyProxy",
"gateway", "issue", "secretRef", certRef)
return err

Check warning on line 468 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L468

Added line #L468 was not covered by tests
}
}
return nil

Check warning on line 471 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L471

Added line #L471 was not covered by tests
}

// managedGatewayClasses returns a list of GatewayClass objects that are managed by the Envoy Gateway Controller.
Expand Down Expand Up @@ -427,7 +504,11 @@
// - EndpointSlices
// - Backends
// - CACertificateRefs in the Backends
func (r *gatewayAPIReconciler) processBackendRefs(ctx context.Context, gwcResource *resource.Resources, resourceMappings *resourceMappings) {
func (r *gatewayAPIReconciler) processBackendRefs(ctx context.Context, gwcResource *resource.Resources, resourceMappings *resourceMappings) error {
// Only transient errors are returned from this function to allow Reconcile to retry.
// All other errors are just logged and ignored - these errors result in missing referenced backend resources
// in the resource tree, which is acceptable as the Gateway API translation layer will handle them.
// The Gateway API translation layer will surface these errors in the status of the resources referencing them.
for backendRef := range resourceMappings.allAssociatedBackendRefs {
backendRefKind := gatewayapi.KindDerefOr(backendRef.Kind, resource.KindService)
r.log.Info("processing Backend", "kind", backendRefKind, "namespace", string(*backendRef.Namespace),
Expand All @@ -439,6 +520,9 @@
service := new(corev1.Service)
err := r.client.Get(ctx, types.NamespacedName{Namespace: string(*backendRef.Namespace), Name: string(backendRef.Name)}, service)
if err != nil {
if isTransientError(err) {

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.

is going to hard to remember to add this for every client call

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.

Yeah, we can figure out a better approach to handle this later.

return err
}

Check warning on line 525 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L523-L525

Added lines #L523 - L525 were not covered by tests
r.log.Error(err, "failed to get Service", "namespace", string(*backendRef.Namespace),
"name", string(backendRef.Name))
} else {
Expand All @@ -453,6 +537,9 @@
serviceImport := new(mcsapiv1a1.ServiceImport)
err := r.client.Get(ctx, types.NamespacedName{Namespace: string(*backendRef.Namespace), Name: string(backendRef.Name)}, serviceImport)
if err != nil {
if isTransientError(err) {
return err
}

Check warning on line 542 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L540-L542

Added lines #L540 - L542 were not covered by tests
r.log.Error(err, "failed to get ServiceImport", "namespace", string(*backendRef.Namespace),
"name", string(backendRef.Name))
} else {
Expand All @@ -471,6 +558,9 @@
backend := new(egv1a1.Backend)
err := r.client.Get(ctx, types.NamespacedName{Namespace: string(*backendRef.Namespace), Name: string(backendRef.Name)}, backend)
if err != nil {
if isTransientError(err) {
return err
}

Check warning on line 563 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L561-L563

Added lines #L561 - L563 were not covered by tests
r.log.Error(err, "failed to get Backend", "namespace", string(*backendRef.Namespace),
"name", string(backendRef.Name))
} else {
Expand Down Expand Up @@ -519,6 +609,9 @@
caRefNew)
}
if err != nil {
if isTransientError(err) {
return err
}

Check warning on line 614 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L612-L614

Added lines #L612 - L614 were not covered by tests
r.log.Error(err,
"failed to process CACertificateRef for Backend",
"backend", backend, "caCertificateRef", caCertRef.Name)
Expand All @@ -538,6 +631,9 @@
client.InNamespace(*backendRef.Namespace),
}
if err := r.client.List(ctx, endpointSliceList, opts...); err != nil {
if isTransientError(err) {
return err
}

Check warning on line 636 in internal/provider/kubernetes/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/provider/kubernetes/controller.go#L634-L636

Added lines #L634 - L636 were not covered by tests
r.log.Error(err, "failed to get EndpointSlices", "namespace", string(*backendRef.Namespace),
backendRefKind, string(backendRef.Name))
} else {
Expand All @@ -554,6 +650,7 @@
}
}
}
return nil
}

// processSecurityPolicyObjectRefs adds the referenced resources in SecurityPolicies
Expand Down
32 changes: 32 additions & 0 deletions internal/provider/kubernetes/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ package kubernetes

import (
"context"
"fmt"
"os"
"testing"

"github.com/stretchr/testify/require"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -1022,3 +1025,32 @@ func setupReferenceGrantReconciler(objs []client.Object) *gatewayAPIReconciler {
Build()
return r
}

func TestIsTransientError(t *testing.T) {
serverTimeoutErr := kerrors.NewServerTimeout(
schema.GroupResource{Group: "core", Resource: "pods"}, "list", 10)
timeoutErr := kerrors.NewTimeoutError("request timeout", 1)
wrappedTooManyRequestsErr := fmt.Errorf("wrapping: %w", kerrors.NewTooManyRequests("too many requests", 1))
serviceUnavailableErr := kerrors.NewServiceUnavailable("service unavailable")
badRequestErr := kerrors.NewBadRequest("bad request")

testCases := []struct {
name string
err error
expected bool
}{
{"ServerTimeout", serverTimeoutErr, true},
{"Timeout", timeoutErr, true},
{"TooManyRequests", wrappedTooManyRequestsErr, true},
{"ServiceUnavailable", serviceUnavailableErr, true},
{"BadRequest", badRequestErr, false},
{"NilError", nil, false},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := isTransientError(tc.err)
require.Equal(t, tc.expected, actual)
})
}
}
Loading