Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (d *backendTlsPolicy) Equals(in any) bool {
}

func registerTypes() {
kubeclient.Register[*gwv1.BackendTLSPolicy](
kubeclient.Register(
backendTlsPolicyGvr,
backendTlsPolicyGroupKind,
func(c kubeclient.ClientGetter, namespace string, o metav1.ListOptions) (runtime.Object, error) {
Expand Down
136 changes: 136 additions & 0 deletions internal/kgateway/setup/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,116 @@ spec:
})
}

func TestServiceAppProtocolUpdate(t *testing.T) {
st, err := envtestutil.BuildSettings()
if err != nil {
t.Fatalf("can't get settings %v", err)
}
setupEnvTestAndRun(t, st, func(t *testing.T, ctx context.Context, kdbg *krt.DebugHandler, client istiokube.CLIClient, xdsPort, _ int) {
client.Kube().CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "gwtest"}}, metav1.CreateOptions{})

err = client.ApplyYAMLContents("gwtest", `kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-gw
namespace: gwtest
spec:
gatewayClassName: kgateway
listeners:
- protocol: HTTP
port: 8080
name: http
allowedRoutes:
namespaces:
from: All`, `apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: ws-route
namespace: gwtest
spec:
parentRefs:
- name: http-gw
hostnames:
- "ws.example.com"
rules:
- backendRefs:
- name: test-service
port: 8080
matches:
- path:
type: PathPrefix
value: /`, `apiVersion: v1
kind: Service
metadata:
name: test-service
namespace: gwtest
spec:
selector:
app: reviews
ports:
- protocol: TCP
port: 8080
targetPort: 8080
`)
if err != nil {
t.Fatalf("failed to apply initial resources: %v", err)
}

time.Sleep(time.Second * 2)
t.Cleanup(func() {
if t.Failed() {
logKrtState(t, fmt.Sprintf("krt state for failed test: %s", t.Name()), kdbg)
} else if os.Getenv("KGW_DUMP_KRT_ON_SUCCESS") == "true" {
logKrtState(t, fmt.Sprintf("krt state for successful test: %s", t.Name()), kdbg)
}
})
initialDump, err := dumpXdsConfig(t, ctx, xdsPort, "http-gw")
if err != nil {
t.Fatalf("failed to get initial xDS dump: %v", err)
}
hasWebsocketUpgrade := hasWebsocketUpgradeConfig(initialDump)
if hasWebsocketUpgrade {
t.Fatalf("expected no websocket upgrade in initial state, but found one")
}

err = client.ApplyYAMLContents("gwtest", `apiVersion: v1
kind: Service
metadata:
name: test-service
namespace: gwtest
spec:
selector:
app: reviews
ports:
- protocol: TCP
appProtocol: kubernetes.io/ws
port: 8080
targetPort: 8080
`)
if err != nil {
t.Fatalf("failed to update service with appProtocol: %v", err)
}

time.Sleep(time.Second * 2)

updatedDump, err := dumpXdsConfig(t, ctx, xdsPort, "http-gw")
if err != nil {
t.Fatalf("failed to get updated xDS dump: %v", err)
}
if len(updatedDump.Routes) > 0 {
hasWebsocketUpgrade := hasWebsocketUpgradeConfig(updatedDump)
if !hasWebsocketUpgrade {
t.Fatalf("expected websocket upgrade configuration after updating appProtocol, but found none")
}
t.Logf("SUCCESS: Found websocket upgrade configuration after updating appProtocol")
} else {
t.Fatalf("No routes found in updated dump")
}

t.Logf("%s finished", t.Name())
})
}

func runScenario(t *testing.T, scenarioDir string, globalSettings *apisettings.Settings) {
setupEnvTestAndRun(t, globalSettings, func(t *testing.T, ctx context.Context, kdbg *krt.DebugHandler, client istiokube.CLIClient, xdsPort, _ int) {
// list all yamls in test data
Expand Down Expand Up @@ -1023,3 +1133,29 @@ func getroutesnames(l *envoylistenerv3.Listener) []string {
}
return routes
}

// dumpXdsConfig dumps the xDS config for the given gateway name. helper function to
// ensure the dumper is closed after the dump is complete.
func dumpXdsConfig(t *testing.T, ctx context.Context, xdsPort int, gwName string) (xdsDump, error) {
dumper := newXdsDumper(t, ctx, xdsPort, gwName)
defer dumper.Close()
return dumper.Dump(t, ctx)
}

// hasWebsocketUpgradeConfig checks if any route in the xDS dump has websocket upgrade configuration
func hasWebsocketUpgradeConfig(dump xdsDump) bool {
for _, route := range dump.Routes {
for _, vhost := range route.GetVirtualHosts() {
for _, r := range vhost.GetRoutes() {
if routeAction := r.GetRoute(); routeAction != nil {
for _, upgradeConfig := range routeAction.GetUpgradeConfigs() {
if upgradeConfig.GetUpgradeType() == "websocket" {
return true
}
}
}
}
}
}
return false
}
60 changes: 37 additions & 23 deletions pkg/pluginsdk/ir/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,10 @@ type BackendObjectIR struct {

// Errors is a list of errors, if any, encountered while constructing this BackendObject
// Not added to Equals() as it is derived from the inner ObjIr, which is already evaluated
// +krtEqualsTodo decide whether construction errors should impact equality
// +noKrtEquals
Errors []error

// Name is the pre-calculated resource name. used as the krt resource name.
// resourceName is the pre-calculated resource name. used as the krt resource name.
resourceName string

// TrafficDistribution is the desired traffic distribution for the backend.
Expand All @@ -167,10 +167,6 @@ func NewBackendObjectIR(objSource ObjectSource, port int32, extraKey string) Bac
}
}

func (c BackendObjectIR) ResourceName() string {
return c.resourceName
}

func BackendResourceName(objSource ObjectSource, port int32, extraKey string) string {
var sb strings.Builder
sb.WriteString(objSource.ResourceName())
Expand All @@ -183,22 +179,32 @@ func BackendResourceName(objSource ObjectSource, port int32, extraKey string) st
return sb.String()
}

// ResourceName returns the pre-calculated resource name for this backend.
// This method is required to implement the krt.Named interface.
func (c BackendObjectIR) ResourceName() string {
return c.resourceName
}

func (c BackendObjectIR) Equals(in BackendObjectIR) bool {
objEq := c.ObjectSource.Equals(in.ObjectSource)
objVersionEq := versionEquals(c.Obj, in.Obj)
polEq := c.AttachedPolicies.Equals(in.AttachedPolicies)
nameEq := c.resourceName == in.resourceName
disableIstioAutoMTLSEq := c.DisableIstioAutoMTLS == in.DisableIstioAutoMTLS

// objIr may currently be nil in the case of k8s Services
// TODO: add an IR for Services to avoid the need for this
// see: internal/kgateway/extensions2/plugins/kubernetes/k8s.go
objIrEq := true
if c.ObjIr != nil {
objIrEq = c.ObjIr.Equals(in.ObjIr)
if !c.ObjectSource.Equals(in.ObjectSource) {
return false
}

return objEq && objVersionEq && objIrEq && polEq && nameEq && disableIstioAutoMTLSEq
if !versionEquals(c.Obj, in.Obj) {
return false
}
if c.ObjIr != nil && !c.ObjIr.Equals(in.ObjIr) {
return false
}
if !c.AttachedPolicies.Equals(in.AttachedPolicies) {
return false
}
if c.resourceName != in.resourceName {
return false
}
if c.DisableIstioAutoMTLS != in.DisableIstioAutoMTLS {
return false
}
return true
}

func (c BackendObjectIR) ClusterName() string {
Expand Down Expand Up @@ -349,20 +355,28 @@ func (c Gateway) Equals(in Gateway) bool {
c.DeniedListenerSets.Equals(in.DeniedListenerSets)
}

type BackendRefIR struct {
// TODO: remove cluster name from here, it's redundant.
ClusterName string
Weight uint32

// backend could be nil if not found or no ref grant
BackendObject *BackendObjectIR
// if nil, error might say why
Err error
}

// Equals returns true if the two BackendRefIR instances are equal in cluster name, weight, backend object equality, and error.
func (a BackendRefIR) Equals(b BackendRefIR) bool {
if a.ClusterName != b.ClusterName || a.Weight != b.Weight {
return false
}

if !backendObjectEqual(a.BackendObject, b.BackendObject) {
return false
}

if !errorsEqual(a.Err, b.Err) {
return false
}

return true
}

Expand Down
42 changes: 25 additions & 17 deletions pkg/pluginsdk/ir/gw.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,23 +254,6 @@ func (l AttachedPolicies) MarshalJSON() ([]byte, error) {
return json.Marshal(m)
}

type BackendRefIR struct {
// TODO: remove cluster name from here, it's redundant.
ClusterName string
Weight uint32

// backend could be nil if not found or no ref grant
BackendObject *BackendObjectIR
// if nil, error might say why
Err error
}

type HttpBackendOrDelegate struct {
Backend *BackendRefIR
Delegate *ObjectSource
AttachedPolicies AttachedPolicies
}

type HttpRouteRuleIR struct {
ExtensionRefs AttachedPolicies
AttachedPolicies AttachedPolicies
Expand All @@ -282,3 +265,28 @@ type HttpRouteRuleIR struct {
// that should be propagated through to translation to any derived ir.HttpRouteRuleMatchIRs
Err error
}

type HttpBackendOrDelegate struct {
AttachedPolicies AttachedPolicies
Backend *BackendRefIR
Delegate *ObjectSource
}

func (h HttpBackendOrDelegate) Equals(other HttpBackendOrDelegate) bool {
if !h.AttachedPolicies.Equals(other.AttachedPolicies) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: order comparision in the order of fields declared in the struct

return false
}
if h.Backend != nil && other.Backend != nil {
return h.Backend.Equals(*other.Backend)
}
if h.Backend == nil && other.Backend == nil {
if h.Delegate == nil && other.Delegate == nil {
return true
}
if h.Delegate != nil && other.Delegate != nil {
return h.Delegate.Equals(*other.Delegate)
}
return false
}
return false
}
19 changes: 3 additions & 16 deletions pkg/pluginsdk/ir/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Route interface {
GetSourceObject() metav1.Object
}

var _ Route = &HttpRouteIR{}

// this is 1:1 with httproute, and is a krt type
// maybe move this to krtcollections package?
type HttpRouteIR struct {
Expand Down Expand Up @@ -54,7 +56,6 @@ func (c HttpRouteIR) ResourceName() string {
return c.ObjectSource.ResourceName()
}

// get hostnames
func (c *HttpRouteIR) GetHostnames() []string {
if c == nil {
return nil
Expand Down Expand Up @@ -99,21 +100,7 @@ func (c HttpRouteIR) rulesEqual(in HttpRouteIR) bool {
return false
}
for j, backend := range backendsa {
otherbackend := backendsb[j]
if backend.Backend == nil && otherbackend.Backend == nil {
continue
}
if backend.Backend != nil && otherbackend.Backend != nil {
if backend.Backend.ClusterName != otherbackend.Backend.ClusterName {
return false
}
if backend.Backend.Weight != otherbackend.Backend.Weight {
return false
}
if !backend.AttachedPolicies.Equals(otherbackend.AttachedPolicies) {
return false
}
} else {
if !backend.Equals(backendsb[j]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Were we intentionally restricting Backend comparison to specific fields before or was this an oversight?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the latter is very likely. If you go through the git history for this file, you'll reach the massive refactor PR that Yuval did during the early days of the donation that predated my time: 5d4e603#diff-5b7e43b2f30d5fce0ff29295ccad60d0882ce08abb34ba7e6f9464b942e02a6f. @lgadban @yuval-k if you can confirm

Copy link
Contributor

Choose a reason for hiding this comment

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

it is intentional - there's no need to propagate changes like app protocol for routes - i believe it is a no-op in envoy

Copy link
Contributor

Choose a reason for hiding this comment

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

i.e. i don't care about backend properties that don't end up on the envoy route object

Copy link
Contributor

Choose a reason for hiding this comment

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

@timflannagan do you know if something about the app protcol impacts the envoy route?

Copy link
Member Author

Choose a reason for hiding this comment

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

the problem is that appProtocol, at least for websockets, does contribute to envoy config:

if back := backend.Backend.BackendObject; back != nil && back.AppProtocol == ir.WebSocketAppProtocol {
// add websocket upgrade if not already present
if !slices.ContainsFunc(action.GetUpgradeConfigs(), func(uc *envoyroutev3.RouteAction_UpgradeConfig) bool {
return uc.GetUpgradeType() == webSocketUpgradeType
}) {
action.UpgradeConfigs = append(action.GetUpgradeConfigs(), &envoyroutev3.RouteAction_UpgradeConfig{
UpgradeType: webSocketUpgradeType,
})
}
}
. the issue is that before this logic, we weren't comparing the Backend.BackendObject, which meant updates to Services that change the appProtocol weren't reflected in the system.

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW for the BackendObjectIR's Equals() not checking the AppProtocol field. I think we're all set due to

func versionEquals(a, b metav1.Object) bool {
var versionEquals bool
if a.GetGeneration() != 0 && b.GetGeneration() != 0 {
versionEquals = a.GetGeneration() == b.GetGeneration()
} else {
versionEquals = a.GetResourceVersion() == b.GetResourceVersion()
}
return versionEquals && a.GetUID() == b.GetUID()
}
. As an aside, that function implementation looks like it wouldn't work with fields that are derived from annotations (e.g. traffic distribution, disable Istio auto mtls) and require those fields to explicitly contribute to the Equals implementation vs. piggybacking off the object check.

So the issue here is solely due to the lack of explicit backend.Backend equals checking.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, we are automatically add an upgrade to the route based on the backend app protocol. then yea the app protocol is effectivly a route policy in this case, so it should be in the equals

return false
}
}
Expand Down
Loading
Loading